# NousResearch/hermes-agent · dot-to-hyphen model name normalization causes breakage

## Open now

- 4 issues, 4 PRs open on GitHub.
- #7164: normalize_model_name() breaks model names for custom base URLs
- #16417: \[Bug\]: custom anthropic_messages providers can rewrite model names by replacing '.' with '-'
- #22196: bug: Anthropic provider converts dots to hyphens for ALL models, breaks third-party Anthropic-compatible APIs (e.g. Xiaomi MiMo)
- #62231: Custom Anthropic-compatible providers need an opt-in to preserve dotted model ids
- PR #14576: fix: preserve dots in model names for custom providers with anthropic_messages
- PR #16429: fix(agent): preserve dots in model names for custom providers
- PR #22246: fix(cli): only convert dots to hyphens for Claude models on Anthropic provider
- PR #40261: fix: only apply dots→hyphens normalization for Claude models on Anthropic provider

Multiple bugs and fixes centered on a model name normalization function that converts dots to hyphens, which breaks model IDs for Bedrock inference profiles, custom providers, and third-party proxies. Members include bug reports, allowlist expansions, narrower normalization scope, and a proposed opt-in config flag.

## TL;DR

- 43 member(s): 4 open issue(s), 13 closed issue(s), 4 open PR(s), 4 merged PR(s), 18 PR(s) closed without merging.
- A fix was merged to main in PR #12793. Verify it addresses your case.
- **Recommended:** read PR #12793 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-12793,pr-15184,pr-17454`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.06`: the share of member 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.

## Open related issues

- #7164: normalize_model_name() breaks model names for custom base URLs
- #16417: \[Bug\]: custom anthropic_messages providers can rewrite model names by replacing '.' with '-'
- #22196: bug: Anthropic provider converts dots to hyphens for ALL models, breaks third-party Anthropic-compatible APIs (e.g. Xiaomi MiMo)
- #62231: Custom Anthropic-compatible providers need an opt-in to preserve dotted model ids

## Closed / duplicate issues

- #11464: \[Bug\]: custom anthropic proxy rewrites z-ai/glm-5.1 to z-ai/glm-5-1
- #11976: \[Bug\]: Bedrock inference profile model ID dots mangled to hyphens → HTTP 400 "invalid model identifier"
- #12295: Bedrock Claude inference-profile IDs get dot-collapsed to hyphens -> BadRequestError 400
- #12727: \[Bug\]: Bedrock inference profile IDs broken by normalize_model_name()
- #13059: custom provider的model id被normalize_model_name错误修改
- #13061: \[Bug\] normalize_model_name rewrites custom provider model IDs, breaking models with dots (e.g. z-ai/glm-5.1 via zenmux)
- #13816: Bedrock Mantle proxy: model name dots mangled to dashes (404)
- #13953: \[Bug\]: Allow users to opt into preserve_dots for third-party Bedrock-style proxies
- #14575: Custom provider model names with dots are incorrectly converted to hyphens
- #15619: Xiaomi/MiMo models require preserve_dots=True to keep model name format
- #16156: \[Bug\]: _anthropic_preserve_dots() whitelist breaks dot-versioned model names for unlisted providers (e.g. Xiaomi MiMo)
- #17452: custom_providers with anthropic_messages mangles dotted model names → 502 from local proxies
- #19239: Xiaomi TokenPlan Anthropic-compatible endpoint should preserve dots in MiMo model IDs

## Prior & competing PRs

