Reference
Errors
Failures use the OpenAI error envelope, so SDK error handling works unchanged. Branch on error.code: it is stable, while messages may be reworded.
Error envelope (permalink)
Every error the gateway itself produces has this shape.
{
"error": {
"message": "Token budget exhausted. Contact your provider to renew.",
"type": "insufficient_quota",
"code": "token_budget_exhausted",
"param": null
}
}messageis human-readable and safe to log or show an operator. It never contains credentials.typeis the broad family:invalid_request_error,permission_error,insufficient_quota,rate_limit_errororapi_error.codeis the specific reason. This is the field your code should switch on.paramis reserved for field-level errors and isnullon everything the gateway raises.
Gateway errors (permalink)
These twelve are the complete set the gateway raises on its own. Anything else you see came from the model provider and passed straight through.
| Status | Code | Type | Meaning | What to do |
|---|---|---|---|---|
| 401 | missing_api_key | invalid_request_error | The request carried no credential at all. | Send Authorization: Bearer with your key, or the x-api-key header. |
| 401 | invalid_api_key | invalid_request_error | Unknown key, or a valid key sent to the other billing surface. | Check the key was copied whole, then confirm you are on the right base URL. |
| 403 | key_suspended | permission_error | The key exists but has been paused by whoever issued it. | Ask them to resume it. The key itself stays valid and does not need replacing. |
| 403 | key_expired | permission_error | The key is past its expiry date. | Request an extension or a new key. It will not recover on its own. |
| 403 | model_not_allowed | permission_error | The key is valid but is not scoped to the model in the request body. | Use a model on your allowlist, or ask for the allowlist to be widened. |
| 403 | model_not_available | permission_error | The model id is not one the gateway can serve: unknown, or switched off centrally. | Check the id against the model catalogue. If it is listed and still refused, it has been disabled. |
| 400 | model_required | invalid_request_error | An inference call whose model could not be read, usually a malformed or empty JSON body. | Send a valid JSON body with a top-level "model" string. |
| 402 | insufficient_credit | insufficient_quota | A credit key whose USD balance has reached zero. | Top the key up on the billing page, or ask whoever issued it. Confirm the balance in the usage checker first. |
| 402 | token_budget_exhausted | insufficient_quota | A token key that has consumed its whole prepaid budget. | Ask for the budget to be renewed. |
| 429 | rate_limit_exceeded | rate_limit_error | The key went over its requests-per-minute limit. Nothing was forwarded or billed. | Wait the number of seconds in the retry-after header, then retry. Ask for a higher limit if you need one. |
| 404 | unknown_endpoint | invalid_request_error | Nothing followed the base URL, or the path did not resolve to a real endpoint. | Append a real path such as /chat/completions. Watch for a doubled or missing /v1. |
| 502 | upstream_unavailable | api_error | The gateway could not reach the model provider at all. Nothing was billed. | Retry with exponential backoff. Repeated 502s mean an outage, not a bad request. |
Order of checks (permalink)
Knowing the order tells you what a status rules out. The gateway stops at the first failing check and never reaches the ones below it.
- Path. A request with nothing after the base URL, or a path that does not resolve, gets
404 unknown_endpoint. This happens before authentication, so a 404 says nothing about your key. - Credential present. No
Authorization: Bearerand nox-api-keygives401 missing_api_key. - Key known. An unrecognised key gives
401 invalid_api_key. - Key usable. Suspended gives
403 key_suspended, past its expiry gives403 key_expired. - Right surface. A credit key on the token base URL, or the reverse, gives
401 invalid_api_keyagain. - Funds. An empty balance gives
402 insufficient_credit, a spent budget gives402 token_budget_exhausted. - Rate. A key over its requests-per-minute limit gives
429 rate_limit_exceeded, with aretry-afterheader holding the seconds left in the current window. This happens before the request body is read, so a throttled call costs nothing. - Model readable. Only now is the request body read. An inference call whose
modelfield cannot be read, because the body is empty or is not valid JSON, gives400 model_requiredrather than being forwarded unpriced. - Model allowed. A model outside the key’s allowlist gives
403 model_not_allowed. - Model servable. A model the gateway cannot serve, because the id is unknown or the model has been switched off centrally, gives
403 model_not_available. Both cases return the same body on purpose. - Forwarded. If the provider cannot be reached at all,
502 upstream_unavailable. Otherwise the provider’s own status is what you get.
Upstream failures (permalink)
A request that passes every check is forwarded to the model provider, and the provider’s status and response body come back to you unchanged. So a 400 about an unsupported parameter, a 429 about rate limits, or a 529 about capacity originates there rather than here, and the message reads in the provider’s own words.
Response headers are filtered rather than mirrored. content-type and x-request-id are preserved, streamed responses also carry cache-control: no-cache, no-transform, and everything else is dropped. The one header the gateway adds itself is retry-after, on its own 429. Do not build logic on a header you have not seen documented here.
| Status | Where it comes from |
|---|---|
| 400, 404, 422 | The provider rejected the request body: a bad parameter, an oversized context, or a missing max_tokens on an Anthropic Messages call. A 400 whose code is model_required came from the gateway instead, and an unknown model id never reaches the provider at all. |
| 429 | Either surface can produce this. A gateway 429 carries code: "rate_limit_exceeded" and a retry-after header; anything else is the provider rate limiting you. Back off with jitter in both cases. |
| 5xx other than 502 | The provider failed or timed out. Retry with backoff, and report it if it persists. |
A 402 can also arrive this way, when the upstream account itself is short of funds. The distinction matters: if the usage checker shows your key still has balance or budget but calls keep returning 402, the problem is upstream and only your provider can clear it.
What to retry (permalink)
Authentication, permission and billing failures are decisions, not accidents. Retrying them wastes time and makes rate limits worse. Only 429 and 5xx deserve another attempt.
const response = await fetch(url, init);
if (!response.ok) {
const body = await response.json().catch(() => null);
const code = body?.error?.code ?? "unknown_error";
// Only 429 and 5xx are worth trying again.
const retryable = response.status === 429 || response.status >= 500;
throw Object.assign(new Error(body?.error?.message ?? response.statusText), {
status: response.status,
code,
retryable,
});
}Then retry only what is retryable, with exponential backoff and jitter.
async function withRetry(send, attempts = 4) {
for (let attempt = 0; ; attempt++) {
try {
return await send();
} catch (err) {
if (!err.retryable || attempt >= attempts - 1) throw err;
const wait = 500 * 2 ** attempt + Math.random() * 250; // jittered backoff
await new Promise((resolve) => setTimeout(resolve, wait));
}
}
}- Cap the attempts. Four tries over roughly ten seconds is plenty for a transient upstream fault.
- Add jitter. Without it, every client that failed at the same moment retries at the same moment.
- On a
429, prefer theretry-afterheader over your own schedule when it is present. It is the exact number of seconds left in the current rate-limit window. - Stop on
402immediately, or you will hammer a key that has nothing left to spend.
What gets billed (permalink)
Failures cost nothing, but they are not all recorded the same way.
| Outcome | Billed | In the usage checker |
|---|---|---|
| Refused by the gateway | No | No. A 400, 401, 402, 403, 404 or 429 stops before the ledger, so it never appears as a request. |
| Forwarded, provider errored | No | Yes, recorded against your key as an error at zero cost. |
| Provider unreachable (502) | No | Yes, recorded as an error at zero cost. |
| Succeeded | Yes | Yes, with model, token counts and cost. |
Successful metadata calls are the one exception in the other direction. A GET /models reports no token usage, so it is neither billed nor listed. More detail on Checking usage.
Reporting a problem (permalink)
When something needs a human, include:
- The
x-request-idresponse header, when the provider supplied one. - The HTTP status and the whole
errorobject. - The key prefix only, meaning the first characters such as
kl_live_a1b2c3d4. Never send the whole key. - The model id, whether you were streaming, and roughly when it happened.