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
42 changes: 15 additions & 27 deletions agent/moa_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,33 +93,21 @@ def _slot_runtime(slot: dict[str, str]) -> dict[str, Any]:
from hermes_cli.runtime_provider import resolve_runtime_provider

rt = resolve_runtime_provider(requested=provider, target_model=model)
resolved_provider = str(rt.get("provider") or provider).strip().lower()
# call_llm treats an explicit base_url as a custom endpoint. That is
# correct for ordinary OpenAI-compatible targets, but wrong for OAuth /
# provider-backed targets whose provider branch adds auth refresh,
# request metadata, or request-shape adapters. Keep those providers
# identified by name.
# ``bedrock`` belongs here too: its provider branch builds an
# AWS-SigV4-signed client (or IAM-role-signed) against the
# bedrock-runtime endpoint. resolve_runtime_provider returns that
# endpoint's base_url plus a PLACEHOLDER api_key ("aws-sdk") — there is
# no real bearer token. Forwarding base_url+api_key makes call_llm treat
# it as a plain OpenAI-compatible endpoint and POST with an unsigned
# fake bearer, which Bedrock answers with an empty/malformed
# ChatCompletion (choices=None). Keeping it identified by name routes it
# through the real signed bedrock branch.
#
# ``anthropic`` likewise: subscription OAuth setup-tokens (sk-ant-oat*)
# require Bearer auth plus the ``anthropic-beta: oauth-*`` header, which
# only the anthropic provider branch adds. Forwarding base_url+api_key
# sends the OAuth token as ``x-api-key``, which Anthropic rejects with a
# bare 429.
if resolved_provider in {"nous", "anthropic", "openai-codex", "xai-oauth", "bedrock"}:
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
# routes call_llm to the right adapter (incl. anthropic_messages mode);
# api_key is the resolved credential for that provider.
# Forward the resolved endpoint through to call_llm unconditionally.
# call_llm's _resolve_task_provider_model() is the single chokepoint that
# decides whether an explicit base_url collapses a call to the generic
# ``custom`` route or keeps the provider's real identity: it preserves
# identity for any first-class provider (via
# _preserve_provider_with_base_url, a provider-catalog capability check),
# so provider branches that add auth refresh / request metadata /
# request-shape adapters — anthropic OAuth (Bearer + anthropic-beta),
# openai-codex Responses wrapping + Cloudflare headers, xai-oauth,
# bedrock SigV4 signing, nous Portal tags — still fire. Those branches
# re-resolve their own credentials by name and ignore a forwarded
# base_url/api_key, so forwarding is safe even for a placeholder key
# (bedrock's "aws-sdk"). We used to maintain a name-preservation set here
# too; that duplicated the chokepoint and drifted out of sync, so the
# single source of truth now lives in call_llm.
if rt.get("base_url"):
out["base_url"] = rt["base_url"]
if rt.get("api_key"):
Expand Down
57 changes: 41 additions & 16 deletions tests/run_agent/test_moa_loop_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,14 @@ def fake_resolve(*, requested, target_model=None):
def test_moa_codex_slot_preserves_provider_identity(monkeypatch):
"""Codex slots must not become custom chat-completions endpoints.

_resolve_task_provider_model treats any explicit base_url as provider=custom.
For openai-codex that bypasses the Codex auxiliary branch, losing the
Cloudflare headers and Responses adapter required for chatgpt.com/backend-api/codex.
_slot_runtime forwards the resolved base_url/api_key/api_mode; the single
chokepoint that must NOT collapse openai-codex to provider=custom is
_resolve_task_provider_model (via _preserve_provider_with_base_url). If it
collapsed, the Codex auxiliary branch — Cloudflare headers + Responses
adapter for chatgpt.com/backend-api/codex — would be bypassed.
"""
from agent import moa_loop
from agent.auxiliary_client import _resolve_task_provider_model

def fake_resolve(*, requested, target_model=None):
return {
Expand All @@ -196,8 +199,20 @@ def fake_resolve(*, requested, target_model=None):
)

rt = moa_loop._slot_runtime({"provider": "openai-codex", "model": "gpt-5.5"})
# _slot_runtime forwards the resolved endpoint unconditionally now.
assert rt["provider"] == "openai-codex"
assert rt["model"] == "gpt-5.5"
assert rt["base_url"] == "https://chatgpt.com/backend-api/codex"

assert rt == {"provider": "openai-codex", "model": "gpt-5.5"}
# The chokepoint preserves openai-codex identity despite the explicit
# base_url (api_mode is forwarded to call_llm directly, not the resolver).
resolver_kwargs = {k: v for k, v in rt.items() if k != "api_mode"}
resolved_provider, _model, base_url, _api_key, _mode = _resolve_task_provider_model(
task="moa_reference",
**resolver_kwargs,
)
assert resolved_provider == "openai-codex"
assert base_url == "https://chatgpt.com/backend-api/codex"


@pytest.mark.parametrize("provider", ["minimax-oauth", "qwen-oauth"])
Expand Down Expand Up @@ -686,17 +701,17 @@ def fake_call_llm(**kwargs):


def test_slot_runtime_anthropic_oauth_routes_through_provider_branch(monkeypatch):
"""Native anthropic slots must NOT forward base_url/api_key.
"""Native anthropic slots must keep their provider identity, not collapse to custom.

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.
``anthropic-beta: oauth-*`` header, which only the anthropic provider branch
of call_llm adds. _slot_runtime forwards the resolved base_url/api_key for
every provider now; the single chokepoint that must NOT collapse anthropic
to provider=custom (which would send the token as x-api-key → bare 429) is
_resolve_task_provider_model via _preserve_provider_with_base_url.
"""
from agent import moa_loop
from agent.auxiliary_client import _resolve_task_provider_model

def fake_resolve(*, requested, target_model=None):
return {
Expand All @@ -709,15 +724,25 @@ def fake_resolve(*, requested, target_model=None):
"hermes_cli.runtime_provider.resolve_runtime_provider", fake_resolve
)

# Whitelisted: anthropic must skip base_url/api_key forwarding.
# _slot_runtime forwards the resolved endpoint for anthropic like any slot.
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
assert anthropic_rt["provider"] == "anthropic"
assert anthropic_rt["base_url"] == "https://resolved.example/v1"

# The chokepoint preserves anthropic identity despite the explicit base_url,
# so call_llm routes through the anthropic provider branch (not custom).
resolved_provider, _model, base_url, _api_key, _mode = _resolve_task_provider_model(
task="moa_reference",
provider="anthropic",
model="claude-opus-4-8",
base_url="https://resolved.example/v1",
api_key="resolved-key",
)
assert resolved_provider == "anthropic"

# Non-whitelisted: openrouter still forwards the resolved endpoint.
# A generic provider (openrouter) is likewise forwarded and preserved.
other_rt = moa_loop._slot_runtime(
{"provider": "openrouter", "model": "some-model"}
)
Expand Down
Loading