/** * Rate limit reason classification and backoff calculation utilities. * Ported from opencode-antigravity-auth plugin for consistency. */ export type RateLimitReason = | "QUOTA_EXHAUSTED" | "RATE_LIMIT_EXCEEDED" | "MODEL_CAPACITY_EXHAUSTED" | "SERVER_ERROR" | "UNKNOWN"; const QUOTA_EXHAUSTED_BACKOFF_MS = 30 * 60 * 1000; // 30 min const RATE_LIMIT_EXCEEDED_BACKOFF_MS = 30 * 1000; // 30s const MODEL_CAPACITY_BASE_MS = 45 * 1000; // 45s base const MODEL_CAPACITY_JITTER_MS = 30 * 1000; // uniform +0–30s above base → 45–75s total const SERVER_ERROR_BACKOFF_MS = 20 * 1000; // 20s /** * Classify a rate-limit error message into a reason category. * Priority order: MODEL_CAPACITY > RATE_LIMIT > QUOTA > SERVER_ERROR > UNKNOWN. * * "resource exhausted" maps to MODEL_CAPACITY (transient, short wait) * "quota exceeded" maps to QUOTA_EXHAUSTED (long wait, switch account) */ export function parseRateLimitReason(errorMessage: string): RateLimitReason { const lower = errorMessage.toLowerCase(); if ( lower.includes("capacity") || lower.includes("overloaded") || lower.includes("529") || lower.includes("503") || lower.includes("resource exhausted") ) { return "MODEL_CAPACITY_EXHAUSTED"; } if ( lower.includes("out_of_credits") || lower.includes("request would exceed your account's rate limit") || lower.includes("request would exceed your accounts rate limit") ) { return "QUOTA_EXHAUSTED"; } if ( lower.includes("per minute") || lower.includes("rate limit") || lower.includes("too many requests") || lower.includes("presque") ) { return "RATE_LIMIT_EXCEEDED"; } if ( lower.includes("exhausted") || lower.includes("quota") || lower.includes("usage limit") || lower.includes("model limit") || lower.includes("model_limit") || lower.includes("message limit") || lower.includes("message_limit") || lower.includes("limit for this model") ) { return "QUOTA_EXHAUSTED"; } if (lower.includes("500") || lower.includes("internal error") || lower.includes("internal server error")) { return "SERVER_ERROR"; } return "UNKNOWN"; } /** * Calculate backoff delay in ms for a given rate limit reason. * MODEL_CAPACITY gets jitter to prevent thundering herd. */ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number { switch (reason) { case "QUOTA_EXHAUSTED": return QUOTA_EXHAUSTED_BACKOFF_MS; case "RATE_LIMIT_EXCEEDED": return RATE_LIMIT_EXCEEDED_BACKOFF_MS; case "MODEL_CAPACITY_EXHAUSTED": return MODEL_CAPACITY_BASE_MS + Math.random() * MODEL_CAPACITY_JITTER_MS; case "SERVER_ERROR": return SERVER_ERROR_BACKOFF_MS; default: return QUOTA_EXHAUSTED_BACKOFF_MS; // conservative default } } /** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */ // ZAI reports durable token exhaustion as "[1310][Weekly/Monthly Limit Exhausted...]". // Keep this explicit so generic "rate limit exhausted, retry..." throttles remain retryable. const USAGE_LIMIT_PATTERN = /usage.?limit|usage_limit_reached|usage_not_included|limit_reached|model.?limit|model_limit_reached|message.?limit|message_limit_reached|limit for this model|weekly\/monthly\s+limit\s+exhausted|quota.?exceeded|out_of_credits|request would exceed your account.?s rate limit|resource has been exhausted[^\n]*(?:quota|limit)/i; export function isUsageLimitError(errorMessage: string): boolean { return USAGE_LIMIT_PATTERN.test(errorMessage); }