Skip to content

fix(agent): preserve MoA facade when rebuilding primary client (stream retry, rotation, fallback+restore) - #83088

Merged
teknium1 merged 6 commits into
NousResearch:mainfrom
x1051445024:fix/moa-facade-rebuild
Aug 15, 2026
Merged

fix(agent): preserve MoA facade when rebuilding primary client (stream retry, rotation, fallback+restore)#83088
teknium1 merged 6 commits into
NousResearch:mainfrom
x1051445024:fix/moa-facade-rebuild

Conversation

@x1051445024

Copy link
Copy Markdown
Contributor

Problem

Issue #78382 covers the MoA facade being dropped when agent.client is replaced — but the fix there only covers three call sites (restore_primary_runtime, try_recover_primary_transport, switch_model). The generic rebuild path _replace_primary_openai_client (used by stream-retry pool cleanup, credential rotation, dead-connection cleanup, and fallback+restore) still goes through create_openai_client, which builds a native OpenAI client while agent.provider stays "moa".

Second symptom (this PR's report): with a MOA preset active, when the aggregator returns an empty stream (EmptyStreamError, HTTP 200 / zero valid chunks), the stream-retry machinery calls _replace_primary_openai_client(reason="stream_retry_pool_cleanup"). The rebuilt native client picks up whatever relay base_url happens to be in _client_kwargs at that point, and the retried request — still carrying the MOA preset name as the model — is sent to an unrelated relay gateway, which answers:

HTTP 503: 分组 Codex 下模型 <preset> 无可用渠道(distributor)
{'error': {'code': 'model_not_found', ... 'type': 'new_api_error'}}

No such group exists anywhere in the user's config; the request simply leaked to a foreign gateway. On top of that, the facade is gone for the rest of the turn, so subsequent primary calls keep hitting the wrong endpoint (or raise the _moa_prepared_request TypeError from #78382).

Fix

In create_openai_client, when agent.provider == "moa", return build_moa_facade(agent, model) instead of building a native OpenAI client. Since every rebuild entry point funnels through create_openai_client, this covers _replace_primary_openai_client and any future rebuild path. The three already-fixed call sites assign the facade directly and never reach create_openai_client, so they are unaffected.

build_moa_facade also re-wires the reference relay (see #53802), so display events (moa.reference / moa.aggregating) survive rebuilds too.

Verification

  • python -m py_compile passes on the modified file.
  • Simulated rebuild: create_openai_client(SimpleNamespace(provider="moa", model=preset, ...), {}, reason="stream_retry_pool_cleanup") returns a MoAClient — facade preserved.
  • Real-world (Hermes 0.19.0 runtime, MOA preset = one reference + one aggregator on custom providers): after the patch, aggregator empty-stream retries re-run the proper reference/aggregator fan-out; the foreign-gateway 503 no longer appears.

Notes

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Aug 10, 2026
x1051445024 and others added 6 commits August 14, 2026 20:53
…m retry, rotation, fallback+restore)

When agent.provider == "moa", the MoAClient facade *is* the client - there is
no real OpenAI wire endpoint behind the moa://local placeholder. Client rebuilds
(_replace_primary_openai_client: stream-retry pool cleanup, credential rotation,
dead-connection cleanup, fallback+restore) go through create_openai_client and
produce a native OpenAI client while provider stays "moa". The next primary
call then either raises a `_moa_prepared_request` TypeError (NousResearch#78382) or, when
_client_kwargs carry an unrelated relay base_url, leaks the request to a foreign
gateway (observed as HTTP 503 "group ... no available channel" from an
unrelated new-api relay right after an aggregator empty-stream retry).

Fix: in create_openai_client, when provider is "moa", return
build_moa_facade(agent, model) instead of a native client. This covers every
rebuild entry point. The three already-fixed call sites
(restore_primary_runtime, try_recover_primary_transport, switch_model) assign
the facade directly and do not go through create_openai_client, so they are
unaffected.

Closes NousResearch#78382
…e client

`_moa_prepared_request` is a private handshake between the conversation
loop and MoAChatCompletions.create. It is attached whenever
agent.provider == "moa", on the assumption that agent.client is still the
in-process MoA facade.

Credential rotation, provider fallback and dead-connection cleanup all
rebuild agent.client from _client_kwargs between attempts, and
pending_moa_prepared_request deliberately carries a prepared request
across exactly that boundary. The rebuilt client is a native OpenAI
client while provider stays "moa", so the key reaches an SDK that has
never heard of it:

    TypeError: Completions.create() got an unexpected keyword argument
    '_moa_prepared_request'

That error is non-retryable, so every remaining turn on the session
fails. Both dispatch paths are affected: the non-streaming one calls
agent.client directly, and _create_request_openai_client returns
agent.client unchanged for provider "moa".

Re-check the live client at the point the key is attached, which covers
both paths at once. When the facade is gone, send the prepared prompt
without the handshake and log the downgrade.
…client

After a client replacement (credential rotation, dead-connection cleanup,
or fallback+restore), agent.client may become a native OpenAI client
while agent.provider stays "moa".  The _moa_prepared_request key was
passed through to the native SDK, causing TypeError on every turn.

Pop the key at the dispatch point (chat_completion_helpers.py:509).
The MoAClient facade already handles a missing key by falling through
to its normal resolution path.

Closes NousResearch#78382
…not the MoA facade

The unconditional pop from the previous commit also stripped the key when
agent.client was still the real MoA facade, forcing the facade to re-prepare
from scratch — a duplicate reference fan-out per turn (caught by
test_moa_virtual_provider_aggregator_is_actor). Gate the defensive strip on
the same prepare()-capability probe used at the injection site so the
handshake survives on the facade while a swapped-in native client is still
protected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants