Errors
Errors are returned in your ingress format’s native error shape with the correct
HTTP status code. Upstream provider failures are mapped to a stable taxonomy — the
code values below never change meaning, so you can switch models and providers
without rewriting error handling.
Error envelopes per format
Chat Completions / Responses
OpenAI-format requests get the OpenAI error object:
{
"error": {
"message": "Workspace balance is not positive. Top up to resume.",
"type": "insufficient_credits",
"code": "insufficient_credits",
"param": null
}
}HTTP status: 402 Payment Required.
The taxonomy
This table is rendered from the gateway’s own error constants
(@hyperinfer/shared), so it cannot drift from the implementation:
| Code | HTTP status | Meaning |
|---|---|---|
provider_auth | 502 | The upstream provider rejected our credentials. Not caused by your key. |
provider_rate_limit | 429 | The upstream provider rate-limited the request. Retry with backoff. |
provider_overloaded | 529 | The upstream provider is overloaded. Retry with backoff. |
context_length_exceeded | 400 | The request exceeds the model's context window. |
content_filter | 400 | The upstream provider's content filter blocked the request or response. |
provider_timeout | 504 | The upstream provider timed out before completing the response. |
provider_unavailable | 502 | The upstream provider could not be reached or returned a server error. |
insufficient_credits | 402 | The workspace balance is not positive. Top up to resume. |
key_limit_exceeded | 402 | A daily, monthly, or total spend limit on this API key was reached. |
model_not_allowed | 403 | The key is pinned to specific models and this slug is not on its list. |
invalid_api_key | 401 | The key is missing, malformed, revoked, disabled, or expired. |
workspace_locked | 403 | The workspace has been locked by operations. |
rate_limit_exceeded | 429 | Too many requests per minute on this key. Honor Retry-After. |
invalid_request | 400 | The request body failed validation for the ingress format. |
payload_too_large | 413 | The request body exceeds the 50 MB limit. |
internal_error | 500 | Unexpected error on our side. Safe to retry. |
Retry-After — when to back off
Rate-limited and overloaded responses tell you exactly when to retry, two ways at once:
- The standard
Retry-AfterHTTP header — integer seconds. - An in-envelope
retry_after_secondsfield — the same integer, inside theerrorobject, so SDKs that don’t surface headers can still read it.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
{
"error": {
"message": "Exceeded the per-key rate limit — retry in ~12s",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"param": null,
"request_id": "req_…",
"retry_after_seconds": 12
}
}The field is present iff the header is present — on these codes only:
| Code | HTTP | Source of the value |
|---|---|---|
rate_limit_exceeded | 429 | token-bucket reset time |
provider_rate_limit | 429 | upstream Retry-After (passed through) |
provider_overloaded | 529 | circuit-breaker cooldown (30 s ± 20% jitter) |
provider_timeout | 504 | upstream Retry-After, or breaker cooldown |
provider_unavailable | 502 | upstream Retry-After, or breaker cooldown |
On every other error the header and the field are both absent — do not assume they
exist. The value is clamped to 1–120 seconds so a misbehaving upstream can’t push
you out indefinitely. The message is human-readable and carries a friendly countdown
("retry in ~12s") for logging surfaces that only show the message.
Mid-stream errors (after the 200 has been sent)
Rate limits and credit checks are pre-flight only — if your key is over the limit
or your workspace is out of credits, you get an HTTP 429/402 before streaming
starts, not mid-stream. The gateway itself never terminates a stream for its own
rate-limit or credit reasons once the 200 status and Content-Type: text/event-stream headers are sent.
However, if the upstream provider returns an error after streaming has started (e.g. an overloaded upstream, a malformed chunk, a network timeout), the error arrives as a mid-stream error frame in the format’s native SSE shape — you don’t get an HTTP status change (the 200 was already sent):
Chat Completions
data: {"id":"gen-…","object":"chat.completion.chunk","created":…,"model":"…",
"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error",
"error":{"code":"provider_unavailable","message":"…","type":"api_error","param":null,
"request_id":"…","retry_after_seconds":12}}]}
data: [DONE]The finish_reason is "error" (not "stop"), the delta.content is empty, and
the full error object is inlined on the choice as error. The retry_after_seconds
field is present when applicable (same as the pre-flight 429).
Responses
event: response.incomplete
data: {"type":"response.incomplete","response":{"id":"resp_…","status":"incomplete",
"incomplete_details":{"reason":"error"},"error":{"code":"…","message":"…","param":null,
"retry_after_seconds":12}}}Anthropic Messages
event: error
data: {"type":"error","error":{"type":"overloaded_error","code":"provider_overloaded",
"message":"…","retry_after_seconds":12}}In all three formats, the error frame is the last data frame before the stream
closes. Partial usage (tokens generated before the error) is still billed and
recorded — the status field on the usage event is "error", not "ok".
Explicitly rejected parameters
The gateway rejects rather than silently ignores parameters it cannot honor — you
get 400 invalid_request in your format’s native error shape, so a request never
quietly does something other than what you asked:
service_tierother than"auto"— the gateway owns tiering.n> 1 (Chat Completions) — multiple choices per call are not supported.modalitiesother than["text"], theaudiooutput param, andinput_audiomessage content — this is a text-only deployment.top_logprobswithoutlogprobs: true(Chat Completions).- Stateful Responses params —
store,background,conversation,previous_response_id,prompt— the gateway is stateless; send the full input. - Anthropic
mcp_servers,container,context_management.
Every other top-level parameter you send is forwarded verbatim to the
OpenAI-compatible upstream, never silently dropped — the full OpenAI sampling/tool/output surface (seed,
frequency_penalty, presence_penalty, logit_bias, logprobs,
parallel_tool_calls, per-tool strict, verbosity, prediction, …), OpenRouter
extensions (min_p, top_a, repetition_penalty, provider, transforms), and any
custom field.
Handling guidance
- Retry with backoff:
provider_rate_limit,provider_overloaded,provider_timeout,provider_unavailable,rate_limit_exceeded,internal_error. Honor theRetry-Afterheader when present. - Fix the request:
invalid_request,context_length_exceeded,payload_too_large,model_not_allowed. - Fix the account:
insufficient_credits(top up),key_limit_exceeded(wait for the reset or raise the limit),invalid_api_key(rotate the key),workspace_locked(contact support).
The gateway itself retries upstream connect failures and pre-stream 5xx/429s at most twice with jittered backoff — and never retries once a response has started streaming, so you will never receive duplicated content.