@providerkit/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/capability.d.ts +41 -0
  2. package/dist/capability.d.ts.map +1 -1
  3. package/dist/capability.js +94 -0
  4. package/dist/capability.js.map +1 -1
  5. package/dist/errors.d.ts +30 -0
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/errors.js +171 -13
  8. package/dist/errors.js.map +1 -1
  9. package/dist/presets.d.ts +173 -28
  10. package/dist/presets.d.ts.map +1 -1
  11. package/dist/presets.js +172 -26
  12. package/dist/presets.js.map +1 -1
  13. package/dist/providers/anthropic.js +2 -2
  14. package/dist/providers/anthropic.js.map +1 -1
  15. package/dist/providers/factory.d.ts.map +1 -1
  16. package/dist/providers/factory.js +1 -0
  17. package/dist/providers/factory.js.map +1 -1
  18. package/dist/providers/gemini.d.ts.map +1 -1
  19. package/dist/providers/gemini.js +28 -10
  20. package/dist/providers/gemini.js.map +1 -1
  21. package/dist/providers/openai.d.ts +6 -0
  22. package/dist/providers/openai.d.ts.map +1 -1
  23. package/dist/providers/openai.js +58 -4
  24. package/dist/providers/openai.js.map +1 -1
  25. package/dist/rate-limit.d.ts +2 -0
  26. package/dist/rate-limit.d.ts.map +1 -1
  27. package/dist/rate-limit.js +91 -34
  28. package/dist/rate-limit.js.map +1 -1
  29. package/dist/retry.d.ts.map +1 -1
  30. package/dist/retry.js +2 -2
  31. package/dist/retry.js.map +1 -1
  32. package/dist/schema.d.ts +20 -0
  33. package/dist/schema.d.ts.map +1 -1
  34. package/dist/schema.js +112 -0
  35. package/dist/schema.js.map +1 -1
  36. package/dist/tool-args.d.ts +13 -1
  37. package/dist/tool-args.d.ts.map +1 -1
  38. package/dist/tool-args.js +50 -1
  39. package/dist/tool-args.js.map +1 -1
  40. package/dist/transport.d.ts.map +1 -1
  41. package/dist/transport.js +3 -0
  42. package/dist/transport.js.map +1 -1
  43. package/dist/usage.d.ts +6 -0
  44. package/dist/usage.d.ts.map +1 -1
  45. package/dist/usage.js +20 -0
  46. package/dist/usage.js.map +1 -1
  47. package/package.json +3 -1
  48. package/src/capability.ts +157 -0
  49. package/src/errors.ts +181 -13
  50. package/src/presets.ts +177 -28
  51. package/src/providers/anthropic.ts +2 -2
  52. package/src/providers/factory.ts +1 -0
  53. package/src/providers/gemini.ts +30 -10
  54. package/src/providers/openai.ts +64 -4
  55. package/src/rate-limit.ts +98 -36
  56. package/src/retry.ts +8 -2
  57. package/src/schema.ts +127 -0
  58. package/src/tool-args.ts +48 -1
  59. package/src/transport.ts +4 -0
  60. package/src/usage.ts +21 -0
package/dist/usage.d.ts CHANGED
@@ -17,6 +17,11 @@ export interface ModelRate {
17
17
  }
18
18
  /** Add two usage records. */
19
19
  export declare function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage;
20
+ /**
21
+ * Subtract usage `b` from `a`, clamping each field at zero.
22
+ * Used when an optimistic turn or speculative stream is rolled back.
23
+ */
24
+ export declare function subtractUsage(a: TokenUsage, b: TokenUsage): TokenUsage;
20
25
  /**
21
26
  * USD for one call.
22
27
  *
@@ -38,6 +43,7 @@ export declare class UsageTracker {
38
43
  * Observability only — never billed. */
39
44
  private saved;
40
45
  add(usage: TokenUsage, rate?: ModelRate): void;
46
+ subtract(usage: TokenUsage, rate?: ModelRate): void;
41
47
  get totals(): TokenUsage;
42
48
  get costUsd(): number;