- **#12793: fix(run_agent): preserve dotted Bedrock inference-profile model IDs (#11976)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `run_agent.py`, `tests/agent/test_bedrock_integration.py`
  - fix hunks:
  - `run_agent.py`

    ```diff
    @@ -6894,11 +6894,31 @@ def _anthropic_preserve_dots(self) -> bool:
             Alibaba/DashScope keeps dots (e.g. qwen3.5-plus).
             MiniMax keeps dots (e.g. MiniMax-M2.7).
             OpenCode Go/Zen keeps dots for non-Claude models (e.g. minimax-m2.5-free).
    -        ZAI/Zhipu keeps dots (e.g. glm-4.7, glm-5.1)."""
    -        if (getattr(self, "provider", "") or "").lower() in {"alibaba", "minimax", "minimax-cn", "opencode-go", "opencode-zen", "zai"}:
    +        ZAI/Zhipu keeps dots (e.g. glm-4.7, glm-5.1).
    +        AWS Bedrock uses dotted inference-profile IDs
    +        (e.g. ``global.anthropic.claude-opus-4-7``,
    +        ``us.anthropic.claude-sonnet-4-5-20250929-v1:0``) and rejects
    +        the hyphenated form with
    +        ``HTTP 400 The provided model identifier is invalid``.
    +        Regression for #11976; mirrors the opencode-go fix for #5211
    +        (commit f77be22c), which extended this same allowlist."""
    +        if (getattr(self, "provider", "") or "").lower() in {
    +            "alibaba", "minimax", "minimax-cn",
    +            "opencode-go", "opencode-zen",
    +            "zai", "bedrock",
    +        }:
                 return True
             base = (getattr(self, "base_url", "") or "").lower()
    -        return "dashscope" in base or "aliyuncs" in base or "minimax" in base or "opencode.ai/zen/" in base or "bigmodel.cn" in base
    +        return (
    +            "dashscope" in base
    +            or "aliyuncs" in base
    +            or "minimax" in base
    +            or "opencode.ai/zen/" in base
    +            or "bigmodel.cn" in base
    +            # AWS Bedrock runtime endpoints — defense-in-depth when
    +            # ``provider`` is unset but ``base_url`` still names Bedrock.
    +            or "bedrock-runtime." in base
    +        )
     
         def _is_qwen_portal(self) -> bool:
             """Return True when the base URL targets Qwen Portal."""
    ```
  - `tests/agent/test_bedrock_integration.py`

    ```diff
    @@ -267,3 +267,174 @@ def test_bedrock_in_all_extra(self):
             from pathlib import Path
             content = (Path(__file__).parent.parent.parent / "pyproject.toml").read_text()
             assert '"hermes-agent[bedrock]"' in content
    +
    +
    +# ---------------------------------------------------------------------------
    +# Model ID dot preservation — regression for #11976
    +# ---------------------------------------------------------------------------
    +# AWS Bedrock inference-profile model IDs embed structural dots:
    +#
    +#   global.anthropic.claude-opus-4-7
    +#   us.anthropic.claude-sonnet-4-5-20250929-v1:0
    +#   apac.anthropic.claude-haiku-4-5
    +#
    +# ``agent.anthropic_adapter.normalize_model_name`` converts dots to hyphens
    +# unless the caller opts in via ``preserve_dots=True``.  Before this fix,
    +# ``AIAgent._anthropic_preserve_dots`` returned False for the ``bedrock``
    +# provider, so Claude-on-Bedrock requests went out with
    +# ``global-anthropic-claude-opus-4-7`` (all dots mangled to hyphens) and
    +# Bedrock rejected them with:
    +#
    +#   HTTP 400: The provided model identifier is invalid.
    +#
    +# The fix adds ``bedrock`` to the preserve-dots provider allowlist and
    +# ``bedrock-runtime.`` to the base-URL heuristic, mirroring the shape of
    +# the opencode-go fix for #5211 (commit f77be22c), which extended this
    +# same allowlist.
    +
    +
    +class TestBedrockPreserveDotsFlag:
    +    """``AIAgent._anthropic_preserve_dots`` must return True on Bedrock so
    +    inference-profile IDs survive the normalize step intact."""
    +
    +    def test_bedrock_provider_preserves_dots(self):
    +        from types import SimpleNamespace
    +        agent = SimpleNamespace(provider="bedrock", base_url="")
    +        from run_agent import AIAgent
    +        assert AIAgent._anthropic_preserve_dots(agent) is True
    +
    +    def test_bedrock_runtime_us_east_1_url_preserves_dots(self):
    +        """Defense-in-depth: even without an explicit ``provider="bedrock"``,
    +        a ``bedrock-runtime.us-east-1.amazonaws.com`` base URL must not
    +        mangle dots."""
    +        from types import SimpleNamespace
    +        agent = SimpleNamespace(
    +            provider="custom",
    +            base_url="https://bedrock-runtime.us-east-1.amazonaws.com",
    +        )
    +        from run_agent import AIAgent
    +        assert AIAgent._anthropic_preserve_dots(agent) is True
    +
    +    def test_bedrock_runtime_ap_northeast_2_url_preserves_dots(self):
    +        """Reporter-reported region (ap-northeast-2) exercises the same
    +        base-URL heuristic."""
    +        from types import SimpleNamespace
    +        agent = SimpleNamespace(
    +            provider="custom",
    +            base_url="https://bedrock-runtime.ap-northeast-2.amazonaws.com",
    +        )
    +        from run_agent import AIAgent
    +        assert AIAgent._anthropic_preserve_dots(agent) is True
    +
    +    def test_non_bedrock_aws_url_does_not_preserve_dots(self):
    +        """Unrelated AWS endpoints (e.g. ``s3.us-east-1.amazonaws.com``)
    +        must not accidentally activate the dot-preservation heuristic —
    +        the heuristic is scoped to the ``bedrock-runtime.`` substring
    +        specifically."""
    +        from types import SimpleNamespace
    +        agent = SimpleNamespace(
    +            provider="custom",
    +            base_url="https://s3.us-east-1.amazonaws.com",
    +        )
    +        from run_agent import AIAgent
    +        assert AIAgent._anthropic_preserve_dots(agent) is False
    +
    +    def test_anthropic_native_still_does_not_preserve_dots(self):
    +        """Canary: adding Bedrock to the allowlist must not weaken the
    +        existing Anthropic native behaviour — ``claude-sonnet-4.6`` still
    +        becomes ``claude-sonnet-4-6`` for the Anthropic API."""
    +        from types import SimpleNamespace
    +        agent = SimpleNamespace(provider="anthropic", base_url="https://api.anthropic.com")
    +        from run_agent import AIAgent
    +        assert AIAgent._anthropic_preserve_dots(agent) is False
    +
    +
    +class TestBedrockModelNameNormalization:
    +    """End-to-end: ``normalize_model_name`` + the preserve-dots flag
    +    reproduce the exact production request shape for each Bedrock model
    +    family, confirming the fix resolves the reporter's HTTP 400."""
    +
    +    def test_global_anthropic_inference_profile_preserved(self):
    +        """The reporter's exact model ID."""
    +        from agent.anthropic_adapter import normalize_model_name
    +        assert normalize_model_name(
    +            "global.anthropic.claude-opus-4-7", preserve_dots=True
    +        ) == "global.anthropic.claude-opus-4-7"
    +
    +    def test_us_anthropic_dated_inference_profile_preserved(self):
    +        """Regional + dated Sonnet inference profile."""
    +        from agent.anthropic_adapter import normalize_model_name
    +        assert normalize_model_name(
    +            "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
    +            preserve_dots=True,
    +        ) == "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
    +
    +    def test_apac_anthropic_haiku_inference_profile_preserved(self):
    +        """APAC inference profile — same structural-dot shape."""
    +        from agent.anthropic_adapter import normalize_model_name
    +        assert normalize_model_name(
    +            "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."""
    +        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"
    +
    +    def test_bare_foundation_model_id_preserved(self):
    +        """Non-inference-profile Bedrock IDs
    +        (e.g. ``anthropic.claude-3-5-sonnet-20241022-v2:0``) use dots as
    +        vendor separators and must also survive intact under
    +        ``preserve_dots=True``."""
    +        from agent.anthropic_adapter import normalize_model_name
    +        assert normalize_model_name(
    +            "anthropic.claude-3-5-sonnet-20241022-v2:0",
    +            preserve_dots=True,
    +        ) == "anthropic.claude-3-5-sonnet-20241022-v2:0"
    +
    +
    +class TestBedrockBuildAnthropicKwargsEndToEnd:
    +    """Integration: calling ``build_anthropic_kwargs`` with a Bedrock-
    +    shaped model ID and ``preserve_dots=True`` produces the unmangled
    +    model string in the outgoing kwargs — the exact body sent to the
    +    ``bedrock-runtime.`` endpoint.  This is the integration-level
    +    regression for the reporter's HTTP 400."""
    +
    +    def test_bedrock_inference_profile_survives_build_kwargs(self):
    +        from agent.anthropic_adapter import build_anthropic_kwargs
    +        kwargs = build_anthropic_kwargs(
    +            model="global.anthropic.claude-opus-4-7",
    +            messages=[{"role": "user", "content": "hi"}],
    +            tools=None,
    +            max_tokens=1024,
    +            reasoning_config=None,
    +            preserve_dots=True,
    +        )
    +        assert kwargs["model"] == "global.anthropic.claude-opus-4-7", (
    +            "Bedrock inference-profile ID was mangled in build_anthropic_kwargs: "
    +            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."""
    +        from agent.anthropic_adapter import build_anthropic_kwargs
    +        kwargs = build_anthropic_kwargs(
    +            model="global.anthropic.claude-opus-4-7",
    +            messages=[{"role": "user", "content": "hi"}],
    +            tools=None,
    +            max_tokens=1024,
    +            reasoning_config=None,
    +            preserve_dots=False,
    +        )
    +        assert kwargs["model"] == "global-anthropic-claude-opus-4-7"
    ```
