From de980c7b97fd068b45e65efc86bd247ae52a796c Mon Sep 17 00:00:00 2001 From: chaoliang yan Date: Mon, 6 Apr 2026 18:56:08 +1000 Subject: [PATCH] fix(failover): classify finish_reason: network_error as timeout (#61281) OpenAI-compatible providers (e.g. Z.AI) surface transport errors as `finish_reason: network_error` in the stream body. The existing `\breason:` pattern does not match `finish_reason:` because `_` is a word character, so no word boundary exists before `reason`. This causes the error to fall through unclassified and skip the timeout-failover path entirely. Add an explicit `\bfinish_reason:` pattern that covers the same set of terminal reasons already handled by the `\breason:` pattern. --- .../pi-embedded-helpers.isbillingerrormessage.test.ts | 11 +++++++++++ src/agents/pi-embedded-helpers/failover-matches.ts | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 2f478af3b6d..1ced537f30d 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -779,6 +779,17 @@ describe("isFailoverErrorMessage", () => { ]); }); + it("matches Provider finish_reason: network_error as timeout (#61281)", () => { + // OpenAI-compatible providers like Z.AI emit this exact format. + // `\breason:` does not match `finish_reason:` because `_` is a word + // character and there is no word boundary before `reason`. + 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..9934075523a 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.ts @@ -114,6 +114,11 @@ 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, + // OpenAI-compatible providers (e.g. Z.AI) surface transport-level errors as + // `finish_reason: network_error` in the stream body. The `\breason:` pattern + // above does NOT match `finish_reason:` because `_` is a word character so + // there is no word boundary before `reason` in `finish_reason` (#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).