refactor(agents): simplify 402 failover classifier

This commit is contained in:
Altay
2026-03-08 01:27:21 +03:00
parent ebe3f6cce4
commit a2e53a3976

View File

@@ -208,7 +208,7 @@ const HTTP_ERROR_HINTS = [
"permission",
];
type Http402Reason = "billing" | "rate_limit";
type PaymentRequiredFailoverReason = Extract<FailoverReason, "billing" | "rate_limit">;
const BILLING_402_HINTS = [
"insufficient credits",
@@ -220,56 +220,54 @@ const BILLING_402_HINTS = [
"top up",
] as const;
const PERIODIC_402_HINTS = ["daily", "weekly", "monthly"] as const;
const RETRYABLE_402_RETRY_HINTS = ["try again", "retry", "temporary", "cooldown"] as const;
const RETRYABLE_402_LIMIT_HINTS = ["usage limit", "rate limit", "organization usage"] as const;
const BILLING_402_HARD_CAP_RE =
/\b(?:billing hard limit|hard limit reached|maximum allowed(?:\s+(?:daily|weekly|monthly))?(?:\s+spend(?:ing)?)?\s+limit)\b/i;
const RETRYABLE_402_SCOPED_HINTS = ["organization", "workspace"] as const;
const RETRYABLE_402_SCOPED_RESULT_HINTS = [
"billing period",
"exceeded",
"reached",
"exhausted",
] as const;
const RAW_402_MARKER_RE =
/["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\s+payment required\b/i;
const LEADING_402_WRAPPER_RE =
/^(?:error[:\s-]+)?(?:(?:http\s*)?402(?:\s+payment required)?|payment required)(?:[:\s-]+|$)/i;
const RETRYABLE_402_PERIODIC_USAGE_OR_SPEND_RE =
/\b(?:daily|weekly|monthly)(?:\/(?:daily|weekly|monthly))*\s+(?:usage|spend(?:ing)?)\s+limit(?:s)?(?:\s+(?:exhausted|reached|exceeded))?\b/i;
const RETRYABLE_402_SCOPED_SPEND_LIMIT_RE =
/\b(?:organization|workspace)\s+(?:spend(?:ing)?\s+)?limit(?:s)?(?:\s+(?:reached|exhausted|exceeded))?\b/i;
const RETRYABLE_402_SCOPED_LIMIT_EXCEEDED_RE =
/\b(?:organization|workspace)\b[\s\S]{0,30}\blimit(?:s)?\b[\s\S]{0,30}\bexceeded\b/i;
const RETRYABLE_402_SCOPED_BILLING_PERIOD_RE =
/\b(?:organization|workspace)\b[\s\S]{0,30}\blimit(?:s)?\b[\s\S]{0,30}\bbilling period\b/i;
function includesAnyHint(text: string, hints: readonly string[]): boolean {
const lower = text.toLowerCase();
return hints.some((hint) => lower.includes(hint));
return hints.some((hint) => text.includes(hint));
}
function hasExplicit402BillingSignal(text: string): boolean {
return includesAnyHint(text, BILLING_402_HINTS) || BILLING_402_HARD_CAP_RE.test(text);
return (
includesAnyHint(text, BILLING_402_HINTS) ||
text.includes("billing hard limit") ||
text.includes("hard limit reached") ||
(text.includes("maximum allowed") && text.includes("limit"))
);
}
function hasRetryable402TransientSignal(text: string): boolean {
const lower = text.toLowerCase();
const hasPeriodicHint = includesAnyHint(text, PERIODIC_402_HINTS);
const hasSpendLimit = text.includes("spend limit") || text.includes("spending limit");
const hasScopedHint = includesAnyHint(text, RETRYABLE_402_SCOPED_HINTS);
return (
(includesAnyHint(text, RETRYABLE_402_RETRY_HINTS) &&
includesAnyHint(text, RETRYABLE_402_LIMIT_HINTS)) ||
RETRYABLE_402_PERIODIC_USAGE_OR_SPEND_RE.test(text) ||
(includesAnyHint(lower, ["daily", "weekly", "monthly"]) &&
lower.includes("limit") &&
lower.includes("reset")) ||
RETRYABLE_402_SCOPED_SPEND_LIMIT_RE.test(text) ||
RETRYABLE_402_SCOPED_LIMIT_EXCEEDED_RE.test(text) ||
RETRYABLE_402_SCOPED_BILLING_PERIOD_RE.test(text)
(hasPeriodicHint && (text.includes("usage limit") || hasSpendLimit)) ||
(hasPeriodicHint && text.includes("limit") && text.includes("reset")) ||
(hasScopedHint &&
text.includes("limit") &&
(hasSpendLimit || includesAnyHint(text, RETRYABLE_402_SCOPED_RESULT_HINTS)))
);
}
function normalize402Message(raw: string): string {
let normalized = raw.trim();
while (LEADING_402_WRAPPER_RE.test(normalized)) {
normalized = normalized.replace(LEADING_402_WRAPPER_RE, "").trim();
}
return normalized;
return raw.trim().toLowerCase().replace(LEADING_402_WRAPPER_RE, "").trim();
}
function classify402Message(message: string): Http402Reason {
function classify402Message(message: string): PaymentRequiredFailoverReason {
const normalized = normalize402Message(message);
if (!normalized) {
return "billing";
@@ -286,7 +284,7 @@ function classify402Message(message: string): Http402Reason {
return "billing";
}
function classifyFailoverReasonFromRaw402Message(raw: string): Http402Reason | null {
function classifyFailoverReasonFrom402Text(raw: string): PaymentRequiredFailoverReason | null {
if (!RAW_402_MARKER_RE.test(raw)) {
return null;
}
@@ -925,9 +923,9 @@ export function classifyFailoverReason(raw: string): FailoverReason | null {
if (isModelNotFoundErrorMessage(raw)) {
return "model_not_found";
}
const raw402Reason = classifyFailoverReasonFromRaw402Message(raw);
if (raw402Reason) {
return raw402Reason;
const reasonFrom402Text = classifyFailoverReasonFrom402Text(raw);
if (reasonFrom402Text) {
return reasonFrom402Text;
}
if (isPeriodicUsageLimitErrorMessage(raw)) {
return isBillingErrorMessage(raw) ? "billing" : "rate_limit";