diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 2b40307217a..9100304533d 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -59,6 +59,30 @@ describe("runWithModelFallback", () => { expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); }); + it("falls back on transient HTTP 5xx errors", async () => { + const cfg = makeCfg(); + const run = vi + .fn() + .mockRejectedValueOnce( + new Error( + "521 Web server is downCloudflare", + ), + ) + .mockResolvedValueOnce("ok"); + + const result = await runWithModelFallback({ + cfg, + provider: "openai", + model: "gpt-4.1-mini", + run, + }); + + expect(result.result).toBe("ok"); + expect(run).toHaveBeenCalledTimes(2); + expect(run.mock.calls[1]?.[0]).toBe("anthropic"); + expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); + }); + it("falls back on 402 payment required", async () => { const cfg = makeCfg(); const run = vi diff --git a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts b/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts index 749a5241406..1b175e77b41 100644 --- a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts +++ b/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts @@ -24,6 +24,11 @@ describe("classifyFailoverReason", () => { expect(classifyFailoverReason("invalid request format")).toBe("format"); expect(classifyFailoverReason("credit balance too low")).toBe("billing"); expect(classifyFailoverReason("deadline exceeded")).toBe("timeout"); + expect( + classifyFailoverReason( + "521 Web server is downCloudflare", + ), + ).toBe("timeout"); expect(classifyFailoverReason("string should match pattern")).toBe("format"); expect(classifyFailoverReason("bad request")).toBeNull(); expect( diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index fe186bd596a..12461074fa6 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -697,6 +697,10 @@ export function classifyFailoverReason(raw: string): FailoverReason | null { if (isImageSizeError(raw)) { return null; } + if (isTransientHttpError(raw)) { + // Treat transient 5xx provider failures as retryable transport issues. + return "timeout"; + } if (isRateLimitErrorMessage(raw)) { return "rate_limit"; }