TypeScript SDK
TypeScript 5.0 or later. Node 20 or later, Bun, Deno, Cloudflare Workers. Zero runtime dependencies. The package ships ESM, CJS and .d.ts.
Start here
r.label === 'shiping' is a compile error here. That is the whole point.
Describe every label. The description is the instruction, and the model reads it literally. Described labels score measurably better than bare names. See writing good statements and labels.
Client options
Every capability takes the same per-call options as its last argument: timeout, maxRetries, signal and headers.
The eight capabilities
classifyTree narrows label to the labels a walk can stop on, and path to every label at every level. probability and confidence are products over the levels, so they fall with depth.
Batching
One text gives one result. A tuple of texts gives a tuple of results, in order.
A hoisted label list needs as const to keep its union: const LABELS = ['billing', 'shipping'] as const. A plain string[], read from a config file for example, types r.label as string. The literal inline forms above need nothing.
A batch holds at most 32 texts, and each text at most 20,000 characters. The SDK never chunks for you.
Extraction
A JSON Schema object literal types the result on its own.
Four degradations are real, and the types state them:
- A missing value is
null. Every scalar leaf is nullable. - An array of objects always comes back
[]. Line-item quality is not good enough to ship. - An array of scalars comes back as strings. The runner calls
String()on every element. - An enum is not checked server side.
(string & {})admits reality and keeps autocomplete.
A nested object is never null. Only its leaves are. See Extract.
zod, valibot and arktype
A zod 4.2 schema passes straight in. Every schema carries its own toJSONSchema() method from that release, and the SDK calls it.
An arktype schema passes straight in too. The SDK calls its toJsonSchema() and reads the output type from ~standard.
valibot and zod 4.0 or 4.1 convert through a module function instead. Brand the output with typed<>, and the result gets your type.
Usage and rate limits
withUsage() returns the result, the usage and the raw Response. Without it you await the result alone.
Request-limit values count inference request units, not HTTP calls. A batch with 24 texts consumes 24 units; text and statement or question counts multiply.
usage.rateLimit carries limitRequests, remainingRequests, resetRequests, limitTokens, remainingTokens and resetTokens. Tiers with unlimited TPM omit the token headers. Read usage.headers if the SDK returns null for a partial header set.
The API reserves units before processing the call. Rate-limit headers show a snapshot after admission; concurrent calls can change the remaining balance.
Honor retry-after on rate-limit refusals. See Limits and rate limits.
Errors and retries
The SDK retries 429 rate_limit_exceeded, 502 runner_error, 529 overloaded, and transport failures and timeouts. Every capability is a pure function, so a retry is always safe. It never retries 400, 401, 403 or 429 insufficient_quota. maxRetries defaults to 2 and takes 0. The backoff is full jitter, capped at 8 seconds, and a retry-after header wins over the backoff.
e.attempts counts the attempts, including the first. e.status is 0 when the call never reached the API.
A 502, 503 or 504 with no JSON body is retried on the status alone. A Cloudflare error page never reaches the worker, so it carries no code.
The SDK also checks your call before it sends anything: the label, statement, scale and text limits, and the two API traps. Those throw code: 'client_error' with status: 0, and no token is billed. They throw synchronously, before the Decision exists. Catch them with try/catch around the call, not with .catch() on it.
Errors lists every code.
Any path, any body
dm.post() reaches the untouched body, and any future path:
Runtimes
Node 20 or later, Bun and Deno work with no configuration. The SDK uses global fetch.
On Cloudflare Workers, pass a service binding’s fetch:
In a browser the constructor throws. Your API key is a secret, and a bundle ships it to every visitor. Call the API from your server. dangerouslyAllowBrowser: true opts out, and is right only when the bundle never reaches a user.
Gotchas
yes-noanswers200with{"results":[]}for a body that carries neithertextnortexts. The SDK always sends one of the two, so that body cannot reach the API. An empty text is a400, and the SDK’s local check only saves you the round trip.classify-treesumsinference_msover every level into the header, whilex-input-charscounts one pass over the body. The per-level numbers do not sum tousage.inputChars.retry-afterrides on429 rate_limit_exceededonly.e.retryAfterisnulloninsufficient_quota.- The six
x-ratelimit-*headers arrive together or not at all.usage.rateLimitisnullin the second case.