Skip to content
Open
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
17 changes: 16 additions & 1 deletion agent/moa_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,22 @@ 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"}:
# ``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
Expand Down
2 changes: 2 additions & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@

# Auto-extracted from noreply emails + manual overrides
AUTHOR_MAP = {
"chufengfan@jackroooc-2.local": "jackroofan", # PR #54609 salvage (add anthropic to MoA _slot_runtime name-preserve set; OAuth sk-ant-oat* needs Bearer + anthropic-beta header)
"igor.izotov@gmail.com": "iizotov", # PR #54912 salvage (add bedrock to MoA _slot_runtime name-preserve set; SigV4-signed client, placeholder aws-sdk api_key)
"186512915+lEWFkRAD@users.noreply.github.com": "lEWFkRAD", # PR #53848 salvage (stream the MoA aggregator response to the user)
"193368749+jimmyjohansson84@users.noreply.github.com": "jimmyjohansson84", # PR #27123 salvage (Kanban unknown-skill warn-instead-of-crash; #27136)
"gxalong@gmail.com": "Jeffgithub0029", # PR #28558 salvage (chunk Telegram text *after* MarkdownV2/HTML formatting so escaping inflation can't push a send over the 4096 UTF-16 limit; #28557)
Expand Down
49 changes: 48 additions & 1 deletion tests/run_agent/test_moa_loop_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def fake_resolve(*, requested, target_model=None):
assert rt == {"provider": "openai-codex", "model": "gpt-5.5"}


@pytest.mark.parametrize("provider", ["anthropic", "minimax-oauth", "qwen-oauth"])
@pytest.mark.parametrize("provider", ["minimax-oauth", "qwen-oauth"])
def test_moa_provider_backed_slot_survives_aux_resolution(monkeypatch, provider):
"""MoA can pass resolved endpoints for provider-backed slots without
call_llm flattening them to generic custom endpoints.
Expand All @@ -211,6 +211,11 @@ def test_moa_provider_backed_slot_survives_aux_resolution(monkeypatch, provider)
via ``_resolve_task_provider_model`` (which takes everything except
``api_mode``, handled separately). The provider identity must survive that
resolution rather than being flattened to ``custom``.

NOTE: providers in the ``_slot_runtime`` name-preservation set (anthropic,
bedrock, nous, openai-codex, xai-oauth) are intentionally NOT forwarded —
they're covered by their own dedicated tests. This case covers the
forward-the-resolved-endpoint path for providers that are NOT in the set.
"""
from agent import moa_loop
from agent.auxiliary_client import _resolve_task_provider_model
Expand Down Expand Up @@ -678,3 +683,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"
Loading