Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
171 changes: 171 additions & 0 deletions tests/agent/test_bedrock_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading