Contents TandemLLM Docs
v0.14.1
Star58

Part VI, Reference

33HTTP API reference

The engine’s server and the standalone proxy speak the same API for clients, and each adds its own routes for the dashboard and the conductor. This chapter lists every route, every header and every error they use. The tables are written in manual/tools/http_ref.py, and gen_reference.py --check compares them with the code on every build: a route a handler tests, a header name or an error type the code uses that is missing here, or a row the code no longer has, fails the check.

33.1 Who may call what

The admin password is the admin token, QSE_ADMIN_TOKEN in secrets.env (TANDEM_ADMIN_TOKEN for the standalone proxy). It is not set by default. Then every route below answers anyone who reaches the port, and only /metrics keeps the metrics token when one is set (Section 3.8). With a password set, an admin route needs it as a bearer header or a dashboard session (a cookie the token buys). A request from the machine itself (loopback, no proxy headers) needs neither, because the watchdog and the measurement tools rely on that (--trust-loopback).

The OpenAI routes are open on the engine, with or without a password. Behind the proxy they are open until the first user key exists; from then on every request from another machine needs a key. Table 33.1 names the labels the route tables use.

Table 33.1 Who may call a route: the labels of the routes’ who column
labelwho may call
openanyone who reaches the port
adminthe admin rule: anyone while no admin password is set; with one, the admin token, a dashboard session, or a tool on the machine itself
admin-beareras admin, but the token only as a bearer header, never the session cookie
dashthe dashboard rule: anyone unless a password is set and the dashboard login is on
metricsthe metrics token when one is set, else as admin
sessiononly under a session address, /s/<token>/...
keyanyone until the first user key exists; then a user key, except from the machine itself
collectthe collect token, from every address

33.2 The engine’s routes

Table 33.2 lists the engine server’s routes. A route the server does not know answers 404. A route whose part is off on this server also answers 404: the conductor’s routes without --conductor-config and the proxy’s without --backends, each with a message that names the flag, and the decision and Laya routes without decision models, as an unknown route. With --backends the engine’s server also serves the proxy’s routes.

