Skip to content

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 tripped for the current period top up on the Billing page (or let 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 (abuse@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 where you stand 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

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.

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)
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. That judgement is made 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 gated: more than 30 pre-auth failures in 30 seconds from one address blocks that address for 30 seconds. Spend is controlled separately — 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.

Streaming requests that stall mid-generation are failed over automatically; you receive one continuous stream or a retryable error.

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 tight-loop on 402, it clears only when balance/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, that refusal is relayed 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 rescue attempt failed. Check error.code, not just the HTTP status.