fix(agents): classify upstream_error as fallbackable to harden model fallback#96372
fix(agents): classify upstream_error as fallbackable to harden model fallback#96372hugenshen wants to merge 0 commit into
Conversation
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Close as superseded: the same upstream_error model-fallback bug is already fixed on current main by the merged canonical PR, while this PR is now conflicting and only leaves an unproven defense-in-depth thrown-error variant. Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Canonical path: GitHub reports #95542 as merged, with merge commit cfd8ba8 fixing the same linked bug and touching the same classifier boundaries. So I’m closing this here and keeping the remaining discussion on #95542. Review detailsBest possible solution: Close this PR as superseded and keep the merged current-main implementation as the canonical fix; handle any remaining direct thrown-error coercion case in a new narrow issue or PR with proof. Do we have a high-confidence way to reproduce the issue? Yes: the linked bug is source-reproducible through assistant and embedded result fallback classifiers, and current main now has focused tests for the reported upstream_error payload shape. I did not run a live provider outage reproduction in this read-only review. Is this the best way to solve the issue? No: this PR is no longer the best landing path because the merged canonical PR covers the same central assistant path plus embedded result-payload path on current main. The remaining thrown-error coercion tweak is a separate unproven case that should not keep this superseded branch open. Security review: Security review cleared: No concrete security or supply-chain concern was found; the diff changes internal agent fallback classification and tests only. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 785ab7477982; fix evidence: commit cfd8ba8fc81d, main fix timestamp 2026-06-29T14:12:09+03:00. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@hugenshen thanks for the PR. ClawSweeper is still waiting on real behavior proof before this can move forward. Useful proof can be a screenshot, short video, terminal output, copied live output, linked artifact, or redacted logs that show the changed behavior after the fix. Please redact private tokens, phone numbers, private endpoints, customer data, and anything else sensitive. Once proof is added to the PR body or a comment, ClawSweeper or a maintainer can re-check it. |
939e1b6 to
ebe9937
Compare
ebe9937 to
453f596
Compare
What Problem This Solves
Fixes #95519. When a primary model's provider returns an error with
type: "upstream_error"(the OpenAI-compatible "the upstream backend failed" shape, e.g.{"error":{"message":"Upstream request failed","type":"upstream_error","param":"","code":null}}), OpenClaw ends the turn withLLM request failedinstead of trying the configuredagents.defaults.model.fallbacks. The trajectory shows only the primary model, nomodel.fallback_stepevent, and no fallback attempt.Root cause is a classification gap. The provider error surfaces as an assistant message with
stopReason: "error"anderrorType: "upstream_error". The embedded runner's assistant-stage failover gate (run.tscallsisFailoverAssistantError/classifyAssistantFailoverReason) is what decides whether to throw aFailoverErrorinto the model-fallback loop.classifyAssistantFailoverReasonrunsclassifyFailoverSignal, and nothing mappedupstream_errorto a failover reason:upstream_erroris lowercasesnake_case, so it was not recognized as a transient provider type, andUpstream request failedmatches no transient classifier, andSo the gate returned
null("not a failover error"), the assistant stage took thecontinue_normalpath instead of throwing, and the run returned a terminal error payload (non_deliverable_terminal_turn) that the model-fallback loop treats as a successful result — the fallback chain never runs.Why This Change Was Made
The fix classifies
upstream_erroras a transient, fallbackable reason at the point the signal is built, so the existing assistant-stage failover machinery throws aFailoverErrorand the model-fallback loop advances to the next configured model — the same path already used for Codexserver_error(run.codex-server-error-fallback.test.ts).Three changes in
src/agents/embedded-agent-helpers/errors.tsandsrc/agents/failover-error.ts:classifyFailoverReasonFromErrorTypeinerrors.ts, run early inclassifyFailoverSignal(before message/status classification):upstream_error → timeout,server_error → server_error; unknown types returnnulland fall through. This is the change that fixes [Bug]: Fallback should trigger on provider upstream_error / LLM request failed #95519, becausebuildAssistantFailoverSignalpasses the assistant message'serrorTypestraight intoclassifyFailoverSignal, so the gate now recognizesupstream_error.isStableProviderErrorTypeinfailover-error.tsnow also acceptslowercase_snake_case(in addition toSCREAMING_SNAKE_CASE). This only affectsreadDirectErrorType, used by the thrown-error coercion path (resolveFailoverReasonFromError), so a thrownupstream_errorpayload is also extractable. The generic-type exclusion list (api_error,server_error, etc.) is unchanged.API_ERROR_TRANSIENT_SIGNALS_REmatchesupstream.error, so bothupstream errorandupstream_errorare caught in the message-text path used forapi_error-typed payloads (defense-in-depth).Non-transient types (auth, context overflow, validation) are unaffected and continue to return
null.User Impact
With
agents.defaults.model.fallbacksconfigured, a primary-modelupstream_erroris now treated as a fallbackable failure: the run throws aFailoverError, the model-fallback loop skips the failed candidate and tries the next model, and the trajectory records a fallback step. If all candidates fail, the user gets aFallbackSummaryErrorwith per-attempt detail instead of the genericLLM request failed. No behavior change when no fallback is configured, and no change for non-transient errors.Evidence
Focused regression coverage in
src/agents/failover-error.test.ts, including a test that drives the exact production gate the runtime uses (classifyAssistantFailoverReason/isFailoverAssistantError) with the #95519 assistant-message shape (stopReason: "error",errorType: "upstream_error", messageUpstream request failed, no HTTP status). This message returnsnullfrom the gate without the fix (verified: message-onlyclassifyFailoverSignalisnull) andtimeoutwith it, so it is a true regression test of the runtime path — not just an isolated signal-classifier call.Command:
Result:
New/relevant cases:
No-regression check on the adjacent failover surfaces the change touches:
The transport layer already extracts a nested provider
typeintoerrorType(proven for the identical code path by therate_limit_errorcase insrc/agents/openai-transport-stream.test.ts), which is howerrorType: "upstream_error"reaches the gate above.