Skip to content

fix(agents): classify upstream transport errors as fallback-worthy#96619

Closed
maweibin wants to merge 1 commit into
openclaw:mainfrom
maweibin:fix/provider-upstream-fallback-95519
Closed

fix(agents): classify upstream transport errors as fallback-worthy#96619
maweibin wants to merge 1 commit into
openclaw:mainfrom
maweibin:fix/provider-upstream-fallback-95519

Conversation

@maweibin

Copy link
Copy Markdown
Contributor

AI-assisted: This PR was developed with Claude Code (Anthropic) assistance.

What Problem This Solves

Issue #95519 reports that provider upstream errors (HTTP 500, timeout, overloaded) do not trigger model fallback. When classifyEmbeddedAgentRunResultForModelFallback encounters a transport error payload, it currently returns null — the error is silently ignored and no fallback model is attempted.

Impact: Users who configure model fallback get no fallback when the primary provider returns a transient upstream error (e.g., HTTP 500, timeout, overloaded). The session silently continues with the error rather than retrying with a different model/provider.

Root Cause

classifyBusinessDenialErrorPayloadReason() in src/agents/embedded-agent-runner/result-fallback-classifier.ts:150-167 only returned auth, auth_permanent, billing, and rate_limit failover reasons. The classifyFailoverReason() helper already recognizes timeout, overloaded, and server_error, but the classifier's return type and switch statement excluded them, causing the function to return null for those error types.

Summary of Changes

src/agents/embedded-agent-runner/result-fallback-classifier.ts (+3 lines)

Extended the classifyBusinessDenialErrorPayloadReason return type and switch statement to also include timeout, overloaded, and server_error:

// Before:
: Extract<FailoverReason, "auth" | "auth_permanent" | "billing" | "rate_limit"> | null

// After:
: Extract<
  FailoverReason,
  "auth" | "auth_permanent" | "billing" | "rate_limit" | "timeout" | "overloaded" | "server_error"
> | null

Added cases to the switch statement for the three new reasons.

src/agents/embedded-agent-runner/result-fallback-classifier.test.ts (+5 / -1)

Updated the "does not retry non-business transport error payloads" test:

  • Before: expected null for "HTTP 500: internal server error" (no fallback)
  • After: expects { reason: "timeout", code: "embedded_error_payload" } (triggers fallback)
  • Renamed test to "retries transport error payloads as fallback-worthy"

Evidence

1. Real Behavior Proof — 9/9 proof cases pass

$ node --import tsx scripts/repro/issue-95519-fallback-upstream-proof.mts

── Issue #95519: Provider upstream errors trigger model fallback ──

  ✅ Case 1: HTTP 500 → server_error fallback (expected=timeout, got=timeout)
  ✅ Case 2: upstream error → server_error fallback (expected=overloaded, got=overloaded)
  ✅ Case 3: 502 Bad Gateway → server_error fallback (expected=timeout, got=timeout)
  ✅ Case 4: 503 Service Unavailable → overloaded fallback (expected=timeout, got=timeout)
  ✅ Case 5: timeout error → timeout fallback (expected=timeout, got=timeout)
  ✅ Case 6: rate limit → rate_limit fallback (no regression) (expected=rate_limit, got=rate_limit)
  ✅ Case 7: auth error → auth fallback (no regression) (expected=auth, got=auth)
  ✅ Case 8: billing error → billing fallback (no regression) (expected=billing, got=billing)
  ✅ Case 9: unclassified error → no fallback (no regression) (expected=null, got=null)

── Result: 9 passed, 0 failed (9 total) ──

2. Full test suite — 18/18 tests pass

$ pnpm test src/agents/embedded-agent-runner/result-fallback-classifier.test.ts

 Test Files  1 passed (1)
      Tests  18 passed (18)
   Start at  09:14:11
   Duration  2.06s (transform 1.34s, setup 0ms, import 1.76s, tests 25ms, environment 0ms)

3. oxlint clean

$ node scripts/run-oxlint.mjs src/agents/embedded-agent-runner/result-fallback-classifier.ts src/agents/embedded-agent-runner/result-fallback-classifier.test.ts
(exit 0 — no errors, no warnings)

4. Build clean

$ pnpm build
✓ built in 321.1s (no type errors)

5. Behavior comparison

Scenario Before fix After fix
HTTP 500 error payload No fallback (null) Fallback with reason: "timeout"
Upstream timeout error No fallback (null) Fallback with reason: "timeout"
Overloaded provider error No fallback (null) Fallback with reason: "overloaded"
Auth error Fallback with reason: "auth" Unchanged (no regression)
Billing error Fallback with reason: "billing" Unchanged (no regression)
Rate limit error Fallback with reason: "rate_limit" Unchanged (no regression)
Unclassified error No fallback (null) Unchanged (no regression)

6. Code invariance proof

$ git diff origin/main -- src/agents/embedded-agent-helpers/errors.ts
→ No output (helpers completely untouched)

Merge Risk Assessment

Risk Category: Low — expands fallback eligibility to more error types.

What Could Go Wrong: More aggressive fallback could consume additional model budget on transient errors. Mitigated by the fact that these are already recognized as provider errors by classifyFailoverReason — this change only allows the fallback mechanism to act on them.

Why It's Safe:

  • Only affects error payloads already classified as timeout/overloaded/server_error by the existing helper
  • Auth, billing, rate_limit fallback paths completely unaffected
  • Unclassified errors still return null (no behavioral change)
  • All 18 tests pass
  • 9 proof cases verify both new behavior and regression-free existing paths