- **#13227: fix(anthropic): preserve Bedrock inference profile IDs in normalize_model_name()**. closed without merge · fix tier `suggested`
  - maintainer direction: category `merged_via`; implication `superseded`; refs `f2fba4f9a`, `f2fba4f9a19726f8cf3ba11d4b630bdf462e38ce`, `#12295`, `#12759`, `#13112`, `#12578`, `#12759`, `#13112`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#13818: fix(agent): preserve dots in model names for Bedrock Mantle endpoints**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#13821`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#13821: fix(bedrock): preserve dots in model names for Bedrock Mantle proxy**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `f2fba4f9a`, `f2fba4f9a19726f8cf3ba11d4b630bdf462e38ce`, `#13816`
- **#15184: fix(bedrock): consolidated — model-ID detection, context length priority, aux aws_sdk, stale-connection eviction, interrupt rebuild**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - 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`
  - fix hunks:
  - `agent/anthropic_adapter.py`

    ```diff
    @@ -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.
     
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -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)
     
     
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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)
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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")
    ```

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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()
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -201,6 +201,9 @@
         "[redacted:email]": "5park1e",
         "[redacted:email]": "5park1e",
         "[redacted:email]": "innocarpe",
    +    "[redacted:email]": "qike-ms",
    +    "[redacted:email]": "AndreKurait",
    +    "[redacted:email]": "bsgdigital",
         "[redacted:email]": "nummanali",
         "[redacted:email]": "whitehatjr1001",
         "[redacted:email]": "0xNyk",
    ```
  - `tests/agent/test_bedrock_adapter.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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",
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
     # =========================================================================
    ```
- **#17454: fix(anthropic): narrow dot→hyphen normalization to Claude models only (#17171)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/anthropic_adapter.py`
  - fix hunks:
  - `agent/anthropic_adapter.py`

    ```diff
    @@ -1076,9 +1076,12 @@ def normalize_model_name(model: str, preserve_dots: bool = False) -> str:
             # 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(".", "-")
    +        # Only convert dots to hyphens for Anthropic/Claude models.
    +        # Non-Anthropic models (gpt-5.4, gemini-2.5, etc.) use dots
    +        # as part of their canonical names.  See issue #17171.
    +        _lower = model.lower()
    +        if _lower.startswith("claude-") or _lower.startswith("anthropic/"):
    +            model = model.replace(".", "-")
         return model
     
     
    ```
