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
10 changes: 9 additions & 1 deletion agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,15 @@ def restore_primary_runtime(agent) -> bool:
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please apply the same virtual-MoA reconstruction to try_recover_primary_transport (agent/agent_runtime_helpers.py:1001-1026). It restores the same empty MoA snapshot and still calls _create_openai_client; the conversation loop invokes it before fallback for transient failures.


# ── Rebuild client for the primary provider ──
if agent.api_mode == "anthropic_messages":
if agent.provider == "moa":
# MoA is a virtual chat-completions provider. It never has real
# OpenAI client kwargs; restoring it after a fallback must recreate
# the facade, not call OpenAI() with an empty api_key.
from agent.moa_loop import MoAClient

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MoA initialization passes reference_callback=_moa_reference_relay (agent/agent_init.py:846-889), but this reconstruction omits it. Please preserve that callback or use a shared facade factory; otherwise moa.reference and moa.aggregating progress events stop after a fallback restore.

agent.client = MoAClient(agent.model or "default")
agent._anthropic_client = None
elif agent.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client
agent._anthropic_api_key = rt["anthropic_api_key"]
agent._anthropic_base_url = rt["anthropic_base_url"]
Expand Down
61 changes: 61 additions & 0 deletions tests/run_agent/test_moa_loop_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,67 @@ def fake_call_llm(**kwargs):
assert calls[1]["tools"] is not None


def test_moa_primary_restore_rebuilds_virtual_facade(monkeypatch, tmp_path):
"""MoA sessions must restore from fallback without constructing OpenAI().

Regression for a long-lived MoA session that failed over to a real provider:
the next turn restored provider/model to MoA but tried to rebuild the shared
client from MoA's empty client_kwargs, raising "api_key client option must be
set" and then "Failed to recreate closed OpenAI client".
"""
home = tmp_path / ".hermes"
home.mkdir()
(home / "config.yaml").write_text(
"""
moa:
default_preset: review
presets:
review:
reference_models:
- provider: openai-codex
model: gpt-5.5
aggregator:
provider: openrouter
model: anthropic/claude-opus-4.8
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(home))

agent = AIAgent(
api_key="moa-virtual-provider",
base_url="moa://local",
model="review",
provider="moa",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
enabled_toolsets=["file"],
max_iterations=1,
)
primary_client = agent.client

def fail_openai_rebuild(*_args, **_kwargs):
raise AssertionError("MoA restore must not build a real OpenAI client")

monkeypatch.setattr(agent, "_create_openai_client", fail_openai_rebuild)
setattr(agent, "_fallback_activated", True)
setattr(agent, "provider", "zai")
setattr(agent, "model", "glm-5.2")
agent.base_url = "https://api.z.ai/api/coding/paas/v4"
agent.api_key = "fallback-key"
setattr(agent, "_client_kwargs", {"api_key": "fallback-key", "base_url": agent.base_url})
agent.client = SimpleNamespace(close=lambda: None, _client=SimpleNamespace(is_closed=True))

assert agent._restore_primary_runtime() is True
assert getattr(agent, "provider") == "moa"
assert getattr(agent, "model") == "review"
assert agent.client is not primary_client
assert hasattr(agent.client.chat, "completions")
assert getattr(agent, "_fallback_activated") is False



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