From 40e078a56773061839a38dd83f496ae2d83b5653 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 2 Mar 2026 14:26:05 +0800 Subject: [PATCH] fix(auth): classify permission_error as auth_permanent for profile fallback (#31324) When an OAuth auth profile returns HTTP 403 with permission_error (e.g. expired plan), the error was not matched by the authPermanent patterns. This caused the profile to receive only a short cooldown instead of being disabled, so the gateway kept retrying the same broken profile indefinitely. Add "permission_error" and "not allowed for this organization" to the authPermanent error patterns so these errors trigger the longer billing/auth_permanent disable window and proper profile rotation. Closes #31306 Made-with: Cursor Co-authored-by: Vincent Koc --- src/agents/failover-error.test.ts | 26 ++++++++++++++++++++++++ src/agents/pi-embedded-helpers/errors.ts | 2 ++ 2 files changed, 28 insertions(+) diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index 8b2cb846298..413e9da8c31 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -100,6 +100,32 @@ describe("failover-error", () => { expect(err?.provider).toBe("anthropic"); }); + it("403 permission_error returns auth_permanent", () => { + expect( + resolveFailoverReasonFromError({ + status: 403, + message: + "permission_error: OAuth authentication is currently not allowed for this organization.", + }), + ).toBe("auth_permanent"); + }); + + it("permission_error in error message string classifies as auth_permanent", () => { + const err = coerceToFailoverError( + "HTTP 403 permission_error: OAuth authentication is currently not allowed for this organization.", + { provider: "anthropic", model: "claude-opus-4-6" }, + ); + expect(err?.reason).toBe("auth_permanent"); + }); + + it("'not allowed for this organization' classifies as auth_permanent", () => { + const err = coerceToFailoverError( + "OAuth authentication is currently not allowed for this organization", + { provider: "anthropic", model: "claude-opus-4-6" }, + ); + expect(err?.reason).toBe("auth_permanent"); + }); + it("describes non-Error values consistently", () => { const described = describeFailoverError(123); expect(described.message).toBe("123"); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 3d608696705..aa64450df6b 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -660,6 +660,8 @@ const ERROR_PATTERNS = { "key has been revoked", "account has been deactivated", /could not (?:authenticate|validate).*(?:api[_ ]?key|credentials)/i, + "permission_error", + "not allowed for this organization", ], auth: [ /invalid[_ ]?api[_ ]?key/,