File size: 6,745 Bytes
cc11e77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
 * 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}`;
}