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
2 changes: 1 addition & 1 deletion agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:
)
agent._client_kwargs = {}
agent.api_key = api_key or "moa-virtual-provider"
agent.base_url = base_url or "moa://local"
agent.base_url = "moa://local"
if not agent.quiet_mode:
print(f"🤖 AI Agent initialized with MoA preset: {agent.model}")
elif agent.api_mode == "bedrock_converse":
Expand Down
9 changes: 8 additions & 1 deletion agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1549,7 +1549,14 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
)

# ── Build new client ──
if api_mode == "anthropic_messages":
if (new_provider or "").strip().lower() == "moa":
from agent.moa_loop import MoAClient

agent.api_key = api_key or "moa-virtual-provider"
agent.base_url = "moa://local"
agent._client_kwargs = {}
agent.client = MoAClient(agent.model or "default")
elif api_mode == "anthropic_messages":
from agent.anthropic_adapter import (
build_anthropic_client,
resolve_anthropic_token,
Expand Down
5 changes: 5 additions & 0 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ def _call():
invalidate_runtime_client(region)
raise
result["response"] = normalize_converse_response(raw_response)
elif agent.provider == "moa":
# MoA is a virtual chat-completions provider backed by the
# in-process MoAClient facade. Do not rebuild a request-local
# OpenAI client from the virtual runtime metadata.
result["response"] = agent.client.chat.completions.create(**api_kwargs)
else:
request_client = _set_request_client(
agent._create_request_openai_client(
Expand Down
7 changes: 7 additions & 0 deletions agent/moa_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ 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 /
# adapter-backed providers whose provider branch adds auth headers and
# request-shape adapters. Keep those providers identified by name.
if resolved_provider in {"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
# routes call_llm to the right adapter (incl. anthropic_messages mode);
Expand Down
2 changes: 1 addition & 1 deletion hermes_cli/runtime_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1404,7 +1404,7 @@ def resolve_runtime_provider(
return {
"provider": "moa",
"api_mode": "chat_completions",
"base_url": "http://127.0.0.1/v1",
"base_url": "moa://local",
"api_key": "moa-virtual-provider",
"source": "moa-virtual-provider",
"requested_provider": requested_provider,
Expand Down
47 changes: 45 additions & 2 deletions tests/run_agent/test_moa_loop_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def fake_call_llm(**kwargs):

agent = AIAgent(
api_key="moa-virtual-provider",
base_url="moa://local",
base_url="http://127.0.0.1/v1",
model="review",
provider="moa",
quiet_mode=True,
Expand All @@ -50,17 +50,35 @@ def fake_call_llm(**kwargs):
enabled_toolsets=["file"],
max_iterations=1,
)
monkeypatch.setattr(
agent,
"_create_request_openai_client",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
AssertionError("MoA calls must use MoAClient, not a request OpenAI client")
),
)

result = agent.run_conversation("solve this")

assert result["final_response"] == "aggregator acted"
assert agent.base_url == "moa://local"
assert [(c["task"], c["provider"], c["model"]) for c in calls] == [
("moa_reference", "openai-codex", "gpt-5.5"),
("moa_aggregator", "openrouter", "anthropic/claude-opus-4.8"),
]
assert calls[1]["tools"] is not None


def test_moa_runtime_provider_uses_virtual_endpoint():
from hermes_cli.runtime_provider import resolve_runtime_provider

runtime = resolve_runtime_provider(requested="moa", target_model="review")

assert runtime["provider"] == "moa"
assert runtime["base_url"] == "moa://local"
assert runtime["api_key"] == "moa-virtual-provider"


def test_moa_does_not_cap_output_tokens(monkeypatch, tmp_path):
"""MoA must not inject an output cap on reference or aggregator calls.

Expand Down Expand Up @@ -154,6 +172,32 @@ def fake_resolve(*, requested, target_model=None):
assert rt["api_key"] == "key-for-minimax"


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.
"""
from agent import moa_loop

def fake_resolve(*, requested, target_model=None):
return {
"provider": requested,
"api_mode": "codex_responses",
"base_url": "https://chatgpt.com/backend-api/codex",
"api_key": "codex-oauth-token",
}

monkeypatch.setattr(
"hermes_cli.runtime_provider.resolve_runtime_provider", fake_resolve
)

rt = moa_loop._slot_runtime({"provider": "openai-codex", "model": "gpt-5.5"})

assert rt == {"provider": "openai-codex", "model": "gpt-5.5"}


def test_moa_slot_runtime_falls_back_on_resolution_error(monkeypatch):
"""A slot whose provider can't be resolved still attempts the call with the
bare provider/model rather than aborting the whole MoA turn."""
Expand Down Expand Up @@ -459,4 +503,3 @@ def fake_call_llm(**kwargs):

# 2 references × 2 distinct turns = 4 reference runs.
assert len(ref_runs) == 4

Loading