Package Exports
- llm-errors
- llm-errors/package.json
Readme
llm-errors
Normalize OpenAI, Anthropic and Gemini API errors into one shape: category, retryable flag and
Retry-Afterdelay. Zero dependencies.
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.
import { normalizeError, getRetryDelayMs } from 'llm-errors';
try {
await client.chat.completions.create(params);
} catch (err) {
const e = normalizeError(err);
// -> { provider: 'openai', category: 'rate_limit', retryable: true, retryAfterMs: 2000, ... }
if (e.category === 'context_length_exceeded') trimHistory();
else if (e.retryable) await sleep(getRetryDelayMs(e, attempt));
else throw err;
}Why
- One
switch, not three. Arate_limitis arate_limitwhether it came from OpenAI'scode, Anthropic'stype, or Gemini'sRESOURCE_EXHAUSTED. - Correct retry decisions.
insufficient_quotaandcontext_length_exceededlook like other 4xx/429s but are not worth retrying.llm-errorsseparates them out. - Honours
Retry-After. Reads theRetry-Afterheader (seconds or HTTP date), OpenAI'sretry-after-ms, and Google'sRetryInfo.retryDelay— then falls back to exponential backoff with jitter when none is given. - Never throws. Feed it an SDK error, a raw
fetchresponse, plain JSON,null, or a string — it always returns aNormalizedError. - Transport errors too. Connection timeouts, resets and DNS failures (
ETIMEDOUT,ECONNRESET,AbortError, …) have no HTTP status, yet they are retryable —llm-errorsclassifies them astimeout/server_errorinstead of dropping them. - Zero dependencies, ESM + CJS, fully typed.
Install
npm install llm-errorsAPI
normalizeError(error, options?) => NormalizedError
Classifies any value into:
interface NormalizedError {
provider: 'openai' | 'anthropic' | 'gemini' | 'unknown';
category: ErrorCategory;
message: string;
status?: number; // HTTP status, when available
code?: string; // provider-specific code / type
retryable: boolean;
retryAfterMs?: number; // provider-supplied delay, if any
raw: unknown; // the original input
}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:
normalizeError(err, { provider: 'anthropic' });ErrorCategory
| Category | Retryable | Typical cause |
|---|---|---|
authentication |
no | Missing / invalid API key (401) |
permission |
no | Key valid but not allowed (403) |
rate_limit |
yes | Too many requests (429) |
insufficient_quota |
no | Billing / credits exhausted (429) |
context_length_exceeded |
no | Prompt + completion over the context window |
request_too_large |
no | Payload too large (413) |
invalid_request |
no | Malformed request (400 / 422) |
not_found |
no | Unknown model or resource (404) |
content_filter |
no | Blocked by a safety policy |
timeout |
yes | Request / upstream timeout (504) |
server_error |
yes | Upstream failure (500) |
overloaded |
yes | Provider temporarily overloaded (503 / 529) |
unknown |
no | Could not be classified |
isRetryableError(error, options?) => boolean
Shorthand for normalizeError(error).retryable.
getRetryDelayMs(error, attempt, options?) => number
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.
getRetryDelayMs(e, attempt, { baseMs: 500, maxMs: 60_000, jitter: 'full' });parseRetryAfter / parseGoogleRetryDelay
The low-level helpers, exported for advanced use.
Example: a provider-agnostic retry loop
import { normalizeError, getRetryDelayMs } from 'llm-errors';
async function withRetries<T>(call: () => Promise<T>, max = 5): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await call();
} catch (err) {
const e = normalizeError(err);
if (!e.retryable || attempt >= max) throw err;
await new Promise((r) => setTimeout(r, getRetryDelayMs(e, attempt)));
}
}
}Related
tool-schema— convert a JSON Schema into OpenAI / Anthropic / Gemini / MCP tool schemas.llm-messages— convert conversations and responses between providers.
License
MIT © Sebastian Legarraga