Error Reference
Every error code the OpenAI-compatible plane can return, what it means, and what to do about it.
Every error on the OpenAI-compatible plane (/v1/) uses the OpenAI error envelope, so an SDK you already have handles it. This page describes the codes that plane returns, what each one means, and what you do next.
The envelope
{
"error": {
"message": "Rate limit exceeded for this tenant. Slow down and retry. (diagnostic: 3f9c1a20d4b7481e9c2f5a6b8d0e7142)",
"type": "rate_limit_error",
"param": null,
"code": "tenant_rate_limited"
}
}
type stays inside the set OpenAI SDKs recognise — invalid_request_error, authentication_error, permission_error, rate_limit_error, insufficient_quota, server_error — so SDK-level branching keeps working. The precise meaning is in code.
code is append-only. New codes get added; existing ones do not change meaning. Write your handling against x-llmbench-error-class (below) so an unrecognised code still lands in the right branch.
Any message we echo from an upstream provider is stripped of key-shaped tokens and truncated to 500 characters before it reaches you.
Diagnostic id vs call id
These are two different headers on two different outcomes. Do not look for one on the other.
| Outcome | Header | What it identifies |
|---|---|---|
| Success (200) | x-llmbench-call-id | the call record for the call that ran |
| Error | x-llmbench-diagnostic | the incident, for support |
The diagnostic id is also appended to error.message as (diagnostic: ...), so it survives a client that logs the body and throws the headers away. Quote it in a support request.
One nuance worth knowing: when a failure got far enough to open a call record, the diagnostic id is that call record’s id — the same value a success would have reported in x-llmbench-call-id. When the request was refused before any model ran (bad key, unknown model, malformed body, streaming, quota), there is no call, and the diagnostic is a freshly minted 32-character hex id instead.
Error headers
| Header | On | Meaning |
|---|---|---|
x-llmbench-diagnostic | errors | incident id — quote it in support |
x-llmbench-error-class | errors | whose problem this is (see below) |
x-llmbench-error-code | errors | same value as error.code |
Retry-After | 429s | seconds to wait before retrying |
x-llmbench-request-id | errors that reached invocation | the route id — quote it in support alongside the diagnostic |
x-llmbench-request-id is deliberately absent on anything refused before invocation. No route was opened, so there is no id, and inventing one would be worse than omitting it.
Not every error carries the header triple. The rule: the triple is attached when the error is raised as a gateway refusal or mapped from an invocation. An error body a plain lookup returns directly carries no x-llmbench-* headers and no (diagnostic: ...) suffix, because the body alone is unambiguous. Lookups in that second group include GET /v1/models/{model_id}, GET /v1/files/{id}, GET /v1/files/{id}/content, DELETE /v1/files/{id}, GET /v1/batches/{id}, and POST /v1/batches/{id}/cancel. Everything from POST /v1/chat/completions, POST /v1/files, and POST /v1/batches carries the full triple.
Triage by class
x-llmbench-error-class tells you who has to act, faster than the code does.
| Class | Whose problem | Default response |
|---|---|---|
provider | a transient upstream fault | retry with backoff |
your_quota | you are out of room | wait for the window, or top up |
your_key | your vaulted provider credential | fix the credential |
your_request | the request content | change the request |
router | configuration of the task you called | fix the task, or pick another |
gateway | our admission control | honour Retry-After |
OpenAI SDKs already retry 429s honouring Retry-After, so short gateway and provider waits self-heal without you writing anything.
Codes refused before a model runs
None of these cost provider spend.
| Code | HTTP | Class | Means | Do |
|---|---|---|---|---|
invalid_api_key | 401 | gateway | No key, or a key we don’t recognise. | Send Authorization: Bearer llmb_i_.... Keys are shown once at issue; if you lost it, rotate rather than hunt for it. |
key_plane_mismatch | 403 | gateway | A real key, on the wrong plane — usually a management key (llmb_m_) sent to /v1/. | Use an inference-scope key (llmb_i_) here. Management keys are for /api/inference/v1/ control routes. |
model_not_found | 404 | router | The model string is not a task this key can call. | Check GET /v1/models, and call the slug your create returned, not the name you sent. |
invalid_request_body | 400 | gateway | The body did not parse or a field is out of range. param names the field. | Read param. Common: an unknown key inside llmbench (the block is strict — it has no objective field, use selection; the task-creation objective on the control plane is a different field), a bad completion_window. An empty model is this code with param: "model". |
unsupported_parameter | 400 | gateway | You asked for a feature this endpoint does not have: tools, tool_choice, functions, function_call, logprobs, audio, modalities, web_search_options, or n other than 1. | Drop the parameter. Benign off-values (tools: [], tool_choice: "none", logprobs: false, modalities: ["text"]) are accepted, so an SDK that always sends them is fine. |
unsupported_value | 400 | gateway | A supported field carried a value v1 cannot serve. | Most often messages: at most one user message, no assistant/tool/function roles. Also llmbench.max_wait_seconds, which is batch-lines-only. |
streaming_not_supported | 400 | router | You sent stream: true. Streaming is not offered — in synchronous or batch mode. | Re-send with stream: false (or omit it). We refuse rather than quietly ignoring the field, so a client written against a streaming API fails visibly instead of appearing to stream. |
credential_mode_conflict | 400 | gateway | You sent an x-llmbench-provider-key-* header, but you already have vaulted credentials. | Pick one mode. Either vault your keys, or send them per request — not both. |
zero_custody_not_enabled | 400 | gateway | You sent a per-request provider key without zero-custody enabled on your account. | Vault the key instead, or ask us to enable zero-custody first. We do not use a credential you were not agreed to send us. |
messages_not_allowed | 400 | your_request | You sent messages to a task that runs its own template. Messages are refused there, not ignored. The message lists the template’s variables. | Send messages: [] and put your content in llmbench.prompt_vars, one entry per variable. GET /v1/models/{id} lists them. |
shared_task_per_call_field | 422 | your_request | You sent a per-call field that changes how one of our tasks chooses models or explores: a selection field other than model_group, forced_model, or on the native plane suppress_fanout, exclude_models or metadata.force_judge. The message names them. On a batch it is refused at create, or fails the batch at validation when the field is on a line. | Send the request without them; force_sync stays allowed. To set them, make the task your own: a copy of ours where copies are available, or a task you define. |
byok_only_field | 400 | your_request | A field a managed task does not take from a call: an output schema (response_format of type json_schema or json_object, with param: "response_format"), a validator, or schema_json_string in your variables. | Drop it. A managed task’s schema, validators and schema variable are the task’s; to use your own, define them on a task of your own. |
unknown_output_contract | 400 | your_request | The output contract you selected by name is not one the task declares. | Omit it for the task’s default, or use a name the task declares. |
model_not_supported | 400 | your_request | The model names one of our internal evaluation tasks, which cannot be called. | Call a task from GET /v1/models. |
template_selection_invalid | 422 | your_request | The template selection is incomplete or contradicts itself: one prompt name without the other, or a template key together with prompt names that are not that template’s. | Send a template key, or both prompt names, or neither for the default. |
template_not_published | 422 | your_request | llmbench.template (or template on the native endpoint) names no published template of the task; or, on a task served on our keys, the call named a prompt pair that is not one of the task’s templates (on one of our tasks, one of its published templates). The message lists the keys you can use. | Use one of the listed keys, or omit it for the task’s default. |
template_resolution_failed | 503 | router | Our side. We could not read the task’s templates for a moment. Nothing ran. | Retry shortly. |
model_group_differs_from_task | 422 | your_request | selection.model_group names a group other than the task’s own (a narrower group included). A task’s model group is part of its definition. The message names both groups and, when you have one, your private task for that group. In a batch or package it is the create’s 422 for a hoisted group without the switch, and an invalid line for a group or switch set on a line (the group is chosen once, in the hoisted block). | Call the task without the group; call your private task for that group; or send create_private_task_for_new_model_group: true to have one made (once) and run on it. In a batch or package, hoist model, selection.model_group and the switch together. |
private_copy_unavailable | 422 | your_request | You asked for a private task with another model group, and copies are not available for your account. | Call a task defined with that group. |
model_group_fork_refused | 422 | your_request | The task cannot be copied for that group; copy_refused_reason says why (for example empty_pool: no model of the group can serve the task, or daily_task_create_limit). | Choose another group, or wait for the limit to reset. |
model_group_no_eligible_model | 422 | your_request | On your own keys: no model of the group is on a provider you connected; missing_providers lists them. | Connect one of those providers, or choose another group. |
model_group_fork_unavailable | 422 | your_request | Retired: no longer sent. A task you defined yourself is now copied for another group like any other task. | See model_group_fork_refused. |
private_task_paused | 409 | your_request | Your private task for that group is paused. The message names it. | Activate it, or call another task. |
model_group_routing_failed | 503 | router | Our side. The call’s model group could not be routed for a moment. Nothing ran. | Retry shortly. |
tenant_rate_limited | 429 | gateway | Your per-second limit on our gateway. | Honour Retry-After and slow down. Not a billing problem. |
tenant_quota_exceeded | 429 | gateway | param names the breached scope. Usually one of your own caps for the window (e.g. calls_day). param: "server_concurrency" is different — that means our gateway is momentarily at its in-flight ceiling, not that you are over a cap. | For your own caps: wait for the window to reset, or raise the limit. For server_concurrency: just honour Retry-After; there is nothing on your side to change. |
internal_error belongs to no band
internal_error (500, or 502 for an unexpected deferred result) carries class router and means something failed on our side. It is listed separately because it is not confined to the pre-invocation stage: it comes from the catch-all around the whole handler, so it can fire after a provider call has already completed. Unlike everything in the table above, it does not guarantee that no provider spend was incurred.
Retry. If it persists, quote the diagnostic id.
The stream: true response, exactly
HTTP/1.1 400 Bad Request
x-llmbench-error-code: streaming_not_supported
x-llmbench-error-class: router
x-llmbench-diagnostic: 7b2e04c9a51f4d3ab8c6e9f012345678
{"error": {
"message": "Streaming is not supported by this Service. Re-send the request with stream=false. (diagnostic: 7b2e04c9a51f4d3ab8c6e9f012345678)",
"type": "invalid_request_error",
"param": "stream",
"code": "streaming_not_supported"
}}
The check runs before a model is selected, so a refused streaming request produces no provider spend and no call record. The same check serves both lanes: on POST /v1/chat/completions it is the response; on a batch input line it becomes that line’s entry in the error file.
Codes from the invocation stage
These come from the invocation stage — either from a model that ran, or from a refusal made once the task had been resolved. Six of them run no model at all, and are marked below.
If a model’s output fails our format checks, or a model returns nothing, we try a different model for that call rather than failing it — unless you pinned one with llmbench.forced_model, in which case there is no alternative and you get the failure directly.
| Code | HTTP | Class | Means | Do |
|---|---|---|---|---|
provider_key_missing | 403 | your_key | No usable credential for the provider that was selected. | Vault one: PUT /api/inference/v1/vault/providers/{slug} with a management key. Partial coverage is fine — one vaulted provider in the pool is enough. |
provider_key_revoked | 403 | your_key | The key was valid and is no longer. | Mint a new one at the provider and re-vault. |
provider_key_invalid | 403 | your_key | The provider rejected the key you vaulted. | Mint a fresh key at the provider and re-PUT it. |
provider_account_suspended | 403 | your_key | The provider rejected a call made on our credentials, or suspended the account behind them. | Nothing for you to do — this is our account, not yours. If it persists, contact support. |
provider_quota_exhausted | 429 | your_quota | Your account at the provider is out of quota. | Raise limits or top up at the provider. This is their quota, not ours. |
insufficient_balance | 429 | your_quota | Runs no model. Not enough prepaid credit for a task we serve on our credentials. The message names the shortfall. | Top up by at least the amount quoted, then resubmit. Nothing was charged and, on a batch, nothing was enrolled. |
billing_not_ready | 503 | router | Runs no model. Your prepaid billing account is still being set up — usually under a minute after signup. Nothing was charged. | Retry after Retry-After (30 seconds). If it persists for more than 15 minutes, contact support. |
provider_rate_limited | 429 | your_quota | Rate-limited upstream, or every eligible model is on a rate-limit cooldown. | Back off and retry — Retry-After is 5 seconds. This clears on its own; it is not a misconfiguration. |
provider_unavailable | 502 | provider | The provider was unreachable or erroring. | Retry with backoff. |
provider_timeout | 504 | provider | The provider did not answer in time. | Retry. If a task times out consistently, consider the batch lane. |
provider_error | 502 | provider | An upstream error we could not classify more precisely. The (redacted) provider message is passed through. | Read the message, then retry. |
content_filtered | 400 | your_request | The provider refused on content grounds, or the input exceeded a length limit. | Change the input. Retrying unchanged will not help. |
missing_prompt_variable | 422 | your_request | Runs no model. The prompt template has a placeholder you did not send in prompt_vars. The message names it. | Send every variable the template uses. The refused call counts against your call quota. |
undeclared_prompt_variable | 422 | your_request | Runs no model. On a task served on our keys you sent a variable that is neither a placeholder in the template nor one of its declared variables. The message names it. | Send only the template’s variables (GET /v1/models/{id} lists them). The refused call counts against your call quota. |
output_validation_failed | 500 | router | Either every model we tried produced output that failed the task’s schema or validator, or the output parsed and validated but did not clear the task’s quality bar. The redacted error.message distinguishes the two. | If you pinned forced_model, unpin it and let failover work. For a schema failure, the task’s output schema is likely too strict for the models in its pool. For a quality-bar failure, the knob is the bar or the pool, not the schema. |
model_not_found | 404 | router | The task disappeared between routing and invocation. | Re-check GET /v1/models. |
no_eligible_model | 503 | router | Runs no model. The candidate pool was empty, so nothing was tried. | The message takes one of three shapes — see below. |
capability_config_invalid | 400 | router | Runs no model. The task’s own configuration is wrong — output schema, prompt, or validator. | Fix the task’s configuration on the management plane, or call a different task. |
insufficient_balance also occurs at batch create (POST /v1/batches), refusing the whole batch before any line is enrolled. There it carries class gateway rather than your_quota; the code, the status and the remedy are the same. billing_not_ready occurs at batch create too, with the same code, status, class and remedy.
Re-vaulting a key does not fail on a bad key
PUT /api/inference/v1/vault/providers/{slug} re-runs an auth check against the provider, but a rejected key is not an error: the credential is still stored and you still get a 200. Read the response body instead of the status:
verifiedistrueonly when the check passed.last_errorcarries the provider’s own reason when it did not.
GET /api/inference/v1/vault/providers shows the same two fields for every vaulted credential, so you can confirm a fix without re-sending the key.
What no_eligible_model tells you
The message is one of three:
- A credential message, when the task runs on your own provider credentials and one or more of its providers has no active vaulted key. It names those providers. A revoked key produces this same message as an empty vault, because a revoked credential stops serving.
- A quality-bar message, when a
min_confidencefloor is set on the task and exploration is off, so no model has enough evidence to clear it. It names both settings. - A generic sentence when neither applies, naming nothing further.
Batch and file codes
| Code | HTTP | Means | Do |
|---|---|---|---|
file_not_found | 404 | No such file id, or its content was deleted. | Files are tombstoned on delete, so a deleted id keeps 404ing. Re-upload. |
batch_not_found | 404 | No such batch id for this account. | Cross-account ids 404 by construction. Check the id. |
batch_not_cancellable | 409 | The batch is already in a terminal state: completed, failed, or expired. | Nothing to do. cancelled and cancelling are absent from that list because cancelling an already-cancelling or cancelled batch is idempotent and returns 200. |
too_many_open_batches | 429 | You are at your open-batch cap. Retry-After is 60. | Wait for one to finish, or ask for a higher cap. A cap of 0 means unlimited; an account with no limits row falls back to 5. |
batch_cannot_complete | 400 | The batch cannot finish before its absolute 24-hour expiry. The message names the line count, the lane, the minimum minutes needed, and any of your work already queued ahead of it. | Split the file, or wait for your queued work to drain. |
batch_expired is a per-line code, not an HTTP status
Every batch carries an absolute expires_at of created + 24 hours, independent of completion_window. When it passes, any still-open line becomes an error-file line:
{"id": "batch_req_412", "custom_id": "row-88", "response": null,
"error": {"message": "request did not complete before the batch expired",
"type": "invalid_request_error", "param": null, "code": "batch_expired"}}
You will never see batch_expired as an HTTP response. Read it from the error file named by error_file_id on GET /v1/batches/{id}.
Reading per-line failures
Error-file lines come in two shapes, and the discriminator is whether a status code is known — not when the failure happened.
- A status code is known. The error sits under
response:{"status_code": ..., "request_id": ..., "body": {"error": {...}}}, and top-levelerrorisnull. This covers per-line refusals such as an unknown model orstream: trueon the line (request_idisnullthere, since no call was opened) as well as failures from an invocation. - No status code.
responseisnulland the error sits at top-levelerror. This isbatch_expiredand whole-batch validation failures.
Every line carries custom_id, which is how you map a failure back to your input line.
Line codes drawn from the invocation and gateway stages use the same vocabulary as the tables above. Validation failures do not — see next.
Batch validation uses its own, smaller vocabulary
A structurally malformed line does not fail only that line. Validation runs over the whole input file, and any failure fails the entire batch: status becomes failed, no line is ever launched, and the error file lists the offending lines with id and custom_id both null.
These entries have a different shape from everything else on this page: they carry a line number and no type field.
| Code | Means |
|---|---|
invalid_line | A line is not valid JSON, or its custom_id / method / url / body is wrong or duplicated. line names it. |
heterogeneous_routing | A line routes differently from the first line. Every line in a batch must route identically. param names the disagreeing key. |
empty_file | The input file contains no request lines. |
input_file_unreadable | The input file could not be read — deleted, or a storage error. |
Task-name and task-edit codes (control plane)
These come from POST / PATCH /api/inference/v1/management/capabilities and, for the model-group codes, from the model-group and selection-policy routes, in the control plane’s {"error", "code", ...} body. Every one is refused before anything is written.
| Code | HTTP | Means | Do |
|---|---|---|---|
invalid_task_name | 422 | The name is outside the grammar: lower case, start with a letter, letters, digits, - and _, up to 64 characters. | Choose a name that fits. |
reserved_task_prefix | 422 | The name starts with digits and a dot, such as 3.foo. That prefix is your account id, and we add it. | Send the bare name, foo. |
task_name_taken | 409 | The name is taken: one of your tasks has it (existing_slug names it), or has it with - and _ swapped, or a prompt name it would use exists. With reason: "catalogue_name" it is the name of one of our tasks, current or retired, which is kept for your private copy of that task. | Choose another name. |
shared_task_locked | 422 | You tried to change what one of our shared tasks measures. fields names what you sent; private_copy_available says whether a private copy can be made. With template_delete: true it was a template delete: a shared task’s template is ours. When you asked for a copy and one could not be made, copy_refused_reason says why (not_in_catalogue, inactive_in_catalogue, replaced, judge_task, no_published_template, template_text_unavailable, bare_contract, unsupported_field:<key> or judging:<code>). With read_only: true the field cannot be changed on a shared task at all, for example judging_sample_per_10k (a shared task’s judging rate is its evaluation policy’s). Nothing was changed. | Create your own task for the change; to stop using a shared task, set is_active to false. |
template_catalogue_owned | 422 | You sent prompts for one of our tasks. Its templates are ours, and they change only when we publish them. | Create your own task for your own templates. |
invalid_template | 422 | A template you sent on your own task is not usable; problems lists each reason, such as a placeholder the template does not declare, or a duplicate key. | Fix each problem listed and send the whole request again. |
invalid_model_group_name | 422 | A model-group name is outside the grammar (lower case, start with a letter, letters, digits, - and _, up to 48 characters), or starts with an account prefix such as 3.. | Send the bare name; we add your account prefix. |
model_group_is_task_level | 422 | You sent model_group on a selection policy. A model group is set on the task, not on a policy. | Set model_group on the task instead. |
private_copy_field_without_copy | 422 | copy_name or records_to_seed was sent on a change that makes no copy. | Drop the field, or send the change that needs a copy with allow_breaking_change: true. |
records_to_seed_out_of_range | 422 | records_to_seed on a copy was outside 0 to 25. | Send a whole number from 0 to 25; 0 means no seed. |
invalid_judging_sample | 400 | judging_sample_per_10k on your own task was not an integer from 0 to 10000, or null. | Send a whole number in range, or null for the default. |
task_create_limit_reached | 429 | You have created as many tasks today as your account allows (drafts_per_day). | Wait retry_after_seconds, until midnight UTC. |
invalid_template | 422 | A template, a declared variable, or a validator’s fit is wrong: a placeholder not declared, a declared variable with the wrong required, a validator reading a variable no template declares or a field the schema does not have, more than 10 templates. problems lists every defect. Also sent for prompts on a create. | Fix each problem listed and send again. |
output_format_required | 422 | The output choice is missing or does not add up: no output_format, json_schema with neither or both of output_schema / output_schema_ref, text with a schema, or a schema dropped without "output_format": "text". | Send "json_schema" with one schema, or "text" with none. |
model_group_conflict | 422 | model_group and the older models list in one create. | Send one of them. |
invalid_model_group_name | 422 | A new model group’s name is outside the grammar, or was sent with your account prefix. | Send the bare name. |
invalid_model_group_members | 422 | A new model group’s models list is empty or names an unknown model; model_group_error gives the detail. | Name models from GET /management/models. |
model_group_name_taken | 409 | You already have a model group of that name. | Name the group as a string ("<account id>.<name>") to use it, or choose another name. |
create_field_not_allowed | 422 | records_to_seed or allow_breaking_change on a create; they apply to a private copy of one of our tasks. | Leave them out. |
catalogue_contract_field | 422 | output_format or validators sent on one of our catalogue tasks (operators only). | — |
template_catalogue_owned | 422 | templates or prompts sent on one of our catalogue tasks (operators only). | — |
model_group_is_task_level | 422 | A selection policy sent a model_group. A model group is set on the task. | PATCH the task with model_group. |
model_group_outside_task_group | 400 | Retired: no longer sent. A call naming another model group now gets model_group_differs_from_task (422). | See that code. |
Notes
Not every non-2xx comes from here. The control plane (/api/inference/v1/) uses a different envelope: ninja’s {"detail": ...} for auth failures, and {"detail", "code", "retry_after"} for 429s. The code vocabulary on this page is the /v1/ plane’s.
A 429 is not always billing. tenant_rate_limited means slow down; tenant_quota_exceeded and insufficient_balance mean stop until something changes. The type field separates them for you — rate_limit_error versus insufficient_quota — which is exactly how OpenAI’s own SDKs branch.
Quality headers are absent, never wrong. On a success, x-llmbench-quality-floor / -met / -requested are omitted together if they cannot be resolved. Absent means “not resolvable”, not “not met”. Do not read a missing header as a failed promise.