Thirteen failure kinds, each named by what fixes it
Wider than this screen. Scroll the plate sideways.
Input
Paste a real one. Nothing is sent anywhere. This is the package running locally.
Verdict
quotaaccount: money or plan fixes it
Top up, or wait for the reset. Retrying will not help.
- isTransient
- false
- isBackupEligible
- false
- retryAfterMs
- -
- status
- 429
providerkit
One seam for every LLM provider, plus the failure handling you only learn in production. Not a framework: no loop, no prompts, no graph. Your loop is where your product lives.
bun add @providerkit/coreGet startedRead the taxonomySource
Reading this as an agent? /llms.txt indexes the site,
/kinds.json is the taxonomy as data, and appending .md to
any page returns its source.
The interface is the easy part.
Section titled “The interface is the easy part.”Most “unified LLM interface” libraries stop at the interface. That is not where the time goes. This is where the time goes.
The stream that never ends
A provider accepts the request and then sends nothing. Every SDK’s default is to wait forever, and the symptom is the worst kind: none at all.
The socket four levels down
A dead connection carries no HTTP status, and the useful code sits on
cause.cause.cause. Miss it and every network blip reads as permanent.
One cause, five statuses
Running out of credit arrives as 429, 402, 403 and 400 depending on the vendor. Status alone tells you to retry an empty balance.
The 429 that is really a full prompt
Waiting fixes a throttle and does nothing for an overflowing context window. They arrive under the same status.
Cache tokens, counted twice
Anthropic reports cache reads outside the input count; OpenAI reports them inside it. Hand both through unreconciled and the same conversation costs two different things.
The answer that was already there
A turn cut off at its output ceiling leaves invalid JSON, and a run reports nothing while the answer sits in the fragments.
Small surface. Boring on purpose.
Section titled “Small surface. Boring on purpose.”One provider interface, one error type, and the handful of primitives a loop actually needs underneath it.
import { createAnthropicProvider } from "@providerkit/core";
const provider = createAnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY, model: "claude-sonnet-5",});
for await (const chunk of provider.createStream(messages, tools, { effort: "medium" })) { if (chunk.content) write(chunk.content); // one usage shape across every provider if (chunk.usage) record(chunk.usage);}import { ProviderError, isTransient, isBackupEligible } from "@providerkit/core";
try { /* ... */} catch (raw) { const err = ProviderError.from("anthropic", raw);
if (err.kind === "context") return compactAndRetry(); if (isBackupEligible(err.kind)) return otherModel(); if (isTransient(err.kind)) return retry(err.retryAfterMs); surface(err); // .body has their actual words}Five codebases learned this separately.
Section titled “Five codebases learned this separately.”providerkit was extracted from five production codebases that had each independently grown the same layer: about 9,100 lines solving one ~2,000-line problem. They had three separate 60-second idle watchdogs, identical down to the constant. On one day in September 2026, two of them shipped the same five fixes independently.
They had also each learned a different part of it. One walked the cause chain for dead sockets. One parsed Gemini’s RetryInfo. One read the body before the status and knew the quota wordings in five languages. One knew Anthropic’s 529 and when a failure is worth a different model. One could rescue an answer from a tool call the model truncated.
The classifier on this page is the union of all five, and the test suite is every failure any of them ever saw. That is the part worth having.
Four of the five run their AI on it in production today. The migrations that put it there were net-negative every time: −452, −1,621 and −197 lines, and the last one trades three vendor SDKs for this package.