- **#19660: fix(agent): preserve dots in model names for Xiaomi MiMo provider**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `run_agent.py`
  - fix hunks:
  - `run_agent.py`

    ```diff
    @@ -8203,6 +8203,7 @@ def _anthropic_preserve_dots(self) -> bool:
             """True when using an anthropic-compatible endpoint that preserves dots in model names.
             Alibaba/DashScope keeps dots (e.g. qwen3.5-plus).
             MiniMax keeps dots (e.g. MiniMax-M2.7).
    +        Xiaomi MiMo keeps dots (e.g. mimo-v2.5, mimo-v2.5-pro).
             OpenCode Go/Zen keeps dots for non-Claude models (e.g. minimax-m2.5-free).
             ZAI/Zhipu keeps dots (e.g. glm-4.7, glm-5.1).
             AWS Bedrock uses dotted inference-profile IDs
    ```

    ```diff
    @@ -8216,6 +8217,7 @@ def _anthropic_preserve_dots(self) -> bool:
                 "alibaba", "minimax", "minimax-cn",
                 "opencode-go", "opencode-zen",
                 "zai", "bedrock",
    +            "xiaomi",
             }:
                 return True
             base = (getattr(self, "base_url", "") or "").lower()
    ```

    ```diff
    @@ -8225,6 +8227,7 @@ def _anthropic_preserve_dots(self) -> bool:
                 or "minimax" in base
                 or "opencode.ai/zen/" in base
                 or "bigmodel.cn" in base
    +            or "xiaomimimo.com" in base
                 # AWS Bedrock runtime endpoints — defense-in-depth when
                 # ``provider`` is unset but ``base_url`` still names Bedrock.
                 or "bedrock-runtime." in base
    ```
