NousResearch/hermes-agent

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

0open issues
0open PRs
4items grouped
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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #15184 fix(bedrock): consolidated — model-ID detection, context length priority, aux aws_sdk, stale-connection eviction, interrupt rebuild

    merged to main

    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.py

    Show 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

  • #11612 fix(bedrock): route auxiliary tasks through AnthropicBedrock SDK · done elsewhere → 7dc6eb9, #13919
  • #14770 fix(agent): handle aws_sdk auth type in resolve_provider_client · done elsewhere → 7dc6eb9, #11612, #13919, #12365
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.00 means 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 Lab

At 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.

For your agent

The same facts, typed.