Skip to content
Closed
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
2 changes: 1 addition & 1 deletion agent/moa_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions tests/run_agent/test_moa_loop_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,3 +635,45 @@ def fake_call_llm(**kwargs):

# 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"