Part VI, Reference
31HTTP 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.
31.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 31.1 names the labels the route tables use.
| label | who may call |
|---|---|
open | anyone who reaches the port |
admin | the 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- | as admin, but the token only as a bearer header, never the session cookie |
dash | the dashboard rule: anyone unless a password is set and the dashboard login is on |
metrics | the metrics token when one is set, else as admin |
session | only under a session address, / |
key | anyone until the first user key exists; then a user key, except from the machine itself |
collect | the collect token, from every address |
31.2 The engine’s routes
Table 31.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.
| route | who | what, body, answer |
|---|---|---|
POST/ | open | A 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_ and the other fields of Section 14.1. Answer: chat.completion, or chat.completion.chunk events and [DONE]; usage carries prompt_ for the conductor’s notes. |
POST/ | open | A plain text completion. Body: prompt, max_ and the sampling fields. Answer: text_, or its chunks and [DONE]. |
POST/ | key | Relayed to the backend that serves the model; only with --backends. Body: OpenAI’s embeddings body. Answer: the backend’s answer, unchanged. |
POST/ | key | OpenAI’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/ | open | The 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:, and decision_ beside data when decision models are served. |
GET/ | open | One model card: the engine’s, a decision model’s, or a backend’s. Answer: the card, or 404 model_. |
GET/ | open | Whether the server takes work. The full body follows the admin rule; anyone else gets the status alone. Also / and / Answer: {status:; the full body adds model, uptime, inflight, the queue, limits, sampling, cache, memory and parallel; 503 while draining. |
GET/ | metrics | The Prometheus families of Chapter 32. Answer: Prometheus text format. |
GET/ | open | One gauge, qse_, so a scraper can tell an engine that is down from a wrong token. Answer: qse_ 1, or 0 while draining. |
GET/ | admin | Every cache’s counters. Answer: the cache report / carries. |
POST/ | admin | Where 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:; 409 while a request holds the engine. |
POST/ | admin | Reads 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 /. Answer: {prewarmed, and the peek’s fields; 409 while busy. |
POST/ | open | Exact 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, or {text}. Answer: {tokens,. |
POST/ | admin-bearer | Empties 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/ | admin-bearer | Gives 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/ | open | The dashboard’s static files. They hold no data. Answer: the page. |
GET/ | dash | The Usage view’s totals for a range. Answer: summary.schema.json in docs/contract/dashboard-v1. |
GET/ | dash | Tokens, requests and cost per hour or day. Answer: usage.schema.json. |
GET/ | dash | Recent request rows of the usage ledger, newest first. Answer: requests.schema.json. |
GET/ | dash | The machine, the engine, the memory plan, the slots and the proxy’s mode. Answer: system.schema.json. |
GET/ | dash | What each request is doing now, as server-sent events. Answer: live.schema.json events. |
GET/ | dash | The 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- events, or logs-; 429 past four streams at once. |
GET/ | dash | The / page under the dashboard’s rule, for the Performance view. Answer: Prometheus text format. |
GET/ | dash | Every request in flight, the engine’s and the relayed ones. Answer: parallel.schema.json. |
GET/ | open | Whether a sign-in exists and is needed. Answer: {authenticated,. |
POST/ | open | The sign-in, with a password set and the login on: sets the qse_ 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/ | open | Signs this session out. Answer: the session, signed out. |
*/ | admin | The conductor’s documents, journal, sessions and memory (Table 31.5). Reads follow the dashboard rule, writes and every memory route the admin rule. 404 without --conductor-. |
*/ | admin | The 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 / are the only routes that answer. |
*/ | admin | The Backends and Setup screens (Table 31.6). Reads follow the dashboard rule, writes the admin rule. 404 without --backends. |
POST/ | collect | A 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:. |
POST/ | admin | Typed decisions from the decision models, in the engine’s idle window. / is the same route. Only with QWEN38_DECIDER_MODELS. Body: {state, model?, questions: {id:, task?, lang?}. Answer: {model,. |
POST/ | admin | The same as / Body: as /. Answer: as /. |
GET/ | open | Whether a decision model is loaded and warm. Answer: 200, or 503 with the reason. |
GET/ | open | What the Laya route takes and answers, its limits, and whether it is on. Answer: a description of the route. |
POST/ | open | A typed answer from Laya on one text or many, for the model’s own scripts; rate-limited. Body: {question,. Answer: {label,, or {answers: for texts. |
POST/ | open | Ends a playtest driver session and writes its journal row. Body: the session’s summary. Answer: the row. |
GET/ | open | A file of the playtest kit the model copies into a project. Answer: the file. |
OPTIONS/ | open | A browser’s CORS preflight for the Laya routes. Every other path answers OPTIONS with 501. Answer: 204 with the CORS headers. |
GET/ | open | The package library: every enabled skill’s name, description and classes. Answer: the list and the two routes below. |
GET/ | open | BM25 search over the skills, best first (k from 1 to 20, default 5). Answer: [{name,]. |
GET/ | open | One skill’s text and its reference. Answer: {name,. |
GET/ | session | The memory index this session sees. The / prefix also serves the library and Laya routes, filed to the session. Answer: the notes’ index lines. |
GET/ | session | Memory notes that match, best first (k from 1 to 10). Answer: [{id,]. |
GET/ | session | One note. Answer: {id,. |
POST/ | session | Writes, changes or archives a note, with the fields of the hidden memory block. Body: {op,. Answer: {op,, or 422 with the reason as the type. |
31.3 The proxy’s own routes
Table 31.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 31.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.
tandem-dash)| route | who | what, body, answer |
|---|---|---|
POST/ | key | Relayed to the backend the model id routes to (Section 5.10); the answer’s bytes go back unchanged. The same for /, / and / Body: the client’s body. Answer: the backend’s answer, with X-Tandem-Backend. |
POST/ | key | As / |
POST/ | key | As /; not for deepseek backends. |
POST/ | key | As /; openai backends only. |
GET/ | key | Every backend’s models under their real ids, owned_ the backend. Answer: {object:. |
GET/ | key | One model’s card. Answer: the card, or 404 model_. |
GET/ | open | Redirects to the dashboard. Answer: 302 to /. |
GET/ | open | The dashboard’s static files. Answer: the page. |
GET/ | dash | summary, 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/ | open | As on the engine. Body: {token}. Answer: the session. |
*/ | admin | The Backends and Setup screens (Table 31.6). |
POST/ | collect | A plugin’s batch (as on the engine). |
GET/ | metrics | The proxy’s own Prometheus families. Answer: Prometheus text format. |
GET/ | open | The proxy’s health, for a container’s check; the full body for the admin or the machine. Also / and / Answer: {status}. |
*/ | admin | The conductor in front of the backends, with --conductor-; the engine-only screens answer 404. |
GET/ | open | The package library, as on the engine, also under / |
GET/ | open | Laya, when the decider role names an endpoint. |
POST/ | open | As on the engine, relayed to the decider role’s endpoint. |
| server | route | answer |
|---|---|---|
tandem- | GET / | the machine’s sample as JSON (the bearer token when --token is set) |
tandem- | GET / | the same sample in the Prometheus text format |
tandem- | GET / | {status:, no token |
ops/ | GET / | 200 with the engine’s last model list and its state, while the engine is away |
ops/ | GET / | 503 with a page that says why the engine is away and opens the dashboard when it is back |
ops/ | * any other path | 503 provider_ with Retry-After and the state as engine_<state> |
31.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.
/v1/conductor/*| Method | Path | What |
|---|---|---|
GET | / | loaded versions, load errors, journal, roles |
GET | / | enums, the task catalogue, the schemas |
GET | / | tiles |
GET | / | settings, router, roles, packages; profiles/{name} |
PUT | / | {doc,: a new version |
GET | .../ | a document’s versions, one version, and the diff between two |
POST | .../ | {version, a new version equal to an old one |
POST | / | {mode,: the three-way switch |
GET | / | the document, health, and what each role’s tokenizer hash allows it to share |
POST | / | one question, its latency and answer |
GET | / | skills found in skill_dirs on the box |
GET | / | every package: category, tokens on the engine’s tokenizer, limit, classes, library switches |
GET | / | one package’s texts, tokens, BM25 neighbours |
POST | / | {texts} the tokens of up to 20 texts (a read) |
GET | / | injections, shadow notes and library picks per package in the last day and week (journal) |
POST | / | {text,: the test box |
GET | / | the live feed (router rows, each with the monitor’s events of its request) and one row |
POST | / | {label: (any row) |
GET | / | the session monitor’s decisions |
GET | / | one of them |
GET | / | the Overview’s tiles and ribbon, monitor too |
GET | / | sessions, one timeline, the honest record: every hidden text of router and monitor alike |
GET | / | classifier and suite reports on the box |
POST | / | {report}: a suite report (tools/tandem_eval.py) |
GET | / | the residency planner: limits, what fits now, the engine’s split, the last refusal |
GET | / | the session tasks’ state per session |
GET | / | the decision models this engine serves: states, limits, numbers, the planner’s answer for a load now, the model behind each question |
POST | / | through the residency planner (write) |
POST | / | {service_: the on_start models, one after the other (ops/start.sh; write) |
POST | / | {state,: the test box |
GET|PUT | / | the deciders document, like the others |
GET | / | the narrator’s slot: its model, state, contract, calls and latency |
POST | / | {events:], budget_ms?, max_tokens?}: each event’s template and the narrator’s line, in the idle window (write) |
POST | / | through the residency planner (write) |
GET | / | the shadow reader (SR1): its slot, the sessions’ states (read, behind, bytes), the router’s agreement, the last answers |
POST | / | {session,, budget_ms?}: a typed decision of the small model over the session’s whole context, in the idle window (write) |
GET | / | the session transcripts on the box: one row per session, the writer’s counters and settings |
GET | / | its blocks, 200 a page (default: the newest page), to the last complete block |
GET | .../ | the transcript itself |
PATCH | .../ | {keep}: meta.json’s keep, which the prune honours |
GET | / | the 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 | / | every entry and count gone (write) |
GET | / | 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 | .../ | the runtime’s search over every scope, with the scores |
GET | .../ | the last requests’ injected note ids (journal) |
GET | .../ | the last writes and refusals (journal) |
GET | .../ | a zip of the user’s directory |
GET | .../ | one note: frontmatter, body, changelog, the revisions, its uses by day |
PUT | .../ | {title,: a person’s edit (a revision; ratified, writer human) |
PATCH | .../ | {status?,: ratify, back to provisional, pin a ratified rule |
POST | .../ | {reason} |
DELETE | .../ | the note, its revisions and archive copy, at once |
DELETE | .../ | {confirm:: the whole directory |
/v1/config/*| Method | Path | What |
|---|---|---|
GET | / | everything the screens show, in one answer |
POST | / | {kind,: test a backend not saved |
POST | / | {id,: add |
GET | / | one, with its models |
PATCH | / | change fields (key: removes the key) |
DELETE | / | |
POST | / | test the saved backend, refresh its models |
PUT | / | public id, enabled, price, fallback, features (creates a manual model) |
DELETE | / | |
PUT | / | {rules:]} |
POST | / | {label}: the answer holds the key, once |
PATCH | / | {enabled} |
DELETE | / | |
GET | / | the public list prices shipped with this release, applied only by a click |
GET | / | the last 50 changes (field names only) |
31.5 Headers
Table 31.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.
| header | direction, where | meaning |
|---|---|---|
X- | both, both | raw, shadow or on: lowers the conductor’s mode for one request; the response says which mode ran |
X- | both, both | picks a conductor profile; the response names the profile that ran; an unknown name is a 400 |
X- | in, engine | off: no memory note is read or written; read: reads only, a block the model writes is hidden and dropped |
X- | in, engine | off takes the request out of the probe snapshots |
X- | in, engine | a proxy’s user name for the memory notes, honoured from loopback or QWEN38_MEMORY_PROXIES |
X- | in, engine | a proxy’s id for a relayed request, for the live counts; a relayed request without X-Tandem-User touches no memory |
X- | in, engine | the request a call of TandemLLM to itself serves; the row becomes client kind internal |
X- | in, engine | 1 serves the request as a regenerate (sampled, a fresh seed); 0 turns the detection off for it |
X- | out, engine | how many earlier images were left out of the prompt |
X- | out, engine | to chatd on a / request: the forward key, the only proof chatd accepts |
X- | out, engine | to chatd: who the request is for (owner) |
X- | out, engine | to chatd: 1 when an admin password is set |
X- | out, engine | to chatd: 1 when the request passed the dashboard’s rule |
X- | out, engine | memory, on a response that waited for memory before its prefill; its stream says so every 15 s |
X- | out, proxy | the id of the backend that answered a relayed request |
X- | out, engine | pattern-stop(<label>) on a response without a stream that a stop pattern ended |
x- | in, both | opencode’s session id: the router’s session and the caches’ session key |
X- | in, both | a conversation id for clients that send no session field; read after the body’s own ids |
X- | in, both | Open WebUI’s chat id (a connection header with {{CHAT_): the router’s session for that chat |
X- | both, proxy | sent to a vllm backend as px-<hex>, which the plugin reports back for the join; a client’s own id is kept |
X- | in, engine | dashboard marks the dashboard’s own requests (client kind dashboard) |
X- | in, both | set by a reverse proxy; a request that carries it is never trusted as local; the sign-in’s rate limit counts per address |
X- | in, both | as X-Real-IP: a request that carries it is never trusted as local |
X- | in, both | https makes the session cookie Secure, and the notes’ {tandem_ https |
X- | in, engine | the host the notes’ {tandem_ names, unless QWEN38_TANDEM_URL sets it |
X- | in, engine | how long a decision request waits for the idle window (default 30,000, at most 120,000) |
X- | in, engine | in_ is refused: decisions run in the idle window only |
X- | out, both | no, on every stream, so nginx sends each event at once |
X- | out, both | nosniff on the dashboard’s static files |
x- | out, proxy | a cloud backend’s rate-limit headers, passed on to the client |
OpenAI- | both, proxy | a client’s OpenAI-Beta header, passed on to an openai backend |
Authorization | in, both | Bearer: 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- | in, both | the client kind in the ledger: open-webui, opencode, openai-sdk, curl, other (a Hermes-shaped body: hermes) |
Last- | in, both | resumes the log stream after that line |
Retry- | out, both | with a 503 (queue full, memory gate, draining, a full backend), a 429, or a decision model that is loading |
WWW- | out, both | Bearer, with a 401 |
Set- | out, both | qse_, the dashboard session: HttpOnly, SameSite=Strict, Secure behind https |
Access- | out, 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- | out, engine | on the Laya preflight |
Access- | out, engine | on the Laya preflight |
Access- | out, engine | on the Laya preflight |
31.6 Errors
Errors are OpenAI-shaped:
{"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 31.8 lists every status and type.
| status, Retry-After | error.type or code | when |
|---|---|---|
400 | invalid_ | a request the engine will not serve as asked: error.param names the field (messages, a role, max_, tools, an image part, a prompt too long, a body that is not JSON or not an object) |
400 | bad_ | a dashboard or conductor query that does not parse (a level, a range, a number) |
400 | confirm_ | a setup reset without the confirmation its level asks for (true, or the typed word) |
401 | unauthorized | a guarded route without the token or the session it needs, once an admin password is set; a wrong collect token |
401 | invalid_ | the proxy, once a user key exists: no key, or a key it does not know (code invalid_) |
403 | forbidden | a memory route outside a known session address |
404 | not_ | no such route, or a route whose part is off on this server (the conductor, the proxy, the decision models) |
404 | unsupported_ | the proxy: a route the backend’s kind does not serve, such as embeddings on deepseek (code unsupported_) |
404 | model_ | a model id no backend serves, or one two backends serve (the message names the prefixed forms) (code model_) |
409 | busy, not_, not_, closed, stale | a conductor action that needs a role or a decision model in another state; a package proposal already closed, or whose base text changed since |
422 | invalid_, invalid_ | a 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 |
422 | schema, secret, cap, user, unknown_, unknown_, rule_, no_, no_, refused_, no_, propose | a 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 |
429with Retry-After | server_ | a request without a stream that waited --queue- for the engine |
429with Retry-After | too_ | a fifth log or live stream at once; a sixth failed sign-in in a minute |
429with Retry-After | rate_ | the library, Laya or memory routes past their calls a minute |
500 | internal_, error | a bug: the message is the exception’s; the request is in the log |
501 | none | OPTIONS on any path but the Laya routes |
502 | connect_, upstream_, key_ | the proxy could not reach the backend or read its stored key, after its retries |
200 | upstream_, upstream_ | a stream that already began: the backend failed or closed early; the client gets an error event, then [DONE] |
503with Retry-After | server_ | the queue is full, the server is draining, or the memory gate or the admission by bytes cannot make room |
503with Retry-After | memory_ | a request without a stream that waited past --queue- for memory to free (type server_, Retry-After: 30) |
503with Retry-After | setup_ | any route but the dashboard, /, / and / on a box in setup mode (Retry-After: 60) |
503with Retry-After | max_ | every backend for the model is at the max_ you set (Retry-After: 1) |
503with Retry-After | warming, loading, refused, failed, not_, busy_ | a decision request while the model loads, was refused memory, failed, or the engine kept decoding past the deadline |
503with Retry-After | provider_ | the status responder while the engine is stopped, starting or held (code engine_<state>) |
504 | upstream_, timeout | a backend that sent no first byte in time; a Laya call past its deadline |
529 | overloaded_ | a decision request the runtime could not take |