import { PolyProviderError, type PolyErrorCode, type ProviderId } from "./types"; import { redactSecrets } from "@/lib/crypto/keys"; export function isRetryableStatus(status?: number): boolean { return status === 408 || status === 409 || status === 429 || (status !== undefined && status >= 500 && status <= 599); } export function codeFromStatus(status: number | undefined): PolyErrorCode { switch (status) { case 401: return "INVALID_API_KEY"; case 402: return "INSUFFICIENT_CREDITS"; case 403: return "PERMISSION_DENIED"; case 404: return "MODEL_NOT_FOUND"; case 408: return "REQUEST_TIMEOUT"; case 413: return "CONTEXT_TOO_LONG"; case 422: case 400: return "INVALID_PARAMETER"; case 429: return "RATE_LIMITED"; case 500: case 502: case 503: case 504: case 529: return "PROVIDER_UNAVAILABLE"; default: return "UNKNOWN_PROVIDER_ERROR"; } } /** Refine a 400-ish error using the provider's message text (all providers put the reason in prose). */ export function refineByMessage(code: PolyErrorCode, message: string): PolyErrorCode { const m = message.toLowerCase(); if (/context.length|context_length|too many tokens|maximum context|prompt is too long|exceeds the (model'?s )?(maximum )?(context|token)|input token count|token limit|max_tokens.*(too large|exceed)/.test(m)) return "CONTEXT_TOO_LONG"; if (/(insufficient|exceeded your current) (quota|credits|balance)|billing|credit balance is too low|payment/.test(m)) return "INSUFFICIENT_CREDITS"; if (/(model|deployment).*(not found|does not exist|not exist|unknown|unsupported model)|not found for model|no such model/.test(m)) return "MODEL_NOT_FOUND"; if (/(invalid|incorrect) (api key|x-api-key|authentication)|api key (not valid|invalid|expired)|bad credentials|unauthenticated|authentication_error/.test(m)) return "INVALID_API_KEY"; if (/rate.?limit|too many requests|resource.?exhausted|overloaded|quota exceeded/.test(m)) return "RATE_LIMITED"; if (/safety|content (policy|filter|management)|blocked|prohibited|refus|harm_category/.test(m)) return "CONTENT_REJECTED"; if (/timed? ?out|deadline exceeded/.test(m)) return "REQUEST_TIMEOUT"; return code; } export function parseRetryAfter(headers?: Headers | Record | null): number | undefined { if (!headers) return undefined; const get = (k: string): string | undefined => { if (headers instanceof Headers) return headers.get(k) ?? undefined; const v = headers[k] ?? headers[k.toLowerCase()]; return Array.isArray(v) ? v[0] : v; }; const ra = get("retry-after"); if (ra) { const secs = Number(ra); if (!Number.isNaN(secs)) return Math.min(secs * 1000, 60_000); const date = Date.parse(ra); if (!Number.isNaN(date)) return Math.max(0, Math.min(date - Date.now(), 60_000)); } const raMs = get("retry-after-ms"); if (raMs && !Number.isNaN(Number(raMs))) return Math.min(Number(raMs), 60_000); return undefined; } /** Human-readable, non-technical explanations shown in the UI. */ export const ERROR_MESSAGES: Record = { INVALID_API_KEY: "The API key for this provider was rejected. Check it in Settings → Providers.", PERMISSION_DENIED: "This key isn't allowed to use that model or feature.", RATE_LIMITED: "The provider is rate-limiting your key. Wait a moment and try again.", MODEL_NOT_FOUND: "This model isn't available for your account or was renamed. Refresh the model list.", CONTEXT_TOO_LONG: "The conversation is longer than this model's context window. Start a new chat or pick a long-context model.", INSUFFICIENT_CREDITS: "Your provider account has no remaining credit or quota.", REQUEST_TIMEOUT: "The provider took too long to respond.", PROVIDER_UNAVAILABLE: "The provider is temporarily unavailable. Try again shortly.", INVALID_PARAMETER: "A setting isn't accepted by this model. Reset the model configuration and retry.", CONTENT_REJECTED: "The provider declined to answer this request (content policy).", NETWORK_ERROR: "PolyLLM couldn't reach the provider.", CANCELLED: "Generation stopped.", UNKNOWN_PROVIDER_ERROR: "The provider returned an unexpected error.", }; /** * Generic normalizer used by adapters as a fallback: works on SDK error objects * exposing `status`, `message`, `headers`, `error.code/type`. Provider-specific * adapters call this then refine. */ export function normalizeGenericError(provider: ProviderId, error: unknown): PolyProviderError { if (error instanceof PolyProviderError) return error; const e = error as { status?: number; statusCode?: number; message?: string; name?: string; code?: string; headers?: Headers | Record; error?: { code?: string; type?: string; message?: string; status?: string } } | undefined; const status = e?.status ?? e?.statusCode; const rawMessage = e?.error?.message ?? e?.message ?? String(error); const message = redactSecrets(rawMessage).slice(0, 600); if (e?.name === "AbortError" || /aborted|abort/i.test(e?.name ?? "") || (e?.code === "ABORT_ERR")) { return new PolyProviderError({ code: "CANCELLED", message: "Cancelled", provider, retryable: false, cause: error }); } if (e?.name === "APIConnectionTimeoutError" || /timeout/i.test(e?.name ?? "")) { return new PolyProviderError({ code: "REQUEST_TIMEOUT", message, provider, retryable: true, cause: error }); } if (e?.name === "APIConnectionError" || e?.code === "ECONNRESET" || e?.code === "ENOTFOUND" || e?.code === "ECONNREFUSED" || e?.code === "UND_ERR_SOCKET" || /fetch failed/i.test(message)) { return new PolyProviderError({ code: "NETWORK_ERROR", message, provider, retryable: true, cause: error }); } let code = codeFromStatus(status); code = refineByMessage(code, message); const retryAfterMs = parseRetryAfter(e?.headers ?? null); return new PolyProviderError({ code, message: code === "INVALID_API_KEY" ? "Invalid API key" : message, provider, status, retryable: isRetryableStatus(status) && code !== "INSUFFICIENT_CREDITS" && code !== "INVALID_API_KEY", retryAfterMs, providerCode: e?.error?.code ?? e?.error?.type ?? e?.error?.status ?? e?.code, cause: error, }); }