# Errors & limits
Every error is JSON with a stable shape (a machine-readable `code` is present where one applies):
```json
{ "error": { "message": "…", "type": "…", "code": "…" } }
```
Branch on the HTTP status and, where there is one, on `error.code`. Message strings are
human-readable prose and can be reworded at any time; `code` values are stable. Never match on
message text.
## Status codes
| Code | Meaning | What to do |
|---|---|---|
| `400` | Malformed request, or a request no route can serve, `type: "invalid_request_error"`, plus a stable `code` for the named cases [below](#named-400-codes) | fix the request |
| `401` | Missing or invalid API key | check the `Authorization: Bearer` header; the key may be revoked |
| `402` | Workspace prepaid balance exhausted, or this key's [budget](../platform/authentication.md) reached for the current period | top up on the [Billing page](../platform/billing.md) (or let [auto top-up](../platform/billing.md#auto-recharge) prevent it); for a budget, raise/clear it, use another key, or wait for the period reset |
| `403` | Account or key suspended, the message names the address to write to (`support@keln.ai`) | contact that address |
| `404` | Unknown route | check the path (`/v1/chat/completions`) |
| `413` | Request body over 32 MB | send less, 32 MB is enough for a full-window prompt on a 1M-context model |
| `429` | Rate limited. A `Retry-After` header gives the wait in seconds; limits apply per key, and the [rate-limit headers](#rate-limit-headers) on every response show your remaining allowance | wait at least `Retry-After`, then retry (see below) |
| `502` | All serving attempts failed | safe to retry; already rare because of hedging/failover |
| `503` | Transient, no capacity for this model right now, or a brief internal overload | retry with backoff; this is a fast-fail, not a queue, and there is no `Retry-After` on a `503` — with one exception, `server_draining` below |
`401`, `402`, `403`, `429`, `502`, and `503` carry `type: "keln_error"` and no `code` field; the
HTTP status is the whole machine-readable signal, so branch on it alone. The one `503` that does
carry a code is `server_draining`: the server your request landed on is shutting down and had not
started serving it. Nothing was billed, the response carries `Retry-After: 1`, and retrying at once
is the right move (another server takes it).
`413` is the one error that isn't JSON: the body is plain text, not the envelope above.
### Named `400` codes
Each of these carries `type: "invalid_request_error"` and a stable `code`:
| `code` | Means |
|---|---|
| `model_not_found` | the `model` id isn't in the catalog, check [`/v1/models`](models.md) |
| `variant_not_supported` | a model-id suffix Keln doesn't offer (`:free`, `:online`) |
| `n_too_large` | `n` asks for more completions per request than the limit (8); send separate requests for more |
| `web_search_name_reserved` | the request opts into [web search](../capabilities/web-search.md) and also defines a function tool named `web_search` |
| `web_search_unavailable` | web search is not enabled on this deployment |
| `modality_not_supported` | image parts sent to a text-only model |
| `context_length_exceeded` | prompt + `max_tokens` exceeds the model's context window, rejected before any tokens are billed; shorten the input or lower `max_tokens`, windows are in [`/v1/models`](models.md) |
| `reasoning_not_disableable` | you sent `reasoning_effort: "none"` on a model that cannot disable reasoning, omit the field (accept thinking) or use another model |
| `tools_not_supported` | no route serving this model can do tool calling for this request |
| `response_format_not_supported` | no route can honor the `response_format` mode you asked for, in the transport you asked for |
| `param_not_supported` | a parameter you demanded is supported by no route serving this model |
| `param_combination_not_supported` | each parameter is fine on its own, but no route supports that combination |
!!! note "Per-route limitations vs model-wide gaps"
A parameter that one particular serving route rejects is handled before dispatch and does
not surface as an error. When no route serving the model supports what you asked for, a
parameter, a combination of parameters, tool calling, a `response_format` mode, image
input, you get a `400` naming the gap. Keln makes that judgment against your exact request,
including the mode you asked for and whether you asked for streaming.
## Limits
Current defaults. Every limit answers with a `429` carrying `Retry-After`; nothing queues, and a
rate-limited request never reaches a model and never bills. If your workload needs more,
contact us and we'll raise it.
| Limit | Default | Scope |
|---|---|---|
| Request rate | 100 requests/second, burst to 200 | per key |
| Concurrent requests | 256 in flight | per workspace |
| `GET /v1/models` | 60 per minute, burst to 120 | per IP (it's unauthenticated) |
| Request body | 32 MB | per request (`413`, not `429`) |
Repeated failed authentication is also limited: more than 30 pre-auth failures in 30 seconds
from one address blocks that address for 30 seconds. Spend is controlled separately, by the
prepaid balance and [per-key budgets](../platform/authentication.md), which answer with a `402`,
not a `429`.
## Rate-limit headers
Requests are limited per key by a token bucket: a sustained rate plus burst headroom,
replenished continuously rather than reset on window boundaries. Successful `/v1` responses and
rate-limit `429`s report the bucket's state at admission (request errors that fail before
dispatch, like a `400` for an unknown model, don't carry them):
| Header | Meaning |
|---|---|
| `x-ratelimit-limit-requests` | the key's sustained rate, in requests per second |
| `x-ratelimit-remaining-requests` | whole requests admissible right now, burst headroom included |
| `x-ratelimit-reset-requests` | seconds until the bucket is completely full again |
A `429` additionally carries `Retry-After`: the computed wait, in seconds, until the next
request will be admitted. Honor it instead of guessing a backoff. The official OpenAI and
Anthropic SDKs already do this automatically.
## No timeout error
Keln does not time out a request that is still being served. A slow attempt is hedged and
re-raced against fresher capacity. `504` is not part of the error surface.
Keln fails over a streaming request that stalls mid-generation automatically; you receive one
continuous stream or a retryable error.
## Every response carries a request id
Every response from `api.keln.ai` — success or error, streamed or not, including refusals such as
`401` and `429` — carries an `x-keln-request-id` header. It is the id Keln's own routing records are
filed under. Quote it when you write to support about a specific request; it is the fastest way for
us to find exactly what happened to it.
## Retry guidance
`429` is safe to retry after waiting the `Retry-After` seconds; a rate-limited request never
reaches a model and bills nothing. `503`/`502` are safe to retry (idempotent from Keln's side, a
failed attempt that produced nothing bills nothing). Use exponential backoff with jitter, e.g.
0.5 s → 1 s → 2 s → 4 s, cap 5 attempts. Do not retry `402` in a loop; it clears only when
balance or budget changes.
## Streaming semantics
- SSE chunks in OpenAI format; the stream ends with `data: [DONE]`, full details in
[Streaming](streaming.md).
- Ask for usage in the final chunk with `"stream_options": {"include_usage": true}`.
- If a mid-stream failover occurred you still receive a single continuous stream; the final
usage chunk covers the whole response.
## Refusals are not errors
If the serving model refuses a prompt, Keln relays the refusal to you as-is: an ordinary
successful response carrying what the model said, with `finish_reason: "content_filter"`. It's a
model refusal, not an error envelope; check `finish_reason` if you want to branch on it.
## Errors after HTTP 200
Once the first byte of a response is on the wire, the HTTP status is committed, a later total
failure is reported in the body, always with a machine-readable `code`:
- **Streaming**, a terminal chunk with `finish_reason: "error"` and a top-level
`error.code: "stream_interrupted"`, then `[DONE]` ([shape](streaming.md#mid-stream-failures)).
The output above it is partial; retry is safe, only delivered tokens bill.
- **Non-streaming**, the standard error envelope with `code: "upstream_incomplete"` as the body
of the 200. Nothing was delivered, nothing bills; retry is safe.
Both are last-resort paths after every hedge, reroute, and failover attempt failed, and both are
also what you get when the server carrying your response has to shut down before finishing it
(it is given a bounded time to finish in-flight work; past that it ends every open response this
way instead of closing the connection). Check `error.code`, not just the HTTP status.
Errors & limits
Every error is JSON with a stable shape (a machine-readable code is present where one applies):
{"error":{"message":"…","type":"…","code":"…"}}
Branch on the HTTP status and, where there is one, on error.code. Message strings are
human-readable prose and can be reworded at any time; code values are stable. Never match on
message text.
Status codes
Code
Meaning
What to do
400
Malformed request, or a request no route can serve, type: "invalid_request_error", plus a stable code for the named cases below
fix the request
401
Missing or invalid API key
check the Authorization: Bearer header; the key may be revoked
402
Workspace prepaid balance exhausted, or this key's budget reached for the current period
top up on the Billing page (or let auto top-up prevent it); for a budget, raise/clear it, use another key, or wait for the period reset
403
Account or key suspended, the message names the address to write to (support@keln.ai)
contact that address
404
Unknown route
check the path (/v1/chat/completions)
413
Request body over 32 MB
send less, 32 MB is enough for a full-window prompt on a 1M-context model
429
Rate limited. A Retry-After header gives the wait in seconds; limits apply per key, and the rate-limit headers on every response show your remaining allowance
wait at least Retry-After, then retry (see below)
502
All serving attempts failed
safe to retry; already rare because of hedging/failover
503
Transient, no capacity for this model right now, or a brief internal overload
retry with backoff; this is a fast-fail, not a queue, and there is no Retry-After on a 503 — with one exception, server_draining below
401, 402, 403, 429, 502, and 503 carry type: "keln_error" and no code field; the
HTTP status is the whole machine-readable signal, so branch on it alone. The one 503 that does
carry a code is server_draining: the server your request landed on is shutting down and had not
started serving it. Nothing was billed, the response carries Retry-After: 1, and retrying at once
is the right move (another server takes it).
413 is the one error that isn't JSON: the body is plain text, not the envelope above.
Named 400 codes
Each of these carries type: "invalid_request_error" and a stable code:
code
Means
model_not_found
the model id isn't in the catalog, check /v1/models
variant_not_supported
a model-id suffix Keln doesn't offer (:free, :online)
n_too_large
n asks for more completions per request than the limit (8); send separate requests for more
web_search_name_reserved
the request opts into web search and also defines a function tool named web_search
web_search_unavailable
web search is not enabled on this deployment
modality_not_supported
image parts sent to a text-only model
context_length_exceeded
prompt + max_tokens exceeds the model's context window, rejected before any tokens are billed; shorten the input or lower max_tokens, windows are in /v1/models
reasoning_not_disableable
you sent reasoning_effort: "none" on a model that cannot disable reasoning, omit the field (accept thinking) or use another model
tools_not_supported
no route serving this model can do tool calling for this request
response_format_not_supported
no route can honor the response_format mode you asked for, in the transport you asked for
param_not_supported
a parameter you demanded is supported by no route serving this model
param_combination_not_supported
each parameter is fine on its own, but no route supports that combination
Per-route limitations vs model-wide gaps
A parameter that one particular serving route rejects is handled before dispatch and does
not surface as an error. When no route serving the model supports what you asked for, a
parameter, a combination of parameters, tool calling, a response_format mode, image
input, you get a 400 naming the gap. Keln makes that judgment against your exact request,
including the mode you asked for and whether you asked for streaming.
Limits
Current defaults. Every limit answers with a 429 carrying Retry-After; nothing queues, and a
rate-limited request never reaches a model and never bills. If your workload needs more,
contact us and we'll raise it.
Limit
Default
Scope
Request rate
100 requests/second, burst to 200
per key
Concurrent requests
256 in flight
per workspace
GET /v1/models
60 per minute, burst to 120
per IP (it's unauthenticated)
Request body
32 MB
per request (413, not 429)
Repeated failed authentication is also limited: more than 30 pre-auth failures in 30 seconds
from one address blocks that address for 30 seconds. Spend is controlled separately, by the
prepaid balance and per-key budgets, which answer with a 402,
not a 429.
Rate-limit headers
Requests are limited per key by a token bucket: a sustained rate plus burst headroom,
replenished continuously rather than reset on window boundaries. Successful /v1 responses and
rate-limit 429s report the bucket's state at admission (request errors that fail before
dispatch, like a 400 for an unknown model, don't carry them):
Header
Meaning
x-ratelimit-limit-requests
the key's sustained rate, in requests per second
x-ratelimit-remaining-requests
whole requests admissible right now, burst headroom included
x-ratelimit-reset-requests
seconds until the bucket is completely full again
A 429 additionally carries Retry-After: the computed wait, in seconds, until the next
request will be admitted. Honor it instead of guessing a backoff. The official OpenAI and
Anthropic SDKs already do this automatically.
No timeout error
Keln does not time out a request that is still being served. A slow attempt is hedged and
re-raced against fresher capacity. 504 is not part of the error surface.
Keln fails over a streaming request that stalls mid-generation automatically; you receive one
continuous stream or a retryable error.
Every response carries a request id
Every response from api.keln.ai — success or error, streamed or not, including refusals such as
401 and 429 — carries an x-keln-request-id header. It is the id Keln's own routing records are
filed under. Quote it when you write to support about a specific request; it is the fastest way for
us to find exactly what happened to it.
Retry guidance
429 is safe to retry after waiting the Retry-After seconds; a rate-limited request never
reaches a model and bills nothing. 503/502 are safe to retry (idempotent from Keln's side, a
failed attempt that produced nothing bills nothing). Use exponential backoff with jitter, e.g.
0.5 s → 1 s → 2 s → 4 s, cap 5 attempts. Do not retry 402 in a loop; it clears only when
balance or budget changes.
Streaming semantics
SSE chunks in OpenAI format; the stream ends with data: [DONE], full details in
Streaming.
Ask for usage in the final chunk with "stream_options": {"include_usage": true}.
If a mid-stream failover occurred you still receive a single continuous stream; the final
usage chunk covers the whole response.
Refusals are not errors
If the serving model refuses a prompt, Keln relays the refusal to you as-is: an ordinary
successful response carrying what the model said, with finish_reason: "content_filter". It's a
model refusal, not an error envelope; check finish_reason if you want to branch on it.
Errors after HTTP 200
Once the first byte of a response is on the wire, the HTTP status is committed, a later total
failure is reported in the body, always with a machine-readable code:
Streaming, a terminal chunk with finish_reason: "error" and a top-level
error.code: "stream_interrupted", then [DONE] (shape).
The output above it is partial; retry is safe, only delivered tokens bill.
Non-streaming, the standard error envelope with code: "upstream_incomplete" as the body
of the 200. Nothing was delivered, nothing bills; retry is safe.
Both are last-resort paths after every hedge, reroute, and failover attempt failed, and both are
also what you get when the server carrying your response has to shut down before finishing it
(it is given a bounded time to finish in-flight work; past that it ends every open response this
way instead of closing the connection). Check error.code, not just the HTTP status.