fix(agent): stop the MoA prepared request reaching a swapped-in native client - #82082
Closed
Drexuxux wants to merge 1 commit into
Closed
fix(agent): stop the MoA prepared request reaching a swapped-in native client#82082Drexuxux wants to merge 1 commit into
Drexuxux wants to merge 1 commit into
Conversation
…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.
Contributor
|
Thanks! Your injection-site |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #78382. Two PRs are already open on that issue — see "Relation to the open PRs" at the bottom before spending review time here.
What
_moa_prepared_requestis a private handshake:agent/conversation_loop.pyattaches it toapi_kwargs, and onlyMoAChatCompletions.createinagent/moa_loop.pypops it back off. It is attached on one condition —agent.provider == "moa"— which assumesagent.clientis still the in-process MoA facade.That assumption does not survive a client swap.
_replace_primary_openai_clientrebuildsagent.clientfrom_client_kwargson credential rotation, provider fallback and dead-connection cleanup, and it leavesagent.providerat"moa". The rebuilt client is a native OpenAI client, so the key reaches an SDK that has never heard of it:The error is non-retryable, so it is not a lost turn — every remaining turn on that session fails the same way.
The timing is not incidental.
pending_moa_prepared_requestdeliberately carries a prepared request across attempt boundaries, and a client swap is exactly what happens at an attempt boundary. Preparation reads the facade; dispatch, one rotation later, does not.Both dispatch paths are affected, for the same reason. The non-streaming one calls
agent.client.chat.completions.create(**api_kwargs)directly, and the streaming one goes through_create_request_openai_client, which returns the primary client unchanged when the provider is"moa".This is the gap left by 3e86df2 (2026-07-27, fix(agent): redecorate prompt-cache breakpoints after provider failover). That commit already recognised, in this same function, that per-client request state has to be re-derived after a failover — it just re-derived the cache breakpoints and not the MoA handshake, which is attached a few lines later and swaps out under the same event.
The fix
Re-read the live client at the point the key is attached, rather than trusting the one that prepared the request. Only the facade exposes
prepare(), which is the same duck-type the preparation step above already uses, so no import and no new coupling:Guarding at the attachment point rather than at the dispatch point covers the streaming and non-streaming paths with one check. The messages in
api_kwargsare already the prepared ones, so the rebuilt client serves a well-formed request instead of raising.Tests and results
New
tests/agent/test_moa_prepared_request_client_swap.py— 4 passed:MoAChatCompletionsischatattribute, andNone, are notTypeErrornaming_moa_prepared_request, and the same call without the key succeedstests/agent -k moa— 68 passed, 3 skipped, no failures.Reproduced the original error before the change by dispatching with
provider="moa"and a native-signature client, and confirmed the same dispatch is clean once the key is withheld.Relation to the open PRs
I found #78409 and #80132 only after opening this. Recording the differences so a reviewer can pick one rather than diff three.
#78409 — preserve the facade across client rebuilds (
run_agent.py). This is the root-cause repair: it restores the invariant thatprovider == "moa"impliesclient is the facade, instead of teaching callers to cope when it does not hold. If it holds up, it is the better fix and this PR is unnecessary. It is also the largest behavioural change of the three, since it alters what rotation produces.#80132 — pop the key at the non-streaming dispatch (
agent/chat_completion_helpers.py). Same intent as this PR, and it would stop the reportedTypeError. Two things a reviewer should weigh:MoAChatCompletions.preparedocuments the contract it relies on — "when the loop supplies the returned private object back tocreate(), the advisor fan-out is not repeated" — andrebase_prepared_requestexists specifically to avoid "a second costly fan-out". Dropping the key on the healthy path sendscreate()down its normal resolution branch, so the advisors run a second time on every non-streaming MoA turn._create_request_openai_client, which returns the primary client unchanged for provider"moa", and still carries the key.This PR withholds the key only when the facade is actually gone, and does it once for both paths. I have no stake in which lands — if #78409 is the direction, close this.