llm-errors 0.1.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.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/index.cjs +495 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +150 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +464 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-06-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `normalizeError(error, options?)` — classify OpenAI, Anthropic and Gemini
|
|
12
|
+
errors into a single `NormalizedError` shape with `provider`, `category`,
|
|
13
|
+
`retryable` and `retryAfterMs`.
|
|
14
|
+
- `isRetryableError(error, options?)` convenience wrapper.
|
|
15
|
+
- `getRetryDelayMs(error, attempt, options?)` — respects provider-supplied
|
|
16
|
+
`Retry-After` and otherwise applies exponential backoff with jitter.
|
|
17
|
+
- `parseRetryAfter` and `parseGoogleRetryDelay` low-level helpers.
|
|
18
|
+
- Provider auto-detection with an explicit `{ provider }` override.
|
|
19
|
+
- Transport-level error detection (timeouts, `ECONNRESET`, `AbortError`, DNS
|
|
20
|
+
failures) classified as retryable `timeout` / `server_error`.
|
|
21
|
+
- Header parsing for `Headers` instances, plain objects, `Map`s and `[k, v]`
|
|
22
|
+
pair arrays.
|
|
23
|
+
- Zero runtime dependencies; ESM + CJS builds with type declarations.
|
|
24
|
+
|
|
25
|
+
[0.1.0]: https://github.com/slegarraga/llm-errors/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Legarraga
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# llm-errors
|
|
2
|
+
|
|
3
|
+
> Normalize OpenAI, Anthropic and Gemini API errors into one shape: category, retryable flag and `Retry-After` delay. **Zero dependencies.**
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { normalizeError, getRetryDelayMs } from 'llm-errors';
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
await client.chat.completions.create(params);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
const e = normalizeError(err);
|
|
14
|
+
// -> { provider: 'openai', category: 'rate_limit', retryable: true, retryAfterMs: 2000, ... }
|
|
15
|
+
|
|
16
|
+
if (e.category === 'context_length_exceeded') trimHistory();
|
|
17
|
+
else if (e.retryable) await sleep(getRetryDelayMs(e, attempt));
|
|
18
|
+
else throw err;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Why
|
|
23
|
+
|
|
24
|
+
- **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`.
|
|
25
|
+
- **Correct retry decisions.** `insufficient_quota` and `context_length_exceeded` look like other 4xx/429s but are _not_ worth retrying. `llm-errors` separates them out.
|
|
26
|
+
- **Honours `Retry-After`.** Reads the `Retry-After` header (seconds _or_ HTTP date), OpenAI's `retry-after-ms`, and Google's `RetryInfo.retryDelay` — then falls back to exponential backoff with jitter when none is given.
|
|
27
|
+
- **Never throws.** Feed it an SDK error, a raw `fetch` response, plain JSON, `null`, or a string — it always returns a `NormalizedError`.
|
|
28
|
+
- **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.
|
|
29
|
+
- **Zero dependencies**, ESM + CJS, fully typed.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install llm-errors
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### `normalizeError(error, options?) => NormalizedError`
|
|
40
|
+
|
|
41
|
+
Classifies any value into:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
interface NormalizedError {
|
|
45
|
+
provider: 'openai' | 'anthropic' | 'gemini' | 'unknown';
|
|
46
|
+
category: ErrorCategory;
|
|
47
|
+
message: string;
|
|
48
|
+
status?: number; // HTTP status, when available
|
|
49
|
+
code?: string; // provider-specific code / type
|
|
50
|
+
retryable: boolean;
|
|
51
|
+
retryAfterMs?: number; // provider-supplied delay, if any
|
|
52
|
+
raw: unknown; // the original input
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The provider is auto-detected from the error shape. Pass `{ provider }` to force it when you already know which client threw or the shape is ambiguous:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
normalizeError(err, { provider: 'anthropic' });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `ErrorCategory`
|
|
63
|
+
|
|
64
|
+
| Category | Retryable | Typical cause |
|
|
65
|
+
| ------------------------- | :-------: | ------------------------------------------- |
|
|
66
|
+
| `authentication` | no | Missing / invalid API key (401) |
|
|
67
|
+
| `permission` | no | Key valid but not allowed (403) |
|
|
68
|
+
| `rate_limit` | **yes** | Too many requests (429) |
|
|
69
|
+
| `insufficient_quota` | no | Billing / credits exhausted (429) |
|
|
70
|
+
| `context_length_exceeded` | no | Prompt + completion over the context window |
|
|
71
|
+
| `request_too_large` | no | Payload too large (413) |
|
|
72
|
+
| `invalid_request` | no | Malformed request (400 / 422) |
|
|
73
|
+
| `not_found` | no | Unknown model or resource (404) |
|
|
74
|
+
| `content_filter` | no | Blocked by a safety policy |
|
|
75
|
+
| `timeout` | **yes** | Request / upstream timeout (504) |
|
|
76
|
+
| `server_error` | **yes** | Upstream failure (500) |
|
|
77
|
+
| `overloaded` | **yes** | Provider temporarily overloaded (503 / 529) |
|
|
78
|
+
| `unknown` | no | Could not be classified |
|
|
79
|
+
|
|
80
|
+
### `isRetryableError(error, options?) => boolean`
|
|
81
|
+
|
|
82
|
+
Shorthand for `normalizeError(error).retryable`.
|
|
83
|
+
|
|
84
|
+
### `getRetryDelayMs(error, attempt, options?) => number`
|
|
85
|
+
|
|
86
|
+
Returns the delay to wait before the next attempt. If the provider supplied `retryAfterMs`, that always wins. Otherwise it computes exponential backoff `baseMs * 2 ** attempt`, capped at `maxMs`, with full jitter by default.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
getRetryDelayMs(e, attempt, { baseMs: 500, maxMs: 60_000, jitter: 'full' });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `parseRetryAfter` / `parseGoogleRetryDelay`
|
|
93
|
+
|
|
94
|
+
The low-level helpers, exported for advanced use.
|
|
95
|
+
|
|
96
|
+
## Example: a provider-agnostic retry loop
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { normalizeError, getRetryDelayMs } from 'llm-errors';
|
|
100
|
+
|
|
101
|
+
async function withRetries<T>(call: () => Promise<T>, max = 5): Promise<T> {
|
|
102
|
+
for (let attempt = 0; ; attempt++) {
|
|
103
|
+
try {
|
|
104
|
+
return await call();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const e = normalizeError(err);
|
|
107
|
+
if (!e.retryable || attempt >= max) throw err;
|
|
108
|
+
await new Promise((r) => setTimeout(r, getRetryDelayMs(e, attempt)));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Related
|
|
115
|
+
|
|
116
|
+
- [`tool-schema`](https://www.npmjs.com/package/tool-schema) — convert a JSON Schema into OpenAI / Anthropic / Gemini / MCP tool schemas.
|
|
117
|
+
- [`llm-messages`](https://www.npmjs.com/package/llm-messages) — convert conversations and responses between providers.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT © Sebastian Legarraga
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
getRetryDelayMs: () => getRetryDelayMs,
|
|
24
|
+
isRetryableError: () => isRetryableError,
|
|
25
|
+
normalizeError: () => normalizeError,
|
|
26
|
+
parseGoogleRetryDelay: () => parseGoogleRetryDelay,
|
|
27
|
+
parseRetryAfter: () => parseRetryAfter
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/internal.ts
|
|
32
|
+
function isObject(value) {
|
|
33
|
+
return typeof value === "object" && value !== null;
|
|
34
|
+
}
|
|
35
|
+
function firstString(...values) {
|
|
36
|
+
for (const value of values) {
|
|
37
|
+
if (typeof value === "string" && value.length > 0) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
function firstNumber(...values) {
|
|
44
|
+
for (const value of values) {
|
|
45
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
function getHeader(headers, name) {
|
|
52
|
+
if (!headers) {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
const lower = name.toLowerCase();
|
|
56
|
+
if (typeof headers.get === "function") {
|
|
57
|
+
const value = headers.get(name);
|
|
58
|
+
return typeof value === "string" ? value : void 0;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(headers)) {
|
|
61
|
+
for (const entry of headers) {
|
|
62
|
+
if (Array.isArray(entry) && typeof entry[0] === "string" && entry[0].toLowerCase() === lower) {
|
|
63
|
+
return typeof entry[1] === "string" ? entry[1] : void 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
if (isObject(headers)) {
|
|
69
|
+
for (const key of Object.keys(headers)) {
|
|
70
|
+
if (key.toLowerCase() === lower) {
|
|
71
|
+
const value = headers[key];
|
|
72
|
+
return typeof value === "string" ? value : void 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
function readStatus(error) {
|
|
79
|
+
if (!isObject(error)) {
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
const response = isObject(error.response) ? error.response : void 0;
|
|
83
|
+
const inner = isObject(error.error) ? error.error : void 0;
|
|
84
|
+
return firstNumber(
|
|
85
|
+
error.status,
|
|
86
|
+
error.statusCode,
|
|
87
|
+
response?.status,
|
|
88
|
+
// Google encodes the status as `error.code` (a numeric HTTP status).
|
|
89
|
+
inner?.code
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
function readHeaders(error) {
|
|
93
|
+
if (!isObject(error)) {
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
const response = isObject(error.response) ? error.response : void 0;
|
|
97
|
+
return error.headers ?? response?.headers;
|
|
98
|
+
}
|
|
99
|
+
function readErrorBody(error) {
|
|
100
|
+
if (!isObject(error)) {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
const candidates = [
|
|
104
|
+
isObject(error.error) && isObject(error.error.error) ? error.error.error : error.error,
|
|
105
|
+
isObject(error.body) ? error.body.error : void 0,
|
|
106
|
+
isObject(error.response) && isObject(error.response.data) ? error.response.data.error : void 0
|
|
107
|
+
];
|
|
108
|
+
for (const candidate of candidates) {
|
|
109
|
+
if (isObject(candidate)) {
|
|
110
|
+
return candidate;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
function readMessage(error, body) {
|
|
116
|
+
const fromBody = body ? firstString(body.message) : void 0;
|
|
117
|
+
if (fromBody) {
|
|
118
|
+
return fromBody;
|
|
119
|
+
}
|
|
120
|
+
if (isObject(error)) {
|
|
121
|
+
const direct = firstString(error.message);
|
|
122
|
+
if (direct) {
|
|
123
|
+
return direct;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (typeof error === "string" && error.length > 0) {
|
|
127
|
+
return error;
|
|
128
|
+
}
|
|
129
|
+
return "Unknown error";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/classify.ts
|
|
133
|
+
function baseCategoryFromStatus(status) {
|
|
134
|
+
switch (status) {
|
|
135
|
+
case 401:
|
|
136
|
+
return "authentication";
|
|
137
|
+
case 403:
|
|
138
|
+
return "permission";
|
|
139
|
+
case 404:
|
|
140
|
+
return "not_found";
|
|
141
|
+
case 408:
|
|
142
|
+
return "timeout";
|
|
143
|
+
case 413:
|
|
144
|
+
return "request_too_large";
|
|
145
|
+
case 400:
|
|
146
|
+
case 422:
|
|
147
|
+
return "invalid_request";
|
|
148
|
+
case 429:
|
|
149
|
+
return "rate_limit";
|
|
150
|
+
case 500:
|
|
151
|
+
return "server_error";
|
|
152
|
+
case 502:
|
|
153
|
+
return "server_error";
|
|
154
|
+
case 503:
|
|
155
|
+
return "overloaded";
|
|
156
|
+
case 504:
|
|
157
|
+
return "timeout";
|
|
158
|
+
case 529:
|
|
159
|
+
return "overloaded";
|
|
160
|
+
default:
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (typeof status === "number") {
|
|
164
|
+
if (status >= 500) {
|
|
165
|
+
return "server_error";
|
|
166
|
+
}
|
|
167
|
+
if (status >= 400) {
|
|
168
|
+
return "invalid_request";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return "unknown";
|
|
172
|
+
}
|
|
173
|
+
var RETRYABLE = /* @__PURE__ */ new Set([
|
|
174
|
+
"rate_limit",
|
|
175
|
+
"server_error",
|
|
176
|
+
"overloaded",
|
|
177
|
+
"timeout"
|
|
178
|
+
]);
|
|
179
|
+
function isRetryableCategory(category) {
|
|
180
|
+
return RETRYABLE.has(category);
|
|
181
|
+
}
|
|
182
|
+
function firstHeader(headers, ...names) {
|
|
183
|
+
for (const name of names) {
|
|
184
|
+
const value = getHeader(headers, name);
|
|
185
|
+
if (value !== void 0) {
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/network.ts
|
|
193
|
+
var RETRYABLE_CODES = {
|
|
194
|
+
ETIMEDOUT: "timeout",
|
|
195
|
+
ESOCKETTIMEDOUT: "timeout",
|
|
196
|
+
ECONNRESET: "server_error",
|
|
197
|
+
ECONNREFUSED: "server_error",
|
|
198
|
+
ECONNABORTED: "server_error",
|
|
199
|
+
EPIPE: "server_error",
|
|
200
|
+
ENOTFOUND: "server_error",
|
|
201
|
+
EAI_AGAIN: "server_error",
|
|
202
|
+
EHOSTUNREACH: "server_error",
|
|
203
|
+
ENETUNREACH: "server_error"
|
|
204
|
+
};
|
|
205
|
+
var NAME_PATTERNS = [
|
|
206
|
+
["timeout", "timeout"],
|
|
207
|
+
["aborterror", "timeout"],
|
|
208
|
+
["connectionerror", "server_error"],
|
|
209
|
+
["connection error", "server_error"],
|
|
210
|
+
["fetcherror", "server_error"]
|
|
211
|
+
];
|
|
212
|
+
function classifyNetworkError(error) {
|
|
213
|
+
if (!isObject(error)) {
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
216
|
+
const code = firstString(error.code);
|
|
217
|
+
if (code && code in RETRYABLE_CODES) {
|
|
218
|
+
return { category: RETRYABLE_CODES[code], code };
|
|
219
|
+
}
|
|
220
|
+
const names = [
|
|
221
|
+
firstString(error.name),
|
|
222
|
+
firstString(error.constructor?.name)
|
|
223
|
+
];
|
|
224
|
+
for (const name of names) {
|
|
225
|
+
if (!name) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const haystack = name.toLowerCase();
|
|
229
|
+
for (const [pattern, category] of NAME_PATTERNS) {
|
|
230
|
+
if (haystack.includes(pattern)) {
|
|
231
|
+
return { category, code: name };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/retry.ts
|
|
239
|
+
function parseRetryAfter(value, unit = "s") {
|
|
240
|
+
if (value === void 0) {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
const trimmed = value.trim();
|
|
244
|
+
if (trimmed === "") {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
const numeric = Number(trimmed);
|
|
248
|
+
if (Number.isFinite(numeric)) {
|
|
249
|
+
const ms = unit === "ms" ? numeric : numeric * 1e3;
|
|
250
|
+
return ms >= 0 ? ms : void 0;
|
|
251
|
+
}
|
|
252
|
+
const date = Date.parse(trimmed);
|
|
253
|
+
if (Number.isFinite(date)) {
|
|
254
|
+
return Math.max(0, date - Date.now());
|
|
255
|
+
}
|
|
256
|
+
return void 0;
|
|
257
|
+
}
|
|
258
|
+
function parseGoogleRetryDelay(details) {
|
|
259
|
+
if (!Array.isArray(details)) {
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
for (const detail of details) {
|
|
263
|
+
if (!isObject(detail)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const type = detail["@type"];
|
|
267
|
+
if (typeof type === "string" && !type.includes("RetryInfo")) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const delay = detail.retryDelay;
|
|
271
|
+
if (typeof delay === "string") {
|
|
272
|
+
const seconds = Number(delay.replace(/s$/, ""));
|
|
273
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
274
|
+
return seconds * 1e3;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (isObject(delay)) {
|
|
278
|
+
const seconds = typeof delay.seconds === "number" ? delay.seconds : Number(delay.seconds);
|
|
279
|
+
const nanos = typeof delay.nanos === "number" ? delay.nanos : 0;
|
|
280
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
281
|
+
return seconds * 1e3 + Math.round(nanos / 1e6);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
function getRetryDelayMs(error, attempt, options = {}) {
|
|
288
|
+
if (typeof error.retryAfterMs === "number") {
|
|
289
|
+
return error.retryAfterMs;
|
|
290
|
+
}
|
|
291
|
+
const baseMs = options.baseMs ?? 500;
|
|
292
|
+
const maxMs = options.maxMs ?? 6e4;
|
|
293
|
+
const jitter = options.jitter ?? "full";
|
|
294
|
+
const safeAttempt = Number.isFinite(attempt) && attempt > 0 ? attempt : 0;
|
|
295
|
+
const exponential = Math.min(maxMs, baseMs * 2 ** safeAttempt);
|
|
296
|
+
if (jitter === "none") {
|
|
297
|
+
return exponential;
|
|
298
|
+
}
|
|
299
|
+
return Math.random() * exponential;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/providers/anthropic.ts
|
|
303
|
+
var ANTHROPIC_TYPES = {
|
|
304
|
+
authentication_error: "authentication",
|
|
305
|
+
permission_error: "permission",
|
|
306
|
+
not_found_error: "not_found",
|
|
307
|
+
request_too_large: "request_too_large",
|
|
308
|
+
rate_limit_error: "rate_limit",
|
|
309
|
+
invalid_request_error: "invalid_request",
|
|
310
|
+
api_error: "server_error",
|
|
311
|
+
overloaded_error: "overloaded",
|
|
312
|
+
billing_error: "insufficient_quota",
|
|
313
|
+
timeout_error: "timeout"
|
|
314
|
+
};
|
|
315
|
+
function matches(ctx) {
|
|
316
|
+
if (firstHeader(
|
|
317
|
+
ctx.headers,
|
|
318
|
+
"anthropic-version",
|
|
319
|
+
"anthropic-ratelimit-requests-limit",
|
|
320
|
+
"anthropic-ratelimit-tokens-limit"
|
|
321
|
+
) !== void 0) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
const body = ctx.body;
|
|
325
|
+
if (!body) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
if ("param" in body) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const type = firstString(body.type);
|
|
332
|
+
return type !== void 0 && type in ANTHROPIC_TYPES;
|
|
333
|
+
}
|
|
334
|
+
function classify(ctx) {
|
|
335
|
+
const body = ctx.body ?? {};
|
|
336
|
+
const type = firstString(body.type);
|
|
337
|
+
const message = (firstString(body.message) ?? "").toLowerCase();
|
|
338
|
+
const mapped = type ? ANTHROPIC_TYPES[type] : void 0;
|
|
339
|
+
let category = mapped ?? baseCategoryFromStatus(ctx.status);
|
|
340
|
+
if (category === "invalid_request" && (message.includes("prompt is too long") || message.includes("maximum context") || message.includes("context window"))) {
|
|
341
|
+
category = "context_length_exceeded";
|
|
342
|
+
}
|
|
343
|
+
const retryAfterMs = parseRetryAfter(firstHeader(ctx.headers, "retry-after"));
|
|
344
|
+
return { category, code: type, retryAfterMs };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/providers/gemini.ts
|
|
348
|
+
var RPC_STATUS = {
|
|
349
|
+
UNAUTHENTICATED: "authentication",
|
|
350
|
+
PERMISSION_DENIED: "permission",
|
|
351
|
+
NOT_FOUND: "not_found",
|
|
352
|
+
INVALID_ARGUMENT: "invalid_request",
|
|
353
|
+
FAILED_PRECONDITION: "invalid_request",
|
|
354
|
+
OUT_OF_RANGE: "invalid_request",
|
|
355
|
+
UNIMPLEMENTED: "invalid_request",
|
|
356
|
+
RESOURCE_EXHAUSTED: "rate_limit",
|
|
357
|
+
INTERNAL: "server_error",
|
|
358
|
+
UNKNOWN: "server_error",
|
|
359
|
+
ABORTED: "server_error",
|
|
360
|
+
DATA_LOSS: "server_error",
|
|
361
|
+
UNAVAILABLE: "overloaded",
|
|
362
|
+
DEADLINE_EXCEEDED: "timeout",
|
|
363
|
+
CANCELLED: "timeout"
|
|
364
|
+
};
|
|
365
|
+
function rpcStatus(ctx) {
|
|
366
|
+
const status = firstString(ctx.body?.status);
|
|
367
|
+
if (status && /^[A-Z][A-Z_]+$/.test(status)) {
|
|
368
|
+
return status;
|
|
369
|
+
}
|
|
370
|
+
return void 0;
|
|
371
|
+
}
|
|
372
|
+
function matches2(ctx) {
|
|
373
|
+
if (rpcStatus(ctx) !== void 0) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
return typeof ctx.body?.code === "number" && Array.isArray(ctx.body?.details);
|
|
377
|
+
}
|
|
378
|
+
function classify2(ctx) {
|
|
379
|
+
const status = rpcStatus(ctx);
|
|
380
|
+
const mapped = status ? RPC_STATUS[status] : void 0;
|
|
381
|
+
const category = mapped ?? baseCategoryFromStatus(ctx.status);
|
|
382
|
+
const retryAfterMs = parseGoogleRetryDelay(ctx.body?.details) ?? parseRetryAfter(firstHeader(ctx.headers, "retry-after"));
|
|
383
|
+
return { category, code: status, retryAfterMs };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/providers/openai.ts
|
|
387
|
+
function matches3(ctx) {
|
|
388
|
+
if (firstHeader(
|
|
389
|
+
ctx.headers,
|
|
390
|
+
"openai-organization",
|
|
391
|
+
"openai-version",
|
|
392
|
+
"openai-processing-ms"
|
|
393
|
+
) !== void 0) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
const body = ctx.body;
|
|
397
|
+
if (!body) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if ("param" in body) {
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
const code = firstString(body.code);
|
|
404
|
+
return code === "context_length_exceeded" || code === "insufficient_quota" || code === "invalid_api_key";
|
|
405
|
+
}
|
|
406
|
+
function classify3(ctx) {
|
|
407
|
+
const body = ctx.body ?? {};
|
|
408
|
+
const type = firstString(body.type);
|
|
409
|
+
const code = firstString(body.code);
|
|
410
|
+
const identifier = `${type ?? ""} ${code ?? ""}`.toLowerCase();
|
|
411
|
+
let category = baseCategoryFromStatus(ctx.status);
|
|
412
|
+
if (identifier.includes("context_length") || identifier.includes("context window")) {
|
|
413
|
+
category = "context_length_exceeded";
|
|
414
|
+
} else if (identifier.includes("insufficient_quota")) {
|
|
415
|
+
category = "insufficient_quota";
|
|
416
|
+
} else if (identifier.includes("content_filter") || identifier.includes("content_policy")) {
|
|
417
|
+
category = "content_filter";
|
|
418
|
+
} else if (code === "invalid_api_key" || identifier.includes("authentication")) {
|
|
419
|
+
category = "authentication";
|
|
420
|
+
} else if (category === "unknown" && identifier.includes("rate_limit")) {
|
|
421
|
+
category = "rate_limit";
|
|
422
|
+
}
|
|
423
|
+
const retryAfterMs = parseRetryAfter(firstHeader(ctx.headers, "retry-after-ms"), "ms") ?? parseRetryAfter(firstHeader(ctx.headers, "retry-after"));
|
|
424
|
+
return { category, code: code ?? type, retryAfterMs };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/normalize.ts
|
|
428
|
+
var DETECTORS = [
|
|
429
|
+
{ name: "gemini", matches: matches2 },
|
|
430
|
+
{ name: "anthropic", matches },
|
|
431
|
+
{ name: "openai", matches: matches3 }
|
|
432
|
+
];
|
|
433
|
+
function detectProvider(ctx) {
|
|
434
|
+
for (const detector of DETECTORS) {
|
|
435
|
+
if (detector.matches(ctx)) {
|
|
436
|
+
return detector.name;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return "unknown";
|
|
440
|
+
}
|
|
441
|
+
function classifyFor(provider, ctx) {
|
|
442
|
+
switch (provider) {
|
|
443
|
+
case "openai":
|
|
444
|
+
return classify3(ctx);
|
|
445
|
+
case "anthropic":
|
|
446
|
+
return classify(ctx);
|
|
447
|
+
case "gemini":
|
|
448
|
+
return classify2(ctx);
|
|
449
|
+
default:
|
|
450
|
+
return {
|
|
451
|
+
category: baseCategoryFromStatus(ctx.status),
|
|
452
|
+
code: firstString(ctx.body?.type, ctx.body?.code)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function normalizeError(error, options = {}) {
|
|
457
|
+
const ctx = {
|
|
458
|
+
status: readStatus(error),
|
|
459
|
+
headers: readHeaders(error),
|
|
460
|
+
body: readErrorBody(error)
|
|
461
|
+
};
|
|
462
|
+
const provider = options.provider ?? detectProvider(ctx);
|
|
463
|
+
const classification = classifyFor(provider, ctx);
|
|
464
|
+
let category = classification.category;
|
|
465
|
+
let code = classification.code;
|
|
466
|
+
if (category === "unknown" && ctx.status === void 0) {
|
|
467
|
+
const network = classifyNetworkError(error);
|
|
468
|
+
if (network) {
|
|
469
|
+
category = network.category;
|
|
470
|
+
code = code ?? network.code;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
provider,
|
|
475
|
+
category,
|
|
476
|
+
message: readMessage(error, ctx.body),
|
|
477
|
+
status: ctx.status,
|
|
478
|
+
code,
|
|
479
|
+
retryable: isRetryableCategory(category),
|
|
480
|
+
retryAfterMs: classification.retryAfterMs,
|
|
481
|
+
raw: error
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function isRetryableError(error, options = {}) {
|
|
485
|
+
return normalizeError(error, options).retryable;
|
|
486
|
+
}
|
|
487
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
488
|
+
0 && (module.exports = {
|
|
489
|
+
getRetryDelayMs,
|
|
490
|
+
isRetryableError,
|
|
491
|
+
normalizeError,
|
|
492
|
+
parseGoogleRetryDelay,
|
|
493
|
+
parseRetryAfter
|
|
494
|
+
});
|
|
495
|
+
//# sourceMappingURL=index.cjs.map
|