fix(failover): classify Gemini MALFORMED_RESPONSE as retryable timeout (#42292)

Merged via squash.

Prepared head SHA: 68f106ff49fc7a28a806601bc8ca1e5e77c6e8c6
Co-authored-by: jnMetaCode <12096460+jnMetaCode@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
JiangNan
2026-03-11 01:34:32 +08:00
committed by GitHub
parent bc9b35d6ce
commit e9e8b81939
3 changed files with 24 additions and 3 deletions

View File

@@ -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

View File

@@ -501,6 +501,26 @@ describe("isFailoverErrorMessage", () => {
expect(isFailoverErrorMessage(sample)).toBe(true);
}
});
it("matches Gemini MALFORMED_RESPONSE stop reason as timeout (#42149)", () => {
const samples = [
"Unhandled stop reason: MALFORMED_RESPONSE",
"Unhandled stop reason: malformed_response",
"stop reason: MALFORMED_RESPONSE",
];
for (const sample of samples) {
expect(isTimeoutErrorMessage(sample)).toBe(true);
expect(classifyFailoverReason(sample)).toBe("timeout");
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", () => {

View File

@@ -40,9 +40,9 @@ const ERROR_PATTERNS = {
/\benotfound\b/i,
/\beai_again\b/i,
/without sending (?:any )?chunks?/i,
/\bstop reason:\s*(?:abort|error)\b/i,
/\breason:\s*(?:abort|error)\b/i,
/\bunhandled stop reason:\s*(?:abort|error)\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,