43
49
  get cacheSavingsUsd(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C;qDACqD;AACrD,MAAM,WAAW,SAAS;IACxB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf;;;wEAGoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB;;0EAEsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,6BAA6B;AAC7B,wBAAgB,QAAQ,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAOjE;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAalE;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAuD;IACpE,OAAO,CAAC,IAAI,CAAK;IACjB;6CACyC;IACzC,OAAO,CAAC,KAAK,CAAK;IAElB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI;IAQ9C,IAAI,MAAM,IAAI,UAAU,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED;;yDAEqD;IACrD,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIrC,KAAK,IAAI,IAAI;CAKd"}
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C;qDACqD;AACrD,MAAM,WAAW,SAAS;IACxB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf;;;wEAGoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB;;0EAEsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,6BAA6B;AAC7B,wBAAgB,QAAQ,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAOjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAOtE;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAalE;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAuD;IACpE,OAAO,CAAC,IAAI,CAAK;IACjB;6CACyC;IACzC,OAAO,CAAC,KAAK,CAAK;IAElB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI;IAQ9C,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI;IAQnD,IAAI,MAAM,IAAI,UAAU,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED;;yDAEqD;IACrD,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIrC,KAAK,IAAI,IAAI;CAKd"}
package/dist/usage.js CHANGED
@@ -8,6 +8,18 @@ export function addUsage(a, b) {
8
8
  outputTokens: a.outputTokens + b.outputTokens,
9
9
  };
10
10
  }
