diff --git a/changelog.d/fixes/13040-friendliai-credit-exhaustion-403.md b/changelog.d/fixes/13040-friendliai-credit-exhaustion-403.md new file mode 100644 index 0000000000..2fbcd9d0c7 --- /dev/null +++ b/changelog.d/fixes/13040-friendliai-credit-exhaustion-403.md @@ -0,0 +1 @@ +- **fix(friendliai):** FriendliAI's free-tier credit-exhaustion 403 (`{"detail":"You've exhausted all your credits..."}`) is now classified as `QUOTA_EXHAUSTED` instead of `AUTH_ERROR`, so omniroute treats it as depleted credits rather than a credential problem ([#13040](https://github.com/diegosouzapw/OmniRoute/pull/13040)) — thanks @turbolego diff --git a/open-sse/services/accountFallback.ts b/open-sse/services/accountFallback.ts index b09308b0b0..bedd556a52 100644 --- a/open-sse/services/accountFallback.ts +++ b/open-sse/services/accountFallback.ts @@ -260,6 +260,8 @@ export const CREDITS_EXHAUSTED_SIGNALS = [ // marked credits_exhausted and keeps being re-selected on every request. "insufficient credits", "insufficient credit", + // FriendliAI 403 when free tier credits are depleted via adaptive rate limits + "exhausted all your credits", ]; // T11: Signals that indicate OAuth token is invalid/expired (not permanent deactivation) diff --git a/tests/unit/account-fallback-service.test.ts b/tests/unit/account-fallback-service.test.ts index d2693a5acf..0a7d5856fb 100644 --- a/tests/unit/account-fallback-service.test.ts +++ b/tests/unit/account-fallback-service.test.ts @@ -1196,6 +1196,22 @@ test("isCreditsExhausted returns true for actual credits-exhausted signals", () ); }); +test("isCreditsExhausted matches FriendliAI credit-exhaustion 403 body (#13040)", () => { + // FriendliAI returns HTTP 403 with body {"detail":"You've exhausted all your + // credits..."} when free tier credits are depleted via Adaptive Rate Limits. + // Before #13040 this fell through every quota/credits check to the generic + // 403 -> AUTH_ERROR fallback; the signal below routes it to QUOTA_EXHAUSTED. + assert.equal(isCreditsExhausted("You've exhausted all your credits"), true); + assert.equal( + isCreditsExhausted('{"detail":"You\'ve exhausted all your credits"}'), + true + ); + assert.equal( + isCreditsExhausted("exhausted all your credits"), + true + ); +}); + test("CREDITS_EXHAUSTED_SIGNALS no longer contains generic gRPC resource-exhausted patterns", () => { // These patterns were removed because they falsely matched Gemini RPM 429 errors assert.equal(CREDITS_EXHAUSTED_SIGNALS.includes("resource has been exhausted"), false);