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.

OutcomeHeaderWhat it identifies
Success (200)x-llmbench-call-idthe call record for the call that ran
Errorx-llmbench-diagnosticthe 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

HeaderOnMeaning
x-llmbench-diagnosticerrorsincident id — quote it in support
x-llmbench-error-classerrorswhose problem this is (see below)
x-llmbench-error-codeerrorssame value as error.code
Retry-After429sseconds to wait before retrying
x-llmbench-request-iderrors that reached invocationthe 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.

ClassWhose problemDefault response
providera transient upstream faultretry with backoff
your_quotayou are out of roomwait for the window, or top up
your_keyyour vaulted provider credentialfix the credential
your_requestthe request contentchange the request
routerconfiguration of the task you calledfix the task, or pick another
gatewayour admission controlhonour 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.

CodeHTTPClassMeansDo
invalid_api_key401gatewayNo 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_mismatch403gatewayA 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_found404routerThe 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_body400gatewayThe 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_parameter400gatewayYou 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_value400gatewayA 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_supported400routerYou 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_conflict400gatewayYou 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_enabled400gatewayYou 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_allowed400your_requestYou 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_field422your_requestYou 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_field400your_requestA 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_contract400your_requestThe 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_supported400your_requestThe model names one of our internal evaluation tasks, which cannot be called.Call a task from GET /v1/models.
template_selection_invalid422your_requestThe 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_published422your_requestllmbench.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_failed503routerOur side. We could not read the task’s templates for a moment. Nothing ran.Retry shortly.
model_group_differs_from_task422your_requestselection.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_unavailable422your_requestYou 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_refused422your_requestThe 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_model422your_requestOn 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_unavailable422your_requestRetired: 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_paused409your_requestYour private task for that group is paused. The message names it.Activate it, or call another task.
model_group_routing_failed503routerOur side. The call’s model group could not be routed for a moment. Nothing ran.Retry shortly.
tenant_rate_limited429gatewayYour per-second limit on our gateway.Honour Retry-After and slow down. Not a billing problem.
tenant_quota_exceeded429gatewayparam 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.

CodeHTTPClassMeansDo
provider_key_missing403your_keyNo 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_revoked403your_keyThe key was valid and is no longer.Mint a new one at the provider and re-vault.
provider_key_invalid403your_keyThe provider rejected the key you vaulted.Mint a fresh key at the provider and re-PUT it.
provider_account_suspended403your_keyThe 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_exhausted429your_quotaYour account at the provider is out of quota.Raise limits or top up at the provider. This is their quota, not ours.
insufficient_balance429your_quotaRuns 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_ready503routerRuns 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_limited429your_quotaRate-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_unavailable502providerThe provider was unreachable or erroring.Retry with backoff.
provider_timeout504providerThe provider did not answer in time.Retry. If a task times out consistently, consider the batch lane.
provider_error502providerAn upstream error we could not classify more precisely. The (redacted) provider message is passed through.Read the message, then retry.
content_filtered400your_requestThe provider refused on content grounds, or the input exceeded a length limit.Change the input. Retrying unchanged will not help.
missing_prompt_variable422your_requestRuns 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_variable422your_requestRuns 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_failed500routerEither 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_found404routerThe task disappeared between routing and invocation.Re-check GET /v1/models.
no_eligible_model503routerRuns no model. The candidate pool was empty, so nothing was tried.The message takes one of three shapes — see below.
capability_config_invalid400routerRuns 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:

  • verified is true only when the check passed.
  • last_error carries 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:

  1. 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.
  2. A quality-bar message, when a min_confidence floor is set on the task and exploration is off, so no model has enough evidence to clear it. It names both settings.
  3. A generic sentence when neither applies, naming nothing further.

Batch and file codes

CodeHTTPMeansDo
file_not_found404No such file id, or its content was deleted.Files are tombstoned on delete, so a deleted id keeps 404ing. Re-upload.
batch_not_found404No such batch id for this account.Cross-account ids 404 by construction. Check the id.
batch_not_cancellable409The 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_batches429You 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_complete400The 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-level error is null. This covers per-line refusals such as an unknown model or stream: true on the line (request_id is null there, since no call was opened) as well as failures from an invocation.
  • No status code. response is null and the error sits at top-level error. This is batch_expired and 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.

CodeMeans
invalid_lineA line is not valid JSON, or its custom_id / method / url / body is wrong or duplicated. line names it.
heterogeneous_routingA line routes differently from the first line. Every line in a batch must route identically. param names the disagreeing key.
empty_fileThe input file contains no request lines.
input_file_unreadableThe 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.

CodeHTTPMeansDo
invalid_task_name422The 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_prefix422The 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_taken409The 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_locked422You 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_owned422You 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_template422A 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_name422A 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_level422You 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_copy422copy_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_range422records_to_seed on a copy was outside 0 to 25.Send a whole number from 0 to 25; 0 means no seed.
invalid_judging_sample400judging_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_reached429You have created as many tasks today as your account allows (drafts_per_day).Wait retry_after_seconds, until midnight UTC.
invalid_template422A 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_required422The 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_conflict422model_group and the older models list in one create.Send one of them.
invalid_model_group_name422A new model group’s name is outside the grammar, or was sent with your account prefix.Send the bare name.
invalid_model_group_members422A 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_taken409You 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_allowed422records_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_field422output_format or validators sent on one of our catalogue tasks (operators only).—
template_catalogue_owned422templates or prompts sent on one of our catalogue tasks (operators only).—
model_group_is_task_level422A selection policy sent a model_group. A model group is set on the task.PATCH the task with model_group.
model_group_outside_task_group400Retired: 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.