Skip to main content

Errors

Every error the API returns, and what to do about each. Debugging a live failure? Skip to the error catalog; the body shapes below explain why the JSON you got may not match what your SDK expects.

Error shapes

The shape depends on where the error happens, not just what it is. There are four:

1. The standard error object: most errors on the OpenAI-compatible endpoints (/v1/chat/completions, /v1/responses, /v1/embeddings, /v1/models):

{
"error": {
"message": "The model 'not-a-model' does not exist or you do not have access to it.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}

2. The Anthropic envelope: everything on /v1/messages and /v1/messages/count_tokens:

{"type": "error", "error": {"type": "not_found_error", "message": "..."}}

3. Plain {"detail": ...}: request-body validation failures (422 with a list of field errors, plus some 400s such as an invalid image data URL) and internal server errors (500) on most OpenAI-compatible endpoints. An OpenAI SDK will not find an error.message in these; read detail. (Exception: /v1/embeddings internal errors use {"error": {"type": "internal_error", ...}}, and its upstream provider errors are relayed with their original OpenAI-shaped bodies.)

4. Gate refusals: your credentials are checked in front of the API, before the endpoint is chosen, so a 401 carries the Anthropic envelope above on every endpoint, including the OpenAI-compatible ones:

{"type": "error", "error": {"type": "authentication_error", "message": "Invalid or missing credentials. See https://docs.mindshub.ai/inference/authentication"}}

A gate 403 arrives in the same shape with error.type of permission_error, and means the credential is real but is not allowed to use that resource. Dispatch on the status code; the message is written for a person and may change. See Authentication.

Credentials are checked twice: once at the edge, and once at the deeper authorization gate the request passes on its way to a model. Both refusals look the same to you, and both are terminal.

Dispatch on the HTTP status code, and treat the body as best-effort. The code field, where present, is stable and safe to switch on.

Error catalog

StatuscodeWhat happenedWhat to do
401none or invalid_credentialsMissing, malformed, or revoked credentials: an API key or a browser session. The code is absent when the edge refused the credential and invalid_credentials when the authorization gate did; both mean the same thing to you.Fix the credentials. Don't retry.
403none or permission_deniedThe credential is real but is not allowed to use this resource, for example a key whose user is no longer a member of its organization.Fix the credential's access. Don't retry. If you believe the credential should have access, report it to support@mindsdb.com with the response headers.
402wallet_emptyThe request needs wallet credit and your organization's balance is empty. Also what you get on the included-token model once the tokens run out, if your organization has ever added a payment method (see the 429 below for the never-topped-up case).Add credit in the console; the X-MindsHub-Recovery-Url header carries the console path. Don't retry until funded. See Billing.
400max_tokens_exceededmax_tokens above the hard cap of 131,072.Lower max_tokens.
400model_not_configuredThe model is in the catalog but isn't currently routable. Rare.Use another model, and report it to support@mindsdb.com.
404model_not_foundUnknown model name: a typo, a raw provider ID, or an alias not in the catalog.Use an alias from GET /v1/models.
422none (detail list)Request body failed validation: missing required field, wrong type, unknown message role.Fix the request.
429rate_limitedToo fast: requests per minute, tokens per minute, or concurrent requests.Wait and retry, honoring Retry-After (seconds, always ≥ 1). See Rate limits.
429included_allowance_exhaustedYour included tokens are used up, and your organization has no payment method on file, so there's no wallet to fall back on.Wait for the reset (X-MindsHub-Reset-At header, ISO-8601) or add a payment method and credit. No Retry-After is sent on this one.
4xx (relayed)variesThe upstream provider rejected something the platform forwarded, for example a temperature the model doesn't accept. Body has "type": "api_error" and the provider's own message and status.Fix the request for that model, or switch models.
5xx (relayed)variesThe upstream provider failed. Body has "type": "api_error". The platform retries transient provider errors (and fails over where a fallback route exists) before you see this — except where the response carries x-should-retry: false, which marks a failure retrying cannot change.Retry with backoff, unless x-should-retry: false is set.
504assembly_timeoutA non-streaming request with a large max_tokens ran past the time we can hold the connection open. The tokens generated up to that point are metered.Don't retry — it will time out the same way and bill again. Resend with "stream": true, or lower max_tokens.
502noneCouldn't reach the upstream provider.Retry with backoff.
503policy_unavailableMindsHub couldn't verify your account's access, so the request was refused instead of run. Transient.Retry in a few seconds.
500none (detail)Unhandled internal error.Retry once; if it persists, report it to support@mindsdb.com.

The two 429s mean opposite things. rate_limited is "slow down": retry after seconds. included_allowance_exhausted is "out of included tokens": retrying won't help until the reset date or a top-up. Tell them apart by the code, or by Retry-After, which only rate_limited sends.

The X-MindsHub-* headers

Denials from access checks carry machine-readable headers that survive even if an intermediary rewrites the response body:

HeaderSent onValue
X-MindsHub-Reasonmodel_not_found, wallet_empty, included_allowance_exhausted, rate_limited, policy_unavailableThe denial reason. Usually matches the body code, except model_not_found, where the header reads unknown_model.
Retry-Afterrate_limited onlySeconds to wait, always ≥ 1.
X-MindsHub-Reset-Atincluded_allowance_exhaustedISO-8601 instant when your included tokens refill.
X-MindsHub-Recovery-Urlwallet_emptyConsole path for adding credit.

Two more X-MindsHub-* headers travel on successful responses rather than errors, reporting how a request was adapted to the target model:

HeaderSent onValue
X-MindsHub-Dropped-ParamsAny request carrying a parameter the model can't takeComma-separated parameter names, e.g. temperature,top_p
X-MindsHub-Clamped-ParamsAny request carrying a parameter outside the model's rangename=requested>applied, e.g. max_tokens=200000>128000

Neither appears when nothing was adapted, so their absence means the request went upstream as you wrote it. This is why a parameter a model dislikes usually produces a 200 rather than a 400 — see Chat completions. A rewritten tool_choice is reported here too, as a clamp: tool_choice=required>auto (why the rewrite happens).

One gap to code around: request-validation errors (max_tokens_exceeded, model_not_configured) carry none of these headers. Everywhere else a denial keeps its Retry-After and its X-MindsHub-* headers, /v1/messages included, so back off on the value we send rather than guessing. See Anthropic compatibility → Errors for that endpoint's envelope quirks.

Streaming failures

A request with "stream": true can fail three ways:

  1. Before any output: you get a normal JSON error response (any of the above) instead of an SSE stream. Check the response's Content-Type before parsing it as SSE.
  2. Mid-stream, visibly: the stream ends abnormally. There is no error event. On the OpenAI-compatible endpoints, a stream that ends without ever delivering a finish_reason chunk was a failed generation.
  3. Mid-stream, invisibly: on /v1/messages, a failed generation can arrive looking like a normal completion: the stream closes with an ordinary message_delta (stop_reason: "end_turn") and message_stop, just with truncated or empty content. There is currently no wire-level signal for this case. If your application must detect it, sanity-check the output (for example, empty content on a prompt that should produce text).

One model-family caveat for rule 2: streams on Gemini-served models (gemini, gemini-flash) always end with a finish_reason, even on failure, so on those, like on /v1/messages, absence of finish_reason can't be your only health check.

Partial output that arrived before a failure is real output and is billed.

A retry recipe

Using plain HTTP (the requests library) so the status and headers are visible; SDK users get the same decisions from their SDK's status-code exceptions:

import random
import time
import requests

class OutOfTokens(Exception): pass
class RequestFailed(Exception): pass

RETRYABLE = {429, 500, 502, 503, 504}

def server_says_dont_retry(response):
# Some failures carry a retryable status but cannot be fixed by retrying —
# a 504 `assembly_timeout` re-runs the same generation and bills again.
# Both the Anthropic and OpenAI SDKs check this header for the same reason.
return response.headers.get("x-should-retry") == "false"

def exhausted_allowance(response):
# Prefer the header; fall back to the body's error code
# (the header isn't sent on every endpoint or error).
if response.headers.get("X-MindsHub-Reason") == "included_allowance_exhausted":
return True
try:
return response.json().get("error", {}).get("code") == "included_allowance_exhausted"
except ValueError:
return False

def post_with_retries(url, headers, body, max_attempts=5, timeout=120):
for attempt in range(max_attempts):
try:
response = requests.post(url, headers=headers, json=body, timeout=timeout)
except requests.RequestException:
# Connection failure or timeout: the request may still have run
# server-side. See the note on duplicates below.
time.sleep(min(2 ** attempt, 30) + random.random())
continue

if response.ok:
return response
if exhausted_allowance(response):
raise OutOfTokens(response.headers.get("X-MindsHub-Reset-At"))
if response.status_code not in RETRYABLE or server_says_dont_retry(response):
raise RequestFailed(response.status_code, response.text)

retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 30)
time.sleep(max(delay, 1.0) + random.random())

raise RequestFailed("gave up", max_attempts)

The decisions that matter: retry 429 rate_limited and 5xx with backoff and jitter; honor Retry-After when present; honor x-should-retry: false even on a status you'd normally retry; stop immediately when included tokens are exhausted; and never blind-retry errors that won't change: 401, 402, 404, 422, or a 400 you caused, like an over-cap max_tokens.

This recipe is written for the OpenAI-compatible endpoints. On /v1/messages there is currently no reliable exhausted-allowance discriminator (no headers, and the Anthropic envelope carries no code), so an out-of-tokens 429 will fall into the generic retry loop; keep max_attempts low there, or inspect the error message text as a heuristic.

Retrying after a timeout can generate twice. A request that timed out client-side may still complete server-side and meter, and there is no idempotency key to deduplicate it. For large generations, prefer a generous timeout over aggressive retry, and treat a timeout as "outcome unknown", not "failed".