Table 33.2 The engine server’s routes
routewhowhat, body, answer
POST
/v1/chat/completions
openA chat completion, streamed as server-sent events or not. With --backends a model of another backend is relayed there. Body: OpenAI’s chat body: model, messages, tools, stream, max_tokens and the other fields of Section 14.1. Answer: chat.completion, or chat.completion.chunk events and [DONE]; usage carries prompt_tokens_details.hidden_tokens for the conductor’s notes.
POST
/v1/completions
openA plain text completion. Body: prompt, max_tokens and the sampling fields. Answer: text_completion, or its chunks and [DONE].
POST
/v1/embeddings
keyRelayed to the backend that serves the model; only with --backends. Body: OpenAI’s embeddings body. Answer: the backend’s answer, unchanged.
POST
/v1/responses
keyOpenAI’s Responses API, relayed to an openai backend; only with --backends. Body: the client’s body, sent on with the backend’s model id. Answer: the backend’s answer, unchanged.
GET
/v1/models
openThe model list: TandemLLM alone with the conductor on, else the served name; with --backends every backend’s models after it, behind the users’ keys. Answer: {object: list, data: [...]}, and decision_models beside data when decision models are served.
GET
/v1/models/{id}
openOne model card: the engine’s, a decision model’s, or a backend’s. Answer: the card, or 404 model_not_found.
GET
/health
openWhether the server takes work. The full body follows the admin rule; anyone else gets the status alone. Also /healthz and /v1/health. Answer: {status: ok|draining}; the full body adds model, uptime, inflight, the queue, limits, sampling, cache, memory and parallel; 503 while draining.
GET
/metrics
metricsThe Prometheus families of Chapter 34. Answer: Prometheus text format.
GET
/metrics/up
openOne gauge, qse_up, so a scraper can tell an engine that is down from a wrong token. Answer: qse_up 1, or 0 while draining.
GET
/v1/cache/stats
adminEvery cache’s counters. Answer: the cache report /health carries.
POST
/v1/cache/peek
adminWhere a chat’s rows are before it is sent: the body is rendered as a request would be, the session’s router notes are replayed, and the cache tiers are asked without running the model (SRV-349). Body: a chat body (messages, tools, the effort fields) and session. Answer: {tier: resident|ram_park|ram_snapshot|nvme_spill|disk_tier|none, cached_tokens, reread_tokens, reread_eta_ms, tokens, restore_ms, session}; 409 while a request holds the engine.
POST
/v1/cache/prewarm
adminReads a conversation the peek finds on the NVMe back into the engine while nothing runs or waits, so its next request resumes at the end anchor. Body: as /v1/cache/peek. Answer: {prewarmed, why?, restore?} and the peek’s fields; 409 while busy.
POST
/v1/tokenize
openExact token counts with the model’s own chat template, split by part; nothing runs on the GPU (SRV-347). 240 a minute from one address. Body: {messages, tools?} or {text}. Answer: {tokens, parts: {system, tools, messages}}.
POST
/v1/cache/clear?tier=ram|disk|all
admin-bearerEmpties the caches in memory (ram, the default), the disk tier and the session spill (disk), or both (all). Nothing else is reset. Answer: the cache report after the clear; 400 for another tier.
POST
/v1/cache/release
admin-bearerGives back memory the engine holds idle, cheapest first: free allocator blocks and caches while slot 0 is idle, the small models, idle side conversations, slot 0’s idle conversation (to the NVMe first). chatd asks it before a sandbox waits (SRV-483). Body: {bytes}. Answer: what each step gave back and the total.
GET
/dashboard/
openThe dashboard’s static files. They hold no data. Answer: the page.
GET
/v1/dashboard/summary?from=&to=&tz=&model=&client=&backend=&user=&internal=
dashThe Usage view’s totals for a range. Answer: summary.schema.json in docs/contract/dashboard-v1.
GET
/v1/dashboard/usage?bucket=hour|day&from=&to=&tz=&model=&client=&backend=&user=&internal=
dashTokens, requests and cost per hour or day. Answer: usage.schema.json.
GET
/v1/dashboard/requests?limit=&before=&finish=&model=&client=&backend=&user=&internal=
dashRecent request rows of the usage ledger, newest first. Answer: requests.schema.json.
GET
/v1/dashboard/system
dashThe machine, the engine, the memory plan, the slots and the proxy’s mode. Answer: system.schema.json.
GET
/v1/dashboard/live
dashWhat each request is doing now, as server-sent events. Answer: live.schema.json events.
GET
/v1/dashboard/logs?level=&grep=&backlog=&since=&follow=
dashThe log: a backlog of up to 5,000 lines, then live lines as server-sent events; Last-Event-ID resumes; follow=0 answers JSON once. Answer: logs-line.schema.json events, or logs-json.schema.json; 429 past four streams at once.
GET
/v1/dashboard/metrics
dashThe /metrics page under the dashboard’s rule, for the Performance view. Answer: Prometheus text format.
GET
/v1/dashboard/parallel?since=
dashEvery request in flight, the engine’s and the relayed ones. Answer: parallel.schema.json.
GET
/v1/dashboard/session
openWhether a sign-in exists and is needed. Answer: {authenticated, login, password_set, expires_at}.
POST
/v1/dashboard/session
openThe sign-in, with a password set and the login on: sets the qse_dash cookie. Five failures a minute from one address and the sixth gets a 429. Body: {token}. Answer: the session; 401 for a wrong token.
DELETE
/v1/dashboard/session
openSigns this session out. Answer: the session, signed out.
*
/v1/conductor/*
adminThe conductor’s documents, journal, sessions and memory (Table 33.5). Reads follow the dashboard rule, writes and every memory route the admin rule. 404 without --conductor-config.
*
/v1/setup/*
adminThe setup guide (docs/setup.md): the model registry, the box, the memory estimate, the review plan, the apply and reset jobs and Wren. Every POST must also be same-origin JSON: 415 for another content type, 403 for another origin. In setup mode (--setup) these, the dashboard and /health are the only routes that answer.
*
/v1/config/*
adminThe Backends and Setup screens (Table 33.6). Reads follow the dashboard rule, writes the admin rule. 404 without --backends.
POST
/v1/collect/{backend}
collectA batch from the vLLM or SGLang plugin, joined to the ledger rows; only with --backends. Body: the plugin’s batch: plugin, engine, requests, steps. Answer: {accepted: {requests, steps}, joined}.
POST
/v1/systemone
adminTyped decisions from the decision models, in the engine’s idle window. /v1/predict is the same route. Only with QWEN38_DECIDER_MODELS. Body: {state, model?, questions: {id: {type, instructions, criteria?}}, task?, lang?}. Answer: {model, answers, usage, routing, latency_ms, path}.
POST
/v1/predict
adminThe same as /v1/systemone. Body: as /v1/systemone. Answer: as /v1/systemone.
GET
/readyz
openWhether a decision model is loaded and warm. Answer: 200, or 503 with the reason.
GET
/v1/laya
openWhat the Laya route takes and answers, its limits, and whether it is on. Answer: a description of the route.
POST
/v1/laya/ask
openA typed answer from Laya on one text or many, for the model’s own scripts; rate-limited. Body: {question, choices | scale | neither, text | texts, threshold?, model?, deadline_ms?}. Answer: {label, p, sure, probs, model, ms, ...}, or {answers: [...]} for texts.
POST
/v1/laya/drive/end
openEnds a playtest driver session and writes its journal row. Body: the session’s summary. Answer: the row.
GET
/v1/laya/kit/{file}
openA file of the playtest kit the model copies into a project. Answer: the file.
OPTIONS
/v1/laya/*
openA browser’s CORS preflight for the Laya routes. Every other path answers OPTIONS with 501. Answer: 204 with the CORS headers.
GET
/v1/library
openThe package library: every enabled skill’s name, description and classes. Answer: the list and the two routes below.
GET
/v1/library/search?q=&k=&class=
openBM25 search over the skills, best first (k from 1 to 20, default 5). Answer: [{name, description, score}].
GET
/v1/library/{name}
openOne skill’s text and its reference. Answer: {name, description, classes, body, reference}.
GET
/s/{token}/v1/memory
sessionThe memory index this session sees. The /s/{token}/ prefix also serves the library and Laya routes, filed to the session. Answer: the notes’ index lines.
GET
/s/{token}/v1/memory/search?q=&k=
sessionMemory notes that match, best first (k from 1 to 10). Answer: [{id, kind, scope, status, title, score}].
GET
/s/{token}/v1/memory/{id}
sessionOne note. Answer: {id, title, kind, scope, status, body, ...}.
POST
/s/{token}/v1/memory
sessionWrites, changes or archives a note, with the fields of the hidden memory block. Body: {op, scope, kind, title, tags, body, id, reason}. Answer: {op, id, title, ...}, or 422 with the reason as the type.

33.3 The proxy’s own routes

Table 33.3 lists the routes of tandem-dash, the standalone proxy. It serves no model of its own: every completion goes to a backend, and the answer’s bytes go back to the client unchanged. Table 33.4 adds the two small servers that run beside the engine or the proxy: the remote host collector and the status responder that answers while the engine is stopped.

Table 33.3 The standalone proxy’s routes (tandem-dash)
routewhowhat, body, answer
POST
/v1/chat/completions
keyRelayed to the backend the model id routes to (Section 5.10); the answer’s bytes go back unchanged. The same for /v1/completions, /v1/embeddings and /v1/responses. Body: the client’s body. Answer: the backend’s answer, with X-Tandem-Backend.
POST
/v1/completions
keyAs /v1/chat/completions.
POST
/v1/embeddings
keyAs /v1/chat/completions; not for deepseek backends.
POST
/v1/responses
keyAs /v1/chat/completions; openai backends only.
GET
/v1/models
keyEvery backend’s models under their real ids, owned_by the backend. Answer: {object: list, data: [... with a tandem block]}.
GET
/v1/models/{id}
keyOne model’s card. Answer: the card, or 404 model_not_found.
GET
/
openRedirects to the dashboard. Answer: 302 to /dashboard/.
GET
/dashboard/
openThe dashboard’s static files. Answer: the page.
GET
/v1/dashboard/{view}
dashsummary, usage, requests, system, live, logs, metrics, parallel and session, as on the engine; the engine-only panels answer 404. Answer: as on the engine.
POST
/v1/dashboard/session
openAs on the engine. Body: {token}. Answer: the session.
*
/v1/config/*
adminThe Backends and Setup screens (Table 33.6).
POST
/v1/collect/{backend}
collectA plugin’s batch (as on the engine).
GET
/metrics
metricsThe proxy’s own Prometheus families. Answer: Prometheus text format.
GET
/health
openThe proxy’s health, for a container’s check; the full body for the admin or the machine. Also /healthz and /v1/health. Answer: {status}.
*
/v1/conductor/*
adminThe conductor in front of the backends, with --conductor-config; the engine-only screens answer 404.
GET
/v1/library
openThe package library, as on the engine, also under /v1/library/*.
GET
/v1/laya
openLaya, when the decider role names an endpoint.
POST
/v1/laya/ask
openAs on the engine, relayed to the decider role’s endpoint.
Table 33.4 The two small servers beside them
serverrouteanswer
tandem-dash hostcollectGET /host?watch=the machine’s sample as JSON (the bearer token when --token is set)
tandem-dash hostcollectGET /metricsthe same sample in the Prometheus text format
tandem-dash hostcollectGET /health{status: ok}, no token
ops/engine_status.pyGET /v1/models200 with the engine’s last model list and its state, while the engine is away
ops/engine_status.pyGET /dashboard/503 with a page that says why the engine is away and opens the dashboard when it is back
ops/engine_status.py* any other path503 provider_unavailable with Retry-After and the state as engine_<state>

33.4 The conductor’s and the configuration routes

The conductor’s routes and the proxy’s configuration routes are generated from the route tables at the top of server/conductor/routes.py and proxy/config_api.py.

Table 33.5 The conductor’s routes, /v1/conductor/*
MethodPathWhat
GET/v1/conductor/configloaded versions, load errors, journal, roles
GET/v1/conductor/metaenums, the task catalogue, the schemas
GET/v1/conductor/profilestiles
GET/v1/conductor/{kind}[/{name}]settings, router, roles, packages; profiles/{name}
PUT/v1/conductor/{kind}[/{name}]{doc, note, base_version?}: a new version
GET.../versions .../versions/{v} .../diff?from=&to=a document’s versions, one version, and the diff between two
POST.../restore{version, note?} a new version equal to an old one
POST/v1/conductor/profiles/{name}/mode{mode, note?}: the three-way switch
GET/v1/conductor/rolesthe document, health, and what each role’s tokenizer hash allows it to share
POST/v1/conductor/roles/{role}/testone question, its latency and answer
GET/v1/conductor/packages/scanskills found in skill_dirs on the box
GET/v1/conductor/packages/catalogevery package: category, tokens on the engine’s tokenizer, limit, classes, library switches
GET/v1/conductor/packages/item/{name}one package’s texts, tokens, BM25 neighbours
POST/v1/conductor/packages/tokens{texts} the tokens of up to 20 texts (a read)
GET/v1/conductor/packages/usage?name=&limit=injections, shadow notes and library picks per package in the last day and week (journal)
POST/v1/conductor/classify{text, profile?, tools?}: the test box
GET/v1/conductor/decisions[/{id}]the live feed (router rows, each with the monitor’s events of its request) and one row
POST/v1/conductor/decisions/{id}/label{label: right|wrong|clear, note?} (any row)
GET/v1/conductor/events?task=&mode=&acted=&session=&request=&since=&limit=the session monitor’s decisions
GET/v1/conductor/events/{id}one of them
GET/v1/conductor/stats?hours=24the Overview’s tiles and ribbon, monitor too
GET/v1/conductor/sessions[/{key}[/export?format=json|md]]sessions, one timeline, the honest record: every hidden text of router and monitor alike
GET/v1/conductor/evalsclassifier and suite reports on the box
POST/v1/conductor/evals{report}: a suite report (tools/tandem_eval.py)
GET/v1/conductor/residency/planthe residency planner: limits, what fits now, the engine’s split, the last refusal
GET/v1/conductor/watchthe session tasks’ state per session
GET/v1/conductor/deciders/statusthe decision models this engine serves: states, limits, numbers, the planner’s answer for a load now, the model behind each question
POST/v1/conductor/deciders/models/{name}/load|unloadthrough the residency planner (write)
POST/v1/conductor/deciders/autoload{service_start?, hold_restart?}: the on_start models, one after the other (ops/start.sh; write)
POST/v1/conductor/deciders/test{state, questions, model?}: the test box
GET|PUT/v1/conductor/decidersthe deciders document, like the others
GET/v1/conductor/narrator/statusthe narrator’s slot: its model, state, contract, calls and latency
POST/v1/conductor/narrator/try{events: [{kind, facts, extra?}], budget_ms?, max_tokens?}: each event’s template and the narrator’s line, in the idle window (write)
POST/v1/conductor/narrator/load|unloadthrough the residency planner (write)
GET/v1/conductor/shadow/statusthe shadow reader (SR1): its slot, the sessions’ states (read, behind, bytes), the router’s agreement, the last answers
POST/v1/conductor/shadow/ask{session, question, choices: {id: text}, budget_ms?}: a typed decision of the small model over the session’s whole context, in the idle window (write)
GET/v1/conductor/memory/sessionsthe session transcripts on the box: one row per session, the writer’s counters and settings
GET/v1/conductor/memory/sessions/{key}?page=Nits blocks, 200 a page (default: the newest page), to the last complete block
GET.../memory/sessions/{key}/export?format=md|jsonthe transcript itself
PATCH.../memory/sessions/{key}/keep{keep}: meta.json’s keep, which the prune honours
GET/v1/conductor/memory/diskthe disk tier: running or not, its settings and their version, bytes, budget, free space, hit rate, the most-hit entries (a preview for opencode heads and packages ...
POST/v1/conductor/memory/disk/clearevery entry and count gone (write)
GET/v1/conductor/memory/notes?user=the memory notes: the users, the tree (scopes with counts), every live note’s frontmatter with its uses in 7 days, the archived ones, the duplicates, the store’s counts
GET.../memory/notes/search?user=&q=&k=the runtime’s search over every scope, with the scores
GET.../memory/notes/injected?user=&limit=the last requests’ injected note ids (journal)
GET.../memory/notes/writes?limit=the last writes and refusals (journal)
GET.../memory/notes/export?user=a zip of the user’s directory
GET.../memory/notes/{id}?user=one note: frontmatter, body, changelog, the revisions, its uses by day
PUT.../memory/notes/{id}?user={title, body, kind, tags, verify_by}: a person’s edit (a revision; ratified, writer human)
PATCH.../memory/notes/{id}?user={status?, pinned?}: ratify, back to provisional, pin a ratified rule
POST.../memory/notes/{id}/archive?user={reason}
DELETE.../memory/notes/{id}?user=the note, its revisions and archive copy, at once
DELETE.../memory/notes/users/{user}{confirm: <user>}: the whole directory
Table 33.6 The proxy’s configuration routes, /v1/config/*
MethodPathWhat
GET/v1/configeverything the screens show, in one answer
POST/v1/config/test{kind, url, key?}: test a backend not saved
POST/v1/config/backends{id, kind, url, key?,...}: add
GET/v1/config/backends/{id}one, with its models
PATCH/v1/config/backends/{id}change fields (key: null removes the key)
DELETE/v1/config/backends/{id}
POST/v1/config/backends/{id}/testtest the saved backend, refresh its models
PUT/v1/config/backends/{id}/models/{model}public id, enabled, price, max_output (null clears), fallback, features (creates a manual model)
DELETE/v1/config/backends/{id}/models/{model}
PUT/v1/config/rules{rules: [{id?, pattern, backend, enabled}]}
POST/v1/config/users{label}: the answer holds the key, once
PATCH/v1/config/users/{id}{enabled}
DELETE/v1/config/users/{id}
GET/v1/config/prices/seedthe public list prices shipped with this release, applied only by a click
GET/v1/config/auditthe last 50 changes (field names only)

33.5 Headers

Table 33.7 lists every header the engine and the proxy read or write beyond the ones HTTP itself needs. in is a header the server reads, out one it writes.

Table 33.7 Every header the engine and the proxy read or write
headerdirection, wheremeaning
X-Tandemboth, bothraw, shadow or on: lowers the conductor’s mode for one request; the response says which mode ran
X-Tandem-Profileboth, bothpicks a conductor profile; the response names the profile that ran; an unknown name is a 400
X-Tandem-Memoryin, engineoff: no memory note is read or written; read: reads only, a block the model writes is hidden and dropped
X-Tandem-Probein, engineoff takes the request out of the probe snapshots
X-Tandem-Userin, enginea proxy’s user name for the memory notes, honoured from loopback or QWEN38_MEMORY_PROXIES
X-Tandem-Relay-Idin, enginea proxy’s id for a relayed request, for the live counts; a relayed request without X-Tandem-User touches no memory
X-Tandem-Parentin, enginethe request a call of TandemLLM to itself serves; the row becomes client kind internal
X-Tandem-Workerin, engine1 on an agent’s worker request: its session comes back, so it is never a guest, and its rows go to the NVMe rather than away while it waits
X-Tandem-Parent-Sessionin, enginea worker’s parent session (the chat’s): for 600 s after the worker’s last request the parent’s rows go to the NVMe rather than away, and its return tries slot 0 first
X-Tandem-Regeneratein, engine1 serves the request as a regenerate (sampled, a fresh seed); 0 turns the detection off for it
X-Tandem-Images-Left-Outout, enginehow many earlier images were left out of the prompt
X-Tandem-Forwardout, engineto chatd on a /v1/chat-app request: the forward key, the only proof chatd accepts
X-Tandem-Principalout, engineto chatd: who the request is for (owner)
X-Tandem-Password-Setout, engineto chatd: 1 when an admin password is set
X-Tandem-Signed-Inout, engineto chatd: 1 when the request passed the dashboard’s rule
X-Tandem-Waitout, enginememory, on a response that waited for memory before its prefill; its stream says so every 15 s
X-Tandem-Backendout, proxythe id of the backend that answered a relayed request
X-Tandem-Costin, proxy1 on a streamed relayed request: one SSE comment before data: [DONE] carries the request’s cost (: tandem-cost {"cost_micros": N, "backend": id}); without it the stream is the backend’s
X-Engine-Stopout, enginepattern-stop(<label>) on a response without a stream that a stop pattern ended
x-session-idin, bothopencode’s session id: the router’s session and the caches’ session key
X-Conversation-Idin, botha conversation id for clients that send no session field; read after the body’s own ids
X-OpenWebUI-Chat-Idin, bothOpen WebUI’s chat id (a connection header with {{CHAT_ID}}): the router’s session for that chat
X-Request-Idboth, proxysent to a vllm backend as px-<hex>, which the plugin reports back for the join; a client’s own id is kept
X-QSE-Clientin, enginedashboard marks the dashboard’s own requests (client kind dashboard)
X-Real-IPin, bothset by a reverse proxy; a request that carries it is never trusted as local; the sign-in’s rate limit counts per address
X-Forwarded-Forin, bothas X-Real-IP: a request that carries it is never trusted as local
X-Forwarded-Protoin, bothhttps makes the session cookie Secure, and the notes’ {tandem_url} https
X-Forwarded-Hostin, enginethe host the notes’ {tandem_url} names, unless QWEN38_TANDEM_URL sets it
X-Decider-Deadline-Msin, enginehow long a decision request waits for the idle window (default 30,000, at most 120,000)
X-Decider-Windowin, enginein_decode is refused: decisions run in the idle window only
X-Accel-Bufferingout, bothno, on every stream, so nginx sends each event at once
X-Content-Type-Optionsout, bothnosniff on the dashboard’s static files
x-ratelimit-*out, proxya cloud backend’s rate-limit headers, passed on to the client
OpenAI-Betaboth, proxya client’s OpenAI-Beta header, passed on to an openai backend
Authorizationin, bothBearer: the admin token, the metrics token, the collect token or a proxy user key, by route; the engine’s OpenAI routes ignore it and file the request under a hash of it
User-Agentin, boththe client kind in the ledger: open-webui, opencode, openai-sdk, curl, other (a Hermes-shaped body: hermes)
Last-Event-IDin, bothresumes the log stream after that line
Retry-Afterout, bothwith a 503 (queue full, memory gate, draining, a full backend), a 429, or a decision model that is loading
WWW-Authenticateout, bothBearer, with a 401
Set-Cookieout, bothqse_dash, the dashboard session: HttpOnly, SameSite=Strict, Secure behind https
Access-Control-Allow-Originout, engine* on the library and Laya routes, so a page the model builds can call them; with Access-Control-Allow-Methods, -Headers and -Max-Age on the preflight
Access-Control-Allow-Methodsout, engineon the Laya preflight
Access-Control-Allow-Headersout, engineon the Laya preflight
Access-Control-Max-Ageout, engineon the Laya preflight

33.6 Errors

Errors are OpenAI-shaped:

json
{"error": {"message": "max_tokens must be at least 1", "type": "invalid_request_error",
           "param": "max_tokens", "code": null}}

A 400 names the field or the message part it refuses in param. A refusal the client can retry (a full queue, the memory gate, a server that drains) has the type server_busy, the status in code, and a Retry-After header. A refusal after a stream opened (a queue timeout, a prompt too long, the memory gate, a backend that failed) ends the stream with a chunk whose finish_reason is "error" and which carries the error object, then [DONE]. The decision routes keep their own body, {"type": "error", "error": {"type", "message"}}. Table 33.8 lists every status and type.

Table 33.8 Every error status and type
status, Retry-Aftererror.type or codewhen
400invalid_request_errora request the engine will not serve as asked: error.param names the field (messages, a role, max_tokens, tools, an image part, a prompt too long, a body that is not JSON or not an object)
400bad_requesta dashboard or conductor query that does not parse (a level, a range, a number)
400confirm_requireda setup reset without the confirmation its level asks for (true, or the typed word)
401unauthorizeda guarded route without the token or the session it needs, once an admin password is set; a wrong collect token
401invalid_api_keythe proxy, once a user key exists: no key, or a key it does not know (code invalid_api_key)
403forbiddena memory route outside a known session address
404not_foundno such route, or a route whose part is off on this server (the conductor, the proxy, the decision models)
404unsupported_routethe proxy: a route the backend’s kind does not serve, such as embeddings on deepseek (code unsupported_route)
404model_not_founda model id no backend serves, or one two backends serve (the message names the prefixed forms) (code model_not_found)
409busy, not_loaded, not_running, closed, stale, role_offa conductor action that needs a role or a decision model in another state, or a role the roles document switches off (role_off); a package proposal already closed, or whose base text changed since
422invalid_request_error, invalid_reporta value the schema refuses: a config field (error.field), a field an environment variable locks (error.variable), a Laya, library or memory call’s fields, an eval report
422schema, secret, cap, user, unknown_id, unknown_package, rule_from_tool, no_session, no_user, refused_root, no_proposals, proposea memory note or a package proposal refused, with the reason as the type: a field (schema), a secret, a cap, an unknown note or package
429
with Retry-After
server_busya request without a stream that waited --queue-timeout for the engine
429
with Retry-After
too_manya fifth log or live stream at once; a sixth failed sign-in in a minute
429
with Retry-After
rate_limitedthe library, Laya or memory routes past their calls a minute
500internal_error, errora bug: the message is the exception’s; the request is in the log
501noneOPTIONS on any path but the Laya routes
502connect_error, upstream_error, key_unreadablethe proxy could not reach the backend or read its stored key, after its retries
200upstream_closed, upstream_errora stream that already began: the backend failed or closed early; the client gets an error event, then [DONE]
503
with Retry-After
server_busythe queue is full, the server is draining, or the memory gate or the admission by bytes cannot make room
503
with Retry-After
memory_wait_timeouta request without a stream that waited past --queue-timeout for memory to free (type server_busy, Retry-After: 30)
503
with Retry-After
setup_modeany route but the dashboard, /health, /v1/models and /v1/setup/* on a box in setup mode (Retry-After: 60)
503
with Retry-After
max_inflightevery backend for the model is at the max_inflight you set (Retry-After: 1)
503
with Retry-After
warming, loading, refused, failed, not_loaded, busy_decodinga decision request while the model loads, was refused memory, failed, or the engine kept decoding past the deadline
503
with Retry-After
engine_faileda completion after the engine’s CUDA context was lost: the sentence says so, the watchdog restarts the engine (Retry-After: 60)
503
with Retry-After
provider_unavailablethe status responder while the engine is stopped, starting or held (code engine_<state>)
504upstream_timeout, timeouta backend that sent no first byte in time; a Laya call past its deadline
529overloaded_errora decision request the runtime could not take