diff --git a/open-sse/config/errorConfig.ts b/open-sse/config/errorConfig.ts index 84c6df8bd4..3bcad8edcc 100644 --- a/open-sse/config/errorConfig.ts +++ b/open-sse/config/errorConfig.ts @@ -105,6 +105,18 @@ export const ERROR_RULES: ErrorRule[] = [ backoff: true, reason: "rate_limit_exceeded", }, + { + id: "hour_quota_exceeded", + text: "hour quota", + backoff: true, + reason: "quota_exhausted", + }, + { + id: "quota_has_been_exceeded", + text: "quota has been exceeded", + backoff: true, + reason: "quota_exhausted", + }, { id: "quota_exceeded", text: "quota exceeded", diff --git a/open-sse/services/accountFallback.ts b/open-sse/services/accountFallback.ts index 446b9510c1..bf6e06d3cd 100644 --- a/open-sse/services/accountFallback.ts +++ b/open-sse/services/accountFallback.ts @@ -755,6 +755,8 @@ export function classifyErrorText(errorText) { lower.includes("quota depleted") || lower.includes("quota will reset") || lower.includes("your quota will reset") || + lower.includes("quota has been exceeded") || + lower.includes("hour quota") || lower.includes("billing") ) { return RateLimitReason.QUOTA_EXHAUSTED; @@ -1038,7 +1040,9 @@ export function checkFallbackError( status === HTTP_STATUS.FORBIDDEN && provider && getProviderCategory(provider) === "apikey" && - !errorStr.toLowerCase().includes("has not been used in project") + !errorStr.toLowerCase().includes("has not been used in project") && + !errorStr.toLowerCase().includes("hour quota") && + !errorStr.toLowerCase().includes("quota has been exceeded") ) { return buildRetryableFallback(RateLimitReason.AUTH_ERROR); } diff --git a/tests/unit/account-fallback-service.test.ts b/tests/unit/account-fallback-service.test.ts index 4d0d9e5a77..d3dac846d1 100644 --- a/tests/unit/account-fallback-service.test.ts +++ b/tests/unit/account-fallback-service.test.ts @@ -759,3 +759,58 @@ test("recordModelLockoutFailure uses regular backoff for non-quota reasons", () clearModelLock("modelscope", "test-conn-modelscope-2", "qwen/Qwen2.5-Coder-32B-Instruct"); } }); + +// Test for hour quota related error messages +test("checkFallbackError classifies hour quota errors correctly", () => { + // For OAuth providers (e.g., codex), hour quota errors should be QUOTA_EXHAUSTED + const result1 = checkFallbackError( + 429, + "Coding Plan hour quota has been exceeded", + 0, + null, + "codex" + ); + assert.equal(result1.shouldFallback, true); + assert.equal(result1.reason, RateLimitReason.QUOTA_EXHAUSTED); + + const result2 = checkFallbackError(429, "hour quota exceeded", 0, null, "codex"); + assert.equal(result2.shouldFallback, true); + assert.equal(result2.reason, RateLimitReason.QUOTA_EXHAUSTED); + + const result3 = checkFallbackError(429, "Your hour quota is exceeded", 0, null, "codex"); + assert.equal(result3.shouldFallback, true); + assert.equal(result3.reason, RateLimitReason.QUOTA_EXHAUSTED); + + const result4 = checkFallbackError(429, "hour quota depleted", 0, null, "codex"); + assert.equal(result4.shouldFallback, true); + assert.equal(result4.reason, RateLimitReason.QUOTA_EXHAUSTED); + + // For API-key providers with 402 status, hour quota errors should be QUOTA_EXHAUSTED + const result5 = checkFallbackError(402, "hour quota has been exceeded", 0, null, "openai"); + assert.equal(result5.shouldFallback, true); + assert.equal(result5.reason, RateLimitReason.QUOTA_EXHAUSTED); + + const result6 = checkFallbackError( + 403, + "Coding Plan hour quota has been exceeded", + 0, + null, + "openai" + ); + assert.equal(result6.shouldFallback, true); + assert.equal(result6.reason, RateLimitReason.QUOTA_EXHAUSTED); +}); + +// Test for classifyErrorText function with hour quota +test("classifyErrorText handles hour quota messages", () => { + const { classifyErrorText } = accountFallback; + + assert.equal( + classifyErrorText("Coding Plan hour quota has been exceeded"), + RateLimitReason.QUOTA_EXHAUSTED + ); + assert.equal(classifyErrorText("hour quota exceeded"), RateLimitReason.QUOTA_EXHAUSTED); + assert.equal(classifyErrorText("Your hour quota is exceeded"), RateLimitReason.QUOTA_EXHAUSTED); + assert.equal(classifyErrorText("hour quota has been exceeded"), RateLimitReason.QUOTA_EXHAUSTED); + assert.equal(classifyErrorText("quota has been exceeded"), RateLimitReason.QUOTA_EXHAUSTED); +});