import { parseResetAt } from "./resets-at"; export type ProviderFamily = "openai-codex" | "openai-compatible" | "anthropic" | "known" | "unknown"; export interface RuleMatch { matchedRule: string; providerHint: string; resetsAt?: number; } export const RULE_IDS = { codexUsage: "codex-usage-limit", codexNotIncluded: "codex-not-included", codexTerminal: "codex-terminal-rate-limit", openaiQuota: "oai-insufficient-quota", anthropicCredit: "anthropic-credit-balance", outageText: "generic-outage-text", outageStatus: "generic-5xx-hint", } as const; const compatible = new Set(["openai", "openai-completions", "openai-responses", "azure-openai-responses"]); const known = new Set(["google", "vertex", "mistral", "amazon-bedrock", "bedrock", "pi-messages"]); export function providerFamily(provider: string): ProviderFamily { const value = provider.trim().toLowerCase(); if (value === "openai-codex") return "openai-codex"; if (compatible.has(value)) return "openai-compatible"; if (value === "anthropic") return "anthropic"; return known.has(value) ? "known" : "unknown"; } export function normalizeProviderError(errorMessage?: string, diagnostics?: unknown): string { let diagnosticText = ""; if (diagnostics !== undefined) { try { diagnosticText = typeof diagnostics === "string" ? diagnostics : JSON.stringify(diagnostics) ?? ""; } catch { diagnosticText = ""; } } return `${errorMessage ?? ""} ${diagnosticText}`.replace(/\s+/g, " ").toLowerCase(); } export function isContextOverflow(text: string): boolean { return /context(?:[ _-]?(?:window|length|overflow|limit))|maximum[ _-]?context|prompt.{0,30}too (?:large|long)|too many tokens/.test(text); } export function matchQuotaRule(family: ProviderFamily, text: string, now: number): RuleMatch | undefined { if (family === "openai-codex") { if (text.includes("usage_limit_reached") || /usage limit (?:has been|is) reached/.test(text)) return { matchedRule: RULE_IDS.codexUsage, providerHint: family, resetsAt: parseResetAt(text, now) }; if (text.includes("usage_not_included")) return { matchedRule: RULE_IDS.codexNotIncluded, providerHint: family }; const resetsAt = text.includes("rate_limit_exceeded") ? parseResetAt(text, now) : undefined; if (resetsAt !== undefined) return { matchedRule: RULE_IDS.codexTerminal, providerHint: family, resetsAt }; } if (family === "openai-compatible" && /\binsufficient_quota\b/.test(text)) return { matchedRule: RULE_IDS.openaiQuota, providerHint: family }; if (family === "anthropic" && text.includes("credit balance is too low to use this model")) return { matchedRule: RULE_IDS.anthropicCredit, providerHint: family }; return undefined; } const outageText = /(throttlingexception|overloaded|internal server error|service unavailable|serviceunavailable|econnreset|etimedout|fetch failed|socket hang up)/; export function matchTransientRule(text: string, status?: number): string | undefined { if ([500, 502, 503, 529].includes(status ?? 0)) return RULE_IDS.outageStatus; return outageText.test(text) ? RULE_IDS.outageText : undefined; }