openai Python and TypeScript SDKs, plus the Anthropic SDK (which parses a superset), expect. Existing client code raises its usual typed exceptions unchanged.
Two planes, one envelope, one difference
type, code, message and meta mean the same thing on both LangWatch surfaces, and a client that reads only those four works against either. The surfaces are:
- The control plane: the management API,
/api/gateway/v1/*and/api/webhooks/v1/*. You create virtual keys, budgets, cache rules and webhook endpoints here. - The data plane: the OpenAI-compatible chat surface the gateway serves. Your application’s model calls go here.
Read the reason chain from the plane you called. Nothing else about the envelope moves, and the data plane’s extra
tips, docs_url and fault fields are additive on both.
402 budget_exceeded is a data-plane refusal because a budget stops traffic, not administration. Creating or editing a budget over the control plane is never refused for being over cap.Fields
Validation failures put the offending fields under
meta: meta.target names the part of the request that failed (json, query, param) and meta.fields lists the offending paths. The per-violation chain carries one { code, message, meta } entry each, with meta.field, meta.type and, where the schema knows them, meta.expected and meta.received. That chain lives at error.meta.reasons on the control plane and at error.reasons on the data plane, as above.
The gateway data plane adds three fields on the errors it can offer a next step for, most notably the 402 budget_exceeded:
A client that reads only
type, code, message and meta works unchanged against both planes; the three fields above are additive.
Clients must ignore unknown fields. Fields are added to this envelope over time and none of the additions above changes an existing one.
Response headers
Errors (like successes) always carry:X-LangWatch-Gateway-Request-Id: req_<hex>, use this when filing support tickets. It is the same id that appears asgateway_request_idon spend events and webhook envelopes, so it joins a failing call to its billing records.X-LangWatch-Provider, present when the error originated from an upstream provider (absent for gateway-internal errors).X-LangWatch-Handled-Error, the top-levelerror.code, present on every LangWatch-authored error. A forwarded provider error is not LangWatch-authored, so this header is absent (or stripped, if the provider’s own response happened to set it) on those responses. Use it to tell the two apart rather than guessing from the body shape.
Type enum
Control-plane codes
The management and webhook APIs answer with the same envelope, and theirtype is the status class. Branch on code:
Budget-warning headers (not errors)
These are soft signals on successful responses:X-LangWatch-Budget-Warning: <scope>:<pct>, a budget scope is at or over the 80 percent soft threshold. Multiple can be present.
warn breach never turns into an error envelope; it’s only a header.
end_user_required (fail-closed attribution)
When a virtual key carries an active attributed-user budget template and a request arrives with no end-user id anywhere the gateway resolves one, the request is rejected with error.code = "end_user_required" rather than passing uncapped. Branch on the code: the fix is always on the request side, send the OpenAI user field or the X-LangWatch-End-User-Id header (X-Litellm-End-User-Id is accepted as an alias), and the error’s message and tips name exactly that. It is terminal for the request as sent; do not retry without adding the id.
Examples
Invalid key
Budget exceeded
Blocked tool
Upstream timeout after fallback exhaustion
Streaming errors
For SSE streaming, a terminalevent: error frame carries the same envelope and the stream ends:
error frame should treat the response as incomplete (partial) and X-LangWatch-Gateway-Request-Id still identifies the session in traces.
Mid-stream code values
Once bytes are flowing, the HTTP status is already 200, so the distinguishing signal for clients is the code field inside the terminal frame:
The
type on a streaming terminal frame always reflects the category (provider_error, guardrail_blocked), clients keying off type will already have a usable classification. The code is the granular discriminator if you need it (for example, a retry policy that distinguishes “upstream flaked, retry with a different VK” from “guardrail policy said no, don’t retry”).