Spaces:
Sleeping
Sleeping
| /** | |
| * src/core/errors/index.ts — Error taxonomy professionale | |
| * | |
| * Ogni errore AI/network ha un codice, un messaggio user-facing, | |
| * e una severity. Usare sempre questi invece di string generici. | |
| * | |
| * Checklist 1.3 — error taxonomy | |
| */ | |
| // ─── Error codes ──────────────────────────────────────────────────────────── | |
| export enum ErrorCode { | |
| NETWORK_ERROR = "NETWORK_ERROR", | |
| RATE_LIMIT = "RATE_LIMIT", | |
| INVALID_KEY = "INVALID_KEY", | |
| MODEL_UNAVAILABLE = "MODEL_UNAVAILABLE", | |
| EXECUTION_FAILED = "EXECUTION_FAILED", | |
| MEMORY_ERROR = "MEMORY_ERROR", | |
| OFFLINE_MODE = "OFFLINE_MODE", | |
| TIMEOUT = "TIMEOUT", | |
| QUOTA_EXCEEDED = "QUOTA_EXCEEDED", | |
| PROVIDER_SWITCHING = "PROVIDER_SWITCHING", | |
| UNKNOWN_ERROR = "UNKNOWN_ERROR", | |
| } | |
| export type ErrorSeverity = "info" | "warn" | "error" | "fatal"; | |
| // ─── Structured app error ─────────────────────────────────────────────────── | |
| export interface AppError { | |
| code: ErrorCode; | |
| message: string; // user-facing (breve) | |
| detail?: string; // tecnico / debug | |
| provider?: string; // es. "Groq", "Gemini" | |
| severity: ErrorSeverity; | |
| retryable: boolean; | |
| timestamp: number; | |
| } | |
| // ─── Factory ───────────────────────────────────────────────────────────────── | |
| export function createError( | |
| code: ErrorCode, | |
| opts: Partial<Omit<AppError, "code" | "timestamp">> = {}, | |
| ): AppError { | |
| return { | |
| code, | |
| message: opts.message ?? ERROR_MESSAGES[code], | |
| detail: opts.detail, | |
| provider: opts.provider, | |
| severity: opts.severity ?? ERROR_SEVERITY[code], | |
| retryable: opts.retryable ?? ERROR_RETRYABLE[code], | |
| timestamp: Date.now(), | |
| }; | |
| } | |
| // ─── Default user-facing messages ──────────────────────────────────────────── | |
| const ERROR_MESSAGES: Record<ErrorCode, string> = { | |
| [ErrorCode.NETWORK_ERROR]: "Errore di rete — controlla la connessione", | |
| [ErrorCode.RATE_LIMIT]: "Limite richieste raggiunto — ritento automaticamente", | |
| [ErrorCode.INVALID_KEY]: "Chiave API non valida — verifica nelle Impostazioni", | |
| [ErrorCode.MODEL_UNAVAILABLE]: "Modello non disponibile — passo al prossimo", | |
| [ErrorCode.EXECUTION_FAILED]: "Esecuzione fallita", | |
| [ErrorCode.MEMORY_ERROR]: "Errore di memoria — prova a liberare spazio", | |
| [ErrorCode.OFFLINE_MODE]: "Modalità offline — funzionalità limitate", | |
| [ErrorCode.TIMEOUT]: "Timeout — la risposta ha impiegato troppo", | |
| [ErrorCode.QUOTA_EXCEEDED]: "Quota esaurita — cambio provider", | |
| [ErrorCode.PROVIDER_SWITCHING]: "Provider non disponibile — cambio in corso", | |
| [ErrorCode.UNKNOWN_ERROR]: "Errore sconosciuto", | |
| }; | |
| // ─── Default severity ──────────────────────────────────────────────────────── | |
| const ERROR_SEVERITY: Record<ErrorCode, ErrorSeverity> = { | |
| [ErrorCode.NETWORK_ERROR]: "warn", | |
| [ErrorCode.RATE_LIMIT]: "warn", | |
| [ErrorCode.INVALID_KEY]: "error", | |
| [ErrorCode.MODEL_UNAVAILABLE]: "warn", | |
| [ErrorCode.EXECUTION_FAILED]: "error", | |
| [ErrorCode.MEMORY_ERROR]: "error", | |
| [ErrorCode.OFFLINE_MODE]: "info", | |
| [ErrorCode.TIMEOUT]: "warn", | |
| [ErrorCode.QUOTA_EXCEEDED]: "warn", | |
| [ErrorCode.PROVIDER_SWITCHING]: "info", | |
| [ErrorCode.UNKNOWN_ERROR]: "error", | |
| }; | |
| // ─── Default retryable ─────────────────────────────────────────────────────── | |
| const ERROR_RETRYABLE: Record<ErrorCode, boolean> = { | |
| [ErrorCode.NETWORK_ERROR]: true, | |
| [ErrorCode.RATE_LIMIT]: true, | |
| [ErrorCode.INVALID_KEY]: false, | |
| [ErrorCode.MODEL_UNAVAILABLE]: true, | |
| [ErrorCode.EXECUTION_FAILED]: false, | |
| [ErrorCode.MEMORY_ERROR]: false, | |
| [ErrorCode.OFFLINE_MODE]: false, | |
| [ErrorCode.TIMEOUT]: true, | |
| [ErrorCode.QUOTA_EXCEEDED]: true, | |
| [ErrorCode.PROVIDER_SWITCHING]: true, | |
| [ErrorCode.UNKNOWN_ERROR]: false, | |
| }; | |
| // ─── Classifier: HTTP status + body → ErrorCode ────────────────────────────── | |
| export function classifyHttpError(status: number, body = ""): ErrorCode { | |
| const b = body.toLowerCase(); | |
| if (status === 401 || status === 403) return ErrorCode.INVALID_KEY; | |
| if (status === 429 || status === 402) return ErrorCode.RATE_LIMIT; | |
| if (status === 404) return ErrorCode.MODEL_UNAVAILABLE; | |
| if (status === 408) return ErrorCode.TIMEOUT; | |
| if (b.includes("quota") || b.includes("exceeded")) return ErrorCode.QUOTA_EXCEEDED; | |
| if (b.includes("rate limit")) return ErrorCode.RATE_LIMIT; | |
| if (b.includes("model not found")) return ErrorCode.MODEL_UNAVAILABLE; | |
| if (status >= 500) return ErrorCode.NETWORK_ERROR; | |
| return ErrorCode.UNKNOWN_ERROR; | |
| } | |
| // ─── Classifier: catch(err) → ErrorCode ───────────────────────────────────── | |
| export function classifyJsError(err: unknown): ErrorCode { | |
| if (!err) return ErrorCode.UNKNOWN_ERROR; | |
| const e = err as { name?: string; message?: string; status?: number; body?: string }; | |
| if (e.name === "AbortError") return ErrorCode.TIMEOUT; | |
| if (e.message?.includes("completion-timeout")) return ErrorCode.TIMEOUT; | |
| if (e.message?.toLowerCase().includes("network")) return ErrorCode.NETWORK_ERROR; | |
| if (e.message?.toLowerCase().includes("offline")) return ErrorCode.OFFLINE_MODE; | |
| if (e.status && e.body !== undefined) return classifyHttpError(e.status, e.body); | |
| return ErrorCode.UNKNOWN_ERROR; | |
| } | |
| // ─── Format for display ────────────────────────────────────────────────────── | |
| export function formatErrorBanner(err: AppError): string { | |
| const prefix = err.provider ? `${err.provider}: ` : ""; | |
| return `${prefix}${err.message}`; | |
| } | |