{"_id":"@ai-router/openai-compatible-errors","name":"@ai-router/openai-compatible-errors","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@ai-router/openai-compatible-errors","version":"0.1.0","description":"Normalize OpenAI-compatible API errors into typed categories, retry plans, request IDs, and redacted diagnostics","keywords":["openai-compatible","openai","openai-api","llm","ai-gateway","api-errors","error-normalization","error-handling","retry","retry-after","rate-limit","request-id","sse","streaming","typescript","fetch","redaction","safe-logging"],"license":"MIT","author":{"name":"airouter.dev contributors"},"repository":{"type":"git","url":"git+https://github.com/airouter-dev/openai-compatible-errors.git"},"homepage":"https://github.com/airouter-dev/openai-compatible-errors#readme","bugs":{"url":"https://github.com/airouter-dev/openai-compatible-errors/issues"},"type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"engines":{"node":">=18.18"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --clean --sourcemap","coverage":"vitest run --coverage","smoke":"node scripts/runtime-smoke.cjs","test":"vitest run","typecheck":"tsc --noEmit","validate":"npm run typecheck && npm run coverage && npm run build","prepack":"npm run validate"},"devDependencies":{"@types/node":"^20.19.0","@vitest/coverage-v8":"^4.1.10","openai":"6.49.0","tsup":"^8.5.0","typescript":"^5.7.0","vitest":"^4.1.10"},"overrides":{"esbuild":"0.27.2"},"gitHead":"a1ee3f77e390fd34036341a0a262a7919686ab0a","_id":"@ai-router/openai-compatible-errors@0.1.0","_nodeVersion":"24.18.0","_npmVersion":"11.18.0","dist":{"integrity":"sha512-JvXL/QvQK6eLer1zcyrsLj4AqdgfEerYQ1Dp0DtD3OC/bOSioXUcYvXBsb5g1/zrR6R2naxdpXJ0KP6FY9WicQ==","shasum":"9451902399eecc59fb99c105a1aba87a014dd87c","tarball":"https://registry.npmjs.org/@ai-router/openai-compatible-errors/-/openai-compatible-errors-0.1.0.tgz","fileCount":10,"unpackedSize":280225,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ai-router%2fopenai-compatible-errors@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICM0etc7w2m/sSv11YLDo1P4Lzf9lkECuhm3MxnViYybAiAfNB8PZ+I3K9oJm4PALM440sSKROyUi3SbWYYS/eXCHw=="}]},"_npmUser":{"name":"ai-router","email":"developer@ai-router.dev"},"directories":{},"maintainers":[{"name":"ai-router","email":"developer@ai-router.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/openai-compatible-errors_0.1.0_1785231045546_0.3107307039263614"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-28T09:30:45.269Z","0.1.0":"2026-07-28T09:30:45.716Z","modified":"2026-07-28T09:30:46.114Z"},"maintainers":[{"name":"ai-router","email":"developer@ai-router.dev"}],"description":"Normalize OpenAI-compatible API errors into typed categories, retry plans, request IDs, and redacted diagnostics","homepage":"https://github.com/airouter-dev/openai-compatible-errors#readme","keywords":["openai-compatible","openai","openai-api","llm","ai-gateway","api-errors","error-normalization","error-handling","retry","retry-after","rate-limit","request-id","sse","streaming","typescript","fetch","redaction","safe-logging"],"repository":{"type":"git","url":"git+https://github.com/airouter-dev/openai-compatible-errors.git"},"author":{"name":"airouter.dev contributors"},"bugs":{"url":"https://github.com/airouter-dev/openai-compatible-errors/issues"},"license":"MIT","readme":"# @ai-router/openai-compatible-errors\n\n[![CI](https://github.com/airouter-dev/openai-compatible-errors/actions/workflows/ci.yml/badge.svg)](https://github.com/airouter-dev/openai-compatible-errors/actions/workflows/ci.yml)\n\nTyped, safe-by-default error normalization and stream-aware retry decisions for OpenAI-compatible APIs.\n\nThis is not an API client. It does not send requests, log data, sleep, or retry automatically. It turns HTTP, SDK-style, Fetch, and SSE failures into a small error object, then helps the caller decide whether replay is safe.\n\n## Install\n\n```bash\nnpm install @ai-router/openai-compatible-errors\n```\n\nNode.js 18.18 or newer is required. Both ESM and CommonJS entry points include TypeScript declarations.\n\n## Quick start\n\n```ts\nimport {\n  decideOpenAICompatibleRetry,\n  normalizeOpenAICompatibleResponse,\n} from \"@ai-router/openai-compatible-errors\";\n\nconst startedAt = Date.now();\nconst response = await fetch(`${baseURL}/v1/chat/completions`, requestInit);\nconst apiError = await normalizeOpenAICompatibleResponse(response);\n\nif (apiError) {\n  console.warn(JSON.stringify(apiError));\n\n  const plan = decideOpenAICompatibleRetry(apiError, {\n    method: \"POST\",\n    phase: \"http_error\",\n    // Use \"safe\" only when your endpoint contract guarantees safe replay.\n    replaySafety: \"unknown\",\n    attempt: 1,\n    elapsedMs: Date.now() - startedAt,\n  });\n\n  // plan.action is \"manual_decision\" here. The library fails closed because\n  // a generation POST can be accepted or billed before the connection fails.\n  console.log(plan);\n}\n```\n\nFor a 429 response with a two-second `Retry-After`, the normalized error is safe to serialize:\n\n```json\n{\n  \"name\": \"OpenAICompatibleError\",\n  \"message\": \"Rate limit exceeded\",\n  \"category\": \"rate_limit\",\n  \"source\": \"http\",\n  \"status\": 429,\n  \"requestId\": \"req_123\",\n  \"retryAfterMs\": 2000\n}\n```\n\nThe original body, complete headers, prompt, response content, cause, and stack are not stored on this object.\n\n## When this package helps\n\nUse it when one application talks to multiple OpenAI-compatible endpoints or mixes raw Fetch, OpenAI SDK-style errors, AI SDK-style errors, and SSE streams. It gives those paths one conservative error and retry contract.\n\nDo not add it when a single SDK already gives your application all the error handling it needs. The official OpenAI Node SDK has its own typed errors and retry behavior. [`llm-errors`](https://www.npmjs.com/package/llm-errors) is broader when you need one taxonomy across OpenAI, Anthropic, and Gemini. AI SDK provider authors can often use [`@ai-sdk/provider-utils`](https://www.npmjs.com/package/@ai-sdk/provider-utils) directly.\n\nThis package is deliberately narrower: it focuses on safe diagnostics and the replay boundary around OpenAI-compatible streaming responses.\n\n## Default-safe errors\n\n`normalizeOpenAICompatibleError(error)` accepts an unknown thrown value. `normalizeOpenAICompatibleResponse(response)` reads a bounded clone of a non-2xx `Response`, leaving the original body untouched. The clone read defaults to 64 KiB and a two-second deadline; both are configurable.\n\nProvider messages are excluded by default because a gateway can echo credentials, prompts, or response fragments into an error message. To include a credential-redacted provider message, opt in explicitly:\n\n```ts\nconst error = normalizeOpenAICompatibleError(caught, {\n  includeProviderMessage: true,\n  maxProviderMessageLength: 1_000,\n});\n\nconsole.log(error.providerMessage);\n```\n\nOpt-in redaction is not PII anonymization. Do not treat arbitrary provider text as safe for public logs.\n\n### Categories\n\n| Category | Typical evidence | Default retry classification |\n| --- | --- | --- |\n| `authentication` | 401, invalid API key | Permanent |\n| `permission` | 403, access denied | Permanent |\n| `rate_limit` | 429, rate-limit code | Transient |\n| `quota` | insufficient quota, billing/credit signal | Permanent |\n| `validation` | 400/409/422 | Permanent |\n| `not_found` | missing model, deployment, or resource | Permanent |\n| `endpoint` | unknown 404 endpoint | Permanent |\n| `payload_too_large` | 413 | Permanent |\n| `timeout` | 408 or timeout signal | Transient |\n| `network` | Fetch/network transport failure | Transient |\n| `upstream` | 502/503/504 | Transient |\n| `server` | other 5xx | Transient |\n| `schema` | malformed JSON or SSE protocol | Manual decision |\n| `stream` | incomplete SSE stream | Transient only if replay is safe |\n| `aborted` | caller abort/cancel | Never retry |\n| `unknown` | insufficient evidence | Manual decision |\n\nClassification uses status, structured `code`/`type`, and a bounded provider message only as input. The default returned message is package-owned text.\n\n### Request IDs and retry hints\n\nRequest IDs are selected in this order: SDK-style `requestID`, `request_id`, or `requestId`, then `x-request-id`, `request-id`, `x-correlation-id`, and `cf-ray`. `Retry-After` supports delay-seconds and HTTP-date values; `retry-after-ms` is also recognized.\n\nThe package does not normalize provider-specific rate-limit counters such as arbitrary `x-ratelimit-limit`, `remaining`, or `reset` headers. Keep those in a separate, explicitly allowlisted metrics path when needed.\n\n## Stream-aware retry decisions\n\nA boolean `retryable` flag is not enough for generation requests. A transient error can still be unsafe to replay after tokens or tool-call arguments were emitted.\n\n```ts\nconst plan = decideOpenAICompatibleRetry(error, {\n  method: \"POST\",\n  phase: streamState.hasOutput\n    ? \"sse_after_output\"\n    : \"sse_before_output\",\n  replaySafety: endpointGuaranteesReplay ? \"safe\" : \"unknown\",\n  attempt: 2,\n  elapsedMs: 4_200,\n});\n```\n\nThe result has one of three actions:\n\n| Action | Meaning |\n| --- | --- |\n| `retry` | Transient error, replay explicitly marked safe, no partial output, and budgets allow it |\n| `do_not_retry` | Permanent error, abort, partial output, unsafe replay, or exhausted budget |\n| `manual_decision` | Error, phase, replay safety, or runtime context is not sufficiently known |\n\nOnly `action === \"retry\"` authorizes an automated caller to retry. The function never performs that retry.\n\n`Retry-After` is honored without shortening it. If the server's delay exceeds `maxDelayMs` or the remaining elapsed-time budget, the result is `do_not_retry` with `retry_after_exceeds_budget`.\n\n```ts\nconst plan = decideOpenAICompatibleRetry(error, context, {\n  maxAttempts: 3,      // Includes the first request.\n  maxElapsedMs: 30_000,\n  baseDelayMs: 500,\n  maxDelayMs: 10_000,\n  jitter: \"full\",\n});\n```\n\nAn idempotency key alone does not prove safe replay. The caller owns `replaySafety` because only the endpoint contract and operation semantics can establish it.\n\n## Inspect an SSE stream\n\n`OpenAICompatibleSSEInspector` accepts arbitrary string or `Uint8Array` chunks. It tracks output without retaining event payloads.\n\n```ts\nimport { OpenAICompatibleSSEInspector } from \"@ai-router/openai-compatible-errors\";\n\nconst inspector = new OpenAICompatibleSSEInspector();\n\nfor await (const chunk of response.body) {\n  const state = inspector.push(chunk);\n  if (state.error) break;\n}\n\nconst state = inspector.finish();\n\nif (state.error) {\n  console.warn({\n    error: state.error,\n    partialOutput: state.hasOutput,\n    unexpectedEof: state.unexpectedEof,\n  });\n}\n```\n\nThe inspector recognizes:\n\n- Chat Completions delta and `{ \"error\": ... }` events.\n- Responses API `response.output_text.delta`, `error`, `response.failed`, and `response.completed` events.\n- Refusal, reasoning, audio, tool-call, and other output-like delta events as conservative replay boundaries.\n- `[DONE]`, CRLF/LF framing, multi-line `data:`, and chunks split inside UTF-8 characters.\n- Malformed JSON, oversized buffered events, empty streams, and premature EOF.\n\nUse `normalizeOpenAICompatibleSSEEvent()` instead when another parser already handles SSE framing.\n\n## Sanitize other diagnostics\n\n`sanitizeForLogs(value)` creates a bounded, JSON-serializable projection. Ordinary user-defined accessors are represented as `[Getter]` rather than invoked.\n\n```ts\nimport { sanitizeForLogs } from \"@ai-router/openai-compatible-errors\";\n\nlogger.warn(sanitizeForLogs({\n  headers,\n  requestId,\n  nestedError,\n}));\n```\n\nIt handles circular references, depth/node/item/key limits, throwing proxies, and common credentials such as Bearer/Basic values, API keys, npm/GitHub token shapes, JWTs, URL userinfo, and sensitive query parameters. Built-in sensitive field rules cannot be disabled.\n\nThe sanitizer is a best-effort defense for common secrets, not arbitrary personal data or a DLP system. Avoid passing prompts, model output, or customer records to logs in the first place.\n\n## Input support\n\n| Input | v0.1 evidence | Notes |\n| --- | --- | --- |\n| Native Fetch `Response` | Loopback HTTP integration plus Node fixtures | Non-2xx only; 64 KiB and 2 s clone-read defaults |\n| OpenAI Node SDK error | `openai@6.49.0` loopback client catch, object tests, and structural fixtures | `APIError`, connection error, timeout, abort, `requestID` |\n| AI SDK `APICallError` shape | Structural fixture tests | `statusCode`, `responseHeaders`, `responseBody`, `isRetryable` |\n| Fetch/Undici exception | Node fixture tests | Abort, timeout, and common network signals |\n| Chat Completions SSE | Chunked fixture tests | Output tracking, errors, `[DONE]` |\n| Responses SSE | Event fixture tests | Delta, failed, completed, error |\n\nThe OpenAI SDK row records one pinned integration version, not support for every SDK release. The AI SDK row remains a structural fixture and does not claim a tested release matrix. Browser, Bun, and Deno support are not claimed in v0.1. Open an issue with a sanitized fixture when a compatible endpoint returns a shape the package misses.\n\n## API\n\n```ts\nnormalizeOpenAICompatibleError(input, options?)\nnormalizeOpenAICompatibleResponse(response, options?)\nnormalizeOpenAICompatibleSSEEvent(event, options?)\ndecideOpenAICompatibleRetry(error, context, policy?)\nnew OpenAICompatibleSSEInspector(options?)\nsanitizeForLogs(value, options?)\nredactSensitiveText(value, maxLength?)\nparseRetryAfterMs(headers, options?)\nextractRequestId(headers)\ngetHeader(headers, name)\n```\n\nThe published TypeScript declarations are the complete API reference. Category additions are backward-compatible; renames or semantic changes follow SemVer.\n\n## Security properties and limits\n\n- Zero runtime dependencies.\n- No network calls, retry execution, logging, telemetry, credential storage, or endpoint defaults.\n- Default errors do not retain raw input, body, complete headers, cause, stack, prompt, or output.\n- Accessor properties are not invoked during unknown-object inspection.\n- Response and SSE reads have configurable hard limits.\n- Unknown replay safety and unknown classifications fail closed to `manual_decision`.\n\nSecret redaction is defense in depth, not a substitute for controlling what reaches your logger. Do not include real credentials, prompts, or customer data in public issue reports; use synthetic canaries and minimal fixtures.\n\nReport suspected vulnerabilities through [GitHub private vulnerability reporting](https://github.com/airouter-dev/openai-compatible-errors/security/advisories/new), not a public issue. Contribution requirements and the synthetic-fixture policy are in [CONTRIBUTING.md](https://github.com/airouter-dev/openai-compatible-errors/blob/main/CONTRIBUTING.md).\n\n## Development\n\nThe packed library targets Node 18.18+. Running the source test suite requires Node 20 or newer because the current Vitest release no longer supports Node 18.\n\n```bash\nnpm install\nnpm run validate\nnpm pack --dry-run\n```\n\nThe test suite covers loopback raw Fetch and OpenAI SDK catch paths, safe serialization, credential canaries, cyclic and hostile objects, pinned OpenAI Node SDK objects, OpenAI/AI SDK structural shapes, Retry-After seconds and dates, retry budgets, split UTF-8 SSE frames, errors after partial output, malformed events, and premature EOF.\n\n## License and naming\n\nMIT. Maintained by [airouter.dev](https://ai-router.dev/) contributors as a provider-neutral developer utility.\n\nThis project is independent and is not affiliated with or endorsed by OpenAI. “OpenAI” is used only to describe API compatibility.\n","readmeFilename":"README.md","_rev":"1-69c393448d57bd0bf1427773fbeafa6e"}