- **#7157: fix: preserve dots in model names for custom base URLs**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#8794`
- **#11480: fix(agent): preserve dotted ZAI models on custom anthropic proxies**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `7141cda967c489d1fec0e17740222db0e6ac6331`, `#13061`
- **#11992: fix(run_agent): preserve dotted Bedrock inference-profile model IDs (#11976)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#12793`
- **#12297: fix(anthropic_adapter): preserve dots in Bedrock Claude inference-profile IDs**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#12336`
- **#12336: fix: preserve dots in Bedrock inference-profile IDs (#12295)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `1cf1016e7`, `f2fba4f9a`, `#11976`, `#12295`, `#12577`, `#12578`, `#13112`
- **#12759: fix(anthropic): preserve dots in Bedrock inference profile IDs**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#13068: fix(agent): preserve dots in custom provider model IDs (#13061)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7141cda967c489d1fec0e17740222db0e6ac6331`, `#17454`, `#13061`
- **#13112: fix: preserve Bedrock inference profile ID namespace separators**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `f2fba4f9a`, `#14664`, `#15184`, `#14664`, `#12727`, `#13112`, `#14664`
- **#13998: fix(run_agent): add model.preserve_dots config flag for Bedrock-style proxies**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `f2fba4f9a`, `f2fba4f9a`, `#12295`, `#13953`
- **#14576: fix: preserve dots in model names for custom providers with anthropic_messages**. open
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `7141cda967c489d1fec0e17740222db0e6ac6331`
  - files: `run_agent.py`, `tests/agent/test_minimax_provider.py`
- **#14664: fix(anthropic): auto-detect Bedrock model IDs in normalize_model_name (#12295)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `f2fba4f9a`, `f2fba4f9a19726f8cf3ba11d4b630bdf462e38ce`
- **#15637: fix(model-normalize): add xiaomi to preserve-dots allowlist (#15619)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `7141cda96`, `9be3ab1a5`, `#17454`, `#17454`, `#15619`
- **#15868: fix: add xiaomi provider to dots-preserve whitelist**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#15637`, `#15619`
- **#16157: fix(agent): preserve dots in model names for Xiaomi MiMo provider**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#19660`
- **#16429: fix(agent): preserve dots in model names for custom providers**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#13068`
  - files: `run_agent.py`, `tests/agent/test_custom_preserve_dots.py`
- **#17823: fix(custom_providers): allow opt-in preserve_model_dots for Anthropic-compatible local proxies (#17452)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7141cda967c489d1fec0e17740222db0e6ac6331`
- **#22246: fix(cli): only convert dots to hyphens for Claude models on Anthropic provider**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `4feb6f6168872a806e68e63e9e61edafa9f8ffed`
  - files: `gateway/platforms/whatsapp.py`, `hermes_cli/model_normalize.py`, `tests/hermes_cli/test_model_normalize.py`, `tests/tools/test_file_tools.py`, `tools/file_tools.py`
- **#40261: fix: only apply dots→hyphens normalization for Claude models on Anthropic provider**. open
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#22246`, `#22196`, `#37056`
  - files: `hermes_cli/model_normalize.py`
- **#105699: fix(model_normalize): only normalize dots-to-hyphens for Claude models on anthropic provider**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#22246`, `#22246`

## Fix status

- Merged to main in PR #12793 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-04`.
- Subsystem: `agent`
