From e0d181063259dfc2d96091ccd43775d773ae2365 Mon Sep 17 00:00:00 2001 From: chaoliang yan Date: Wed, 15 Apr 2026 03:42:16 +1000 Subject: [PATCH] fix(failover): classify finish_reason: network_error as timeout (#61281) (#61784) Merged via squash. Prepared head SHA: f4ab2f9e0ba8cff05744d5f075b9cc3f4a8e9985 Co-authored-by: lawrence3699 <247479654+lawrence3699@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf --- CHANGELOG.md | 1 + .../pi-embedded-helpers.isbillingerrormessage.test.ts | 8 ++++++++ src/agents/pi-embedded-helpers/failover-matches.ts | 2 ++ 3 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b22af205d70..053e3088aaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai - Agents/compaction: cap the compaction reserve-token floor to the model context window so small-context local models (e.g. Ollama with 16K tokens) no longer trigger context-overflow errors or infinite compaction loops on every prompt. (#65671) Thanks @openperf. - Agents/OpenAI Responses: classify the exact `Unknown error (no error details in response)` transport failure as failover reason `unknown` so assistant/model fallback still runs for that no-details failure path. (#65254) Thanks @OpenCodeEngineer. - Models/probe: surface invalid-model probe failures as `format` instead of `unknown` in `models list --probe`, and lock the invalid-model fallback path in with regression coverage. (#50028) Thanks @xiwuqi. +- Agents/failover: classify OpenAI-compatible `finish_reason: network_error` stream failures as timeout so model fallback retries continue instead of stopping with an unknown failover reason. (#61784) thanks @lawrence3699. ## 2026.4.14 diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 2f478af3b6d..7dfe826e158 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -779,6 +779,14 @@ describe("isFailoverErrorMessage", () => { ]); }); + it("matches Provider finish_reason: network_error as timeout (#61281)", () => { + expectTimeoutFailoverSamples([ + "Provider finish_reason: network_error", + "Provider finish_reason: abort", + "Provider finish_reason: malformed_response", + ]); + }); + it("does not classify MALFORMED_FUNCTION_CALL as timeout", () => { const sample = "Unhandled stop reason: MALFORMED_FUNCTION_CALL"; expect(isTimeoutErrorMessage(sample)).toBe(false); diff --git a/src/agents/pi-embedded-helpers/failover-matches.ts b/src/agents/pi-embedded-helpers/failover-matches.ts index f6b7a5ed239..2130ff7fa0f 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.ts @@ -114,6 +114,8 @@ const ERROR_PATTERNS = { /\bstop reason:\s*(?:abort|error|malformed_response|network_error)\b/i, /\breason:\s*(?:abort|error|malformed_response|network_error)\b/i, /\bunhandled stop reason:\s*(?:abort|error|malformed_response|network_error)\b/i, + // `\breason:` does not match provider payloads like `finish_reason: network_error` (#61281). + /\bfinish_reason:\s*(?:abort|error|malformed_response|network_error)\b/i, // AbortError messages from fetch/stream aborts (Ollama NDJSON stream // timeouts, signal aborts, etc.) — without these the flattened message // falls through to reason=unknown (#58315).