Gateway
Errors & retries
6 classes, chosen so each one implies a different response. Every failure carries the request id, including the ones that fail before authentication finishes.
type is the class. code repeats the HTTP status. request_id is also returned as x-lobstack-request-id and is the id of the trace row, so quoting it in a support thread turns a description into a lookup. The taxonomy is deliberately small: 6 buckets that each imply a different action beats a long list nobody can group by.
The 6 classes
| Class | Status | Whose problem | Retry? |
|---|---|---|---|
| auth | 401, 403 | The credential | No |
| quota | 402, 429 | Allowance or rate limit | 402 no, 429 yes |
| validation | 400, 409, 422 | The request body | No |
| provider | 502, 503, other 5xx | The upstream model provider | Yes, with backoff |
| timeout | 408, 504 | We gave up waiting | Yes |
| internal | 500 | Ours | Once, then report it |
The mapping runs from status to class, so a status the Gateway raises and a status a provider returns land in the same bucket. The statuses above are the ones each class claims by name; any other 5xx is attributed to the provider, which is why provider reads “other 5xx” and internal does not — 500 is claimed by name and never reaches that rule.
An unknown throw is classified internal rather than provider: blaming upstream for our own crashes would make the error rate look better than it is. A sub-500 status no class claims goes the same way, for the same reason.
auth
The credential was rejected. Causes: no bearer token; a key that does not match the lsk_<env>_<8 hex><48 hex> shape; a key that is unknown, revoked or expired; a key without the inference scope; and a legacy agent credential whose row is not active, which returns 403 with that status in the message.
Do not retry. Revoked and expired keys are reported distinctly from unknown ones in the message, because "this key was revoked" beats "unauthorized" when somebody is debugging at two in the morning, but every one of them is a rejection.
quota
Two different things share this class. A 402 means the allowance for the period is gone. A 429 means a rate limit was hit — in practice the provider's, passed through with its status.
Which allowance depends on the meter, and the message says so rather than making the caller guess. x-lobstack-quota-meter on the same response carries spend, requests or legacy; the three are described on Pricing & plans.
Enforcement is asymmetric on purpose. API keys are enforced from the start; legacy agent credentials get the quota headers but are not blocked. Turning enforcement on for them would start returning 402 to callers that have been over their allowance for weeks and working fine, which is a billing-policy decision rather than something to switch on inside a refactor. Both kinds of caller get the same headers, so the data to make that decision exists.
retry-after is in seconds and is computed from the period reset, so on a 402 it is usually days rather than seconds. Treat it as "this will not clear on its own before then", not as a backoff hint: top up or upgrade the plan. Purchased balance is consumed after the included allowance — as dollars on a spend meter, and as x-lobstack-quota-credits on the counted meters.
A request allowance counts this calendar month's successful traced requests. Requests that failed at the provider are excluded, so an outage does not consume the allowance, and the same rule holds on a spend meter: an error row accrues no spend. A quota lookup that itself errors fails open — an unavailable quota table must never stop inference, so an allowance that cannot be read is treated as empty rather than as exhausted and the failure is logged rather than turned into a 402.
validation
The request was malformed, and it will fail identically until you change it. Causes: messages missing or empty; a body that is not JSON; a model key the registry does not know, including after alias resolution; a model that exists but is not available in managed mode; and BYOK mode with no key on the agent.
Provider 400s also land here, because the status passes through with the provider's own message inside. That is the right bucket — the caller can fix it — but it means a validation error can be about something you never wrote yourself. The worked example below is exactly that case.
provider
The upstream failed and it is not your fault. Causes: the network call to the provider threw, which becomes a 502; the provider returned a 5xx, which passes through with its status; and 503 when a managed provider key is not configured on the deployment.
A provider 401 is deliberately remapped to 502. Lobstack's own credential being wrong is our failure, and reporting it as 401 would tell the caller to go and check a key that is fine.
Retry with exponential backoff and jitter. The exception is the 503 whose message says no managed provider key is set — that one is a configuration problem and will fail forever until a key is added or the agent is switched to BYOK.
timeout
The provider call was aborted. Adapters abort at 120 seconds by default, and the route itself allows up to 300 seconds. Anything whose message looks like an abort or a timeout is classified here even when it arrives as an unrecognised throw.
Retryable, but read the warning about duplicate billing below before you do it automatically.
internal
Our bug. Any throw that is not a recognised Gateway error and does not look like a timeout lands here as a 500. Retry once. If it repeats, the request id is the whole conversation: the trace row holds the status, the latency, the model, the credential and the first 1,000 characters of the error message.
Worked example: the temperature 400
Anthropic deprecated the sampling parameters on Opus 4.7 and everything after it, including Opus 5, Sonnet 5 and the Fable line. The API does not ignore a temperature on those models; it returns a hard 400.
Passed through the Gateway, that arrived as a 400 of class validation with the provider's message wrapped inside. It was the most common failure in this account's history, and not because customers were setting temperatures: the Gateway sent temperature: 0.7 on every Anthropic request whether or not anyone had asked for one, and every one of them failed.
Two changes removed it. A temperature is forwarded only when the caller actually set one — inventing a sampling default was never the Gateway's decision to make, since it silently changed the character of the output. And it is forwarded only to models that still accept it, matched on the provider-native model id that goes on the wire. When one is dropped the response says so in x-lobstack-dropped-params: temperature. A check in the repo holds the expected answer for every Anthropic model in the registry, so a new one cannot default in by accident.
If you see this 400 today, you sent a temperature to a model that rejects it through a path that is not this Gateway. The behaviour of each family is tabulated on Chat Completions.
Retrying
| Class | What to do |
|---|---|
| auth | Fix the credential. Never retry. |
| quota | 402: top up, upgrade, or wait for the reset. 429: back off exponentially with jitter. |
| validation | Fix the body. Retrying reproduces the failure exactly. |
| provider | Retry with backoff, except a 503 saying no managed key is configured. |
| timeout | Retry, aware that the first attempt may have completed. |
| internal | Retry once, then report the request id. |
Failures inside a stream
A stream that fails part-way through cannot change its status code, which is already 200 and whose headers are long gone. The Gateway sends an error object into the stream and then [DONE]. The trace row records the real status, so a stream that appeared to succeed to a naive client still shows as a failure in the usage API.
"type": "gateway_error" rather than one of the6 class names. The class is still recorded on the trace row; group by it through the usage API rather than by parsing the stream.Counting failures
GET /api/v1/usage returns summary.errors_by_class over any range, which is the fastest way to see whether a bad hour was upstream or yours. A failed round also writes a ledger row with zero tokens, so error rate stays computable from the ledger alone. See Metering & cost.