npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

llm-errors

v0.1.7

Published

Normalize OpenAI, Anthropic and Gemini API errors into one shape: category, retryable flag and Retry-After delay. Zero dependencies.

Readme

llm-errors

npm version npm downloads CI OpenSSF Scorecard license zero dependencies

Normalize OpenAI, Anthropic and Gemini API errors into one shape: category, retryable flag and Retry-After delay. Zero dependencies.

Security posture is tracked in docs/security-posture.md, including CodeQL, OpenSSF Scorecard, Dependabot and branch rules.

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.

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. A rate_limit is a rate_limit whether it came from OpenAI's code, Anthropic's type, or Gemini's RESOURCE_EXHAUSTED.
  • Correct retry decisions. insufficient_quota and context_length_exceeded look like other 4xx/429s but are not worth retrying. llm-errors separates them out.
  • 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.
  • Never throws. Feed it an SDK error, a raw fetch response, plain JSON, null, or a string — it always returns a NormalizedError.
  • 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.
  • Zero dependencies, ESM + CJS, fully typed.

Install

npm install llm-errors

Fixture corpus

The npm package includes a public fixture corpus under fixtures/. It pairs raw SDK-like, fetch-like and transport-level provider errors with the normalized output expected from normalizeError.

These fixtures are useful for downstream regression tests when you want to verify provider-portable retry and error handling without importing OpenAI, Anthropic or Gemini SDKs.

API

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 for retryable errors, if any
  raw: unknown; // the original input
}

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:

normalizeError(err, { provider: 'anthropic' });

Unknown providers still get safe status-based behavior. For example, a plain { status: 503, headers: { "Retry-After": "4" } } normalizes to provider: "unknown", category: "overloaded", retryable: true and retryAfterMs: 4000. A non-retryable unknown status ignores the same header.

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 |

Only rate_limit, server_error, overloaded and timeout are retryable. unknown is deliberately not retryable, so unrecognized shapes fail closed instead of causing accidental retry storms.

isRetryableError(error, options?) => boolean

Shorthand for normalizeError(error).retryable.

getRetryDelayMs(error, attempt, options?) => number

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.

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