diff --git a/agent/moa_loop.py b/agent/moa_loop.py index fcc76c2cf0f..01cd1ca4e24 100644 --- a/agent/moa_loop.py +++ b/agent/moa_loop.py @@ -99,7 +99,7 @@ def _slot_runtime(slot: dict[str, str]) -> dict[str, Any]: # provider-backed targets whose provider branch adds auth refresh, # request metadata, or request-shape adapters. Keep those providers # identified by name. - if resolved_provider in {"nous", "openai-codex", "xai-oauth"}: + if resolved_provider in {"nous", "anthropic", "openai-codex", "xai-oauth"}: return out # Pass the resolved endpoint through so call_llm builds the request for # the provider's actual API surface instead of auto-detecting. base_url diff --git a/tests/run_agent/test_moa_loop_mode.py b/tests/run_agent/test_moa_loop_mode.py index c2dc887e605..19057b21be1 100644 --- a/tests/run_agent/test_moa_loop_mode.py +++ b/tests/run_agent/test_moa_loop_mode.py @@ -678,3 +678,45 @@ def test_moa_facade_reruns_references_on_new_turn(monkeypatch, tmp_path): # 2 references × 2 distinct turns = 4 reference runs. assert len(ref_runs) == 4 + + +def test_slot_runtime_anthropic_oauth_routes_through_provider_branch(monkeypatch): + """Native anthropic slots must NOT forward base_url/api_key. + + anthropic OAuth setup-tokens (sk-ant-oat*) require Bearer auth + the + ``anthropic-beta: oauth-*`` header, which only the provider branch of + call_llm adds. If _slot_runtime forwarded base_url/api_key, call_llm would + treat the slot as a plain custom endpoint and send the token as x-api-key, + which Anthropic rejects with a bare 429. So a whitelisted provider + (anthropic) returns only provider/model, while a non-whitelisted provider + (openrouter) forwards the resolved base_url/api_key. + """ + from agent import moa_loop + + def fake_resolve(*, requested, target_model=None): + return { + "provider": requested, + "base_url": "https://resolved.example/v1", + "api_key": "resolved-key", + } + + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", fake_resolve + ) + + # Whitelisted: anthropic must skip base_url/api_key forwarding. + anthropic_rt = moa_loop._slot_runtime( + {"provider": "anthropic", "model": "claude-opus-4-8"} + ) + assert anthropic_rt == {"provider": "anthropic", "model": "claude-opus-4-8"} + assert "base_url" not in anthropic_rt + assert "api_key" not in anthropic_rt + + # Non-whitelisted: openrouter still forwards the resolved endpoint. + other_rt = moa_loop._slot_runtime( + {"provider": "openrouter", "model": "some-model"} + ) + assert other_rt["provider"] == "openrouter" + assert other_rt["model"] == "some-model" + assert other_rt["base_url"] == "https://resolved.example/v1" + assert other_rt["api_key"] == "resolved-key"