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).