dot-to-hyphen model name normalization causes breakage
NousResearch/hermes-agent · cluster 7164 · agent
Data as of
8 open items. Fix merged in #12793. Check it covers your case.
The short answer
- Known?
- Yes. 43 related items grouped here, 13 issues already closed.
- Fix event
- A fix was merged to main in PR #12793. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PRs #12793, #15184, #17454. That is separate from the fix event. - Still open
- 4 issues and 4 PRs.
Next step: read #12793, then check whether your report is one of the 4 still open.
Read #12793Pick up a Lab taskMultiple 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.
Open issues4
- Issue#7164normalize_model_name() breaks model names for custom base URLs
- Issue#16417[Bug]: custom anthropic_messages providers can rewrite model names by replacing '.' with '-'
- Issue#22196bug: Anthropic provider converts dots to hyphens for ALL models, breaks third-party Anthropic-compatible APIs (e.g. Xiaomi MiMo)
- Issue#62231Custom Anthropic-compatible providers need an opt-in to preserve dotted model ids
Open PRs4
- PR#14576fix: preserve dots in model names for custom providers with anthropic_messages
- PR#16429fix(agent): preserve dots in model names for custom providers
- PR#22246fix(cli): only convert dots to hyphens for Claude models on Anthropic provider
- PR#40261fix: only apply dots→hyphens normalization for Claude models on Anthropic provider
Fixes on record
-
#12793 fix(run_agent): preserve dotted Bedrock inference-profile model IDs (#11976)
files:
run_agent.py,tests/agent/test_bedrock_integration.pyShow the diff
-
run_agent.py@@ -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@@ -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"
-
-
#15184 fix(bedrock): consolidated — model-ID detection, context length priority, aux aws_sdk, stale-connection eviction, interrupt rebuild
files:
agent/anthropic_adapter.py,agent/auxiliary_client.py,agent/bedrock_adapter.py,agent/model_metadata.py,run_agent.py,scripts/release.py,tests/agent/test_bedrock_adapter.py,tests/agent/test_bedrock_integration.py,tests/agent/test_model_metadata.pyShow the diff
-
agent/anthropic_adapter.py@@ -986,6 +986,26 @@ def read_hermes_oauth_credentials() -> Optional[Dict[str, Any]]: # --------------------------------------------------------------------------- +def _is_bedrock_model_id(model: str) -> bool: + """Detect AWS Bedrock model IDs that use dots as namespace separators. + + Bedrock model IDs come in two forms: + - Bare: ``anthropic.claude-opus-4-7`` + - Regional (inference profiles): ``us.anthropic.claude-sonnet-4-5-v1:0`` + + In both cases the dots separate namespace components, not version + numbers, and must be preserved verbatim for the Bedrock API. + """ + lower = model.lower() + # Regional inference-profile prefixes + if any(lower.startswith(p) for p in ("global.", "us.", "eu.", "ap.", "jp.")): + return True + # Bare Bedrock model IDs: provider.model-family + if lower.startswith("anthropic."): + return True + return False + + def normalize_model_name(model: str, preserve_dots: bool = False) -> str: """Normalize a model name for the Anthropic API. @@ -993,11 +1013,19 @@ def normalize_model_name(model: str, preserve_dots: bool = False) -> str: - Converts dots to hyphens in version numbers (OpenRouter uses dots, Anthropic uses hyphens: claude-opus-4.6 → claude-opus-4-6), unless preserve_dots is True (e.g. for Alibaba/DashScope: qwen3.5-plus). + - Preserves Bedrock model IDs (``anthropic.claude-opus-4-7``) and + regional inference profiles (``us.anthropic.claude-*``) whose dots + are namespace separators, not version separators. """ lower = model.lower() if lower.startswith("anthropic/"): model = model[len("anthropic/"):] if not preserve_dots: + # Bedrock model IDs use dots as namespace separators + # (e.g. "anthropic.claude-opus-4-7", "us.anthropic.claude-*"). + # These must not be converted to hyphens. See issue #12295. + if _is_bedrock_model_id(model): + return model # OpenRouter uses dots for version separators (claude-opus-4.6), # Anthropic uses hyphens (claude-opus-4-6). Convert dots to hyphens. model = model.replace(".", "-") -
agent/auxiliary_client.py@@ -1993,6 +1993,39 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = ""): "directly supported", provider) return None, None + elif pconfig.auth_type == "aws_sdk": + # AWS SDK providers (Bedrock) — use the Anthropic Bedrock client via + # boto3's credential chain (IAM roles, SSO, env vars, instance metadata). + try: + from agent.bedrock_adapter import has_aws_credentials, resolve_bedrock_region + from agent.anthropic_adapter import build_anthropic_bedrock_client + except ImportError: + logger.warning("resolve_provider_client: bedrock requested but " + "boto3 or anthropic SDK not installed") + return None, None + + if not has_aws_credentials(): + logger.debug("resolve_provider_client: bedrock requested but " + "no AWS credentials found") + return None, None + + region = resolve_bedrock_region() + default_model = "anthropic.claude-haiku-4-5-20251001-v1:0" + final_model = _normalize_resolved_model(model or default_model, provider) + try: + real_client = build_anthropic_bedrock_client(region) + except ImportError as exc: + logger.warning("resolve_provider_client: cannot create Bedrock " + "client: %s", exc) + return None, None + client = AnthropicAuxiliaryClient( + real_client, final_model, api_key="[redacted:secret-assignment]", + base_url=f"https://bedrock-runtime.{region}.amazonaws.com", + ) + logger.debug("resolve_provider_client: bedrock (%s, %s)", final_model, region) + return (_to_async_client(client, final_model) if async_mode + else (client, final_model)) + elif pconfig.auth_type in ("oauth_device_code", "oauth_external"): # OAuth providers — route through their specific try functions if provider == "nous": -
agent/bedrock_adapter.py@@ -87,6 +87,114 @@ def reset_client_cache(): _bedrock_control_client_cache.clear() +def invalidate_runtime_client(region: str) -> bool: + """Evict the cached ``bedrock-runtime`` client for a single region. + + Per-region counterpart to :func:`reset_client_cache`. Used by the converse + call wrappers to discard clients whose underlying HTTP connection has + gone stale, so the next call allocates a fresh client (with a fresh + connection pool) instead of reusing a dead socket. + + Returns True if a cached entry was evicted, False if the region was not + cached. + """ + existed = region in _bedrock_runtime_client_cache + _bedrock_runtime_client_cache.pop(region, None) + return existed + + +# --------------------------------------------------------------------------- +# Stale-connection detection +# --------------------------------------------------------------------------- +# +# boto3 caches its HTTPS connection pool inside the client object. When a +# pooled connection is killed out from under us (NAT timeout, VPN flap, +# server-side TCP RST, proxy idle cull, etc.), the next use surfaces as +# one of a handful of low-level exceptions — most commonly +# ``botocore.exceptions.ConnectionClosedError`` or +# ``urllib3.exceptions.ProtocolError``. urllib3 also trips an internal +# ``assert`` in a couple of paths (connection pool state checks, chunked +# response readers) which bubbles up as a bare ``AssertionError`` with an +# empty ``str(exc)``. +# +# In all of these cases the client is the problem, not the request: retrying +# with the same cached client reproduces the failure until the process +# restarts. The fix is to evict the region's cached client so the next +# attempt builds a new one. + +_STALE_LIB_MODULE_PREFIXES = ( + "urllib3.", + "botocore.", + "boto3.", +) + + +def _traceback_frames_modules(exc: BaseException): + """Yield ``__name__``-style module strings for each frame in exc's traceback.""" + tb = getattr(exc, "__traceback__", None) + while tb is not None: + frame = tb.tb_frame + module = frame.f_globals.get("__name__", "") + yield module or "" + tb = tb.tb_next + + +def is_stale_connection_error(exc: BaseException) -> bool: + """Return True if ``exc`` indicates a dead/stale Bedrock HTTP connection. + + Matches: + * ``botocore.exceptions.ConnectionError`` and subclasses + (``ConnectionClosedError``, ``EndpointConnectionError``, + ``ReadTimeoutError``, ``ConnectTimeoutError``). + * ``urllib3.exceptions.ProtocolError`` / ``NewConnectionError`` / + ``ConnectionError`` (best-effort import — urllib3 is a transitive + dependency of botocore so it is always available in practice). + * Bare ``AssertionError`` raised from a frame inside urllib3, botocore, + or boto3. These are internal-invariant failures (typically triggered + by corrupted connection-pool state after a dropped socket) and are + recoverable by swapping the client. + + Non-library ``AssertionError``s (from application code or tests) are + intentionally not matched — only library-internal asserts signal stale + connection state. + """ + # botocore: the canonical signal — HTTPClientError is the umbrella for + # ConnectionClosedError, ReadTimeoutError, EndpointConnectionError, + # ConnectTimeoutError, and ProxyConnectionError. ConnectionError covers + # the same family via a different branch of the hierarchy. + try: + from botocore.exceptions import ( + ConnectionError as BotoConnectionError, + HTTPClientError, + ) + botocore_errors: tuple = (BotoConnectionError, HTTPClientError) + except ImportError: # pragma: no cover — botocore always present with boto3 + botocore_errors = () + if botocore_errors and isinstance(exc, botocore_errors): + return True + + # urllib3: low-level transport failures + try: + from urllib3.exceptions import ( + ProtocolError, + NewConnectionError, + ConnectionError as Urllib3ConnectionError, + ) + urllib3_errors = (ProtocolError, NewConnectionError, Urllib3ConnectionError) + except ImportError: # pragma: no cover + urllib3_errors = () + if urllib3_errors and isinstance(exc, urllib3_errors): + return True + + # Library-internal AssertionError (urllib3 / botocore / boto3) + if isinstance(exc, AssertionError): + for module in _traceback_frames_modules(exc): + if any(module.startswith(prefix) for prefix in _STALE_LIB_MODULE_PREFIXES): + return True + + return False + + # --------------------------------------------------------------------------- # AWS credential detection # --------------------------------------------------------------------------- @@ -787,7 +895,17 @@ def call_converse( guardrail_config=guardrail_config, ) - response = client.converse(**kwargs) + try: + response = client.converse(**kwargs) + except Exception as exc: + if is_stale_connection_error(exc): + logger.warning( + "bedrock: stale-connection error on converse(region=%s, model=%s): " + "%s — evicting cached client so the next call reconnects.", + region, model, type(exc).__name__, + ) + invalidate_runtime_client(region) + raise return normalize_converse_response(response) @@ -819,7 +937,17 @@ def call_converse_stream( guardrail_config=guardrail_config, ) - response = client.converse_stream(**kwargs) + try: + response = client.converse_stream(**kwargs) + except Exception as exc: + if is_stale_connection_error(exc): + logger.warning( + "bedrock: stale-connection error on converse_stream(region=%s, " + "model=%s): %s — evicting cached client so the next call reconnects.", + region, model, type(exc).__name__, + ) + invalidate_runtime_client(region) + raise return normalize_converse_stream_events(response) -
agent/model_metadata.py@@ -1199,6 +1199,7 @@ def get_model_context_length( Resolution order: 0. Explicit config override (model.context_length or custom_providers per-model) 1. Persistent cache (previously discovered via probing) + 1b. AWS Bedrock static table (must precede custom-endpoint probe) 2. Active endpoint metadata (/models for explicit custom endpoints) 3. Local server query (for local endpoints) 4. Anthropic /v1/models API (API-key users only, not OAuth) @@ -1237,6 +1238,26 @@ def get_model_context_length( else: return cached + # 1b. AWS Bedrock — use static context length table. + # Bedrock's ListFoundationModels API doesn't expose context window sizes, + # so we maintain a curated table in bedrock_adapter.py that reflects + # AWS-imposed limits (e.g. 200K for Claude models vs 1M on the native + # Anthropic API). This must run BEFORE the custom-endpoint probe at + # step 2 — bedrock-runtime.<region>.amazonaws.com is not in + # _URL_TO_PROVIDER, so it would otherwise be treated as a custom endpoint, + # fail the /models probe (Bedrock doesn't expose that shape), and fall + # back to the 128K default before reaching the original step 4b branch. + if provider == "bedrock" or ( + base_url + and base_url_hostname(base_url).startswith("bedrock-runtime.") + and base_url_host_matches(base_url, "amazonaws.com") + ): + try: + from agent.bedrock_adapter import get_bedrock_context_length + return get_bedrock_context_length(model) + except ImportError: + pass # boto3 not installed — fall through to generic resolution + # 2. Active endpoint metadata for truly custom/unknown endpoints. # Known providers (Copilot, OpenAI, Anthropic, etc.) skip this — their # /models endpoint may report a provider-imposed limit (e.g. Copilot @@ -1282,19 +1303,7 @@ def get_model_context_length( if ctx: return ctx - # 4b. AWS Bedrock — use static context length table. - # Bedrock's ListFoundationModels doesn't expose context window sizes, - # so we maintain a curated table in bedrock_adapter.py. - if provider == "bedrock" or ( - base_url - and base_url_hostname(base_url).startswith("bedrock-runtime.") - and base_url_host_matches(base_url, "amazonaws.com") - ): - try: - from agent.bedrock_adapter import get_bedrock_context_length - return get_bedrock_context_length(model) - except ImportError: - pass # boto3 not installed — fall through to generic resolution + # 4b. (Bedrock handled earlier at step 1b — before custom-endpoint probe.) # 5. Provider-aware lookups (before generic OpenRouter cache) # These are provider-specific and take priority over the generic OR cache, -
run_agent.py@@ -5432,6 +5432,26 @@ def _anthropic_messages_create(self, api_kwargs: dict): self._try_refresh_anthropic_client_credentials() return self._anthropic_client.messages.create(**api_kwargs) + def _rebuild_anthropic_client(self) -> None: + """Rebuild the Anthropic client after an interrupt or stale call. + + Handles both direct Anthropic and Bedrock-hosted Anthropic models + correctly — rebuilding with the Bedrock SDK when provider is bedrock, + rather than always falling back to build_anthropic_client() which + requires a direct Anthropic API key. + """ + if getattr(self, "provider", None) == "bedrock": + from agent.anthropic_adapter import build_anthropic_bedrock_client + region = getattr(self, "_bedrock_region", "us-east-1") or "us-east-1" + self._anthropic_client = build_anthropic_bedrock_client(region) + else: + from agent.anthropic_adapter import build_anthropic_client + self._anthropic_client = build_anthropic_client( + self._anthropic_api_key, + getattr(self, "_anthropic_base_url", None), + timeout=get_provider_request_timeout(self.provider, self.model), + ) + def _interruptible_api_call(self, api_kwargs: dict): """ Run the API call in a background thread so the main conversation loop @@ -5467,12 +5487,21 @@ def _call(): # bedrock responses like chat_completions responses. from agent.bedrock_adapter import ( _get_bedrock_runtime_client, + invalidate_runtime_client, + is_stale_connection_error, normalize_converse_response, ) region = api_kwargs.pop("__bedrock_region__", "us-east-1") api_kwargs.pop("__bedrock_converse__", None) client = _get_bedrock_runtime_client(region) - raw_response = client.converse(**api_kwargs) + try: + raw_response = client.converse(**api_kwargs) + except Exception as _bedrock_exc: + # Evict the cached client on stale-connection failures + # so the outer retry loop builds a fresh client/pool. + if is_stale_connection_error(_bedrock_exc): + invalidate_runtime_client(region) + raise result["response"] = normalize_converse_response(raw_response) else: request_client_holder["client"] = self._create_request_openai_client(reason="chat_completion_request") @@ -5530,14 +5559,8 @@ def _call(): ) try: if self.api_mode == "anthropic_messages": - from agent.anthropic_adapter import build_anthropic_client - self._anthropic_client.close() - self._anthropic_client = build_anthropic_client( - self._anthropic_api_key, - getattr(self, "_anthropic_base_url", None), - timeout=get_provider_request_timeout(self.provider, self.model), - ) + self._rebuild_anthropic_client() else: rc = request_client_holder.get("client") if rc is not None: @@ -5562,14 +5585,8 @@ def _call(): # seed future retries. try: if self.api_mode == "anthropic_messages": - from agent.anthropic_adapter import build_anthropic_client - self._anthropic_client.close() - self._anthropic_client = build_anthropic_client( - self._anthropic_api_key, - getattr(self, "_anthropic_base_url", None), - timeout=get_provider_request_timeout(self.provider, self.model), - ) + self._rebuild_anthropic_client() else: request_client = request_client_holder.get("client") if request_client is not None: @@ -5725,12 +5742,21 @@ def _bedrock_call(): try: from agent.bedrock_adapter import ( _get_bedrock_runtime_client, + invalidate_runtime_client, + is_stale_connection_error, stream_converse_with_callbacks, ) region = api_kwargs.pop("__bedrock_region__", "us-east-1") api_kwargs.pop("__bedrock_converse__", None) client = _get_bedrock_runtime_client(region) - raw_response = client.converse_stream(**api_kwargs) + try: + raw_response = client.converse_stream(**api_kwargs) + except Exception as _bedrock_exc: + # Evict the cached client on stale-connection failures + # so the outer retry loop builds a fresh client/pool. + if is_stale_connection_error(_bedrock_exc): + invalidate_runtime_client(region) + raise def _on_text(text): _fire_first() @@ -6410,14 +6436,8 @@ def _call(): if self._interrupt_requested: try: if self.api_mode == "anthropic_messages": - from agent.anthropic_adapter import build_anthropic_client - self._anthropic_client.close() - self._anthropic_client = build_anthropic_client( - self._anthropic_api_key, - getattr(self, "_anthropic_base_url", None), - timeout=get_provider_request_timeout(self.provider, self.model), - ) + self._rebuild_anthropic_client() else: request_client = request_client_holder.get("client") if request_client is not None: -
scripts/release.py@@ -201,6 +201,9 @@ "[redacted:email]": "5park1e", "[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@@ -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 # =========================================================================
-
-
#17454 fix(anthropic): narrow dot→hyphen normalization to Claude models only (#17171)
files:
agent/anthropic_adapter.pyShow the diff
-
agent/anthropic_adapter.py@@ -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
files:
run_agent.pyShow the diff
-
run_agent.py@@ -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 @@ -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() @@ -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
-
18 PRs closed without merging
From the close comments: done elsewhere 6 · merged via another change 5 · duplicate 3 · superseded by another PR 2 · stale or abandoned 1 · other 1
-
#7157 fix: preserve dots in model names for custom base URLs · merged via another change →
#8794 -
#11480 fix(agent): preserve dotted ZAI models on custom anthropic proxies · other →
7141cda,#13061 -
#11992 fix(run_agent): preserve dotted Bedrock inference-profile model IDs (#11976) · merged via another change →
#12793 -
#12297 fix(anthropic_adapter): preserve dots in Bedrock Claude inference-profile IDs · duplicate →
#12336 -
#12336 fix: preserve dots in Bedrock inference-profile IDs (#12295) · merged via another change →
1cf1016,f2fba4f,#11976,#12295,#12577,#12578,#13112
Show 13 more
- #12759 fix(anthropic): preserve dots in Bedrock inference profile IDs · stale or abandoned
-
#13068 fix(agent): preserve dots in custom provider model IDs (#13061) · done elsewhere →
7141cda,#17454,#13061 -
#13112 fix: preserve Bedrock inference profile ID namespace separators · done elsewhere →
f2fba4f,#14664,#15184,#12727,#13112 -
#13227 fix(anthropic): preserve Bedrock inference profile IDs in normalize_model_name() · merged via another change →
f2fba4f,#12295,#12759,#13112,#12578 -
#13818 fix(agent): preserve dots in model names for Bedrock Mantle endpoints · superseded by another PR →
#13821 -
#13821 fix(bedrock): preserve dots in model names for Bedrock Mantle proxy · done elsewhere →
f2fba4f,#13816 -
#13998 fix(run_agent): add model.preserve_dots config flag for Bedrock-style proxies · done elsewhere →
f2fba4f,#12295,#13953 -
#14664 fix(anthropic): auto-detect Bedrock model IDs in normalize_model_name (#12295) · done elsewhere →
f2fba4f -
#15637 fix(model-normalize): add xiaomi to preserve-dots allowlist (#15619) · superseded by another PR →
7141cda,9be3ab1,#17454,#15619 -
#15868 fix: add xiaomi provider to dots-preserve whitelist · duplicate →
#15637,#15619 -
#16157 fix(agent): preserve dots in model names for Xiaomi MiMo provider · merged via another change →
#19660 -
#17823 fix(custom_providers): allow opt-in preserve_model_dots for Anthropic-compatible local proxies (#17452) · done elsewhere →
7141cda -
#105699 fix(model_normalize): only normalize dots-to-hyphens for Claude models on anthropic provider · duplicate →
#22246
Closed issues · 13
- Closed#11464[Bug]: custom anthropic proxy rewrites z-ai/glm-5.1 to z-ai/glm-5-1
- Closed#11976[Bug]: Bedrock inference profile model ID dots mangled to hyphens → HTTP 400 "invalid model identifier"
- Closed#12295Bedrock Claude inference-profile IDs get dot-collapsed to hyphens -> BadRequestError 400
- Closed#12727[Bug]: Bedrock inference profile IDs broken by normalize_model_name()
- Closed#13059custom provider的model id被normalize_model_name错误修改
- Closed#13061[Bug] normalize_model_name rewrites custom provider model IDs, breaking models with dots (e.g. z-ai/glm-5.1 via zenmux)
- Closed#13816Bedrock Mantle proxy: model name dots mangled to dashes (404)
- Closed#13953[Bug]: Allow users to opt into preserve_dots for third-party Bedrock-style proxies
- Closed#14575Custom provider model names with dots are incorrectly converted to hyphens
- Closed#15619Xiaomi/MiMo models require preserve_dots=True to keep model name format
- Closed#16156[Bug]: _anthropic_preserve_dots() whitelist breaks dot-versioned model names for unlisted providers (e.g. Xiaomi MiMo)
- Closed#17452custom_providers with anthropic_messages mangles dotted model names → 502 from local proxies
- Closed#19239Xiaomi TokenPlan Anthropic-compatible endpoint should preserve dots in MiMo model IDs
How this cluster was grouped
-
Link evidence (
partly structural links): 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 item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab