Errors
The two ModelBeat error envelopes, every status the gateway returns, and how to use X-Request-Id.
ModelBeat returns two different error envelopes. Which one you get tells you where the failure happened, so handle both.
Every message below is the literal string the gateway emits, taken from the source rather than paraphrased.
Envelope 1: ModelBeat refused (error is a string)
Returned for failures ModelBeat raises itself, before any provider is called:
{ "error": "modelbeat: invalid or revoked api key" }No provider was contacted and nothing was charged.
Envelope 2: the provider failed (error is an object)
OpenAI-compatible, so existing error handling mostly works unchanged:
{
"error": {
"message": "the model does not exist",
"type": "invalid_request_error",
"code": "model_not_found"
},
"status_code": 404
}Other fields may appear alongside these; ignore any you do not recognise.
Telling them apart
Check the type of error: a string means ModelBeat refused, an object means a
provider responded with an error. The two shapes never overlap, and a test in the gateway
pins that property, so you can rely on it.
Every status ModelBeat itself returns
| Status | Message | What happened | What to do |
|---|---|---|---|
400 | modelbeat: <reason> | Routing refused on your configuration. The reason comes from the router: no_eligible_candidate (nothing in the catalogue satisfies the request), pin_not_permitted (you named a model your routing policy forbids), policy_excludes_all (the policy leaves nothing eligible). | Fix the request or the policy. Retrying unchanged will not help. |
401 | modelbeat: invalid or revoked api key | Key missing, malformed, invalid, or revoked. | See Authentication. |
402 | modelbeat: insufficient balance | The worst-case hold exceeds your prepaid balance, which is not necessarily the final cost. | Top up, or lower max_tokens to shrink the hold. See Costs. |
501 | modelbeat: this request shape is not supported; use /v1/chat/completions | You called /v1/messages or /v1/responses. | Use the OpenAI chat shape. See Limits. |
503 | modelbeat: auth unavailable | The control plane could not verify your key. | Retry with backoff. Not your bug. |
503 | modelbeat: billing unavailable | The balance could not be reserved against. | Retry with backoff. |
503 | modelbeat: routing is temporarily unavailable; please retry | The router could not verify your routing constraints, including when the model catalogue is empty. | Retry with backoff. |
503 | modelbeat: routing selected an unconfigured provider | Routing chose a provider the gateway holds no credentials for. Ours to fix. | Report it with the request id. |
Anything else carrying an object error came from the provider. Handle it as you would
calling that provider directly.
400 and 503 are a deliberate split
Both can come out of routing, and the difference matters:
400means the router ran and decided your request cannot be served as configured. That is a statement about your request, so retrying it unchanged will fail again.503means the router could not determine whether your constraints are satisfiable which is an outage on our side. ModelBeat fails closed rather than guessing a model, because substituting one that ignores your routing policy would be a policy bypass. Retry.
Why 503 rather than a best-effort attempt
If ModelBeat cannot verify your key, reserve against your balance, or evaluate your routing
policy, it refuses. It will not serve traffic it cannot authenticate, account for, or route
within your constraints. A 503 therefore means "try again". It never means "this succeeded but went
unbilled".
X-Request-Id
Every response, success or failure, carries an X-Request-Id header:
X-Request-Id: req_01HXYZ...Log it, and quote it when you contact us. It is the fastest path to the exact call in our records. Read it off the response headers, and off the error's response on a failure:
from openai import OpenAI, APIStatusError
client = OpenAI(api_key=..., base_url="https://api.modelbeat.ai/v1")
try:
r = client.chat.completions.with_raw_response.create(model="auto", messages=[...])
log.info("ok (request_id=%s)", r.headers.get("x-request-id"))
except APIStatusError as e:
log.error(
"modelbeat call failed: %s (request_id=%s)",
e.message,
e.response.headers.get("x-request-id"),
)You may also send your own X-Request-Id; it is echoed back rather than replaced, so you can
correlate against your own tracing.