diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd42d4601d..1eb5fd711a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai - Agents/fallback: recognize Poe `402 You've used up your points!` billing errors so configured model fallbacks trigger instead of surfacing the raw provider error. (#42278) Thanks @CryUshio. - Telegram/outbound HTML sends: chunk long HTML-mode messages, preserve plain-text fallback and silent-delivery params across retries, and cut over to plain text when HTML chunk planning cannot safely preserve the full message. (#42240) thanks @obviyus. - Agents/embedded overload logs: include the failing model and provider in error-path console output, with lifecycle regression coverage for the rendered and sanitized `consoleMessage`. (#41236) thanks @jiarung. +- Agents/failover: treat Gemini `MALFORMED_RESPONSE` stop reasons as retryable timeouts so preview-model enum drift falls back cleanly instead of crashing the run, without also reclassifying malformed function-call errors. (#42292) Thanks @jnMetaCode. ## 2026.3.8 diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 37d0f4d80c6..608483b99bf 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -506,7 +506,6 @@ describe("isFailoverErrorMessage", () => { const samples = [ "Unhandled stop reason: MALFORMED_RESPONSE", "Unhandled stop reason: malformed_response", - "Unhandled stop reason: MALFORMED_FUNCTION_CALL", "stop reason: MALFORMED_RESPONSE", ]; for (const sample of samples) { @@ -515,6 +514,13 @@ describe("isFailoverErrorMessage", () => { expect(isFailoverErrorMessage(sample)).toBe(true); } }); + + it("does not classify MALFORMED_FUNCTION_CALL as timeout", () => { + const sample = "Unhandled stop reason: MALFORMED_FUNCTION_CALL"; + expect(isTimeoutErrorMessage(sample)).toBe(false); + expect(classifyFailoverReason(sample)).toBe(null); + expect(isFailoverErrorMessage(sample)).toBe(false); + }); }); describe("parseImageSizeError", () => { diff --git a/src/agents/pi-embedded-helpers/failover-matches.ts b/src/agents/pi-embedded-helpers/failover-matches.ts index b61eb4c79b2..a7948703f39 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/pi-embedded-helpers/failover-matches.ts @@ -40,9 +40,9 @@ const ERROR_PATTERNS = { /\benotfound\b/i, /\beai_again\b/i, /without sending (?:any )?chunks?/i, - /\bstop reason:\s*(?:abort|error|malformed_\w+)\b/i, - /\breason:\s*(?:abort|error|malformed_\w+)\b/i, - /\bunhandled stop reason:\s*(?:abort|error|malformed_\w+)\b/i, + /\bstop reason:\s*(?:abort|error|malformed_response)\b/i, + /\breason:\s*(?:abort|error|malformed_response)\b/i, + /\bunhandled stop reason:\s*(?:abort|error|malformed_response)\b/i, ], billing: [ /["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\s+payment/i,