fix(failover): disambiguate periodic limit errors

This commit is contained in:
Altay
2026-03-06 01:56:19 +03:00
parent effd037c89
commit e0767f767a
4 changed files with 30 additions and 2 deletions

View File

@@ -123,6 +123,19 @@ describe("failover-error", () => {
message: ZHIPUAI_WEEKLY_MONTHLY_LIMIT_EXHAUSTED_MESSAGE,
}),
).toBe("rate_limit");
expect(
resolveFailoverReasonFromError({
message: "LLM error: monthly limit reached",
}),
).toBe("rate_limit");
});
it("keeps raw-text 402 weekly/monthly limit errors in billing", () => {
expect(
resolveFailoverReasonFromError({
message: "402 Payment Required: Weekly/Monthly Limit Exhausted",
}),
).toBe("billing");
});
it("infers format errors from error messages", () => {

View File

@@ -540,6 +540,9 @@ describe("classifyFailoverReason", () => {
expect(
classifyFailoverReason("HTTP 402 payment required. Your limit exhausted for this plan."),
).toBe("billing");
expect(classifyFailoverReason("402 Payment Required: Weekly/Monthly Limit Exhausted")).toBe(
"billing",
);
expect(classifyFailoverReason(INSUFFICIENT_QUOTA_PAYLOAD)).toBe("billing");
expect(classifyFailoverReason("deadline exceeded")).toBe("timeout");
expect(classifyFailoverReason("request ended without sending any chunks")).toBe("timeout");
@@ -595,8 +598,10 @@ describe("classifyFailoverReason", () => {
"LLM error 1310: Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-03-06 22:19:54 (request_id: 20260303141547610b7f574d1b44cb)",
),
).toBe("rate_limit");
// Independent coverage for /weekly\/monthly limit/i (no generic "limit exhausted")
// Independent coverage for broader periodic limit patterns.
expect(classifyFailoverReason("LLM error: weekly/monthly limit reached")).toBe("rate_limit");
expect(classifyFailoverReason("LLM error: monthly limit reached")).toBe("rate_limit");
expect(classifyFailoverReason("LLM error: daily limit exceeded")).toBe("rate_limit");
});
it("classifies permanent auth errors as auth_permanent", () => {
expect(classifyFailoverReason("invalid_api_key")).toBe("auth_permanent");

View File

@@ -8,6 +8,7 @@ import {
isAuthPermanentErrorMessage,
isBillingErrorMessage,
isOverloadedErrorMessage,
isPeriodicUsageLimitErrorMessage,
isRateLimitErrorMessage,
isTimeoutErrorMessage,
matchesFormatErrorPattern,
@@ -842,6 +843,9 @@ export function classifyFailoverReason(raw: string): FailoverReason | null {
if (isJsonApiInternalServerError(raw)) {
return "timeout";
}
if (isPeriodicUsageLimitErrorMessage(raw)) {
return isBillingErrorMessage(raw) ? "billing" : "rate_limit";
}
if (isRateLimitErrorMessage(raw)) {
return "rate_limit";
}

View File

@@ -1,5 +1,8 @@
type ErrorPattern = RegExp | string;
const PERIODIC_USAGE_LIMIT_RE =
/\b(?:daily|weekly|monthly)(?:\/(?:daily|weekly|monthly))* (?:usage )?limit(?:s)?(?: (?:exhausted|reached|exceeded))?\b/i;
const ERROR_PATTERNS = {
rateLimit: [
/rate[_ ]limit|too many requests|429/,
@@ -9,7 +12,6 @@ const ERROR_PATTERNS = {
"quota exceeded",
"resource_exhausted",
"usage limit",
/weekly\/monthly limit/i,
/\btpm\b/i,
"tokens per minute",
],
@@ -118,6 +120,10 @@ export function isTimeoutErrorMessage(raw: string): boolean {
return matchesErrorPatterns(raw, ERROR_PATTERNS.timeout);
}
export function isPeriodicUsageLimitErrorMessage(raw: string): boolean {
return PERIODIC_USAGE_LIMIT_RE.test(raw);
}
export function isBillingErrorMessage(raw: string): boolean {
const value = raw.toLowerCase();
if (!value) {