import type { ProviderCode, ProviderError, ProviderErrorCategory } from "./types"; type ErrorLike = { name?: string; message?: string; status?: number; statusCode?: number; code?: string; response?: { status?: number; data?: unknown; }; cause?: unknown; }; export class NormalizedProviderError extends Error { readonly providerError: ProviderError; constructor(providerError: ProviderError) { super(providerError.message); this.name = "NormalizedProviderError"; this.providerError = providerError; } } export function normalizeProviderError(provider: ProviderCode | string, error: unknown): ProviderError { if (error instanceof NormalizedProviderError) { return error.providerError; } const statusCode = getStatusCode(error); const providerCode = getProviderErrorCode(error, statusCode); const message = getProviderErrorMessage(error); const category = categorizeProviderError(statusCode, providerCode, message); return { provider, category, code: providerCode, message, retryable: isRetryable(category, statusCode), ...(statusCode !== undefined ? { statusCode } : {}), raw: error }; } export function providerHttpError(provider: ProviderCode | string, statusCode: number, body: unknown): ProviderError { const code = getBodyErrorCode(body) ?? `HTTP_${statusCode}`; const message = getBodyErrorMessage(body) ?? `Provider request failed with HTTP ${statusCode}`; const category = categorizeProviderError(statusCode, code, message); return { provider, category, code, message, statusCode, retryable: isRetryable(category, statusCode), raw: body }; } function getStatusCode(error: unknown): number | undefined { const value = error as ErrorLike; return value?.statusCode ?? value?.status ?? value?.response?.status; } function getProviderErrorCode(error: unknown, statusCode?: number): string { const value = error as ErrorLike; const fromBody = getBodyErrorCode(value?.response?.data) ?? getBodyErrorCode(error); return fromBody ?? value?.code ?? (statusCode ? `HTTP_${statusCode}` : "PROVIDER_ERROR"); } function getProviderErrorMessage(error: unknown): string { const value = error as ErrorLike; return getBodyErrorMessage(value?.response?.data) ?? getBodyErrorMessage(error) ?? value?.message ?? "Provider request failed"; } function getBodyErrorCode(body: unknown): string | undefined { if (!body || typeof body !== "object") return undefined; const record = body as Record; const nested = record.error && typeof record.error === "object" ? (record.error as Record) : undefined; const code = nested?.code ?? nested?.type ?? record.code ?? record.error_code; return typeof code === "string" ? code : undefined; } function getBodyErrorMessage(body: unknown): string | undefined { if (!body || typeof body !== "object") return undefined; const record = body as Record; const nested = record.error && typeof record.error === "object" ? (record.error as Record) : undefined; const message = nested?.message ?? record.message ?? record.error_msg ?? record.detail; return typeof message === "string" ? message : undefined; } function categorizeProviderError(statusCode: number | undefined, code: string, message: string): ProviderErrorCategory { const normalized = `${code} ${message}`.toLowerCase(); if (statusCode === 401 || normalized.includes("invalid api key") || normalized.includes("unauthorized")) { return "authentication"; } if (statusCode === 403 || normalized.includes("permission") || normalized.includes("forbidden")) { return normalized.includes("policy") || normalized.includes("safety") ? "content_policy" : "permission"; } if (statusCode === 408 || normalized.includes("timeout") || normalized.includes("aborted")) { return "timeout"; } if (statusCode === 429 || normalized.includes("rate limit") || normalized.includes("too many request")) { return "rate_limit"; } if (normalized.includes("quota") || normalized.includes("insufficient") || normalized.includes("balance")) { return "quota"; } if (normalized.includes("content policy") || normalized.includes("safety") || normalized.includes("sensitive")) { return "content_policy"; } if (statusCode === 400 || statusCode === 422) { return "bad_request"; } if (statusCode === 404) { return "not_found"; } if (statusCode && statusCode >= 500) { return "provider_unavailable"; } if (normalized.includes("fetch failed") || normalized.includes("econn") || normalized.includes("network")) { return "network"; } return statusCode ? "provider_error" : "unknown"; } function isRetryable(category: ProviderErrorCategory, statusCode?: number): boolean { if (category === "rate_limit" || category === "timeout" || category === "network" || category === "provider_unavailable") { return true; } return typeof statusCode === "number" && statusCode >= 500; }