diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index 4e4379bf5da..d550683923e 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -37,6 +37,20 @@ const GROQ_SERVICE_UNAVAILABLE_MESSAGE = describe("failover-error", () => { it("infers failover reason from HTTP status", () => { expect(resolveFailoverReasonFromError({ status: 402 })).toBe("billing"); + // Anthropic Claude Max plan surfaces rate limits as HTTP 402 (#30484) + expect( + resolveFailoverReasonFromError({ + status: 402, + message: "HTTP 402: request reached organization usage limit, try again later", + }), + ).toBe("rate_limit"); + // Explicit billing messages on 402 stay classified as billing + expect( + resolveFailoverReasonFromError({ + status: 402, + message: "insufficient credits — please top up your account", + }), + ).toBe("billing"); expect(resolveFailoverReasonFromError({ statusCode: "429" })).toBe("rate_limit"); expect(resolveFailoverReasonFromError({ status: 403 })).toBe("auth"); expect(resolveFailoverReasonFromError({ status: 408 })).toBe("timeout"); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index e4944b0731c..6ac244ba3ea 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -260,6 +260,12 @@ export function classifyFailoverReasonFromHttpStatus( } if (status === 402) { + // Some providers (e.g. Anthropic Claude Max plan) surface temporary + // usage/rate-limit failures as HTTP 402. Prefer the explicit rate-limit + // signal from the payload text when available (#30484). + if (message && isRateLimitErrorMessage(message)) { + return "rate_limit"; + } return "billing"; } if (status === 429) {