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";
}