11
+ /**
12
+ * Subtract usage `b` from `a`, clamping each field at zero.
13
+ * Used when an optimistic turn or speculative stream is rolled back.
14
+ */
15
+ export function subtractUsage(a, b) {
16
+ return {
17
+ inputTokens: Math.max(0, a.inputTokens - b.inputTokens),
18
+ cachedInputTokens: Math.max(0, a.cachedInputTokens - b.cachedInputTokens),
19
+ cacheWriteTokens: Math.max(0, (a.cacheWriteTokens ?? 0) - (b.cacheWriteTokens ?? 0)),
20
+ outputTokens: Math.max(0, a.outputTokens - b.outputTokens),
21
+ };
22
+ }
11
23
  /**
12
24
  * USD for one call.
13
25
  *
@@ -47,6 +59,14 @@ export class UsageTracker {
47
59
  const cached = Math.min(Math.max(0, usage.cachedInputTokens), Math.max(0, usage.inputTokens));
48
60
  this.saved += (cached * (rate.input - rate.cacheRead)) / 1_000_000;
49
61
  }
62
+ subtract(usage, rate) {
63
+ this.usage = subtractUsage(this.usage, usage);
64
+ if (!rate)
65
+ return;
66
+ this.cost = Math.max(0, this.cost - costUsd(usage, rate));
67
+ const cached = Math.min(Math.max(0, usage.cachedInputTokens), Math.max(0, usage.inputTokens));
68
+ this.saved = Math.max(0, this.saved - (cached * (rate.input - rate.cacheRead)) / 1_000_000);
69
+ }
50
70
  get totals() {
51
71
  return { ...this.usage };
52
72
  }
package/dist/usage.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@providerkit/core",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "The layer under your agent loop: one seam for every LLM provider, plus the failure handling you only learn in production.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,6 +47,8 @@
47
47
  "test:watch": "vitest",
48
48
  "lint": "eslint src test",
49
49
  "format": "prettier --write .",
50
+ "check:presets": "bun run scripts/refresh-presets.ts",
51
+ "probe:model": "bun run scripts/probe-model.ts",
50
52
  "sync:docs": "cp ../README.md ../LICENSE .",
51
53
  "prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build && bun run sync:docs",
52
54
  "release": "bun publish --access public",
package/src/capability.ts CHANGED
@@ -139,3 +139,160 @@ export async function probeJsonWithTools(
139
139
  // than the equal.
140
140
  return { use: SHAPES.find((shape) => calls[shape] === samples) ?? null, calls, samples };
141
141
  }
142
+
143
+ // ── Model capability resolution ──────────────────────────────────────────
144
+
145
+ export interface ModelCapabilities {
146
+ /** Canonical model identifier. */
147
+ id: string;
148
+ /** Human-readable model display name. */
149
+ name?: string;
150
+ /** Total context window in tokens. */
151
+ contextWindow?: number;
152
+ /** Maximum generation/output tokens. */
153
+ maxOutput?: number;
154
+ /** Whether the model natively supports tool / function calling. */
155
+ supportsTools?: boolean;
156
+ /** Whether the model natively supports JSON schema / structured output. */
157
+ supportsStructuredOutput?: boolean;
158
+ /** Whether the model can receive image inputs. */
159
+ supportsVision?: boolean;
160
+ /** Whether the model has extended thinking / reasoning capability. */
161
+ supportsReasoning?: boolean;
162
+ }
163
+
164
+ export interface ResolveModelOptions {
165
+ /** Custom catalog URL. Defaults to https://models.dev/api.json */
166
+ catalogUrl?: string;
167
+ /** Pre-loaded catalog object (useful for tests or offline execution). */
168
+ catalog?: Record<string, unknown>;
169
+ /** Custom fetch implementation (for proxies, custom headers, or tests). */
170
+ fetchImpl?: typeof fetch;
171
+ /** Max cache age in ms. Defaults to 24 hours (86_400_000 ms). */
172
+ maxAgeMs?: number;
173
+ }
174
+
175
+ interface RawModelEntry {
176
+ id?: string;
177
+ name?: string;
178
+ tool_call?: boolean;
179
+ structured_output?: boolean;
180
+ reasoning?: boolean;
181
+ attachment?: boolean;
182
+ modalities?: {
183
+ input?: string[];
184
+ output?: string[];
185
+ };
186
+ limit?: {
187
+ context?: number;
188
+ output?: number;
189
+ };
190
+ }
191
+
192
+ interface RawProviderEntry {
193
+ models?: Record<string, RawModelEntry>;
194
+ }
195
+
196
+ const DEFAULT_CATALOG_URL = "https://models.dev/api.json";
197
+ const DEFAULT_MAX_AGE_MS = 86_400_000; // 24 hours
198
+
199
+ let cachedCatalog: { timestamp: number; data: Record<string, RawProviderEntry> } | null = null;
200
+
201
+ function normalizeId(id: string): string {
202
+ // Strip gateway prefixes (z-ai/, deepseek/, openai/, accounts/fireworks/models/, etc.)
203
+ const bare = id.replace(/^(?:accounts\/[^/]+\/models\/|[^/]+\/)/, "");
204
+ // Normalize punctuation variations like 5p3 -> 5.3 or v4p1 -> v4.1
205
+ return bare.replace(/([a-z0-9])p([0-9])/gi, "$1.$2").toLowerCase();
206
+ }
207
+
208
+ /**
209
+ * Resolve model capabilities (context window, tool calling, vision, reasoning)
210
+ * by looking up the model in a live or cached models.dev catalog.
211
+ *
212
+ * Normalizes model identifiers across gateway spellings:
213
+ * - `z-ai/glm-5.3-flash` matches `glm-5.3-flash`
214
+ * - `deepseek/deepseek-v4.1-flash` matches `deepseek-v4.1-flash`
215
+ * - `accounts/fireworks/models/glm-5p3-flash` matches `glm-5.3-flash`
216
+ *
217
+ * Never throws — if the catalog cannot be retrieved or the model is unknown,
218
+ * returns `undefined` so capability lookups never break caller execution.
219
+ */
220
+ export async function resolveModelCapabilities(
221
+ modelId: string,
222
+ options: ResolveModelOptions = {},
223
+ ): Promise<ModelCapabilities | undefined> {
224
+ if (!modelId) return undefined;
225
+
226
+ let catalogData: Record<string, RawProviderEntry>;
227
+
228
+ if (options.catalog) {
229
+ catalogData = options.catalog as Record<string, RawProviderEntry>;
230
+ } else {
231
+ const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
232
+ const now = Date.now();
233
+
234
+ if (cachedCatalog && now - cachedCatalog.timestamp < maxAge) {
235
+ catalogData = cachedCatalog.data;
236
+ } else {
237
+ try {
238
+ const fetcher = options.fetchImpl ?? fetch;
239
+ const res = await fetcher(options.catalogUrl ?? DEFAULT_CATALOG_URL);
240
+ if (!res.ok) return undefined;
241
+ catalogData = (await res.json()) as Record<string, RawProviderEntry>;
242
+ cachedCatalog = { timestamp: now, data: catalogData };
243
+ } catch {
244
+ // Degrade gracefully if offline or request failed
245
+ if (cachedCatalog) return extractCapabilities(modelId, cachedCatalog.data);
246
+ return undefined;
247
+ }
248
+ }
249
+ }
250
+
251
+ return extractCapabilities(modelId, catalogData);
252
+ }
253
+
254
+ function extractCapabilities(
255
+ modelId: string,
256
+ catalog: Record<string, RawProviderEntry>,
257
+ ): ModelCapabilities | undefined {
258
+ const targetNorm = normalizeId(modelId);
259
+ let bestMatch: RawModelEntry | undefined;
260
+
261
+ // Pass 1: exact match
262
+ for (const provider of Object.values(catalog)) {
263
+ if (!provider?.models) continue;
264
+ if (provider.models[modelId]) {
265
+ bestMatch = provider.models[modelId];
266
+ break;
267
+ }
268
+ }
269
+
270
+ // Pass 2: normalized match across vendor prefixes and punctuation
271
+ if (!bestMatch) {
272
+ for (const provider of Object.values(catalog)) {
273
+ if (!provider?.models) continue;
274
+ for (const [key, entry] of Object.entries(provider.models)) {
275
+ if (normalizeId(key) === targetNorm) {
276
+ bestMatch = entry;
277
+ break;
278
+ }
279
+ }
280
+ if (bestMatch) break;
281
+ }
282
+ }
283
+
284
+ if (!bestMatch) return undefined;
285
+
286
+ const inputs = bestMatch.modalities?.input ?? [];
287
+ return {
288
+ id: bestMatch.id ?? modelId,
289
+ name: bestMatch.name,
290
+ contextWindow: bestMatch.limit?.context,
291
+ maxOutput: bestMatch.limit?.output,
292
+ supportsTools: bestMatch.tool_call === true,
293
+ supportsStructuredOutput: bestMatch.structured_output === true,
294
+ supportsVision: inputs.includes("image") || bestMatch.attachment === true,
295
+ supportsReasoning: bestMatch.reasoning === true,
296
+ };
297
+ }
298
+
package/src/errors.ts CHANGED
@@ -53,6 +53,17 @@ const TRANSIENT: ReadonlySet<ErrorKind> = new Set<ErrorKind>([
53
53
  * overload are per-model-endpoint; nothing else on this list is. */
54
54
  const BACKUP_ELIGIBLE: ReadonlySet<ErrorKind> = new Set<ErrorKind>(["overload", "rate"]);
55
55
 
56
+ /** Kinds that a retry on the same endpoint can never resolve. */
57
+ const NON_RETRYABLE_KINDS: ReadonlySet<ErrorKind> = new Set<ErrorKind>([
58
+ "quota",
59
+ "context",
60
+ "model",
61
+ "entitlement",
62
+ "content",
63
+ "invalid",
64
+ "aborted",
65
+ ]);
66
+
56
67
  export function isTransient(kind: ErrorKind): boolean {
57
68
  return TRANSIENT.has(kind);
58
69
  }
@@ -61,6 +72,35 @@ export function isBackupEligible(kind: ErrorKind): boolean {
61
72
  return BACKUP_ELIGIBLE.has(kind);
62
73
  }
63
74
 
75
+ /**
76
+ * Decides whether any failure (ProviderError, thrown Error, TypeError, HTTP response)
77
+ * is worth a retry.
78
+ *
79
+ * Checks in order:
80
+ * 1. Caller aborts are never retryable.
81
+ * 2. An explicit `shouldRetry` directive on ProviderError (e.g. from server `x-should-retry`)
82
+ * 3. Non-retryable kinds (quota, context, model, entitlement, content, invalid) return false.
83
+ * 4. Excessive retry-after waits (exceeding `maxWaitMs`) return false — long waits belong to fallbacks.
84
+ * 5. Transient kinds (rate, overload, network, timeout) return true.
85
+ * 6. Transport-level network faults (fetch failed, connection lost, SSL blips) return true.
86
+ */
87
+ export function isRetryable(err: unknown, maxWaitMs = 60_000): boolean {
88
+ if (isAbort(err)) return false;
89
+
90
+ if (err instanceof ProviderError) {
91
+ if (err.shouldRetry !== undefined) return err.shouldRetry;
92
+ if (NON_RETRYABLE_KINDS.has(err.kind)) return false;
93
+ if (err.retryAfterMs !== undefined && err.retryAfterMs > maxWaitMs) return false;
94
+ return err.isTransient;
95
+ }
96
+
97
+ if (isTransportFailure(err)) return true;
98
+
99
+ const kind = classify(err);
100
+ if (NON_RETRYABLE_KINDS.has(kind)) return false;
101
+ return isTransient(kind);
102
+ }
103
+
64
104
  export class ProviderError extends Error {
65
105
  readonly provider: string;
66
106
  readonly kind: ErrorKind;
@@ -72,6 +112,8 @@ export class ProviderError extends Error {
72
112
  /** Absolute reset of the binding limit. Can be days beyond Retry-After. */
73
113
  readonly resetAtMs?: number;
74
114
  readonly window?: RateLimitWindow;
115
+ /** Explicit server directive (`x-should-retry`), overriding default classification. */
116
+ readonly shouldRetry?: boolean;
75
117
  /** The provider's own response body, truncated — the actual reason, which is
76
118
  * otherwise lost behind "400 status code (no body)". */
77
119
  readonly body?: string;
@@ -86,6 +128,7 @@ export class ProviderError extends Error {
86
128
  retryAfterMs?: number;
87
129
  resetAtMs?: number;
88
130
  window?: RateLimitWindow;
131
+ shouldRetry?: boolean;
89
132
  body?: string;
90
133
  cause?: unknown;
91
134
  } = {},
@@ -99,10 +142,12 @@ export class ProviderError extends Error {
99
142
  this.retryAfterMs = opts.retryAfterMs;
100
143
  this.resetAtMs = opts.resetAtMs;
101
144
  this.window = opts.window;
145
+ this.shouldRetry = opts.shouldRetry;
102
146
  this.body = opts.body;
103
147
  }
104
148
 
105
149
  get isTransient(): boolean {
150
+ if (this.shouldRetry !== undefined) return this.shouldRetry;
106
151
  return isTransient(this.kind);
107
152
  }
108
153
 
@@ -143,15 +188,23 @@ function readString(err: unknown, key: string): string | undefined {
143
188
  return typeof value === "string" ? value : undefined;
144
189
  }
145
190
 
191
+ function sanitizeHtmlError(text: string): string {
192
+ if (/^\s*<!doctype|^\s*<html/i.test(text) || text.includes("<title>")) {
193
+ const match = text.match(/<title>([^<]+)<\/title>/i);
194
+ if (match?.[1]) return match[1].trim();
195
+ }
196
+ return text;
197
+ }
198
+
146
199
  export function messageOf(err: unknown): string {
147
- if (err instanceof Error) return err.message;
148
- if (typeof err === "string") return err;
200
+ if (err instanceof Error) return sanitizeHtmlError(err.message);
201
+ if (typeof err === "string") return sanitizeHtmlError(err);
149
202
  const message = readString(err, "message");
150
- if (message !== undefined) return message;
203
+ if (message !== undefined) return sanitizeHtmlError(message);
151
204
  try {
152
- return JSON.stringify(err) ?? String(err);
205
+ return sanitizeHtmlError(JSON.stringify(err) ?? String(err));
153
206
  } catch {
154
- return String(err);
207
+ return sanitizeHtmlError(String(err));
155
208
  }
156
209
  }
157
210
 
@@ -190,15 +243,28 @@ const TRANSPORT_CODES: ReadonlySet<string> = new Set([
190
243
  "EHOSTUNREACH",
191
244
  "EAI_AGAIN",
192
245
  "ERR_STREAM_PREMATURE_CLOSE",
246
+ // SSL/TLS connection codes (Node, Bun, corporate proxies/firewalls)
247
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
248
+ "UNABLE_TO_GET_ISSUER_CERT",
249
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
250
+ "CERT_SIGNATURE_FAILURE",
251
+ "CERT_HAS_EXPIRED",
252
+ "CERT_NOT_YET_VALID",
253
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
254
+ "SELF_SIGNED_CERT_IN_CHAIN",
255
+ "ERR_TLS_CERT_ALTNAME_INVALID",
256
+ "HOSTNAME_MISMATCH",
257
+ "ERR_SSL_WRONG_VERSION_NUMBER",
193
258
  ]);
194
259
 
195
260
  /**
196
261
  * The same faults when only a message survives. Engine wordings differ:
197
262
  * Chromium says "Failed to fetch", Firefox "NetworkError when attempting to
198
- * fetch resource", Safari "Load failed", Node/Bun "fetch failed".
263
+ * fetch resource", Safari "Load failed", Node/Bun "fetch failed", WebKit/macOS
264
+ * "The network connection was lost".
199
265
  */
200
266
  const TRANSPORT_MESSAGES =
201
- /failed to fetch|fetch failed|network\s?error|load failed|unable to connect|socket hang up|socket (?:connection )?(?:was )?closed|connection (?:error|closed|refused|reset)|premature close|stream (?:ended|closed) unexpectedly|und_err/i;
267
+ /failed to fetch|fetch failed|network\s?error|network connection (?:was )?lost|load failed|unable to connect|socket hang up|socket (?:connection )?(?:was )?closed|connection (?:error|closed|refused|reset|lost)|premature close|stream (?:ended|closed) unexpectedly|und_err/i;
202
268
 
203
269
  /** How far up the `cause` chain to look before giving up. */
204
270
  const CAUSE_DEPTH = 5;
@@ -206,7 +272,20 @@ const CAUSE_DEPTH = 5;
206
272
  function isAbort(err: unknown): boolean {
207
273
  const name = readString(err, "name");
208
274
  const code = readString(err, "code");
209
- return name === "AbortError" || name === "APIUserAbortError" || code === "ABORT_ERR";
275
+ if (name === "AbortError" || name === "APIUserAbortError" || code === "ABORT_ERR") return true;
276
+ const msg = readString(err, "message")?.toLowerCase();
277
+ if (msg) {
278
+ if (
279
+ msg.includes("client closed request") ||
280
+ msg.includes("client cancelled request") ||
281
+ msg.includes("client canceled request") ||
282
+ msg.includes("request canceled by client") ||
283
+ msg.includes("request cancelled by client")
284
+ ) {
285
+ return true;
286
+ }
287
+ }
288
+ return false;
210
289
  }
211
290
 
212
291
  /**
@@ -259,6 +338,10 @@ const CONTEXT_PATTERNS: readonly RegExp[] = [
259
338
  /input is too long/i,
260
339
  /too many (?:input )?tokens/i,
261
340
  /reduce the length of the (?:messages|prompt|input)/i,
341
+ // Anthropic's exact wording when input + max_tokens overflows context limit:
342
+ // "input length and `max_tokens` exceed context limit: 188059 + 20000 > 200000"
343
+ /input length and `?max_tokens`? exceed context limit/i,
344
+ /exceed(?:s|ed)? (?:the )?context limit/i,
262
345
  // Scoped to the thing that overflowed: a bare "exceeds the maximum" also
263
346
  // covers image counts and per-minute token rates, which compaction can't fix.
264
347
  /exceeds? the (?:model'?s? )?maximum (?:input |prompt |context )?(?:tokens?|length|context)/i,
@@ -271,7 +354,15 @@ const ENTITLEMENT_PATTERNS: readonly RegExp[] = [
271
354
  /plan does(?:n't| not) (?:include|support)/i,
272
355
  /not included (?:in|with) your .{0,40}plan/i,
273
356
  /upgrade to [\w ]{1,30}(?:or higher|plan)/i,
357
+ /requires? (?:the |a |an )?[\w\s]{1,30}plan/i,
358
+ /requires? (?:a )?subscription/i,
359
+ /subscription[_\s]?required/i,
360
+ /upgrade for access/i,
274
361
  /no api access/i,
362
+ /OAuth authentication is currently not allowed for this organization/i,
363
+ // The ChatGPT backend's error code, which arrives with no prose at all.
364
+ /usage_not_included/i,
365
+ /usage not included/i,
275
366
  ];
276
367
 
277
368
  // Balance or usage window used up. Chinese-market providers report it in
@@ -281,7 +372,8 @@ const QUOTA_PATTERNS: readonly RegExp[] = [
281
372
  /exceeded your current quota/i,
282
373
  /insufficient (?:balance|credits?)/i,
283
374
  /credit balance is too low/i,
284
- /(?:no|out of) credits/i,
375
+ /(?:no|any|out of) credits/i,
376
+ /(?:credits?|balance) depleted/i,
285
377
  // Both word orders occur in the wild.
286
378
  /usage limits? (?:reached|exceeded|hit)/i,
287
379
  /reached your (?:usage|weekly|monthly|daily) limit/i,
@@ -290,6 +382,11 @@ const QUOTA_PATTERNS: readonly RegExp[] = [
290
382
  /upgrade your plan/i,
291
383
  /quota\b[^.]{0,40}\b(?:exhausted|exceeded|will be refreshed)/i,
292
384
  /balance (?:is )?(?:too low|not enough|insufficient)/i,
385
+ // The OpenAI-shape `type` field for billing failures, serialized into the
386
+ // scanned text — a quota signal with no prose wording at all (codex-router).
387
+ /billing_error/i,
388
+ // Alibaba intl's English spelling of 欠费.
389
+ /\barrears?\b/i,
293
390
  /余额不足/,
294
391
  /欠费/,
295
392
  /额度(?:不足|已用完)/,
@@ -302,17 +399,26 @@ const MODEL_PATTERNS: readonly RegExp[] = [
302
399
  ];
303
400
 
304
401
  const AUTH_PATTERNS: readonly RegExp[] = [
305
- /invalid (?:x-api-key|api[ _-]?key|token|credentials?)/i,
402
+ /invalid[_\s]?(?:x-api-key|api[ _-]?key|token|credentials?)/i,
306
403
  /api[ _-]?key (?:is )?(?:not valid|invalid|incorrect)/i,
307
404
  /unauthorized|UNAUTHENTICATED|PERMISSION_DENIED/i,
308
405
  /authentication[_\s](?:failed|invalid|error)/i,
309
406
  /account (?:disabled|suspended|deactivated|banned)/i,
407
+ /unrecognizedclient|unauthorizedexception|accessdeniedexception/i,
408
+ /expired[_\s]?token/i,
409
+ /OAuth token has been revoked/i,
310
410
  ];
311
411
 
312
412
  const CONTENT_PATTERNS: readonly RegExp[] = [
313
413
  /content[_\s]filter/i,
314
414
  /content policy/i,
315
- /safety|PROHIBITED_CONTENT|blocked|refusal/i,
415
+ /cyber[_\s]policy/i,
416
+ /safety|PROHIBITED_CONTENT|blocked|refusal|policy violation|misalignment/i,
417
+ // Document and media boundary violations
418
+ /maximum of \d+ PDF pages/i,
419
+ /PDF specified is password protected/i,
420
+ /image(?:s)? exceed(?:s)? .* maximum/i,
421
+ /image dimensions exceed/i,
316
422
  ];
317
423
 
318
424
  /**
@@ -326,8 +432,19 @@ const CONTENT_PATTERNS: readonly RegExp[] = [
326
432
  */
327
433
  const RATE_PATTERNS: readonly RegExp[] = [
328
434
  /rate[_\s-]?limit/i,
329
- /too many requests/i,
330
- /RESOURCE_EXHAUSTED/,
435
+ // Spaces, snake and kebab all occur in the wild — opencode's list carries
436
+ // the snake form for providers that forward Google/gRPC error codes raw.
437
+ /too[_\s-]?many[_\s-]?requests/i,
438
+ // Google's quota code, upper-case in the status enum and snake_case in the
439
+ // type field (codex-router reads the latter) — a per-minute quota is a
440
+ // throttle, so it lands on rate, not quota.
441
+ /resource[_\s]?exhausted/i,
442
+ // AWS Bedrock throttles by exception name (ThrottlingException); the bare
443
+ // word also arrives alone.
444
+ /throttl(?:e|ed|ing)/i,
445
+ // A per-minute window named without the word "rate" — Anthropic's TPM prose
446
+ // ("Limit 30000 tokens per min") and gateway RPM counters.
447
+ /(?:requests?|tokens?) per (?:second|sec|minute|min)\b/i,
331
448
  ];
332
449
 
333
450
  /** Theirs and temporary, said in words rather than a status. Gemini reports
@@ -340,7 +457,11 @@ const OVERLOAD_PATTERNS: readonly RegExp[] = [
340
457
  // does not swallow the word in ordinary prose.
341
458
  /\bINTERNAL\b/,
342
459
  /\bcapacity\b/i,
460
+ /server is busy/i,
461
+ /high demand/i,
343
462
  /"code"\s*:\s*5\d\d/,
463
+ // OpenAI's 5xx wording, often forwarded by gateways with the status lost.
464
+ /(?:try|retry) your request again/i,
344
465
  ];
345
466
 
346
467
  const matches = (patterns: readonly RegExp[], text: string): boolean =>
@@ -446,6 +567,53 @@ export function parseRetryAfterMs(err: unknown, body?: string): number | undefin
446
567
  return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
447
568
  }
448
569
 
570
+ export interface ContextOverflowInfo {
571
+ inputTokens?: number;
572
+ maxTokens?: number;
573
+ contextLimit: number;
574
+ excessTokens: number;
575
+ }
576
+
577
+ /**
578
+ * Extracts context overflow details from provider error messages.
579
+ *
580
+ * Covers:
581
+ * - Anthropic / Bedrock: `input length and \`max_tokens\` exceed context limit: 188059 + 20000 > 200000`
582
+ * - Standard / Gateway: `prompt is too long: 137500 tokens > 135000 maximum`
583
+ */
584
+ export function parseContextOverflow(err: unknown, body?: string): ContextOverflowInfo | undefined {
585
+ const text = body ?? bodyTextOf(err) ?? messageOf(err);
586
+ if (!text) return undefined;
587
+
588
+ // Anthropic / Bedrock: "input length and `max_tokens` exceed context limit: 188059 + 20000 > 200000"
589
+ const m1 = /(?:input length and [`']?max_tokens[`']? exceed context limit|exceeds context limit):\s*(\d+)\s*\+\s*(\d+)\s*>\s*(\d+)/i.exec(text);
590
+ if (m1) {
591
+ const input = parseInt(m1[1]!, 10);
592
+ const max = parseInt(m1[2]!, 10);
593
+ const limit = parseInt(m1[3]!, 10);
594
+ return {
595
+ inputTokens: input,
596
+ maxTokens: max,
597
+ contextLimit: limit,
598
+ excessTokens: input + max - limit,
599
+ };
600
+ }
601
+
602
+ // "prompt is too long: 137500 tokens > 135000 maximum"
603
+ const m2 = /(?:prompt is too long|context length exceeded|maximum context length is \d+ tokens?)[^:]*:\s*(\d+)\s*(?:tokens)?\s*>\s*(\d+)/i.exec(text);
604
+ if (m2) {
605
+ const actual = parseInt(m2[1]!, 10);
606
+ const limit = parseInt(m2[2]!, 10);
607
+ return {
608
+ inputTokens: actual,
609
+ contextLimit: limit,
610
+ excessTokens: actual - limit,
611
+ };
612
+ }
613
+
614
+ return undefined;
615
+ }
616
+
449
617
  /**
450
618
  * A failure the backend reports INSIDE an already-200 stream.
451
619
  *