AWS Bedrock auxiliary task routing and auth fixes
Cluster 13919. Data as of
Nothing open on GitHub. Fix merged in #15184 on 24 Apr 2026. Check it covers your case.
The short answer
- Known?
- Yes. 4 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #15184. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #15184. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Multiple fixes addressing AWS Bedrock (aws_sdk) authentication support and routing of auxiliary tasks such as context compression and summarization through the appropriate Bedrock SDK or Converse API. Includes related improvements to model-ID detection, context length handling, and connection management.
Open issues0
Open PRs0
Fixes on record
-
#15184 fix(bedrock): consolidated — model-ID detection, context length priority, aux aws_sdk, stale-connection eviction, interrupt rebuild
files:
agent/anthropic_adapter.py,agent/auxiliary_client.py,agent/bedrock_adapter.py,agent/model_metadata.py,run_agent.py,scripts/release.py,tests/agent/test_bedrock_adapter.py,tests/agent/test_bedrock_integration.py,tests/agent/test_model_metadata.pyShow the diff
-
agent/anthropic_adapter.py@@ -986,6 +986,26 @@ def read_hermes_oauth_credentials() -> Optional[Dict[str, Any]]: # --------------------------------------------------------------------------- +def _is_bedrock_model_id(model: str) -> bool: + """Detect AWS Bedrock model IDs that use dots as namespace separators. + + Bedrock model IDs come in two forms: + - Bare: ``anthropic.claude-opus-4-7`` + - Regional (inference profiles): ``us.anthropic.claude-sonnet-4-5-v1:0`` + + In both cases the dots separate namespace components, not version + numbers, and must be preserved verbatim for the Bedrock API. + """ + lower = model.lower() + # Regional inference-profile prefixes + if any(lower.startswith(p) for p in ("global.", "us.", "eu.", "ap.", "jp.")): + return True + # Bare Bedrock model IDs: provider.model-family + if lower.startswith("anthropic."): + return True + return False + + def normalize_model_name(model: str, preserve_dots: bool = False) -> str: """Normalize a model name for the Anthropic API. @@ -993,11 +1013,19 @@ def normalize_model_name(model: str, preserve_dots: bool = False) -> str: - Converts dots to hyphens in version numbers (OpenRouter uses dots, Anthropic uses hyphens: claude-opus-4.6 → claude-opus-4-6), unless preserve_dots is True (e.g. for Alibaba/DashScope: qwen3.5-plus). + - Preserves Bedrock model IDs (``anthropic.claude-opus-4-7``) and + regional inference profiles (``us.anthropic.claude-*``) whose dots + are namespace separators, not version separators. """ lower = model.lower() if lower.startswith("anthropic/"): model = model[len("anthropic/"):] if not preserve_dots: + # Bedrock model IDs use dots as namespace separators + # (e.g. "anthropic.claude-opus-4-7", "us.anthropic.claude-*"). + # These must not be converted to hyphens. See issue #12295. + if _is_bedrock_model_id(model): + return model # OpenRouter uses dots for version separators (claude-opus-4.6), # Anthropic uses hyphens (claude-opus-4-6). Convert dots to hyphens. model = model.replace(".", "-") -
agent/auxiliary_client.py@@ -1993,6 +1993,39 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = ""): "directly supported", provider) return None, None + elif pconfig.auth_type == "aws_sdk": + # AWS SDK providers (Bedrock) — use the Anthropic Bedrock client via + # boto3's credential chain (IAM roles, SSO, env vars, instance metadata). + try: + from agent.bedrock_adapter import has_aws_credentials, resolve_bedrock_region + from agent.anthropic_adapter import build_anthropic_bedrock_client + except ImportError: + logger.warning("resolve_provider_client: bedrock requested but " + "boto3 or anthropic SDK not installed") + return None, None + + if not has_aws_credentials(): + logger.debug("resolve_provider_client: bedrock requested but " + "no AWS credentials found") + return None, None + + region = resolve_bedrock_region() + default_model = "anthropic.claude-haiku-4-5-20251001-v1:0" + final_model = _normalize_resolved_model(model or default_model, provider) + try: + real_client = build_anthropic_bedrock_client(region) + except ImportError as exc: + logger.warning("resolve_provider_client: cannot create Bedrock " + "client: %s", exc) + return None, None + client = AnthropicAuxiliaryClient( + real_client, final_model, api_key="[redacted:secret-assignment]", + base_url=f"https://bedrock-runtime.{region}.amazonaws.com", + ) + logger.debug("resolve_provider_client: bedrock (%s, %s)", final_model, region) + return (_to_async_client(client, final_model) if async_mode + else (client, final_model)) + elif pconfig.auth_type in ("oauth_device_code", "oauth_external"): # OAuth providers — route through their specific try functions if provider == "nous": -
agent/bedrock_adapter.py@@ -87,6 +87,114 @@ def reset_client_cache(): _bedrock_control_client_cache.clear() +def invalidate_runtime_client(region: str) -> bool: + """Evict the cached ``bedrock-runtime`` client for a single region. + + Per-region counterpart to :func:`reset_client_cache`. Used by the converse + call wrappers to discard clients whose underlying HTTP connection has + gone stale, so the next call allocates a fresh client (with a fresh + connection pool) instead of reusing a dead socket. + + Returns True if a cached entry was evicted, False if the region was not + cached. + """ + existed = region in _bedrock_runtime_client_cache + _bedrock_runtime_client_cache.pop(region, None) + return existed + + +# --------------------------------------------------------------------------- +# Stale-connection detection +# --------------------------------------------------------------------------- +# +# boto3 caches its HTTPS connection pool inside the client object. When a +# pooled connection is killed out from under us (NAT timeout, VPN flap, +# server-side TCP RST, proxy idle cull, etc.), the next use surfaces as +# one of a handful of low-level exceptions — most commonly +# ``botocore.exceptions.ConnectionClosedError`` or +# ``urllib3.exceptions.ProtocolError``. urllib3 also trips an internal +# ``assert`` in a couple of paths (connection pool state checks, chunked +# response readers) which bubbles up as a bare ``AssertionError`` with an +# empty ``str(exc)``. +# +# In all of these cases the client is the problem, not the request: retrying +# with the same cached client reproduces the failure until the process +# restarts. The fix is to evict the region's cached client so the next +# attempt builds a new one. + +_STALE_LIB_MODULE_PREFIXES = ( + "urllib3.", + "botocore.", + "boto3.", +) + + +def _traceback_frames_modules(exc: BaseException): + """Yield ``__name__``-style module strings for each frame in exc's traceback.""" + tb = getattr(exc, "__traceback__", None) + while tb is not None: + frame = tb.tb_frame + module = frame.f_globals.get("__name__", "") + yield module or "" + tb = tb.tb_next + + +def is_stale_connection_error(exc: BaseException) -> bool: + """Return True if ``exc`` indicates a dead/stale Bedrock HTTP connection. + + Matches: + * ``botocore.exceptions.ConnectionError`` and subclasses + (``ConnectionClosedError``, ``EndpointConnectionError``, + ``ReadTimeoutError``, ``ConnectTimeoutError``). + * ``urllib3.exceptions.ProtocolError`` / ``NewConnectionError`` / + ``ConnectionError`` (best-effort import — urllib3 is a transitive + dependency of botocore so it is always available in practice). + * Bare ``AssertionError`` raised from a frame inside urllib3, botocore, + or boto3. These are internal-invariant failures (typically triggered + by corrupted connection-pool state after a dropped socket) and are + recoverable by swapping the client. + + Non-library ``AssertionError``s (from application code or tests) are + intentionally not matched — only library-internal asserts signal stale + connection state. + """ + # botocore: the canonical signal — HTTPClientError is the umbrella for + # ConnectionClosedError, ReadTimeoutError, EndpointConnectionError, + # ConnectTimeoutError, and ProxyConnectionError. ConnectionError covers + # the same family via a different branch of the hierarchy. + try: + from botocore.exceptions import ( + ConnectionError as BotoConnectionError, + HTTPClientError, + ) + botocore_errors: tuple = (BotoConnectionError, HTTPClientError) + except ImportError: # pragma: no cover — botocore always present with boto3 + botocore_errors = () + if botocore_errors and isinstance(exc, botocore_errors): + return True + + # urllib3: low-level transport failures + try: + from urllib3.exceptions import ( + ProtocolError, + NewConnectionError, + ConnectionError as Urllib3ConnectionError, + ) + urllib3_errors = (ProtocolError, NewConnectionError, Urllib3ConnectionError) + except ImportError: # pragma: no cover + urllib3_errors = () + if urllib3_errors and isinstance(exc, urllib3_errors): + return True + + # Library-internal AssertionError (urllib3 / botocore / boto3) + if isinstance(exc, AssertionError): + for module in _traceback_frames_modules(exc): + if any(module.startswith(prefix) for prefix in _STALE_LIB_MODULE_PREFIXES): + return True + + return False + + # --------------------------------------------------------------------------- # AWS credential detection # --------------------------------------------------------------------------- @@ -787,7 +895,17 @@ def call_converse( guardrail_config=guardrail_config, ) - response = client.converse(**kwargs) + try: + response = client.converse(**kwargs) + except Exception as exc: + if is_stale_connection_error(exc): + logger.warning( + "bedrock: stale-connection error on converse(region=%s, model=%s): " + "%s — evicting cached client so the next call reconnects.", + region, model, type(exc).__name__, + ) + invalidate_runtime_client(region) + raise return normalize_converse_response(response) @@ -819,7 +937,17 @@ def call_converse_stream( guardrail_config=guardrail_config, ) - response = client.converse_stream(**kwargs) + try: + response = client.converse_stream(**kwargs) + except Exception as exc: + if is_stale_connection_error(exc): + logger.warning( + "bedrock: stale-connection error on converse_stream(region=%s, " + "model=%s): %s — evicting cached client so the next call reconnects.", + region, model, type(exc).__name__, + ) + invalidate_runtime_client(region) + raise return normalize_converse_stream_events(response) -
agent/model_metadata.py@@ -1199,6 +1199,7 @@ def get_model_context_length( Resolution order: 0. Explicit config override (model.context_length or custom_providers per-model) 1. Persistent cache (previously discovered via probing) + 1b. AWS Bedrock static table (must precede custom-endpoint probe) 2. Active endpoint metadata (/models for explicit custom endpoints) 3. Local server query (for local endpoints) 4. Anthropic /v1/models API (API-key users only, not OAuth) @@ -1237,6 +1238,26 @@ def get_model_context_length( else: return cached + # 1b. AWS Bedrock — use static context length table. + # Bedrock's ListFoundationModels API doesn't expose context window sizes, + # so we maintain a curated table in bedrock_adapter.py that reflects + # AWS-imposed limits (e.g. 200K for Claude models vs 1M on the native + # Anthropic API). This must run BEFORE the custom-endpoint probe at + # step 2 — bedrock-runtime.<region>.amazonaws.com is not in + # _URL_TO_PROVIDER, so it would otherwise be treated as a custom endpoint, + # fail the /models probe (Bedrock doesn't expose that shape), and fall + # back to the 128K default before reaching the original step 4b branch. + if provider == "bedrock" or ( + base_url + and base_url_hostname(base_url).startswith("bedrock-runtime.") + and base_url_host_matches(base_url, "amazonaws.com") + ): + try: + from agent.bedrock_adapter import get_bedrock_context_length + return get_bedrock_context_length(model) + except ImportError: + pass # boto3 not installed — fall through to generic resolution + # 2. Active endpoint metadata for truly custom/unknown endpoints. # Known providers (Copilot, OpenAI, Anthropic, etc.) skip this — their # /models endpoint may report a provider-imposed limit (e.g. Copilot @@ -1282,19 +1303,7 @@ def get_model_context_length( if ctx: return ctx - # 4b. AWS Bedrock — use static context length table. - # Bedrock's ListFoundationModels doesn't expose context window sizes, - # so we maintain a curated table in bedrock_adapter.py. - if provider == "bedrock" or ( - base_url - and base_url_hostname(base_url).startswith("bedrock-runtime.") - and base_url_host_matches(base_url, "amazonaws.com") - ): - try: - from agent.bedrock_adapter import get_bedrock_context_length - return get_bedrock_context_length(model) - except ImportError: - pass # boto3 not installed — fall through to generic resolution + # 4b. (Bedrock handled earlier at step 1b — before custom-endpoint probe.) # 5. Provider-aware lookups (before generic OpenRouter cache) # These are provider-specific and take priority over the generic OR cache, -
run_agent.py@@ -5432,6 +5432,26 @@ def _anthropic_messages_create(self, api_kwargs: dict): self._try_refresh_anthropic_client_credentials() return self._anthropic_client.messages.create(**api_kwargs) + def _rebuild_anthropic_client(self) -> None: + """Rebuild the Anthropic client after an interrupt or stale call. + + Handles both direct Anthropic and Bedrock-hosted Anthropic models + correctly — rebuilding with the Bedrock SDK when provider is bedrock, + rather than always falling back to build_anthropic_client() which + requires a direct Anthropic API key. + """ + if getattr(self, "provider", None) == "bedrock": + from agent.anthropic_adapter import build_anthropic_bedrock_client + region = getattr(self, "_bedrock_region", "us-east-1") or "us-east-1" + self._anthropic_client = build_anthropic_bedrock_client(region) + else: + from agent.anthropic_adapter import build_anthropic_client + self._anthropic_client = build_anthropic_client( + self._anthropic_api_key, + getattr(self, "_anthropic_base_url", None), + timeout=get_provider_request_timeout(self.provider, self.model), + ) + def _interruptible_api_call(self, api_kwargs: dict): """ Run the API call in a background thread so the main conversation loop @@ -5467,12 +5487,21 @@ def _call(): # bedrock responses like chat_completions responses. from agent.bedrock_adapter import ( _get_bedrock_runtime_client, + invalidate_runtime_client, + is_stale_connection_error, normalize_converse_response, ) region = api_kwargs.pop("__bedrock_region__", "us-east-1") api_kwargs.pop("__bedrock_converse__", None) client = _get_bedrock_runtime_client(region) - raw_response = client.converse(**api_kwargs) + try: + raw_response = client.converse(**api_kwargs) + except Exception as _bedrock_exc: + # Evict the cached client on stale-connection failures + # so the outer retry loop builds a fresh client/pool. + if is_stale_connection_error(_bedrock_exc): + invalidate_runtime_client(region) + raise result["response"] = normalize_converse_response(raw_response) else: request_client_holder["client"] = self._create_request_openai_client(reason="chat_completion_request") @@ -5530,14 +5559,8 @@ def _call(): ) try: if self.api_mode == "anthropic_messages": - from agent.anthropic_adapter import build_anthropic_client - self._anthropic_client.close() - self._anthropic_client = build_anthropic_client( - self._anthropic_api_key, - getattr(self, "_anthropic_base_url", None), - timeout=get_provider_request_timeout(self.provider, self.model), - ) + self._rebuild_anthropic_client() else: rc = request_client_holder.get("client") if rc is not None: @@ -5562,14 +5585,8 @@ def _call(): # seed future retries. try: if self.api_mode == "anthropic_messages": - from agent.anthropic_adapter import build_anthropic_client - self._anthropic_client.close() - self._anthropic_client = build_anthropic_client( - self._anthropic_api_key, - getattr(self, "_anthropic_base_url", None), - timeout=get_provider_request_timeout(self.provider, self.model), - ) + self._rebuild_anthropic_client() else: request_client = request_client_holder.get("client") if request_client is not None: @@ -5725,12 +5742,21 @@ def _bedrock_call(): try: from agent.bedrock_adapter import ( _get_bedrock_runtime_client, + invalidate_runtime_client, + is_stale_connection_error, stream_converse_with_callbacks, ) region = api_kwargs.pop("__bedrock_region__", "us-east-1") api_kwargs.pop("__bedrock_converse__", None) client = _get_bedrock_runtime_client(region) - raw_response = client.converse_stream(**api_kwargs) + try: + raw_response = client.converse_stream(**api_kwargs) + except Exception as _bedrock_exc: + # Evict the cached client on stale-connection failures + # so the outer retry loop builds a fresh client/pool. + if is_stale_connection_error(_bedrock_exc): + invalidate_runtime_client(region) + raise def _on_text(text): _fire_first() @@ -6410,14 +6436,8 @@ def _call(): if self._interrupt_requested: try: if self.api_mode == "anthropic_messages": - from agent.anthropic_adapter import build_anthropic_client - self._anthropic_client.close() - self._anthropic_client = build_anthropic_client( - self._anthropic_api_key, - getattr(self, "_anthropic_base_url", None), - timeout=get_provider_request_timeout(self.provider, self.model), - ) + self._rebuild_anthropic_client() else: request_client = request_client_holder.get("client") if request_client is not None: -
scripts/release.py@@ -201,6 +201,9 @@ "[redacted:email]": "5park1e", "[email protected]": "5park1e", "[redacted:email]": "innocarpe", + "[redacted:email]": "qike-ms", + "[redacted:email]": "AndreKurait", + "[email protected]": "bsgdigital", "[redacted:email]": "nummanali", "[redacted:email]": "whitehatjr1001", "[email protected]": "0xNyk", -
tests/agent/test_bedrock_adapter.py@@ -1230,3 +1230,210 @@ def test_real_text_preserved(self): from agent.bedrock_adapter import _convert_content_to_converse blocks = _convert_content_to_converse("Hello") assert blocks[0]["text"] == "Hello" + + +# --------------------------------------------------------------------------- +# Stale-connection detection and per-region client invalidation +# --------------------------------------------------------------------------- + +class TestInvalidateRuntimeClient: + """Per-region eviction used to discard dead/stale bedrock-runtime clients.""" + + def test_evicts_only_the_target_region(self): + from agent.bedrock_adapter import ( + _bedrock_runtime_client_cache, + invalidate_runtime_client, + reset_client_cache, + ) + reset_client_cache() + _bedrock_runtime_client_cache["us-east-1"] = "dead-client" + _bedrock_runtime_client_cache["us-west-2"] = "live-client" + + evicted = invalidate_runtime_client("us-east-1") + + assert evicted is True + assert "us-east-1" not in _bedrock_runtime_client_cache + assert _bedrock_runtime_client_cache["us-west-2"] == "live-client" + + def test_returns_false_when_region_not_cached(self): + from agent.bedrock_adapter import invalidate_runtime_client, reset_client_cache + reset_client_cache() + assert invalidate_runtime_client("eu-west-1") is False + + +class TestIsStaleConnectionError: + """Classifier that decides whether an exception warrants client eviction.""" + + def test_detects_botocore_connection_closed_error(self): + from agent.bedrock_adapter import is_stale_connection_error + from botocore.exceptions import ConnectionClosedError + exc = ConnectionClosedError(endpoint_url="https://bedrock.example") + assert is_stale_connection_error(exc) is True + + def test_detects_botocore_endpoint_connection_error(self): + from agent.bedrock_adapter import is_stale_connection_error + from botocore.exceptions import EndpointConnectionError + exc = EndpointConnectionError(endpoint_url="https://bedrock.example") + assert is_stale_connection_error(exc) is True + + def test_detects_botocore_read_timeout(self): + from agent.bedrock_adapter import is_stale_connection_error + from botocore.exceptions import ReadTimeoutError + exc = ReadTimeoutError(endpoint_url="https://bedrock.example") + assert is_stale_connection_error(exc) is True + + def test_detects_urllib3_protocol_error(self): + from agent.bedrock_adapter import is_stale_connection_error + from urllib3.exceptions import ProtocolError + exc = ProtocolError("Connection broken") + assert is_stale_connection_error(exc) is True + + def test_detects_library_internal_assertion_error(self): + """A bare AssertionError raised from inside urllib3/botocore signals + a corrupted connection-pool invariant and should trigger eviction.""" + from agent.bedrock_adapter import is_stale_connection_error + + # Fabricate an AssertionError whose traceback's last frame belongs + # to a module named "urllib3.connectionpool". We do this by exec'ing + # a tiny `assert False` under a fake globals dict — the resulting + # frame's ``f_globals["__name__"]`` is what the classifier inspects. + fake_globals = {"__name__": "urllib3.connectionpool"} + try: + exec("def _boom():\n assert False\n_boom()", fake_globals) + except AssertionError as exc: + assert is_stale_connection_error(exc) is True + else: + pytest.fail("AssertionError not raised") + + def test_detects_botocore_internal_assertion_error(self): + """Same as above but for a frame inside the botocore namespace.""" + from agent.bedrock_adapter import is_stale_connection_error + fake_globals = {"__name__": "botocore.httpsession"} + try: + exec("def _boom():\n assert False\n_boom()", fake_globals) + except AssertionError as exc: + assert is_stale_connection_error(exc) is True + else: + pytest.fail("AssertionError not raised") + + def test_ignores_application_assertion_error(self): + """AssertionError from application code (not urllib3/botocore) should + NOT be classified as stale — those are real test/code bugs.""" + from agent.bedrock_adapter import is_stale_connection_error + try: + assert False, "test-only" # noqa: B011 + except AssertionError as exc: + assert is_stale_connection_error(exc) is False + + def test_ignores_unrelated_exceptions(self): + from agent.bedrock_adapter import is_stale_connection_error + assert is_stale_connection_error(ValueError("bad input")) is False + assert is_stale_connection_error(KeyError("missing")) is False + + +class TestCallConverseInvalidatesOnStaleError: + """call_converse / call_converse_stream evict the cached client when the + boto3 call raises a stale-connection error — so the next invocation + reconnects instead of reusing the dead socket.""" + + def test_converse_evicts_client_on_stale_error(self): + from agent.bedrock_adapter import ( + _bedrock_runtime_client_cache, + call_converse, + reset_client_cache, + ) + from botocore.exceptions import ConnectionClosedError + + reset_client_cache() + dead_client = MagicMock() + dead_client.converse.side_effect = ConnectionClosedError( + endpoint_url="https://bedrock.example", + ) + _bedrock_runtime_client_cache["us-east-1"] = dead_client + + with pytest.raises(ConnectionClosedError): + call_converse( + region="us-east-1", + model="anthropic.claude-3-sonnet-20240229-v1:0", + messages=[{"role": "user", "content": "hi"}], + ) + + assert "us-east-1" not in _bedrock_runtime_client_cache, ( + "stale client should have been evicted so the retry reconnects" + ) + + def test_converse_stream_evicts_client_on_stale_error(self): + from agent.bedrock_adapter import ( + _bedrock_runtime_client_cache, + call_converse_stream, + reset_client_cache, + ) + from botocore.exceptions import ConnectionClosedError + + reset_client_cache() + dead_client = MagicMock() + dead_client.converse_stream.side_effect = ConnectionClosedError( + endpoint_url="https://bedrock.example", + ) + _bedrock_runtime_client_cache["us-east-1"] = dead_client + + with pytest.raises(ConnectionClosedError): + call_converse_stream( + region="us-east-1", + model="anthropic.claude-3-sonnet-20240229-v1:0", + messages=[{"role": "user", "content": "hi"}], + ) + + assert "us-east-1" not in _bedrock_runtime_client_cache + + def test_converse_does_not_evict_on_non_stale_error(self): + """Non-stale errors (e.g. ValidationException) leave the client cache alone.""" + from agent.bedrock_adapter import ( + _bedrock_runtime_client_cache, + call_converse, + reset_client_cache, + ) + from botocore.exceptions import ClientError + + reset_client_cache() + live_client = MagicMock() + live_client.converse.side_effect = ClientError( + error_response={"Error": {"Code": "ValidationException", "Message": "bad"}}, + operation_name="Converse", + ) + _bedrock_runtime_client_cache["us-east-1"] = live_client + + with pytest.raises(ClientError): + call_converse( + region="us-east-1", + model="anthropic.claude-3-sonnet-20240229-v1:0", + messages=[{"role": "user", "content": "hi"}], + ) + + assert _bedrock_runtime_client_cache.get("us-east-1") is live_client, ( + "validation errors do not indicate a dead connection — keep the client" + ) + + def test_converse_leaves_successful_client_in_cache(self): + from agent.bedrock_adapter import ( + _bedrock_runtime_client_cache, + call_converse, + reset_client_cache, + ) + + reset_client_cache() + live_client = MagicMock() + live_client.converse.return_value = { + "output": {"message": {"role": "assistant", "content": [{"text": "hi"}]}}, + "stopReason": "end_turn", + "usage": {"inputTokens": 1, "outputTokens": 1, "totalTokens": 2}, + } + _bedrock_runtime_client_cache["us-east-1"] = live_client + + call_converse( + region="us-east-1", + model="anthropic.claude-3-sonnet-20240229-v1:0", + messages=[{"role": "user", "content": "hi"}], + ) + + assert _bedrock_runtime_client_cache.get("us-east-1") is live_client -
tests/agent/test_bedrock_integration.py@@ -376,17 +376,15 @@ def test_apac_anthropic_haiku_inference_profile_preserved(self): "apac.anthropic.claude-haiku-4-5", preserve_dots=True ) == "apac.anthropic.claude-haiku-4-5" - def test_preserve_false_mangles_as_documented(self): - """Canary: with ``preserve_dots=False`` the function still - produces the broken all-hyphen form — this is the shape that - Bedrock rejected and that the fix avoids. Keeping this test - locks in the existing behaviour of ``normalize_model_name`` so a - future refactor doesn't accidentally decouple the knob from its - effect.""" + def test_bedrock_prefix_preserved_without_preserve_dots(self): + """Bedrock inference profile IDs are auto-detected by prefix and + always returned unmangled -- ``preserve_dots`` is irrelevant for + these IDs because the dots are namespace separators, not version + separators. Regression for #12295.""" from agent.anthropic_adapter import normalize_model_name assert normalize_model_name( "global.anthropic.claude-opus-4-7", preserve_dots=False - ) == "global-anthropic-claude-opus-4-7" + ) == "global.anthropic.claude-opus-4-7" def test_bare_foundation_model_id_preserved(self): """Non-inference-profile Bedrock IDs @@ -422,12 +420,11 @@ def test_bedrock_inference_profile_survives_build_kwargs(self): f"{kwargs['model']!r}" ) - def test_bedrock_model_mangled_without_preserve_dots(self): - """Inverse canary: without the flag, ``build_anthropic_kwargs`` - still produces the broken form — so the fix in - ``_anthropic_preserve_dots`` is the load-bearing piece that - wires ``preserve_dots=True`` through to this builder for the - Bedrock case.""" + def test_bedrock_model_preserved_without_preserve_dots(self): + """Bedrock inference profile IDs survive ``build_anthropic_kwargs`` + even without ``preserve_dots=True`` -- the prefix auto-detection + in ``normalize_model_name`` is the load-bearing piece. + Regression for #12295.""" from agent.anthropic_adapter import build_anthropic_kwargs kwargs = build_anthropic_kwargs( model="global.anthropic.claude-opus-4-7", @@ -437,4 +434,157 @@ def test_bedrock_model_mangled_without_preserve_dots(self): reasoning_config=None, preserve_dots=False, ) - assert kwargs["model"] == "global-anthropic-claude-opus-4-7" + assert kwargs["model"] == "global.anthropic.claude-opus-4-7" + + +class TestBedrockModelIdDetection: + """Tests for ``_is_bedrock_model_id`` and the auto-detection that + makes ``normalize_model_name`` preserve dots for Bedrock IDs + regardless of ``preserve_dots``. Regression for #12295.""" + + def test_bare_bedrock_id_detected(self): + from agent.anthropic_adapter import _is_bedrock_model_id + assert _is_bedrock_model_id("anthropic.claude-opus-4-7") is True + + def test_regional_us_prefix_detected(self): + from agent.anthropic_adapter import _is_bedrock_model_id + assert _is_bedrock_model_id("us.anthropic.claude-sonnet-4-5-v1:0") is True + + def test_regional_global_prefix_detected(self): + from agent.anthropic_adapter import _is_bedrock_model_id + assert _is_bedrock_model_id("global.anthropic.claude-opus-4-7") is True + + def test_regional_eu_prefix_detected(self): + from agent.anthropic_adapter import _is_bedrock_model_id + assert _is_bedrock_model_id("eu.anthropic.claude-sonnet-4-6") is True + + def test_openrouter_format_not_detected(self): + from agent.anthropic_adapter import _is_bedrock_model_id + assert _is_bedrock_model_id("claude-opus-4.6") is False + + def test_bare_claude_not_detected(self): + from agent.anthropic_adapter import _is_bedrock_model_id + assert _is_bedrock_model_id("claude-opus-4-7") is False + + def test_bare_bedrock_id_preserved_without_flag(self): + """The primary bug from #12295: ``anthropic.claude-opus-4-7`` + sent to bedrock-mantle via auxiliary clients that don't pass + ``preserve_dots=True``.""" + from agent.anthropic_adapter import normalize_model_name + assert normalize_model_name( + "anthropic.claude-opus-4-7", preserve_dots=False + ) == "anthropic.claude-opus-4-7" + + def test_openrouter_dots_still_converted(self): + """Non-Bedrock dotted model names must still be converted.""" + from agent.anthropic_adapter import normalize_model_name + assert normalize_model_name("claude-opus-4.6") == "claude-opus-4-6" + + def test_bare_bedrock_id_survives_build_kwargs(self): + """End-to-end: bare Bedrock ID through ``build_anthropic_kwargs`` + without ``preserve_dots=True`` -- the auxiliary client path.""" + from agent.anthropic_adapter import build_anthropic_kwargs + kwargs = build_anthropic_kwargs( + model="anthropic.claude-opus-4-7", + messages=[{"role": "user", "content": "hi"}], + tools=None, + max_tokens=1024, + reasoning_config=None, + preserve_dots=False, + ) + assert kwargs["model"] == "anthropic.claude-opus-4-7" + + +# --------------------------------------------------------------------------- +# auxiliary_client Bedrock resolution — fix for #13919 +# --------------------------------------------------------------------------- +# Before the fix, resolve_provider_client("bedrock", ...) fell through to the +# "unhandled auth_type" warning and returned (None, None), breaking all +# auxiliary tasks (compression, memory, summarization) for Bedrock users. + + +class TestAuxiliaryClientBedrockResolution: + """Verify resolve_provider_client handles Bedrock's aws_sdk auth type.""" + + def test_bedrock_returns_client_with_credentials(self, monkeypatch): + """With valid AWS credentials, Bedrock should return a usable client.""" + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "[redacted:aws-access-key]") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + monkeypatch.setenv("AWS_REGION", "us-west-2") + + mock_anthropic_bedrock = MagicMock() + with patch("agent.anthropic_adapter.build_anthropic_bedrock_client", + return_value=mock_anthropic_bedrock): + from agent.auxiliary_client import resolve_provider_client, AnthropicAuxiliaryClient + client, model = resolve_provider_client("bedrock", None) + + assert client is not None, ( + "resolve_provider_client('bedrock') returned None — " + "aws_sdk auth type is not handled" + ) + assert isinstance(client, AnthropicAuxiliaryClient) + assert model is not None + assert client.api_key == "aws-sdk" + assert "us-west-2" in client.base_url + + def test_bedrock_returns_none_without_credentials(self, monkeypatch): + """Without AWS credentials, Bedrock should return (None, None) gracefully.""" + with patch("agent.bedrock_adapter.has_aws_credentials", return_value=False): + from agent.auxiliary_client import resolve_provider_client + client, model = resolve_provider_client("bedrock", None) + + assert client is None + assert model is None + + def test_bedrock_uses_configured_region(self, monkeypatch): + """Bedrock client base_url should reflect AWS_REGION.""" + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "[redacted:aws-access-key]") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + monkeypatch.setenv("AWS_REGION", "eu-central-1") + + with patch("agent.anthropic_adapter.build_anthropic_bedrock_client", + return_value=MagicMock()): + from agent.auxiliary_client import resolve_provider_client + client, _ = resolve_provider_client("bedrock", None) + + assert client is not None + assert "eu-central-1" in client.base_url + + def test_bedrock_respects_explicit_model(self, monkeypatch): + """When caller passes an explicit model, it should be used.""" + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "[redacted:aws-access-key]") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + + with patch("agent.anthropic_adapter.build_anthropic_bedrock_client", + return_value=MagicMock()): + from agent.auxiliary_client import resolve_provider_client + _, model = resolve_provider_client( + "bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0" + ) + + assert "claude-sonnet" in model + + def test_bedrock_async_mode(self, monkeypatch): + """Async mode should return an AsyncAnthropicAuxiliaryClient.""" + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "[redacted:aws-access-key]") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + + with patch("agent.anthropic_adapter.build_anthropic_bedrock_client", + return_value=MagicMock()): + from agent.auxiliary_client import resolve_provider_client, AsyncAnthropicAuxiliaryClient + client, model = resolve_provider_client("bedrock", None, async_mode=True) + + assert client is not None + assert isinstance(client, AsyncAnthropicAuxiliaryClient) + + def test_bedrock_default_model_is_haiku(self, monkeypatch): + """Default auxiliary model for Bedrock should be Haiku (fast, cheap).""" + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "[redacted:aws-access-key]") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + + with patch("agent.anthropic_adapter.build_anthropic_bedrock_client", + return_value=MagicMock()): + from agent.auxiliary_client import resolve_provider_client + _, model = resolve_provider_client("bedrock", None) + + assert "haiku" in model.lower() -
tests/agent/test_model_metadata.py@@ -588,6 +588,57 @@ def test_config_context_length_none_is_ignored(self, mock_fetch): assert result == 200000 +# ========================================================================= +# Bedrock context resolution — must run BEFORE custom-endpoint probe +# ========================================================================= + +class TestBedrockContextResolution: + """Regression tests for Bedrock context-length resolution order. + + Bug: because ``bedrock-runtime.<region>.amazonaws.com`` is not listed in + ``_URL_TO_PROVIDER``, ``_is_known_provider_base_url`` returned False and + the custom-endpoint probe at step 2 ran first — fetching ``/models`` from + Bedrock (which it doesn't serve), returning the 128K default-fallback + before execution ever reached the Bedrock branch. + + Fix: promote the Bedrock branch ahead of the custom-endpoint probe. + """ + + @patch("agent.model_metadata.fetch_endpoint_model_metadata") + def test_bedrock_provider_returns_static_table_before_probe(self, mock_fetch): + """provider='bedrock' resolves via static table, bypasses /models probe.""" + ctx = get_model_context_length( + "anthropic.claude-opus-4-v1:0", + provider="bedrock", + base_url="https://bedrock-runtime.us-east-1.amazonaws.com", + ) + # Must return the static Bedrock table value (200K for Claude), + # NOT DEFAULT_FALLBACK_CONTEXT (128K). + assert ctx == 200000 + mock_fetch.assert_not_called() + + @patch("agent.model_metadata.fetch_endpoint_model_metadata") + def test_bedrock_url_without_provider_hint(self, mock_fetch): + """bedrock-runtime host infers Bedrock even when provider is omitted.""" + ctx = get_model_context_length( + "anthropic.claude-sonnet-4-v1:0", + base_url="https://bedrock-runtime.us-west-2.amazonaws.com", + ) + assert ctx == 200000 + mock_fetch.assert_not_called() + + @patch("agent.model_metadata.fetch_endpoint_model_metadata") + def test_non_bedrock_url_still_probes(self, mock_fetch): + """Non-Bedrock hosts still reach the custom-endpoint probe.""" + mock_fetch.return_value = {"some-model": {"context_length": 50000}} + ctx = get_model_context_length( + "some-model", + base_url="https://api.example.com/v1", + ) + assert ctx == 50000 + assert mock_fetch.called + + # ========================================================================= # _strip_provider_prefix — Ollama model:tag vs provider:model # =========================================================================
-
2 PRs closed without merging
From the close comments: done elsewhere 2
Closed issues · 1
- Closed#13919[Bug]: auxiliary_client が Bedrock (aws_sdk) 認証に未対応 — context compression / summarization が動作しない
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #15184
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 24 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.