fix(ai): recognized OpenRouter daily key limits
- Classified free-models-per-day failures as credential-scoped quota exhaustion. - Added regression coverage proving auth retries switch to a healthy sibling key. Fixes #4832
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenRouter daily free-model allowance errors (`free-models-per-day`) being treated as transient rate limits, so requests rotate from an exhausted API key to a healthy sibling credential. ([#4832](https://github.com/can1357/oh-my-pi/issues/4832))
|
||||
|
||||
## [16.3.11] - 2026-07-06
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -19,6 +19,7 @@ const SERVER_ERROR_BACKOFF_MS = 20 * 1000; // 20s
|
||||
const ACCOUNT_RATE_LIMIT_PATTERN =
|
||||
/\baccount(?:'s)?\b[^\n]{0,80}\brate.?limit\b|\brate.?limit\b[^\n]{0,80}\baccount\b/i;
|
||||
const INSUFFICIENT_BALANCE_PATTERN = /insufficient.?balance/i;
|
||||
const OPENROUTER_DAILY_FREE_LIMIT_PATTERN = /\bfree[-_ ]models[-_ ]per[-_ ]day\b/i;
|
||||
|
||||
/**
|
||||
* Classify a rate-limit error message into a reason category.
|
||||
@@ -54,6 +55,10 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
||||
return "QUOTA_EXHAUSTED";
|
||||
}
|
||||
|
||||
if (OPENROUTER_DAILY_FREE_LIMIT_PATTERN.test(errorMessage)) {
|
||||
return "QUOTA_EXHAUSTED";
|
||||
}
|
||||
|
||||
if (
|
||||
lower.includes("per minute") ||
|
||||
lower.includes("rate limit") ||
|
||||
@@ -157,5 +162,9 @@ export function isOpaqueStatusBody(message: string): boolean {
|
||||
* {@link isUsageLimitOutcome} uses it for the account-rotation decision.
|
||||
*/
|
||||
export function matchesUsageLimitText(errorMessage: string): boolean {
|
||||
return USAGE_LIMIT_PATTERN.test(errorMessage) || ACCOUNT_RATE_LIMIT_PATTERN.test(errorMessage);
|
||||
return (
|
||||
USAGE_LIMIT_PATTERN.test(errorMessage) ||
|
||||
ACCOUNT_RATE_LIMIT_PATTERN.test(errorMessage) ||
|
||||
OPENROUTER_DAILY_FREE_LIMIT_PATTERN.test(errorMessage)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,6 +99,26 @@ describe("withAuth", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("switches credentials when OpenRouter exhausts the daily free-model allowance", async () => {
|
||||
const keys: string[] = [];
|
||||
const result = await withAuth(
|
||||
ctx => (ctx.error === undefined || !ctx.lastChance ? "exhausted-key" : "healthy-key"),
|
||||
async key => {
|
||||
keys.push(key);
|
||||
if (key === "healthy-key") return "success";
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
"429 Rate limit exceeded: free-models-per-day. Add 10 credits to unlock 1000 free model requests per day",
|
||||
),
|
||||
{ status: 429 },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe("success");
|
||||
expect(keys).toEqual(["exhausted-key", "healthy-key"]);
|
||||
});
|
||||
|
||||
it("stops retrying when the resolver returns undefined", async () => {
|
||||
const keys: string[] = [];
|
||||
const original = authError();
|
||||
|
||||
Reference in New Issue
Block a user