mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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:
parent
fcc05284fc
commit
f2fba4f9a1
2 changed files with 98 additions and 15 deletions
|
|
@ -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(".", "-")
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue