llm-errors 0.1.4 → 0.1.5
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.
- package/CHANGELOG.md +24 -0
- package/README.md +14 -5
- package/dist/index.cjs +133 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.js +133 -31
- package/dist/index.js.map +1 -1
- package/fixtures/README.md +5 -2
- package/fixtures/cases/anthropic/rate-limit-credit-balance.json +17 -0
- package/fixtures/cases/anthropic/sdk-billing-error.json +14 -0
- package/fixtures/cases/gemini/plain-unavailable-body.json +9 -0
- package/fixtures/cases/gemini/rpc-quota-exhausted-billing.json +11 -0
- package/fixtures/cases/gemini/rpc-quota-exhausted-rate-bucket.json +11 -0
- package/fixtures/cases/generic/http-400-retry-after.json +15 -0
- package/fixtures/cases/generic/http-503-retry-after.json +15 -0
- package/fixtures/cases/openai/plain-rate-limit-body.json +10 -0
- package/fixtures/cases/openai/sdk-billing-hard-limit.json +12 -0
- package/fixtures/expected/anthropic/rate-limit-credit-balance.json +8 -0
- package/fixtures/expected/anthropic/sdk-billing-error.json +8 -0
- package/fixtures/expected/gemini/plain-unavailable-body.json +8 -0
- package/fixtures/expected/gemini/rpc-quota-exhausted-billing.json +8 -0
- package/fixtures/expected/gemini/rpc-quota-exhausted-rate-bucket.json +8 -0
- package/fixtures/expected/generic/http-400-retry-after.json +7 -0
- package/fixtures/expected/generic/http-503-retry-after.json +8 -0
- package/fixtures/expected/openai/plain-rate-limit-body.json +7 -0
- package/fixtures/expected/openai/sdk-billing-hard-limit.json +7 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
|
|
5
5
|
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.5] - 2026-06-07
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added fixtures for direct provider error bodies, generic HTTP `Retry-After`
|
|
14
|
+
handling, OpenAI billing hard-limit errors, Anthropic billing errors and
|
|
15
|
+
Gemini quota/billing exhaustion.
|
|
16
|
+
- Added fixtures for mixed billing/rate-limit signals, generic non-retryable
|
|
17
|
+
`Retry-After` headers and Gemini rate-bucket quota exhaustion.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Preserve `Retry-After` / `retry-after-ms` delays for provider-unknown HTTP
|
|
22
|
+
errors.
|
|
23
|
+
- Detect direct provider error bodies, case-insensitive `Map` headers and common
|
|
24
|
+
Node-style numeric or array header values.
|
|
25
|
+
- Classify more provider code/type strings without requiring an HTTP status,
|
|
26
|
+
including quota, permission, not-found, timeout, overload and server failures.
|
|
27
|
+
- Only expose provider retry delays on retryable normalized errors, validate
|
|
28
|
+
HTTP status candidates and reject invalid retry delay values.
|
|
29
|
+
|
|
7
30
|
## [0.1.4] - 2026-06-05
|
|
8
31
|
|
|
9
32
|
### Added
|
|
@@ -54,6 +77,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
54
77
|
pair arrays.
|
|
55
78
|
- Zero runtime dependencies; ESM + CJS builds with type declarations.
|
|
56
79
|
|
|
80
|
+
[Unreleased]: https://github.com/slegarraga/llm-errors/compare/v0.1.4...HEAD
|
|
57
81
|
[0.1.4]: https://github.com/slegarraga/llm-errors/releases/tag/v0.1.4
|
|
58
82
|
[0.1.3]: https://github.com/slegarraga/llm-errors/releases/tag/v0.1.3
|
|
59
83
|
[0.1.2]: https://github.com/slegarraga/llm-errors/releases/tag/v0.1.2
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
Security posture is tracked in [docs/security-posture.md](./docs/security-posture.md),
|
|
13
13
|
including CodeQL, OpenSSF Scorecard, Dependabot and branch rules.
|
|
14
14
|
|
|
15
|
-
Every LLM provider fails differently. OpenAI nests `{ error: { type, code, param } }`, Anthropic wraps `{ type: "error", error: { type } }`, Gemini speaks Google RPC status strings, and each puts retry hints in a different place. `llm-errors` collapses all of that into a single, predictable object so your retry and error-handling code stays provider-agnostic.
|
|
15
|
+
Every LLM provider fails differently. OpenAI nests `{ error: { type, code, param } }`, Anthropic wraps `{ type: "error", error: { type } }`, Gemini speaks Google RPC status strings, and each puts retry hints in a different place. Generic HTTP failures add their own wrinkles with status-only errors and `Retry-After` headers. `llm-errors` collapses all of that into a single, predictable object so your retry and error-handling code stays provider-agnostic.
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
import { normalizeError, getRetryDelayMs } from 'llm-errors';
|
|
@@ -33,7 +33,7 @@ try {
|
|
|
33
33
|
|
|
34
34
|
- **One `switch`, not three.** A `rate_limit` is a `rate_limit` whether it came from OpenAI's `code`, Anthropic's `type`, or Gemini's `RESOURCE_EXHAUSTED`.
|
|
35
35
|
- **Correct retry decisions.** `insufficient_quota` and `context_length_exceeded` look like other 4xx/429s but are _not_ worth retrying. `llm-errors` separates them out.
|
|
36
|
-
- **Honours `Retry-After
|
|
36
|
+
- **Honours `Retry-After` safely.** Reads the `Retry-After` header (seconds _or_ HTTP date), `retry-after-ms`, and Google's `RetryInfo.retryDelay` for retryable errors — then falls back to exponential backoff with jitter when none is given.
|
|
37
37
|
- **Never throws.** Feed it an SDK error, a raw `fetch` response, plain JSON, `null`, or a string — it always returns a `NormalizedError`.
|
|
38
38
|
- **Transport errors too.** Connection timeouts, resets and DNS failures (`ETIMEDOUT`, `ECONNRESET`, `AbortError`, …) have no HTTP status, yet they are retryable — `llm-errors` classifies them as `timeout` / `server_error` instead of dropping them.
|
|
39
39
|
- **Zero dependencies**, ESM + CJS, fully typed.
|
|
@@ -69,17 +69,22 @@ interface NormalizedError {
|
|
|
69
69
|
status?: number; // HTTP status, when available
|
|
70
70
|
code?: string; // provider-specific code / type
|
|
71
71
|
retryable: boolean;
|
|
72
|
-
retryAfterMs?: number; // provider-supplied delay, if any
|
|
72
|
+
retryAfterMs?: number; // provider-supplied delay for retryable errors, if any
|
|
73
73
|
raw: unknown; // the original input
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
The provider is auto-detected from
|
|
77
|
+
The provider is auto-detected from SDK errors, parsed fetch envelopes and direct provider error bodies. Pass `{ provider }` to force it when you already know which client threw or the shape is ambiguous:
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
80
|
normalizeError(err, { provider: 'anthropic' });
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
Unknown providers still get safe status-based behavior. For example, a plain
|
|
84
|
+
`{ status: 503, headers: { "Retry-After": "4" } }` normalizes to
|
|
85
|
+
`provider: "unknown"`, `category: "overloaded"`, `retryable: true` and
|
|
86
|
+
`retryAfterMs: 4000`. A non-retryable unknown status ignores the same header.
|
|
87
|
+
|
|
83
88
|
### `ErrorCategory`
|
|
84
89
|
|
|
85
90
|
| Category | Retryable | Typical cause |
|
|
@@ -98,13 +103,17 @@ normalizeError(err, { provider: 'anthropic' });
|
|
|
98
103
|
| `overloaded` | **yes** | Provider temporarily overloaded (503 / 529) |
|
|
99
104
|
| `unknown` | no | Could not be classified |
|
|
100
105
|
|
|
106
|
+
Only `rate_limit`, `server_error`, `overloaded` and `timeout` are retryable.
|
|
107
|
+
`unknown` is deliberately not retryable, so unrecognized shapes fail closed
|
|
108
|
+
instead of causing accidental retry storms.
|
|
109
|
+
|
|
101
110
|
### `isRetryableError(error, options?) => boolean`
|
|
102
111
|
|
|
103
112
|
Shorthand for `normalizeError(error).retryable`.
|
|
104
113
|
|
|
105
114
|
### `getRetryDelayMs(error, attempt, options?) => number`
|
|
106
115
|
|
|
107
|
-
Returns the delay to wait before the next attempt. If the provider supplied `retryAfterMs`, that
|
|
116
|
+
Returns the delay to wait before the next attempt. Non-retryable errors return `0`. If the provider supplied a valid `retryAfterMs`, that wins. Otherwise it computes exponential backoff `baseMs * 2 ** attempt`, capped at `maxMs`, with full jitter by default.
|
|
108
117
|
|
|
109
118
|
```ts
|
|
110
119
|
getRetryDelayMs(e, attempt, { baseMs: 500, maxMs: 60_000, jitter: 'full' });
|
package/dist/index.cjs
CHANGED
|
@@ -40,14 +40,45 @@ function firstString(...values) {
|
|
|
40
40
|
}
|
|
41
41
|
return void 0;
|
|
42
42
|
}
|
|
43
|
-
function
|
|
43
|
+
function httpStatus(value) {
|
|
44
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" && /^[1-5]\d{2}$/.test(value.trim()) ? Number(value) : void 0;
|
|
45
|
+
if (typeof numeric === "number" && Number.isInteger(numeric) && numeric >= 100 && numeric <= 599) {
|
|
46
|
+
return numeric;
|
|
47
|
+
}
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
function firstHttpStatus(...values) {
|
|
44
51
|
for (const value of values) {
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
const status = httpStatus(value);
|
|
53
|
+
if (status !== void 0) {
|
|
54
|
+
return status;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
function headerValueToString(value) {
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
64
|
+
return String(value);
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
for (const entry of value) {
|
|
68
|
+
const stringValue = headerValueToString(entry);
|
|
69
|
+
if (stringValue !== void 0) {
|
|
70
|
+
return stringValue;
|
|
71
|
+
}
|
|
47
72
|
}
|
|
48
73
|
}
|
|
49
74
|
return void 0;
|
|
50
75
|
}
|
|
76
|
+
function headerPairValue(entry, lower) {
|
|
77
|
+
if (Array.isArray(entry) && typeof entry[0] === "string" && entry[0].toLowerCase() === lower) {
|
|
78
|
+
return headerValueToString(entry[1]);
|
|
79
|
+
}
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
51
82
|
function getHeader(headers, name) {
|
|
52
83
|
if (!headers) {
|
|
53
84
|
return void 0;
|
|
@@ -55,36 +86,44 @@ function getHeader(headers, name) {
|
|
|
55
86
|
const lower = name.toLowerCase();
|
|
56
87
|
if (typeof headers.get === "function") {
|
|
57
88
|
const value = headers.get(name);
|
|
58
|
-
|
|
89
|
+
const stringValue = headerValueToString(value);
|
|
90
|
+
if (stringValue !== void 0) {
|
|
91
|
+
return stringValue;
|
|
92
|
+
}
|
|
59
93
|
}
|
|
60
|
-
if (
|
|
94
|
+
if (typeof headers[Symbol.iterator] === "function") {
|
|
61
95
|
for (const entry of headers) {
|
|
62
|
-
|
|
63
|
-
|
|
96
|
+
const value = headerPairValue(entry, lower);
|
|
97
|
+
if (value !== void 0) {
|
|
98
|
+
return value;
|
|
64
99
|
}
|
|
65
100
|
}
|
|
66
|
-
return void 0;
|
|
67
101
|
}
|
|
68
102
|
if (isObject(headers)) {
|
|
69
103
|
for (const key of Object.keys(headers)) {
|
|
70
104
|
if (key.toLowerCase() === lower) {
|
|
71
105
|
const value = headers[key];
|
|
72
|
-
return
|
|
106
|
+
return headerValueToString(value);
|
|
73
107
|
}
|
|
74
108
|
}
|
|
75
109
|
}
|
|
76
110
|
return void 0;
|
|
77
111
|
}
|
|
112
|
+
function looksLikeProviderErrorBody(value) {
|
|
113
|
+
return typeof value.type === "string" || typeof value.code === "string" || typeof value.code === "number" && typeof value.status === "string" || typeof value.status === "string" || Array.isArray(value.details) || "param" in value;
|
|
114
|
+
}
|
|
78
115
|
function readStatus(error) {
|
|
79
116
|
if (!isObject(error)) {
|
|
80
117
|
return void 0;
|
|
81
118
|
}
|
|
82
119
|
const response = isObject(error.response) ? error.response : void 0;
|
|
83
120
|
const inner = isObject(error.error) ? error.error : void 0;
|
|
84
|
-
return
|
|
121
|
+
return firstHttpStatus(
|
|
85
122
|
error.status,
|
|
86
123
|
error.statusCode,
|
|
124
|
+
looksLikeProviderErrorBody(error) ? error.code : void 0,
|
|
87
125
|
response?.status,
|
|
126
|
+
response?.statusCode,
|
|
88
127
|
// Google encodes the status as `error.code` (a numeric HTTP status).
|
|
89
128
|
inner?.code
|
|
90
129
|
);
|
|
@@ -100,10 +139,15 @@ function readErrorBody(error) {
|
|
|
100
139
|
if (!isObject(error)) {
|
|
101
140
|
return void 0;
|
|
102
141
|
}
|
|
142
|
+
const body = isObject(error.body) ? error.body : void 0;
|
|
143
|
+
const responseData = isObject(error.response) && isObject(error.response.data) ? error.response.data : void 0;
|
|
103
144
|
const candidates = [
|
|
104
145
|
isObject(error.error) && isObject(error.error.error) ? error.error.error : error.error,
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
body?.error,
|
|
147
|
+
responseData?.error,
|
|
148
|
+
body && looksLikeProviderErrorBody(body) ? body : void 0,
|
|
149
|
+
responseData && looksLikeProviderErrorBody(responseData) ? responseData : void 0,
|
|
150
|
+
looksLikeProviderErrorBody(error) ? error : void 0
|
|
107
151
|
];
|
|
108
152
|
for (const candidate of candidates) {
|
|
109
153
|
if (isObject(candidate)) {
|
|
@@ -236,6 +280,9 @@ function classifyNetworkError(error) {
|
|
|
236
280
|
}
|
|
237
281
|
|
|
238
282
|
// src/retry.ts
|
|
283
|
+
function nonNegativeFinite(value) {
|
|
284
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
285
|
+
}
|
|
239
286
|
function parseRetryAfter(value, unit = "s") {
|
|
240
287
|
if (value === void 0) {
|
|
241
288
|
return void 0;
|
|
@@ -249,6 +296,9 @@ function parseRetryAfter(value, unit = "s") {
|
|
|
249
296
|
const ms = unit === "ms" ? numeric : numeric * 1e3;
|
|
250
297
|
return ms >= 0 ? ms : void 0;
|
|
251
298
|
}
|
|
299
|
+
if (unit === "ms") {
|
|
300
|
+
return void 0;
|
|
301
|
+
}
|
|
252
302
|
const date = Date.parse(trimmed);
|
|
253
303
|
if (Number.isFinite(date)) {
|
|
254
304
|
return Math.max(0, date - Date.now());
|
|
@@ -276,8 +326,8 @@ function parseGoogleRetryDelay(details) {
|
|
|
276
326
|
}
|
|
277
327
|
if (isObject(delay)) {
|
|
278
328
|
const seconds = typeof delay.seconds === "number" ? delay.seconds : Number(delay.seconds);
|
|
279
|
-
const nanos = typeof delay.nanos === "number" ? delay.nanos :
|
|
280
|
-
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
329
|
+
const nanos = delay.nanos === void 0 ? 0 : typeof delay.nanos === "number" ? delay.nanos : Number(delay.nanos);
|
|
330
|
+
if (Number.isFinite(seconds) && seconds >= 0 && Number.isFinite(nanos) && nanos >= 0 && nanos < 1e9) {
|
|
281
331
|
return seconds * 1e3 + Math.round(nanos / 1e6);
|
|
282
332
|
}
|
|
283
333
|
}
|
|
@@ -285,13 +335,17 @@ function parseGoogleRetryDelay(details) {
|
|
|
285
335
|
return void 0;
|
|
286
336
|
}
|
|
287
337
|
function getRetryDelayMs(error, attempt, options = {}) {
|
|
288
|
-
if (
|
|
289
|
-
return
|
|
338
|
+
if (!error.retryable) {
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
const explicitDelay = nonNegativeFinite(error.retryAfterMs);
|
|
342
|
+
if (explicitDelay !== void 0) {
|
|
343
|
+
return explicitDelay;
|
|
290
344
|
}
|
|
291
|
-
const baseMs = options.baseMs ?? 500;
|
|
292
|
-
const maxMs = options.maxMs ?? 6e4;
|
|
345
|
+
const baseMs = nonNegativeFinite(options.baseMs) ?? 500;
|
|
346
|
+
const maxMs = nonNegativeFinite(options.maxMs) ?? 6e4;
|
|
293
347
|
const jitter = options.jitter ?? "full";
|
|
294
|
-
const safeAttempt = Number.isFinite(attempt) && attempt > 0 ? attempt : 0;
|
|
348
|
+
const safeAttempt = Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;
|
|
295
349
|
const exponential = Math.min(maxMs, baseMs * 2 ** safeAttempt);
|
|
296
350
|
if (jitter === "none") {
|
|
297
351
|
return exponential;
|
|
@@ -339,8 +393,10 @@ function classify(ctx) {
|
|
|
339
393
|
let category = mapped ?? baseCategoryFromStatus(ctx.status);
|
|
340
394
|
if (category === "invalid_request" && (message.includes("prompt is too long") || message.includes("maximum context") || message.includes("context window"))) {
|
|
341
395
|
category = "context_length_exceeded";
|
|
396
|
+
} else if (message.includes("credit balance") || message.includes("billing") || message.includes("insufficient quota")) {
|
|
397
|
+
category = "insufficient_quota";
|
|
342
398
|
}
|
|
343
|
-
const retryAfterMs = parseRetryAfter(firstHeader(ctx.headers, "retry-after"));
|
|
399
|
+
const retryAfterMs = parseRetryAfter(firstHeader(ctx.headers, "retry-after-ms"), "ms") ?? parseRetryAfter(firstHeader(ctx.headers, "retry-after"));
|
|
344
400
|
return { category, code: type, retryAfterMs };
|
|
345
401
|
}
|
|
346
402
|
|
|
@@ -369,6 +425,10 @@ function rpcStatus(ctx) {
|
|
|
369
425
|
}
|
|
370
426
|
return void 0;
|
|
371
427
|
}
|
|
428
|
+
function isQuotaExhaustedMessage(message) {
|
|
429
|
+
const lower = message.toLowerCase();
|
|
430
|
+
return lower.includes("quota") && (lower.includes("billing") || lower.includes("paid plan") || lower.includes("free tier") || lower.includes("check your plan") || lower.includes("upgrade"));
|
|
431
|
+
}
|
|
372
432
|
function matches2(ctx) {
|
|
373
433
|
if (rpcStatus(ctx) !== void 0) {
|
|
374
434
|
return true;
|
|
@@ -377,13 +437,31 @@ function matches2(ctx) {
|
|
|
377
437
|
}
|
|
378
438
|
function classify2(ctx) {
|
|
379
439
|
const status = rpcStatus(ctx);
|
|
440
|
+
const message = firstString(ctx.body?.message) ?? "";
|
|
441
|
+
const retryAfterMs = parseGoogleRetryDelay(ctx.body?.details) ?? parseRetryAfter(firstHeader(ctx.headers, "retry-after-ms"), "ms") ?? parseRetryAfter(firstHeader(ctx.headers, "retry-after"));
|
|
380
442
|
const mapped = status ? RPC_STATUS[status] : void 0;
|
|
381
|
-
|
|
382
|
-
|
|
443
|
+
let category = mapped ?? baseCategoryFromStatus(ctx.status);
|
|
444
|
+
if (status === "RESOURCE_EXHAUSTED" && retryAfterMs === void 0 && isQuotaExhaustedMessage(message)) {
|
|
445
|
+
category = "insufficient_quota";
|
|
446
|
+
}
|
|
383
447
|
return { category, code: status, retryAfterMs };
|
|
384
448
|
}
|
|
385
449
|
|
|
386
450
|
// src/providers/openai.ts
|
|
451
|
+
var OPENAI_CODES = /* @__PURE__ */ new Set([
|
|
452
|
+
"billing_hard_limit_reached",
|
|
453
|
+
"billing_not_active",
|
|
454
|
+
"context_length_exceeded",
|
|
455
|
+
"content_filter",
|
|
456
|
+
"content_policy_violation",
|
|
457
|
+
"insufficient_quota",
|
|
458
|
+
"invalid_api_key",
|
|
459
|
+
"model_not_found",
|
|
460
|
+
"rate_limit_exceeded"
|
|
461
|
+
]);
|
|
462
|
+
function includesAny(haystack, needles) {
|
|
463
|
+
return needles.some((needle) => haystack.includes(needle));
|
|
464
|
+
}
|
|
387
465
|
function matches3(ctx) {
|
|
388
466
|
if (firstHeader(
|
|
389
467
|
ctx.headers,
|
|
@@ -401,24 +479,46 @@ function matches3(ctx) {
|
|
|
401
479
|
return true;
|
|
402
480
|
}
|
|
403
481
|
const code = firstString(body.code);
|
|
404
|
-
return code
|
|
482
|
+
return code !== void 0 && OPENAI_CODES.has(code);
|
|
405
483
|
}
|
|
406
484
|
function classify3(ctx) {
|
|
407
485
|
const body = ctx.body ?? {};
|
|
408
486
|
const type = firstString(body.type);
|
|
409
487
|
const code = firstString(body.code);
|
|
410
|
-
const
|
|
488
|
+
const message = firstString(body.message) ?? "";
|
|
489
|
+
const identifier = `${type ?? ""} ${code ?? ""} ${message}`.toLowerCase();
|
|
411
490
|
let category = baseCategoryFromStatus(ctx.status);
|
|
412
491
|
if (identifier.includes("context_length") || identifier.includes("context window")) {
|
|
413
492
|
category = "context_length_exceeded";
|
|
414
|
-
} else if (identifier
|
|
493
|
+
} else if (includesAny(identifier, [
|
|
494
|
+
"insufficient_quota",
|
|
495
|
+
"billing_hard_limit",
|
|
496
|
+
"billing_not_active",
|
|
497
|
+
"exceeded your current quota"
|
|
498
|
+
])) {
|
|
415
499
|
category = "insufficient_quota";
|
|
416
|
-
} else if (
|
|
500
|
+
} else if (includesAny(identifier, [
|
|
501
|
+
"content_filter",
|
|
502
|
+
"content_policy",
|
|
503
|
+
"safety_policy"
|
|
504
|
+
])) {
|
|
417
505
|
category = "content_filter";
|
|
418
|
-
} else if (code === "invalid_api_key" || identifier
|
|
506
|
+
} else if (code === "invalid_api_key" || includesAny(identifier, ["authentication", "unauthorized"])) {
|
|
419
507
|
category = "authentication";
|
|
420
|
-
} else if (
|
|
508
|
+
} else if (includesAny(identifier, ["permission", "forbidden"])) {
|
|
509
|
+
category = "permission";
|
|
510
|
+
} else if (includesAny(identifier, ["not_found", "model_not_found"])) {
|
|
511
|
+
category = "not_found";
|
|
512
|
+
} else if (includesAny(identifier, ["timeout", "timed out"])) {
|
|
513
|
+
category = "timeout";
|
|
514
|
+
} else if (includesAny(identifier, ["overload", "unavailable"])) {
|
|
515
|
+
category = "overloaded";
|
|
516
|
+
} else if (includesAny(identifier, ["server_error", "api_error"])) {
|
|
517
|
+
category = "server_error";
|
|
518
|
+
} else if (identifier.includes("rate_limit")) {
|
|
421
519
|
category = "rate_limit";
|
|
520
|
+
} else if (category === "unknown" && identifier.includes("invalid_request")) {
|
|
521
|
+
category = "invalid_request";
|
|
422
522
|
}
|
|
423
523
|
const retryAfterMs = parseRetryAfter(firstHeader(ctx.headers, "retry-after-ms"), "ms") ?? parseRetryAfter(firstHeader(ctx.headers, "retry-after"));
|
|
424
524
|
return { category, code: code ?? type, retryAfterMs };
|
|
@@ -449,7 +549,8 @@ function classifyFor(provider, ctx) {
|
|
|
449
549
|
default:
|
|
450
550
|
return {
|
|
451
551
|
category: baseCategoryFromStatus(ctx.status),
|
|
452
|
-
code: firstString(ctx.body?.type, ctx.body?.code)
|
|
552
|
+
code: firstString(ctx.body?.type, ctx.body?.code),
|
|
553
|
+
retryAfterMs: parseRetryAfter(firstHeader(ctx.headers, "retry-after-ms"), "ms") ?? parseRetryAfter(firstHeader(ctx.headers, "retry-after"))
|
|
453
554
|
};
|
|
454
555
|
}
|
|
455
556
|
}
|
|
@@ -470,14 +571,15 @@ function normalizeError(error, options = {}) {
|
|
|
470
571
|
code = code ?? network.code;
|
|
471
572
|
}
|
|
472
573
|
}
|
|
574
|
+
const retryable = isRetryableCategory(category);
|
|
473
575
|
return {
|
|
474
576
|
provider,
|
|
475
577
|
category,
|
|
476
578
|
message: readMessage(error, ctx.body),
|
|
477
579
|
status: ctx.status,
|
|
478
580
|
code,
|
|
479
|
-
retryable
|
|
480
|
-
retryAfterMs: classification.retryAfterMs,
|
|
581
|
+
retryable,
|
|
582
|
+
retryAfterMs: retryable ? classification.retryAfterMs : void 0,
|
|
481
583
|
raw: error
|
|
482
584
|
};
|
|
483
585
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/internal.ts","../src/classify.ts","../src/network.ts","../src/retry.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/providers/openai.ts","../src/normalize.ts"],"sourcesContent":["export { normalizeError, isRetryableError } from './normalize.ts';\nexport {\n getRetryDelayMs,\n parseRetryAfter,\n parseGoogleRetryDelay,\n} from './retry.ts';\nexport type {\n Provider,\n ErrorCategory,\n NormalizedError,\n NormalizeOptions,\n RetryDelayOptions,\n} from './types.ts';\n","/**\n * Internal probing helpers. These intentionally accept `unknown` and never\n * throw: error objects arrive in many shapes (SDK error classes, raw `fetch`\n * responses, plain JSON) and the library must degrade gracefully on any of\n * them.\n */\n\nexport function isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Return the first argument that is a non-empty string. */\nexport function firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === 'string' && value.length > 0) {\n return value;\n }\n }\n return undefined;\n}\n\n/** Return the first argument that is a finite number. */\nexport function firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n }\n return undefined;\n}\n\n/**\n * Read a header by name from the many container shapes an error may carry:\n * a `Headers` instance, a plain object, a `Map`, or an array of `[k, v]`\n * pairs. Lookup is case-insensitive.\n */\nexport function getHeader(headers: unknown, name: string): string | undefined {\n if (!headers) {\n return undefined;\n }\n const lower = name.toLowerCase();\n\n // `Headers` (fetch) or `Map`-like: has a `.get` method.\n if (typeof (headers as { get?: unknown }).get === 'function') {\n const value = (headers as { get(key: string): unknown }).get(name);\n return typeof value === 'string' ? value : undefined;\n }\n\n // Array of [key, value] pairs.\n if (Array.isArray(headers)) {\n for (const entry of headers) {\n if (\n Array.isArray(entry) &&\n typeof entry[0] === 'string' &&\n entry[0].toLowerCase() === lower\n ) {\n return typeof entry[1] === 'string' ? entry[1] : undefined;\n }\n }\n return undefined;\n }\n\n // Plain object: case-insensitive key scan.\n if (isObject(headers)) {\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lower) {\n const value = headers[key];\n return typeof value === 'string' ? value : undefined;\n }\n }\n }\n\n return undefined;\n}\n\n/** Extract an HTTP status code from common error shapes. */\nexport function readStatus(error: unknown): number | undefined {\n if (!isObject(error)) {\n return undefined;\n }\n const response = isObject(error.response) ? error.response : undefined;\n const inner = isObject(error.error) ? error.error : undefined;\n return firstNumber(\n error.status,\n error.statusCode,\n response?.status,\n // Google encodes the status as `error.code` (a numeric HTTP status).\n inner?.code,\n );\n}\n\n/** Extract the header container from common error shapes. */\nexport function readHeaders(error: unknown): unknown {\n if (!isObject(error)) {\n return undefined;\n }\n const response = isObject(error.response) ? error.response : undefined;\n return error.headers ?? response?.headers;\n}\n\n/**\n * Extract the provider error body — the object that holds `type` / `code` /\n * `message`. SDK error classes expose it at `error.error`; raw responses may\n * nest it under `error.body.error` or `error.response.data.error`.\n */\nexport function readErrorBody(\n error: unknown,\n): Record<string, unknown> | undefined {\n if (!isObject(error)) {\n return undefined;\n }\n\n // Anthropic wraps as `{ type: 'error', error: {...} }`; OpenAI/Gemini SDK\n // error objects expose the inner payload directly at `.error`.\n const candidates: unknown[] = [\n isObject(error.error) && isObject(error.error.error)\n ? error.error.error\n : error.error,\n isObject(error.body)\n ? (error.body as Record<string, unknown>).error\n : undefined,\n isObject(error.response) && isObject(error.response.data)\n ? (error.response.data as Record<string, unknown>).error\n : undefined,\n ];\n\n for (const candidate of candidates) {\n if (isObject(candidate)) {\n return candidate;\n }\n }\n return undefined;\n}\n\n/** Best-effort human-readable message from an error of any shape. */\nexport function readMessage(\n error: unknown,\n body: Record<string, unknown> | undefined,\n): string {\n const fromBody = body ? firstString(body.message) : undefined;\n if (fromBody) {\n return fromBody;\n }\n if (isObject(error)) {\n const direct = firstString(error.message);\n if (direct) {\n return direct;\n }\n }\n if (typeof error === 'string' && error.length > 0) {\n return error;\n }\n return 'Unknown error';\n}\n","import { getHeader } from './internal.ts';\nimport type { ErrorCategory } from './types.ts';\n\n/**\n * The pre-parsed pieces of an error that every provider classifier and the\n * detector operate on. Kept internal to the package.\n */\nexport interface ProviderContext {\n status?: number;\n /** The provider error body (`{ type, code, message, ... }`), if found. */\n body: Record<string, unknown> | undefined;\n /** The raw header container (`Headers`, object, pairs), if found. */\n headers: unknown;\n}\n\n/** The result a provider classifier contributes to the normalized error. */\nexport interface Classification {\n category: ErrorCategory;\n code?: string;\n retryAfterMs?: number;\n}\n\n/**\n * Map an HTTP status code to a category, ignoring provider specifics. Provider\n * classifiers start here and then refine using their `code` / `type` strings.\n */\nexport function baseCategoryFromStatus(status?: number): ErrorCategory {\n switch (status) {\n case 401:\n return 'authentication';\n case 403:\n return 'permission';\n case 404:\n return 'not_found';\n case 408:\n return 'timeout';\n case 413:\n return 'request_too_large';\n case 400:\n case 422:\n return 'invalid_request';\n case 429:\n return 'rate_limit';\n case 500:\n return 'server_error';\n case 502:\n return 'server_error';\n case 503:\n return 'overloaded';\n case 504:\n return 'timeout';\n case 529:\n return 'overloaded';\n default:\n break;\n }\n if (typeof status === 'number') {\n if (status >= 500) {\n return 'server_error';\n }\n if (status >= 400) {\n return 'invalid_request';\n }\n }\n return 'unknown';\n}\n\n/** Categories that are safe to retry after a delay. */\nconst RETRYABLE: ReadonlySet<ErrorCategory> = new Set<ErrorCategory>([\n 'rate_limit',\n 'server_error',\n 'overloaded',\n 'timeout',\n]);\n\n/** Whether a category represents a transient, retryable condition. */\nexport function isRetryableCategory(category: ErrorCategory): boolean {\n return RETRYABLE.has(category);\n}\n\n/** Read the first present header from a list of candidate names. */\nexport function firstHeader(\n headers: unknown,\n ...names: string[]\n): string | undefined {\n for (const name of names) {\n const value = getHeader(headers, name);\n if (value !== undefined) {\n return value;\n }\n }\n return undefined;\n}\n","import { firstString, isObject } from './internal.ts';\nimport type { ErrorCategory } from './types.ts';\n\n/**\n * Transport-level failures never reach an HTTP response, so they carry no\n * status code or provider body — yet most of them (timeouts, dropped\n * connections, DNS hiccups) are very much worth retrying. This recognizes\n * them from the Node `code`, the `name`, or the SDK error class name.\n */\n\n/** Node `error.code` values that mean \"the connection failed; try again\". */\nconst RETRYABLE_CODES: Record<string, ErrorCategory> = {\n ETIMEDOUT: 'timeout',\n ESOCKETTIMEDOUT: 'timeout',\n ECONNRESET: 'server_error',\n ECONNREFUSED: 'server_error',\n ECONNABORTED: 'server_error',\n EPIPE: 'server_error',\n ENOTFOUND: 'server_error',\n EAI_AGAIN: 'server_error',\n EHOSTUNREACH: 'server_error',\n ENETUNREACH: 'server_error',\n};\n\n/**\n * Error `name` / constructor names, matched case-insensitively as substrings,\n * mapped to a category. Covers `AbortError`, the Fetch `TimeoutError`, and the\n * OpenAI / Anthropic SDK connection error classes.\n */\nconst NAME_PATTERNS: ReadonlyArray<[pattern: string, category: ErrorCategory]> =\n [\n ['timeout', 'timeout'],\n ['aborterror', 'timeout'],\n ['connectionerror', 'server_error'],\n ['connection error', 'server_error'],\n ['fetcherror', 'server_error'],\n ];\n\nexport interface NetworkClassification {\n category: ErrorCategory;\n code?: string;\n}\n\n/**\n * Try to classify a transport-level error. Returns `undefined` when the value\n * does not look like a network failure, so the caller can fall back to its\n * normal (HTTP-based) classification.\n */\nexport function classifyNetworkError(\n error: unknown,\n): NetworkClassification | undefined {\n if (!isObject(error)) {\n return undefined;\n }\n\n const code = firstString(error.code);\n if (code && code in RETRYABLE_CODES) {\n return { category: RETRYABLE_CODES[code], code };\n }\n\n // Check both the instance `name` and the constructor name: a subclass of\n // `Error` that doesn't override `name` still reports `\"Error\"`, so the\n // distinguishing signal lives on `constructor.name`.\n const names = [\n firstString(error.name),\n firstString((error.constructor as { name?: unknown } | undefined)?.name),\n ];\n for (const name of names) {\n if (!name) {\n continue;\n }\n const haystack = name.toLowerCase();\n for (const [pattern, category] of NAME_PATTERNS) {\n if (haystack.includes(pattern)) {\n return { category, code: name };\n }\n }\n }\n\n return undefined;\n}\n","import { isObject } from './internal.ts';\nimport type { NormalizedError, RetryDelayOptions } from './types.ts';\n\n/**\n * Parse a `Retry-After` header value into milliseconds.\n *\n * Per RFC 7231 the value is either a number of seconds (`\"30\"`) or an\n * HTTP date (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Some providers also send a\n * fractional `retry-after-ms` value, which is accepted when `unit` is `'ms'`.\n */\nexport function parseRetryAfter(\n value: string | undefined,\n unit: 's' | 'ms' = 's',\n): number | undefined {\n if (value === undefined) {\n return undefined;\n }\n const trimmed = value.trim();\n if (trimmed === '') {\n return undefined;\n }\n\n const numeric = Number(trimmed);\n if (Number.isFinite(numeric)) {\n const ms = unit === 'ms' ? numeric : numeric * 1000;\n return ms >= 0 ? ms : undefined;\n }\n\n const date = Date.parse(trimmed);\n if (Number.isFinite(date)) {\n return Math.max(0, date - Date.now());\n }\n\n return undefined;\n}\n\n/**\n * Parse a Google `RetryInfo.retryDelay` value into milliseconds.\n *\n * Google encodes it either as a duration string (`\"30s\"`, `\"1.5s\"`) or as a\n * `{ seconds, nanos }` object inside `error.details[]`.\n */\nexport function parseGoogleRetryDelay(details: unknown): number | undefined {\n if (!Array.isArray(details)) {\n return undefined;\n }\n for (const detail of details) {\n if (!isObject(detail)) {\n continue;\n }\n const type = detail['@type'];\n if (typeof type === 'string' && !type.includes('RetryInfo')) {\n continue;\n }\n const delay = detail.retryDelay;\n if (typeof delay === 'string') {\n const seconds = Number(delay.replace(/s$/, ''));\n if (Number.isFinite(seconds) && seconds >= 0) {\n return seconds * 1000;\n }\n }\n if (isObject(delay)) {\n const seconds =\n typeof delay.seconds === 'number'\n ? delay.seconds\n : Number(delay.seconds);\n const nanos = typeof delay.nanos === 'number' ? delay.nanos : 0;\n if (Number.isFinite(seconds) && seconds >= 0) {\n return seconds * 1000 + Math.round(nanos / 1e6);\n }\n }\n }\n return undefined;\n}\n\n/**\n * Suggested delay before retrying, in milliseconds.\n *\n * When the provider supplied an explicit delay (`error.retryAfterMs`) it is\n * always respected. Otherwise this falls back to exponential backoff:\n * `baseMs * 2 ** attempt`, capped at `maxMs`, with optional full jitter.\n *\n * @param error A {@link NormalizedError}.\n * @param attempt Zero-based retry attempt number (0 for the first retry).\n * @param options Backoff tuning. See {@link RetryDelayOptions}.\n */\nexport function getRetryDelayMs(\n error: NormalizedError,\n attempt: number,\n options: RetryDelayOptions = {},\n): number {\n if (typeof error.retryAfterMs === 'number') {\n return error.retryAfterMs;\n }\n\n const baseMs = options.baseMs ?? 500;\n const maxMs = options.maxMs ?? 60000;\n const jitter = options.jitter ?? 'full';\n\n const safeAttempt = Number.isFinite(attempt) && attempt > 0 ? attempt : 0;\n const exponential = Math.min(maxMs, baseMs * 2 ** safeAttempt);\n\n if (jitter === 'none') {\n return exponential;\n }\n return Math.random() * exponential;\n}\n","import {\n baseCategoryFromStatus,\n firstHeader,\n type Classification,\n type ProviderContext,\n} from '../classify.ts';\nimport { firstString } from '../internal.ts';\nimport { parseRetryAfter } from '../retry.ts';\nimport type { ErrorCategory } from '../types.ts';\n\nconst ANTHROPIC_TYPES: Record<string, ErrorCategory> = {\n authentication_error: 'authentication',\n permission_error: 'permission',\n not_found_error: 'not_found',\n request_too_large: 'request_too_large',\n rate_limit_error: 'rate_limit',\n invalid_request_error: 'invalid_request',\n api_error: 'server_error',\n overloaded_error: 'overloaded',\n billing_error: 'insufficient_quota',\n timeout_error: 'timeout',\n};\n\n/** Heuristic: does this error look like it came from the Anthropic API? */\nexport function matches(ctx: ProviderContext): boolean {\n if (\n firstHeader(\n ctx.headers,\n 'anthropic-version',\n 'anthropic-ratelimit-requests-limit',\n 'anthropic-ratelimit-tokens-limit',\n ) !== undefined\n ) {\n return true;\n }\n const body = ctx.body;\n if (!body) {\n return false;\n }\n // `param` is an OpenAI-only field; its presence rules Anthropic out even\n // though both providers share type strings like `invalid_request_error`.\n if ('param' in body) {\n return false;\n }\n const type = firstString(body.type);\n return type !== undefined && type in ANTHROPIC_TYPES;\n}\n\nexport function classify(ctx: ProviderContext): Classification {\n const body = ctx.body ?? {};\n const type = firstString(body.type);\n const message = (firstString(body.message) ?? '').toLowerCase();\n\n const mapped = type ? ANTHROPIC_TYPES[type] : undefined;\n let category: ErrorCategory = mapped ?? baseCategoryFromStatus(ctx.status);\n\n // Anthropic reports an over-long prompt as `invalid_request_error` with a\n // \"prompt is too long\" message rather than a dedicated type.\n if (\n category === 'invalid_request' &&\n (message.includes('prompt is too long') ||\n message.includes('maximum context') ||\n message.includes('context window'))\n ) {\n category = 'context_length_exceeded';\n }\n\n const retryAfterMs = parseRetryAfter(firstHeader(ctx.headers, 'retry-after'));\n\n return { category, code: type, retryAfterMs };\n}\n","import {\n baseCategoryFromStatus,\n firstHeader,\n type Classification,\n type ProviderContext,\n} from '../classify.ts';\nimport { firstString } from '../internal.ts';\nimport { parseGoogleRetryDelay, parseRetryAfter } from '../retry.ts';\nimport type { ErrorCategory } from '../types.ts';\n\n/** Canonical google.rpc.Code names → provider-agnostic categories. */\nconst RPC_STATUS: Record<string, ErrorCategory> = {\n UNAUTHENTICATED: 'authentication',\n PERMISSION_DENIED: 'permission',\n NOT_FOUND: 'not_found',\n INVALID_ARGUMENT: 'invalid_request',\n FAILED_PRECONDITION: 'invalid_request',\n OUT_OF_RANGE: 'invalid_request',\n UNIMPLEMENTED: 'invalid_request',\n RESOURCE_EXHAUSTED: 'rate_limit',\n INTERNAL: 'server_error',\n UNKNOWN: 'server_error',\n ABORTED: 'server_error',\n DATA_LOSS: 'server_error',\n UNAVAILABLE: 'overloaded',\n DEADLINE_EXCEEDED: 'timeout',\n CANCELLED: 'timeout',\n};\n\nfunction rpcStatus(ctx: ProviderContext): string | undefined {\n const status = firstString(ctx.body?.status);\n if (status && /^[A-Z][A-Z_]+$/.test(status)) {\n return status;\n }\n return undefined;\n}\n\n/** Heuristic: does this error look like it came from the Gemini API? */\nexport function matches(ctx: ProviderContext): boolean {\n if (rpcStatus(ctx) !== undefined) {\n return true;\n }\n // A numeric `code` alongside a `details` array is the Google error envelope.\n return typeof ctx.body?.code === 'number' && Array.isArray(ctx.body?.details);\n}\n\nexport function classify(ctx: ProviderContext): Classification {\n const status = rpcStatus(ctx);\n\n const mapped = status ? RPC_STATUS[status] : undefined;\n const category: ErrorCategory = mapped ?? baseCategoryFromStatus(ctx.status);\n\n const retryAfterMs =\n parseGoogleRetryDelay(ctx.body?.details) ??\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after'));\n\n return { category, code: status, retryAfterMs };\n}\n","import {\n baseCategoryFromStatus,\n firstHeader,\n type Classification,\n type ProviderContext,\n} from '../classify.ts';\nimport { firstString } from '../internal.ts';\nimport { parseRetryAfter } from '../retry.ts';\nimport type { ErrorCategory } from '../types.ts';\n\n/** Heuristic: does this error look like it came from the OpenAI API? */\nexport function matches(ctx: ProviderContext): boolean {\n if (\n firstHeader(\n ctx.headers,\n 'openai-organization',\n 'openai-version',\n 'openai-processing-ms',\n ) !== undefined\n ) {\n return true;\n }\n const body = ctx.body;\n if (!body) {\n return false;\n }\n // OpenAI error objects carry a `param` field; Anthropic and Gemini do not.\n if ('param' in body) {\n return true;\n }\n const code = firstString(body.code);\n return (\n code === 'context_length_exceeded' ||\n code === 'insufficient_quota' ||\n code === 'invalid_api_key'\n );\n}\n\nexport function classify(ctx: ProviderContext): Classification {\n const body = ctx.body ?? {};\n const type = firstString(body.type);\n const code = firstString(body.code);\n const identifier = `${type ?? ''} ${code ?? ''}`.toLowerCase();\n\n let category: ErrorCategory = baseCategoryFromStatus(ctx.status);\n\n if (\n identifier.includes('context_length') ||\n identifier.includes('context window')\n ) {\n category = 'context_length_exceeded';\n } else if (identifier.includes('insufficient_quota')) {\n category = 'insufficient_quota';\n } else if (\n identifier.includes('content_filter') ||\n identifier.includes('content_policy')\n ) {\n category = 'content_filter';\n } else if (\n code === 'invalid_api_key' ||\n identifier.includes('authentication')\n ) {\n category = 'authentication';\n } else if (category === 'unknown' && identifier.includes('rate_limit')) {\n category = 'rate_limit';\n }\n\n const retryAfterMs =\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after-ms'), 'ms') ??\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after'));\n\n return { category, code: code ?? type, retryAfterMs };\n}\n","import {\n baseCategoryFromStatus,\n isRetryableCategory,\n type Classification,\n type ProviderContext,\n} from './classify.ts';\nimport {\n firstString,\n readErrorBody,\n readHeaders,\n readMessage,\n readStatus,\n} from './internal.ts';\nimport { classifyNetworkError } from './network.ts';\nimport * as anthropic from './providers/anthropic.ts';\nimport * as gemini from './providers/gemini.ts';\nimport * as openai from './providers/openai.ts';\nimport type { NormalizedError, NormalizeOptions, Provider } from './types.ts';\n\n/**\n * Detection order is deliberate: Gemini's canonical RPC status string is the\n * most distinctive signal, then Anthropic's typed errors / headers, then\n * OpenAI (whose `param` field and headers are the remaining tell).\n */\nconst DETECTORS: ReadonlyArray<{\n name: Provider;\n matches: (ctx: ProviderContext) => boolean;\n}> = [\n { name: 'gemini', matches: gemini.matches },\n { name: 'anthropic', matches: anthropic.matches },\n { name: 'openai', matches: openai.matches },\n];\n\nfunction detectProvider(ctx: ProviderContext): Provider {\n for (const detector of DETECTORS) {\n if (detector.matches(ctx)) {\n return detector.name;\n }\n }\n return 'unknown';\n}\n\nfunction classifyFor(provider: Provider, ctx: ProviderContext): Classification {\n switch (provider) {\n case 'openai':\n return openai.classify(ctx);\n case 'anthropic':\n return anthropic.classify(ctx);\n case 'gemini':\n return gemini.classify(ctx);\n default:\n return {\n category: baseCategoryFromStatus(ctx.status),\n code: firstString(ctx.body?.type, ctx.body?.code),\n };\n }\n}\n\n/**\n * Normalize an error thrown by an OpenAI, Anthropic or Gemini client into a\n * single consistent {@link NormalizedError} shape.\n *\n * Accepts SDK error objects, raw `fetch` responses with a parsed body, or\n * plain JSON. It never throws: anything unrecognized comes back as\n * `{ provider: 'unknown', category: 'unknown', retryable: false }`.\n *\n * @example\n * ```ts\n * try {\n * await client.messages.create(params);\n * } catch (err) {\n * const e = normalizeError(err);\n * if (e.retryable) await sleep(e.retryAfterMs ?? 1000);\n * }\n * ```\n */\nexport function normalizeError(\n error: unknown,\n options: NormalizeOptions = {},\n): NormalizedError {\n const ctx: ProviderContext = {\n status: readStatus(error),\n headers: readHeaders(error),\n body: readErrorBody(error),\n };\n\n const provider = options.provider ?? detectProvider(ctx);\n const classification = classifyFor(provider, ctx);\n\n let category = classification.category;\n let code = classification.code;\n\n // No HTTP response reached us: this may be a transport-level failure\n // (timeout, dropped connection) that is retryable despite having no status.\n if (category === 'unknown' && ctx.status === undefined) {\n const network = classifyNetworkError(error);\n if (network) {\n category = network.category;\n code = code ?? network.code;\n }\n }\n\n return {\n provider,\n category,\n message: readMessage(error, ctx.body),\n status: ctx.status,\n code,\n retryable: isRetryableCategory(category),\n retryAfterMs: classification.retryAfterMs,\n raw: error,\n };\n}\n\n/**\n * Convenience wrapper around {@link normalizeError} that returns only whether\n * the error is worth retrying.\n */\nexport function isRetryableError(\n error: unknown,\n options: NormalizeOptions = {},\n): boolean {\n return normalizeError(error, options).retryable;\n}\n"],"mappings":"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","names":["matches","classify","matches","classify","matches","classify"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/internal.ts","../src/classify.ts","../src/network.ts","../src/retry.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/providers/openai.ts","../src/normalize.ts"],"sourcesContent":["export { normalizeError, isRetryableError } from './normalize.ts';\nexport {\n getRetryDelayMs,\n parseRetryAfter,\n parseGoogleRetryDelay,\n} from './retry.ts';\nexport type {\n Provider,\n ErrorCategory,\n NormalizedError,\n NormalizeOptions,\n RetryDelayOptions,\n} from './types.ts';\n","/**\n * Internal probing helpers. These intentionally accept `unknown` and never\n * throw: error objects arrive in many shapes (SDK error classes, raw `fetch`\n * responses, plain JSON) and the library must degrade gracefully on any of\n * them.\n */\n\nexport function isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Return the first argument that is a non-empty string. */\nexport function firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === 'string' && value.length > 0) {\n return value;\n }\n }\n return undefined;\n}\n\n/** Return the first argument that is a finite number. */\nexport function firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n }\n return undefined;\n}\n\nfunction httpStatus(value: unknown): number | undefined {\n const numeric =\n typeof value === 'number'\n ? value\n : typeof value === 'string' && /^[1-5]\\d{2}$/.test(value.trim())\n ? Number(value)\n : undefined;\n\n if (\n typeof numeric === 'number' &&\n Number.isInteger(numeric) &&\n numeric >= 100 &&\n numeric <= 599\n ) {\n return numeric;\n }\n return undefined;\n}\n\nfunction firstHttpStatus(...values: unknown[]): number | undefined {\n for (const value of values) {\n const status = httpStatus(value);\n if (status !== undefined) {\n return status;\n }\n }\n return undefined;\n}\n\nfunction headerValueToString(value: unknown): string | undefined {\n if (typeof value === 'string') {\n return value;\n }\n if (typeof value === 'number' && Number.isFinite(value)) {\n return String(value);\n }\n if (Array.isArray(value)) {\n for (const entry of value) {\n const stringValue = headerValueToString(entry);\n if (stringValue !== undefined) {\n return stringValue;\n }\n }\n }\n return undefined;\n}\n\nfunction headerPairValue(entry: unknown, lower: string): string | undefined {\n if (\n Array.isArray(entry) &&\n typeof entry[0] === 'string' &&\n entry[0].toLowerCase() === lower\n ) {\n return headerValueToString(entry[1]);\n }\n return undefined;\n}\n\n/**\n * Read a header by name from the many container shapes an error may carry:\n * a `Headers` instance, a plain object, a `Map`, or an array of `[k, v]`\n * pairs. Lookup is case-insensitive and accepts common Node-style values such\n * as numbers or string arrays.\n */\nexport function getHeader(headers: unknown, name: string): string | undefined {\n if (!headers) {\n return undefined;\n }\n const lower = name.toLowerCase();\n\n // `Headers` (fetch) or `Map`-like: has a `.get` method.\n if (typeof (headers as { get?: unknown }).get === 'function') {\n const value = (headers as { get(key: string): unknown }).get(name);\n const stringValue = headerValueToString(value);\n if (stringValue !== undefined) {\n return stringValue;\n }\n }\n\n // Iterable containers (`Map`, `Headers`, arrays of [key, value] pairs).\n if (\n typeof (headers as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===\n 'function'\n ) {\n for (const entry of headers as Iterable<unknown>) {\n const value = headerPairValue(entry, lower);\n if (value !== undefined) {\n return value;\n }\n }\n }\n\n // Plain object: case-insensitive key scan.\n if (isObject(headers)) {\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lower) {\n const value = headers[key];\n return headerValueToString(value);\n }\n }\n }\n\n return undefined;\n}\n\nfunction looksLikeProviderErrorBody(value: Record<string, unknown>): boolean {\n return (\n typeof value.type === 'string' ||\n typeof value.code === 'string' ||\n (typeof value.code === 'number' && typeof value.status === 'string') ||\n typeof value.status === 'string' ||\n Array.isArray(value.details) ||\n 'param' in value\n );\n}\n\n/** Extract an HTTP status code from common error shapes. */\nexport function readStatus(error: unknown): number | undefined {\n if (!isObject(error)) {\n return undefined;\n }\n const response = isObject(error.response) ? error.response : undefined;\n const inner = isObject(error.error) ? error.error : undefined;\n return firstHttpStatus(\n error.status,\n error.statusCode,\n looksLikeProviderErrorBody(error) ? error.code : undefined,\n response?.status,\n response?.statusCode,\n // Google encodes the status as `error.code` (a numeric HTTP status).\n inner?.code,\n );\n}\n\n/** Extract the header container from common error shapes. */\nexport function readHeaders(error: unknown): unknown {\n if (!isObject(error)) {\n return undefined;\n }\n const response = isObject(error.response) ? error.response : undefined;\n return error.headers ?? response?.headers;\n}\n\n/**\n * Extract the provider error body — the object that holds `type` / `code` /\n * `message`. SDK error classes expose it at `error.error`; raw responses may\n * nest it under `error.body.error` or `error.response.data.error`.\n */\nexport function readErrorBody(\n error: unknown,\n): Record<string, unknown> | undefined {\n if (!isObject(error)) {\n return undefined;\n }\n const body = isObject(error.body) ? error.body : undefined;\n const responseData =\n isObject(error.response) && isObject(error.response.data)\n ? error.response.data\n : undefined;\n\n // Anthropic wraps as `{ type: 'error', error: {...} }`; OpenAI/Gemini SDK\n // error objects expose the inner payload directly at `.error`.\n const candidates: unknown[] = [\n isObject(error.error) && isObject(error.error.error)\n ? error.error.error\n : error.error,\n body?.error,\n responseData?.error,\n body && looksLikeProviderErrorBody(body) ? body : undefined,\n responseData && looksLikeProviderErrorBody(responseData)\n ? responseData\n : undefined,\n looksLikeProviderErrorBody(error) ? error : undefined,\n ];\n\n for (const candidate of candidates) {\n if (isObject(candidate)) {\n return candidate;\n }\n }\n return undefined;\n}\n\n/** Best-effort human-readable message from an error of any shape. */\nexport function readMessage(\n error: unknown,\n body: Record<string, unknown> | undefined,\n): string {\n const fromBody = body ? firstString(body.message) : undefined;\n if (fromBody) {\n return fromBody;\n }\n if (isObject(error)) {\n const direct = firstString(error.message);\n if (direct) {\n return direct;\n }\n }\n if (typeof error === 'string' && error.length > 0) {\n return error;\n }\n return 'Unknown error';\n}\n","import { getHeader } from './internal.ts';\nimport type { ErrorCategory } from './types.ts';\n\n/**\n * The pre-parsed pieces of an error that every provider classifier and the\n * detector operate on. Kept internal to the package.\n */\nexport interface ProviderContext {\n status?: number;\n /** The provider error body (`{ type, code, message, ... }`), if found. */\n body: Record<string, unknown> | undefined;\n /** The raw header container (`Headers`, object, pairs), if found. */\n headers: unknown;\n}\n\n/** The result a provider classifier contributes to the normalized error. */\nexport interface Classification {\n category: ErrorCategory;\n code?: string;\n retryAfterMs?: number;\n}\n\n/**\n * Map an HTTP status code to a category, ignoring provider specifics. Provider\n * classifiers start here and then refine using their `code` / `type` strings.\n */\nexport function baseCategoryFromStatus(status?: number): ErrorCategory {\n switch (status) {\n case 401:\n return 'authentication';\n case 403:\n return 'permission';\n case 404:\n return 'not_found';\n case 408:\n return 'timeout';\n case 413:\n return 'request_too_large';\n case 400:\n case 422:\n return 'invalid_request';\n case 429:\n return 'rate_limit';\n case 500:\n return 'server_error';\n case 502:\n return 'server_error';\n case 503:\n return 'overloaded';\n case 504:\n return 'timeout';\n case 529:\n return 'overloaded';\n default:\n break;\n }\n if (typeof status === 'number') {\n if (status >= 500) {\n return 'server_error';\n }\n if (status >= 400) {\n return 'invalid_request';\n }\n }\n return 'unknown';\n}\n\n/** Categories that are safe to retry after a delay. */\nconst RETRYABLE: ReadonlySet<ErrorCategory> = new Set<ErrorCategory>([\n 'rate_limit',\n 'server_error',\n 'overloaded',\n 'timeout',\n]);\n\n/** Whether a category represents a transient, retryable condition. */\nexport function isRetryableCategory(category: ErrorCategory): boolean {\n return RETRYABLE.has(category);\n}\n\n/** Read the first present header from a list of candidate names. */\nexport function firstHeader(\n headers: unknown,\n ...names: string[]\n): string | undefined {\n for (const name of names) {\n const value = getHeader(headers, name);\n if (value !== undefined) {\n return value;\n }\n }\n return undefined;\n}\n","import { firstString, isObject } from './internal.ts';\nimport type { ErrorCategory } from './types.ts';\n\n/**\n * Transport-level failures never reach an HTTP response, so they carry no\n * status code or provider body — yet most of them (timeouts, dropped\n * connections, DNS hiccups) are very much worth retrying. This recognizes\n * them from the Node `code`, the `name`, or the SDK error class name.\n */\n\n/** Node `error.code` values that mean \"the connection failed; try again\". */\nconst RETRYABLE_CODES: Record<string, ErrorCategory> = {\n ETIMEDOUT: 'timeout',\n ESOCKETTIMEDOUT: 'timeout',\n ECONNRESET: 'server_error',\n ECONNREFUSED: 'server_error',\n ECONNABORTED: 'server_error',\n EPIPE: 'server_error',\n ENOTFOUND: 'server_error',\n EAI_AGAIN: 'server_error',\n EHOSTUNREACH: 'server_error',\n ENETUNREACH: 'server_error',\n};\n\n/**\n * Error `name` / constructor names, matched case-insensitively as substrings,\n * mapped to a category. Covers `AbortError`, the Fetch `TimeoutError`, and the\n * OpenAI / Anthropic SDK connection error classes.\n */\nconst NAME_PATTERNS: ReadonlyArray<[pattern: string, category: ErrorCategory]> =\n [\n ['timeout', 'timeout'],\n ['aborterror', 'timeout'],\n ['connectionerror', 'server_error'],\n ['connection error', 'server_error'],\n ['fetcherror', 'server_error'],\n ];\n\nexport interface NetworkClassification {\n category: ErrorCategory;\n code?: string;\n}\n\n/**\n * Try to classify a transport-level error. Returns `undefined` when the value\n * does not look like a network failure, so the caller can fall back to its\n * normal (HTTP-based) classification.\n */\nexport function classifyNetworkError(\n error: unknown,\n): NetworkClassification | undefined {\n if (!isObject(error)) {\n return undefined;\n }\n\n const code = firstString(error.code);\n if (code && code in RETRYABLE_CODES) {\n return { category: RETRYABLE_CODES[code], code };\n }\n\n // Check both the instance `name` and the constructor name: a subclass of\n // `Error` that doesn't override `name` still reports `\"Error\"`, so the\n // distinguishing signal lives on `constructor.name`.\n const names = [\n firstString(error.name),\n firstString((error.constructor as { name?: unknown } | undefined)?.name),\n ];\n for (const name of names) {\n if (!name) {\n continue;\n }\n const haystack = name.toLowerCase();\n for (const [pattern, category] of NAME_PATTERNS) {\n if (haystack.includes(pattern)) {\n return { category, code: name };\n }\n }\n }\n\n return undefined;\n}\n","import { isObject } from './internal.ts';\nimport type { NormalizedError, RetryDelayOptions } from './types.ts';\n\nfunction nonNegativeFinite(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0\n ? value\n : undefined;\n}\n\n/**\n * Parse a `Retry-After` header value into milliseconds.\n *\n * Per RFC 7231 the value is either a number of seconds (`\"30\"`) or an\n * HTTP date (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Some providers also send a\n * fractional `retry-after-ms` value, which is accepted when `unit` is `'ms'`.\n */\nexport function parseRetryAfter(\n value: string | undefined,\n unit: 's' | 'ms' = 's',\n): number | undefined {\n if (value === undefined) {\n return undefined;\n }\n const trimmed = value.trim();\n if (trimmed === '') {\n return undefined;\n }\n\n const numeric = Number(trimmed);\n if (Number.isFinite(numeric)) {\n const ms = unit === 'ms' ? numeric : numeric * 1000;\n return ms >= 0 ? ms : undefined;\n }\n\n if (unit === 'ms') {\n return undefined;\n }\n\n const date = Date.parse(trimmed);\n if (Number.isFinite(date)) {\n return Math.max(0, date - Date.now());\n }\n\n return undefined;\n}\n\n/**\n * Parse a Google `RetryInfo.retryDelay` value into milliseconds.\n *\n * Google encodes it either as a duration string (`\"30s\"`, `\"1.5s\"`) or as a\n * `{ seconds, nanos }` object inside `error.details[]`.\n */\nexport function parseGoogleRetryDelay(details: unknown): number | undefined {\n if (!Array.isArray(details)) {\n return undefined;\n }\n for (const detail of details) {\n if (!isObject(detail)) {\n continue;\n }\n const type = detail['@type'];\n if (typeof type === 'string' && !type.includes('RetryInfo')) {\n continue;\n }\n const delay = detail.retryDelay;\n if (typeof delay === 'string') {\n const seconds = Number(delay.replace(/s$/, ''));\n if (Number.isFinite(seconds) && seconds >= 0) {\n return seconds * 1000;\n }\n }\n if (isObject(delay)) {\n const seconds =\n typeof delay.seconds === 'number'\n ? delay.seconds\n : Number(delay.seconds);\n const nanos =\n delay.nanos === undefined\n ? 0\n : typeof delay.nanos === 'number'\n ? delay.nanos\n : Number(delay.nanos);\n if (\n Number.isFinite(seconds) &&\n seconds >= 0 &&\n Number.isFinite(nanos) &&\n nanos >= 0 &&\n nanos < 1e9\n ) {\n return seconds * 1000 + Math.round(nanos / 1e6);\n }\n }\n }\n return undefined;\n}\n\n/**\n * Suggested delay before retrying, in milliseconds.\n *\n * Non-retryable errors return `0`. When the provider supplied an explicit\n * valid delay (`error.retryAfterMs`) it is respected. Otherwise this falls\n * back to exponential backoff:\n * `baseMs * 2 ** attempt`, capped at `maxMs`, with optional full jitter.\n *\n * @param error A {@link NormalizedError}.\n * @param attempt Zero-based retry attempt number (0 for the first retry).\n * @param options Backoff tuning. See {@link RetryDelayOptions}.\n */\nexport function getRetryDelayMs(\n error: NormalizedError,\n attempt: number,\n options: RetryDelayOptions = {},\n): number {\n if (!error.retryable) {\n return 0;\n }\n\n const explicitDelay = nonNegativeFinite(error.retryAfterMs);\n if (explicitDelay !== undefined) {\n return explicitDelay;\n }\n\n const baseMs = nonNegativeFinite(options.baseMs) ?? 500;\n const maxMs = nonNegativeFinite(options.maxMs) ?? 60000;\n const jitter = options.jitter ?? 'full';\n\n const safeAttempt =\n Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;\n const exponential = Math.min(maxMs, baseMs * 2 ** safeAttempt);\n\n if (jitter === 'none') {\n return exponential;\n }\n return Math.random() * exponential;\n}\n","import {\n baseCategoryFromStatus,\n firstHeader,\n type Classification,\n type ProviderContext,\n} from '../classify.ts';\nimport { firstString } from '../internal.ts';\nimport { parseRetryAfter } from '../retry.ts';\nimport type { ErrorCategory } from '../types.ts';\n\nconst ANTHROPIC_TYPES: Record<string, ErrorCategory> = {\n authentication_error: 'authentication',\n permission_error: 'permission',\n not_found_error: 'not_found',\n request_too_large: 'request_too_large',\n rate_limit_error: 'rate_limit',\n invalid_request_error: 'invalid_request',\n api_error: 'server_error',\n overloaded_error: 'overloaded',\n billing_error: 'insufficient_quota',\n timeout_error: 'timeout',\n};\n\n/** Heuristic: does this error look like it came from the Anthropic API? */\nexport function matches(ctx: ProviderContext): boolean {\n if (\n firstHeader(\n ctx.headers,\n 'anthropic-version',\n 'anthropic-ratelimit-requests-limit',\n 'anthropic-ratelimit-tokens-limit',\n ) !== undefined\n ) {\n return true;\n }\n const body = ctx.body;\n if (!body) {\n return false;\n }\n // `param` is an OpenAI-only field; its presence rules Anthropic out even\n // though both providers share type strings like `invalid_request_error`.\n if ('param' in body) {\n return false;\n }\n const type = firstString(body.type);\n return type !== undefined && type in ANTHROPIC_TYPES;\n}\n\nexport function classify(ctx: ProviderContext): Classification {\n const body = ctx.body ?? {};\n const type = firstString(body.type);\n const message = (firstString(body.message) ?? '').toLowerCase();\n\n const mapped = type ? ANTHROPIC_TYPES[type] : undefined;\n let category: ErrorCategory = mapped ?? baseCategoryFromStatus(ctx.status);\n\n // Anthropic reports an over-long prompt as `invalid_request_error` with a\n // \"prompt is too long\" message rather than a dedicated type.\n if (\n category === 'invalid_request' &&\n (message.includes('prompt is too long') ||\n message.includes('maximum context') ||\n message.includes('context window'))\n ) {\n category = 'context_length_exceeded';\n } else if (\n message.includes('credit balance') ||\n message.includes('billing') ||\n message.includes('insufficient quota')\n ) {\n category = 'insufficient_quota';\n }\n\n const retryAfterMs =\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after-ms'), 'ms') ??\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after'));\n\n return { category, code: type, retryAfterMs };\n}\n","import {\n baseCategoryFromStatus,\n firstHeader,\n type Classification,\n type ProviderContext,\n} from '../classify.ts';\nimport { firstString } from '../internal.ts';\nimport { parseGoogleRetryDelay, parseRetryAfter } from '../retry.ts';\nimport type { ErrorCategory } from '../types.ts';\n\n/** Canonical google.rpc.Code names → provider-agnostic categories. */\nconst RPC_STATUS: Record<string, ErrorCategory> = {\n UNAUTHENTICATED: 'authentication',\n PERMISSION_DENIED: 'permission',\n NOT_FOUND: 'not_found',\n INVALID_ARGUMENT: 'invalid_request',\n FAILED_PRECONDITION: 'invalid_request',\n OUT_OF_RANGE: 'invalid_request',\n UNIMPLEMENTED: 'invalid_request',\n RESOURCE_EXHAUSTED: 'rate_limit',\n INTERNAL: 'server_error',\n UNKNOWN: 'server_error',\n ABORTED: 'server_error',\n DATA_LOSS: 'server_error',\n UNAVAILABLE: 'overloaded',\n DEADLINE_EXCEEDED: 'timeout',\n CANCELLED: 'timeout',\n};\n\nfunction rpcStatus(ctx: ProviderContext): string | undefined {\n const status = firstString(ctx.body?.status);\n if (status && /^[A-Z][A-Z_]+$/.test(status)) {\n return status;\n }\n return undefined;\n}\n\nfunction isQuotaExhaustedMessage(message: string): boolean {\n const lower = message.toLowerCase();\n return (\n lower.includes('quota') &&\n (lower.includes('billing') ||\n lower.includes('paid plan') ||\n lower.includes('free tier') ||\n lower.includes('check your plan') ||\n lower.includes('upgrade'))\n );\n}\n\n/** Heuristic: does this error look like it came from the Gemini API? */\nexport function matches(ctx: ProviderContext): boolean {\n if (rpcStatus(ctx) !== undefined) {\n return true;\n }\n // A numeric `code` alongside a `details` array is the Google error envelope.\n return typeof ctx.body?.code === 'number' && Array.isArray(ctx.body?.details);\n}\n\nexport function classify(ctx: ProviderContext): Classification {\n const status = rpcStatus(ctx);\n const message = firstString(ctx.body?.message) ?? '';\n const retryAfterMs =\n parseGoogleRetryDelay(ctx.body?.details) ??\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after-ms'), 'ms') ??\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after'));\n\n const mapped = status ? RPC_STATUS[status] : undefined;\n let category: ErrorCategory = mapped ?? baseCategoryFromStatus(ctx.status);\n if (\n status === 'RESOURCE_EXHAUSTED' &&\n retryAfterMs === undefined &&\n isQuotaExhaustedMessage(message)\n ) {\n category = 'insufficient_quota';\n }\n\n return { category, code: status, retryAfterMs };\n}\n","import {\n baseCategoryFromStatus,\n firstHeader,\n type Classification,\n type ProviderContext,\n} from '../classify.ts';\nimport { firstString } from '../internal.ts';\nimport { parseRetryAfter } from '../retry.ts';\nimport type { ErrorCategory } from '../types.ts';\n\nconst OPENAI_CODES = new Set([\n 'billing_hard_limit_reached',\n 'billing_not_active',\n 'context_length_exceeded',\n 'content_filter',\n 'content_policy_violation',\n 'insufficient_quota',\n 'invalid_api_key',\n 'model_not_found',\n 'rate_limit_exceeded',\n]);\n\nfunction includesAny(\n haystack: string,\n needles: ReadonlyArray<string>,\n): boolean {\n return needles.some((needle) => haystack.includes(needle));\n}\n\n/** Heuristic: does this error look like it came from the OpenAI API? */\nexport function matches(ctx: ProviderContext): boolean {\n if (\n firstHeader(\n ctx.headers,\n 'openai-organization',\n 'openai-version',\n 'openai-processing-ms',\n ) !== undefined\n ) {\n return true;\n }\n const body = ctx.body;\n if (!body) {\n return false;\n }\n // OpenAI error objects carry a `param` field; Anthropic and Gemini do not.\n if ('param' in body) {\n return true;\n }\n const code = firstString(body.code);\n return code !== undefined && OPENAI_CODES.has(code);\n}\n\nexport function classify(ctx: ProviderContext): Classification {\n const body = ctx.body ?? {};\n const type = firstString(body.type);\n const code = firstString(body.code);\n const message = firstString(body.message) ?? '';\n const identifier = `${type ?? ''} ${code ?? ''} ${message}`.toLowerCase();\n\n let category: ErrorCategory = baseCategoryFromStatus(ctx.status);\n\n if (\n identifier.includes('context_length') ||\n identifier.includes('context window')\n ) {\n category = 'context_length_exceeded';\n } else if (\n includesAny(identifier, [\n 'insufficient_quota',\n 'billing_hard_limit',\n 'billing_not_active',\n 'exceeded your current quota',\n ])\n ) {\n category = 'insufficient_quota';\n } else if (\n includesAny(identifier, [\n 'content_filter',\n 'content_policy',\n 'safety_policy',\n ])\n ) {\n category = 'content_filter';\n } else if (\n code === 'invalid_api_key' ||\n includesAny(identifier, ['authentication', 'unauthorized'])\n ) {\n category = 'authentication';\n } else if (includesAny(identifier, ['permission', 'forbidden'])) {\n category = 'permission';\n } else if (includesAny(identifier, ['not_found', 'model_not_found'])) {\n category = 'not_found';\n } else if (includesAny(identifier, ['timeout', 'timed out'])) {\n category = 'timeout';\n } else if (includesAny(identifier, ['overload', 'unavailable'])) {\n category = 'overloaded';\n } else if (includesAny(identifier, ['server_error', 'api_error'])) {\n category = 'server_error';\n } else if (identifier.includes('rate_limit')) {\n category = 'rate_limit';\n } else if (category === 'unknown' && identifier.includes('invalid_request')) {\n category = 'invalid_request';\n }\n\n const retryAfterMs =\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after-ms'), 'ms') ??\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after'));\n\n return { category, code: code ?? type, retryAfterMs };\n}\n","import {\n baseCategoryFromStatus,\n firstHeader,\n isRetryableCategory,\n type Classification,\n type ProviderContext,\n} from './classify.ts';\nimport {\n firstString,\n readErrorBody,\n readHeaders,\n readMessage,\n readStatus,\n} from './internal.ts';\nimport { classifyNetworkError } from './network.ts';\nimport * as anthropic from './providers/anthropic.ts';\nimport * as gemini from './providers/gemini.ts';\nimport * as openai from './providers/openai.ts';\nimport { parseRetryAfter } from './retry.ts';\nimport type { NormalizedError, NormalizeOptions, Provider } from './types.ts';\n\n/**\n * Detection order is deliberate: Gemini's canonical RPC status string is the\n * most distinctive signal, then Anthropic's typed errors / headers, then\n * OpenAI (whose `param` field and headers are the remaining tell).\n */\nconst DETECTORS: ReadonlyArray<{\n name: Provider;\n matches: (ctx: ProviderContext) => boolean;\n}> = [\n { name: 'gemini', matches: gemini.matches },\n { name: 'anthropic', matches: anthropic.matches },\n { name: 'openai', matches: openai.matches },\n];\n\nfunction detectProvider(ctx: ProviderContext): Provider {\n for (const detector of DETECTORS) {\n if (detector.matches(ctx)) {\n return detector.name;\n }\n }\n return 'unknown';\n}\n\nfunction classifyFor(provider: Provider, ctx: ProviderContext): Classification {\n switch (provider) {\n case 'openai':\n return openai.classify(ctx);\n case 'anthropic':\n return anthropic.classify(ctx);\n case 'gemini':\n return gemini.classify(ctx);\n default:\n return {\n category: baseCategoryFromStatus(ctx.status),\n code: firstString(ctx.body?.type, ctx.body?.code),\n retryAfterMs:\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after-ms'), 'ms') ??\n parseRetryAfter(firstHeader(ctx.headers, 'retry-after')),\n };\n }\n}\n\n/**\n * Normalize an error thrown by an OpenAI, Anthropic or Gemini client into a\n * single consistent {@link NormalizedError} shape.\n *\n * Accepts SDK error objects, raw `fetch` responses with a parsed body, or\n * plain JSON. It never throws: anything unrecognized comes back as\n * `{ provider: 'unknown', category: 'unknown', retryable: false }`.\n *\n * @example\n * ```ts\n * try {\n * await client.messages.create(params);\n * } catch (err) {\n * const e = normalizeError(err);\n * if (e.retryable) await sleep(e.retryAfterMs ?? 1000);\n * }\n * ```\n */\nexport function normalizeError(\n error: unknown,\n options: NormalizeOptions = {},\n): NormalizedError {\n const ctx: ProviderContext = {\n status: readStatus(error),\n headers: readHeaders(error),\n body: readErrorBody(error),\n };\n\n const provider = options.provider ?? detectProvider(ctx);\n const classification = classifyFor(provider, ctx);\n\n let category = classification.category;\n let code = classification.code;\n\n // No HTTP response reached us: this may be a transport-level failure\n // (timeout, dropped connection) that is retryable despite having no status.\n if (category === 'unknown' && ctx.status === undefined) {\n const network = classifyNetworkError(error);\n if (network) {\n category = network.category;\n code = code ?? network.code;\n }\n }\n\n const retryable = isRetryableCategory(category);\n\n return {\n provider,\n category,\n message: readMessage(error, ctx.body),\n status: ctx.status,\n code,\n retryable,\n retryAfterMs: retryable ? classification.retryAfterMs : undefined,\n raw: error,\n };\n}\n\n/**\n * Convenience wrapper around {@link normalizeError} that returns only whether\n * the error is worth retrying.\n */\nexport function isRetryableError(\n error: unknown,\n options: NormalizeOptions = {},\n): boolean {\n return normalizeError(error, options).retryable;\n}\n"],"mappings":"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","names":["matches","classify","matches","classify","matches","classify"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -61,8 +61,9 @@ interface NormalizedError {
|
|
|
61
61
|
retryable: boolean;
|
|
62
62
|
/**
|
|
63
63
|
* Suggested delay in milliseconds before retrying, derived from the provider
|
|
64
|
-
* (`Retry-After` header, Google `RetryInfo`, etc.)
|
|
65
|
-
* provider did not specify one
|
|
64
|
+
* (`Retry-After` header, Google `RetryInfo`, etc.) for retryable errors.
|
|
65
|
+
* `undefined` when the provider did not specify one, or when the normalized
|
|
66
|
+
* category is not retryable — use {@link getRetryDelayMs} for a fallback.
|
|
66
67
|
*/
|
|
67
68
|
retryAfterMs?: number;
|
|
68
69
|
/** The original value passed to {@link normalizeError}, untouched. */
|
|
@@ -89,7 +90,8 @@ interface RetryDelayOptions {
|
|
|
89
90
|
/**
|
|
90
91
|
* Jitter strategy applied to the exponential delay. `'full'` picks a random
|
|
91
92
|
* value in `[0, delay]`, `'none'` disables jitter. Default `'full'`.
|
|
92
|
-
* Ignored when the provider supplied an explicit `retryAfterMs
|
|
93
|
+
* Ignored when the provider supplied an explicit `retryAfterMs`, or when the
|
|
94
|
+
* error is not retryable.
|
|
93
95
|
*/
|
|
94
96
|
jitter?: 'full' | 'none';
|
|
95
97
|
}
|
|
@@ -137,8 +139,9 @@ declare function parseGoogleRetryDelay(details: unknown): number | undefined;
|
|
|
137
139
|
/**
|
|
138
140
|
* Suggested delay before retrying, in milliseconds.
|
|
139
141
|
*
|
|
140
|
-
* When the provider supplied an explicit
|
|
141
|
-
*
|
|
142
|
+
* Non-retryable errors return `0`. When the provider supplied an explicit
|
|
143
|
+
* valid delay (`error.retryAfterMs`) it is respected. Otherwise this falls
|
|
144
|
+
* back to exponential backoff:
|
|
142
145
|
* `baseMs * 2 ** attempt`, capped at `maxMs`, with optional full jitter.
|
|
143
146
|
*
|
|
144
147
|
* @param error A {@link NormalizedError}.
|