diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 8f6dbe652bb..fac44b8888c 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -208,7 +208,7 @@ const HTTP_ERROR_HINTS = [ "permission", ]; -type Http402Reason = "billing" | "rate_limit"; +type PaymentRequiredFailoverReason = Extract; 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";