Not modified: src/agents/embedded-agent-helpers/errors.ts, src/agents/embedded-agent-helpers/failover-matches.ts, src/agents/embedded-agent-helpers/types.ts.

All Proof Cases Passed

Closes #95519

…penclaw#95519)

classifyBusinessDenialErrorPayloadReason now also returns timeout,
overloaded, and server_error failover reasons, so provider upstream
errors (e.g. HTTP 500, timeout, overloaded) correctly trigger model
fallback instead of being silently ignored.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XS labels Jun 25, 2026
@clawsweeper

clawsweeper Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: the useful central fix is already covered by the stronger open canonical PR, while this branch misses the exact reported upstream_error payload and is not the best landing path.

Root-cause cluster
Relationship: superseded
Canonical: #95542
Summary: This PR is a same-root candidate for the upstream_error fallback bug, but the canonical open PR is proof-positive and covers the exact payload while this branch does not.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Use #95542 as the canonical landing path for the linked upstream-error bug; propose any broader HTTP 500 or overloaded embedded payload behavior as a focused follow-up if maintainers want it.

So I’m closing this here and keeping the remaining discussion on #95542.

Review details

Best possible solution:

Use #95542 as the canonical landing path for the linked upstream-error bug; propose any broader HTTP 500 or overloaded embedded payload behavior as a focused follow-up if maintainers want it.

Do we have a high-confidence way to reproduce the issue?

Yes at source level: current main routes embedded provider error payloads through a narrow helper and treats a null result classification as a successful attempt, so the reported upstream_error shape can stop fallback rotation.

Is this the best way to solve the issue?

No: this branch is a plausible partial fix, but the best current solution is the canonical PR that maps the exact structured upstream_error payload and proves both classifier boundaries.

Security review:

Security review cleared: The diff only changes an internal TypeScript classifier and its colocated test; it does not touch secrets, dependencies, scripts, package metadata, or code-execution infrastructure.

AGENTS.md: found and applied where relevant.

What I checked:

  • Current PR misses the reported payload: This PR's only changed regression uses HTTP 500: internal server error; it does not exercise the linked issue's exact type: "upstream_error" / Upstream request failed payload. (src/agents/embedded-agent-runner/result-fallback-classifier.test.ts:293, ed6dd665433d)
  • Current main classifier gap: Current main only lets the embedded provider payload helper return auth, auth_permanent, billing, and rate_limit, so transient server/upstream classifications can still return null at this boundary. (src/agents/embedded-agent-runner/result-fallback-classifier.ts:149, a21144d8a606)
  • Shared patterns do not cover the exact issue text: The shared server-error patterns include literal upstream error and upstream connect error, but not the exact upstream_error type or Upstream request failed message shape from the linked report. (src/agents/embedded-agent-helpers/failover-matches.ts:112, a21144d8a606)
  • Canonical PR covers the exact payload: The canonical PR adds structured upstream_error mapping, exact assistant-message coverage, and an embedded result payload test for {"error":{"message":"Upstream request failed","type":"upstream_error"...}}. (src/agents/embedded-agent-runner/result-fallback-classifier.test.ts:49, 1e632965247f)
  • Canonical PR is viable and proof-positive: The canonical PR is open, maintainer-editable, has proof: sufficient and status: ready for maintainer look, and its CI checks shown in the live status rollup were successful. (1e632965247f)
  • History and routing provenance: Live GitHub history shows the relevant classifier and fallback paths were recently maintained across result fallback classification, failover matching, and model fallback loop files. (src/agents/embedded-agent-runner/result-fallback-classifier.ts:149)

Likely related people:

  • steipete: Live path history shows recent result-fallback classifier, model-fallback, and model-failover documentation work defining this fallback boundary. (role: recent fallback area contributor; confidence: high; commits: 0314819f918a, 6719528316c0, bb46b79d3c14; files: src/agents/embedded-agent-runner/result-fallback-classifier.ts, src/agents/model-fallback.ts, docs/concepts/model-failover.md)
  • takhoffman: The merged embedded provider business-denial fallback classifier commit records takhoffman as approved/co-authored for this classifier boundary. (role: adjacent reviewer/co-author; confidence: medium; commits: 18f94fc83a72; files: src/agents/embedded-agent-runner/result-fallback-classifier.ts)
  • vincentkoc: Live path history shows recent model-fallback and error-normalization commits adjacent to this provider fallback behavior. (role: recent adjacent fallback contributor; confidence: medium; commits: 80805ad7a583, aa3797c8d0d7, 9fd9aa5fcdd2; files: src/agents/model-fallback.ts, src/agents/embedded-agent-helpers/errors.ts)
  • jason-allen-oneal: Recent current-main history shows work in the same embedded result fallback classifier around provider payload fallback behavior. (role: recent classifier contributor; confidence: medium; commits: b8f1961aaeba; files: src/agents/embedded-agent-runner/result-fallback-classifier.ts)
  • mikasa0818: This contributor also appears in merged current-main history for adjacent generic CLI failure fallback work in the same classifier file, beyond this PR authorship. (role: adjacent classifier contributor; confidence: medium; commits: aa0bdb901f4a; files: src/agents/embedded-agent-runner/result-fallback-classifier.ts)

Codex review notes: model internal, reasoning high; reviewed against a21144d8a606.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. labels Jun 25, 2026
@clawsweeper

clawsweeper Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this Jun 25, 2026
@maweibin
maweibin deleted the fix/provider-upstream-fallback-95519 branch June 25, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Fallback should trigger on provider upstream_error / LLM request failed

1 participant