Skip to main content
The routing, translation, and error classification all live in the Rust core. The Python and Node packages are thin bindings over the same compiled binary, not reimplementations — so behaviour cannot drift between them.
Pick a language once. Every tabbed block on this page stays in sync, so selecting Python below switches the whole reference to Python.

Install

Requires Python 3.10 or newer. WarpLLM is synchronous; AsyncWarpLLM is the await-able twin, and both take the same arguments.

Constructing a client

All four parameters are keyword-only and optional. AsyncWarpLLM takes the same four.
Construction reads the environment once and keeps the providers it can authenticate. It never requires credentials — a client built where no keys exist is valid, and each request then says which variable it wanted.
Keys are a snapshot. Exporting one after the client is built has no effect on that client, and a rotated key needs a new client. See Authentication.

Configuration options

All timeouts are seconds, in every language. See Timeouts.
There is no client-wide base URL. To point a provider somewhere else — a corporate proxy, or a test double — set base_url on that provider’s entry under Declaring providers. It affects that provider alone.

Declaring providers

By default a client serves warpllm’s whole roster and reads every provider’s environment variable. Declaring narrows both at once.
ProviderOptions is a TypedDict with two optional fields, api_key and base_url. To point a provider elsewhere:
An entry’s base_url overrides that provider’s address — a proxy, or a test double — and wins over the roster’s own. It is per-provider: it never touches any other. It narrows two things:
1

What is read

Only the declared providers’ environment variables are consulted, so a key exported for something else is not quietly adopted.
2

What is routable

A model under a provider you did not declare is refused at admission with provider_not_declared, before any socket opens — so a client serving one provider cannot be talked into billing another by a model string.
Absent and empty are different claims. Omitting providers serves the whole roster, which is what every client did before the option existed. Declaring an empty map says “none”, and nothing routes.
An inline key is for callers holding keys somewhere the process environment cannot reach — a secret manager, a per-tenant database row. It wins over the variable the roster names, and it can authenticate a provider whose roster entry names no variable at all. An empty string is treated as unstated, so the variable still gets its turn. A declaration selects from the roster; it does not extend it. A provider name the roster does not hold fails when the client is built, not at the request that happened to route there.

Chat completions

The request crosses verbatim, so fields warpllm does not model reach the provider untouched. The parameter is a Mapping, so any key type-checks.AsyncWarpLLM has the same method, awaited:
A request is admitted only when both halves hold: the roster registers the model, and this client holds a key for the provider serving it. They are checked in that order — a name nothing registers is a typo, and hearing “set DEEPSEEK_API_KEY” for deepseek/typo would send you after a credential you already have.

Streaming

stream=True returns a ChatCompletionStream, matching the official OpenAI SDK’s one-method shape. Dispatch is on the value, so behaviour is correct wherever a checker cannot resolve the overload.
Static typing here is best-effort: a dict literal carrying keys warpllm does not model matches no TypedDict, so a checker may infer the non-streaming return type. Where the annotation has to be right, call chat_completions_stream(request) — same behaviour, precisely typed, and it sets stream for you.
AsyncWarpLLM returns an AsyncChatCompletionStream for async for. It awaits its own native method rather than borrowing the sync client’s: opening a stream is a request whose headers a provider may sit on, and blocking on that inside a coroutine would stop the event loop.
Streaming is its own roster entry, so a model that serves whole replies says nothing about whether it serves streamed ones. Both are checked against the model’s supported_apis. The stream ending cleanly means the reply is complete. An upstream that stopped early raises StreamTruncated after every chunk that did arrive, so a truncated answer is never mistaken for a finished one. An error is terminal — whatever produced it also ended the stream. See Streaming.

Errors

The hierarchy is APIError → APIConnectionError, and APIError → APIStatusError → each status class. So except APIStatusError catches everything an upstream answered while letting a connection failure through.
Python and Node raise the classes the official OpenAI SDK raises, selected from the same status warpllm rendered. No binding interprets a status on its own. See Errors for the full taxonomy and what is worth retrying.

Types

They are TypedDicts, so they cost nothing at runtime and are optional. chat_completions accepts any Mapping precisely because warpllm forwards fields it does not model.Responses come back as plain dicts, so you subscript: completion["choices"][0]["message"]["content"].

Absent versus null

Response fields that are both optional and nullable distinguish a key the provider omitted from one it sent as null.
A missing key versus a None value. chunk["choices"][0]["delta"].get("content") returns None for both — use in to tell them apart when it matters.

Version

Shared behaviour

Everything below holds identically in all three:
  • Model strings are provider/model, matched against a closed roster.
  • API keys are read from the environment once, when the client is built.
  • Request fields warpllm does not model are forwarded to the provider untouched.
  • The response’s model echoes your routing string, not the upstream name.
  • A stream that ends cleanly is a complete reply.
  • There are no built-in retries and no fallback chain yet; both are planned. Rust additionally has an unreleased BalancedClient for weighted load balancing — see Model routing.

Where the surfaces differ, and why

chat_completions with stream: Some(true) returns Error::InvalidInput pointing at chat_completions_stream. A whole-reply return type cannot carry chunks, so the entrypoint says where to go rather than quietly serving the wrong shape.Python and Node dispatch on the value instead, matching the official OpenAI SDK’s one-method shape in each language.
warpllm::Error distinguishes RateLimited from QuotaExceeded, and origin() says whether a provider ever answered. The bindings receive the OpenAI rendering of that taxonomy instead.This is deliberate. A field on an SDK error object is a promise to keep across versions; warpllm’s own vocabulary stays Rust-side, where changing it costs a recompile instead of a major version. The gateway’s HTTP envelope is the one other surface that carries it.
Rust has already parsed and validated the response. Rebuilding it into Python or JavaScript objects would redo that work to hand back the same fields under the same names.Python therefore subscripts (completion["choices"][0]) while Node and Rust use dot access.
warpllm takes one mapping so unmodeled fields cross untouched, which means the streaming overload has to match the mapping’s type — and a dict literal carrying extra keys matches no TypedDict. Runtime behaviour is exact; where the annotation has to be right, call chat_completions_stream.

Next

Errors

The full taxonomy and what is worth retrying.

Run the gateway

The same core behind an HTTP surface.