From f087a9050d3f9b208d670d1016701db96bdabeb5 Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 7 Aug 2026 13:24:24 +0200 Subject: [PATCH] fix(ai): keep plan per-minute limits transient --- packages/ai/src/error/rate-limit.ts | 11 +++++++++-- packages/ai/test/rate-limit-utils.test.ts | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/error/rate-limit.ts b/packages/ai/src/error/rate-limit.ts index 8cf0cdfe0..295ba1e61 100644 --- a/packages/ai/src/error/rate-limit.ts +++ b/packages/ai/src/error/rate-limit.ts @@ -24,6 +24,13 @@ const INSUFFICIENT_BALANCE_PATTERN = /insufficient.?balance/i; const SPEND_LIMIT_PATTERN = /spend.?limit/i; const SUBSCRIPTION_CAP_PATTERN = /\b(?:subscription|plan|membership)\b[^\n]{0,80}\b(?:rate.?limits?|quota|cap)\b|\b(?:rate.?limits?|quota|cap)\b[^\n]{0,80}\b(?:subscription|plan|membership)\b/i; +const TRANSIENT_INTERVAL_RATE_LIMIT_PATTERN = /\bper\s+(?:second|minute)\b/i; + +function matchesSubscriptionCapText(errorMessage: string): boolean { + return ( + SUBSCRIPTION_CAP_PATTERN.test(errorMessage) && !TRANSIENT_INTERVAL_RATE_LIMIT_PATTERN.test(errorMessage) + ); +} const OPENROUTER_DAILY_FREE_LIMIT_PATTERN = /\bfree[-_ ]models[-_ ]per[-_ ]day\b/i; // gRPC/Connect end-streams carry the status as its name (`resource_exhausted`), // while HTTP bodies use the phrase ("resource exhausted"). Strip either form @@ -109,7 +116,7 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason { return "QUOTA_EXHAUSTED"; } - if (SUBSCRIPTION_CAP_PATTERN.test(errorMessage)) { + if (matchesSubscriptionCapText(errorMessage)) { return "QUOTA_EXHAUSTED"; } @@ -273,7 +280,7 @@ export function matchesUsageLimitText(errorMessage: string): boolean { (CN_QUOTA_EXHAUSTED_PATTERN.test(errorMessage) && !CN_TRANSIENT_CAP_PATTERN.test(errorMessage)) || SPEND_LIMIT_PATTERN.test(errorMessage) || ACCOUNT_RATE_LIMIT_PATTERN.test(errorMessage) || - SUBSCRIPTION_CAP_PATTERN.test(errorMessage) || + matchesSubscriptionCapText(errorMessage) || OPENROUTER_DAILY_FREE_LIMIT_PATTERN.test(errorMessage) ); } diff --git a/packages/ai/test/rate-limit-utils.test.ts b/packages/ai/test/rate-limit-utils.test.ts index 7ee281255..3d06e6d11 100644 --- a/packages/ai/test/rate-limit-utils.test.ts +++ b/packages/ai/test/rate-limit-utils.test.ts @@ -324,6 +324,11 @@ describe("isUsageLimitOutcome", () => { expect(parseRateLimitReason(transient)).toBe("RATE_LIMIT_EXCEEDED"); expect(isUsageLimitOutcome(429, transient)).toBe(false); expect(isUsageLimit(Object.assign(new Error(transient), { status: 429 }))).toBe(false); + + const planPerMinuteLimit = "429 Your plan has a rate limit of 60 requests per minute"; + expect(parseRateLimitReason(planPerMinuteLimit)).toBe("RATE_LIMIT_EXCEEDED"); + expect(isUsageLimitOutcome(429, planPerMinuteLimit)).toBe(false); + expect(isUsageLimit(Object.assign(new Error(planPerMinuteLimit), { status: 429 }))).toBe(false); }); it("still rotates on 429 with explicit account rate-limit framing", () => {