fix(anthropic): auto-detect Bedrock model IDs in normalize_model_name (#12295)

Bedrock model IDs use dots as namespace separators (anthropic.claude-opus-4-7,
us.anthropic.claude-sonnet-4-5-v1:0), not version separators.
normalize_model_name() was unconditionally converting all dots to hyphens,
producing invalid IDs that Bedrock rejects with HTTP 400/404.

This affected both the main agent loop (partially mitigated by
_anthropic_preserve_dots in run_agent.py) and all auxiliary client calls
(compression, session_search, vision, etc.) which go through
_AnthropicCompletionsAdapter and never pass preserve_dots=True.

Fix: add _is_bedrock_model_id() to detect Bedrock namespace prefixes
(anthropic., us., eu., ap., jp., global.) and skip dot-to-hyphen
conversion for these IDs regardless of the preserve_dots flag.
This commit is contained in:
Qi Ke 2026-04-23 10:26:23 -07:00 committed by Teknium
parent fcc05284fc
commit f2fba4f9a1
2 changed files with 98 additions and 15 deletions

View file

@ -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: def normalize_model_name(model: str, preserve_dots: bool = False) -> str:
"""Normalize a model name for the Anthropic API. """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, - Converts dots to hyphens in version numbers (OpenRouter uses dots,
Anthropic uses hyphens: claude-opus-4.6 claude-opus-4-6), unless Anthropic uses hyphens: claude-opus-4.6 claude-opus-4-6), unless
preserve_dots is True (e.g. for Alibaba/DashScope: qwen3.5-plus). 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() lower = model.lower()
if lower.startswith("anthropic/"): if lower.startswith("anthropic/"):
model = model[len("anthropic/"):] model = model[len("anthropic/"):]
if not preserve_dots: 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), # OpenRouter uses dots for version separators (claude-opus-4.6),
# Anthropic uses hyphens (claude-opus-4-6). Convert dots to hyphens. # Anthropic uses hyphens (claude-opus-4-6). Convert dots to hyphens.
model = model.replace(".", "-") model = model.replace(".", "-")

View file

@ -376,17 +376,15 @@ class TestBedrockModelNameNormalization:
"apac.anthropic.claude-haiku-4-5", preserve_dots=True "apac.anthropic.claude-haiku-4-5", preserve_dots=True
) == "apac.anthropic.claude-haiku-4-5" ) == "apac.anthropic.claude-haiku-4-5"
def test_preserve_false_mangles_as_documented(self): def test_bedrock_prefix_preserved_without_preserve_dots(self):
"""Canary: with ``preserve_dots=False`` the function still """Bedrock inference profile IDs are auto-detected by prefix and
produces the broken all-hyphen form this is the shape that always returned unmangled -- ``preserve_dots`` is irrelevant for
Bedrock rejected and that the fix avoids. Keeping this test these IDs because the dots are namespace separators, not version
locks in the existing behaviour of ``normalize_model_name`` so a separators. Regression for #12295."""
future refactor doesn't accidentally decouple the knob from its
effect."""
from agent.anthropic_adapter import normalize_model_name from agent.anthropic_adapter import normalize_model_name
assert normalize_model_name( assert normalize_model_name(
"global.anthropic.claude-opus-4-7", preserve_dots=False "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): def test_bare_foundation_model_id_preserved(self):
"""Non-inference-profile Bedrock IDs """Non-inference-profile Bedrock IDs
@ -422,12 +420,11 @@ class TestBedrockBuildAnthropicKwargsEndToEnd:
f"{kwargs['model']!r}" f"{kwargs['model']!r}"
) )
def test_bedrock_model_mangled_without_preserve_dots(self): def test_bedrock_model_preserved_without_preserve_dots(self):
"""Inverse canary: without the flag, ``build_anthropic_kwargs`` """Bedrock inference profile IDs survive ``build_anthropic_kwargs``
still produces the broken form so the fix in even without ``preserve_dots=True`` -- the prefix auto-detection
``_anthropic_preserve_dots`` is the load-bearing piece that in ``normalize_model_name`` is the load-bearing piece.
wires ``preserve_dots=True`` through to this builder for the Regression for #12295."""
Bedrock case."""
from agent.anthropic_adapter import build_anthropic_kwargs from agent.anthropic_adapter import build_anthropic_kwargs
kwargs = build_anthropic_kwargs( kwargs = build_anthropic_kwargs(
model="global.anthropic.claude-opus-4-7", model="global.anthropic.claude-opus-4-7",
@ -437,4 +434,62 @@ class TestBedrockBuildAnthropicKwargsEndToEnd:
reasoning_config=None, reasoning_config=None,
preserve_dots=False, 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"