Part VI, Reference
31Metrics and the ledger
31.1 Prometheus metrics
GET /metrics serves the families below in the Prometheus text format, to anyone while no token is set, else behind the metrics or admin token, or to a request from the machine itself. ops/monitoring/ has a scrape configuration, alert rules with their tests, and a Grafana dashboard generated by tools/grafana_dashboard.py. With the metrics token set, a scrape job needs it as a bearer token:
- job_name: tandemllm
authorization: {type: Bearer, credentials_file: /etc/prometheus/tandem-metrics.token}
static_configs: [{targets: ['gpu-host:8000']}]The engine’s families never count a relayed request, and the proxy’s families never see the engine’s own. The table lists every family registered with a literal name in the code; families built at scrape time from a table (the mapped qse_backend_* engine families, the cache families by cache label) are listed by their prefix in the help text of the page itself.
| Metric | Type, labels | What it counts |
|---|---|---|
qse_ | counter, | generations that finished, by the reason they finished |
qse_ | counter | generations that finished on the model’s own stop token or the caller’s token budget |
qse_ | counter | prompt tokens accepted, including tokens served from a cache |
qse_ | counter | tokens written by the engine |
qse_ | counter, | exceptions that ended a generation, by exception class |
qse_ | counter | a conversation’s earlier images left out of a request over the image caps (server/images.py) |
qse_ | histogram | arrival of the request to its first token, queue wait included |
qse_ | histogram | gap between two tokens of one generation. |
qse_ | histogram | arrival of the request to the end of its stream, queue wait included |
qse_ | counter | drafted tokens the target agreed with. The token the target writes itself is not counted here, so accepted over drafted is the draft acceptance rate |
qse_ | counter | tokens proposed by the drafter, anchor excluded |
qse_ | counter | blocks the drafter proposed |
qse_ | histogram | tokens committed by one verified block, the target’s own token included |
qse_ | counter, | blocks proposed at each width, anchor included. |
qse_ | histogram | one verify pass over a draft block, as the decode loop paid for it |
qse_ | histogram | one call to the drafter, lookup and block drafter together |
qse_ | counter, | conductor decisions, passes included: the monitor’s tasks and the router’s class (task route) |
qse_ | counter, | decisions the conductor executed, by task and action |
qse_ | counter | tokens the model saw that the client did not send or receive (hidden closes, hidden turns, reminders, router notes new and replayed) |
qse_ | counter | turns the conductor ended with its GIVE_UP sentence |
qse_ | histogram | one tier 1 question (a classifier stage: the decider or Qwen), answered or not |
qse_ | counter, | tier 1 questions without a usable answer, by reason |
qse_ | counter | reasoning tokens written before a hidden close cut the block (the size of the blocks the conductor closed) |
qse_ | gauge | generations the engine is working on |
qse_ | gauge | requests holding a socket and waiting for the engine |
qse_ | counter, | requests turned away with a Retry-After before the engine saw them |
qse_ | counter, | prefix lookups that found nothing |
qse_ | counter, | entries dropped to stay inside the byte budget |
qse_ | gauge, | bytes held by each cache |
qse_ | gauge, | the engine’s memory against its limits, by part |
qse_ | counter, | caches given back to keep the engine under its ceiling or its peak, by level |
qse_ | gauge, | entries held by each cache |
qse_ | gauge, | the disk tier: what its entries hold, its budget, the file system’s free space and the floor it never crosses, the host staging buffer |
qse_ | gauge, | the disk tier’s files: anchors (one state each) and chunks (1,024 tokens of rows each) |
qse_ | counter | prefills the disk tier could have shortened by a chunk or more |
qse_ | counter | prefills that started from the disk tier |
qse_ | counter | lookups the disk tier did not answer |
qse_ | counter | prompt tokens read back from the disk instead of prefilled |
qse_ | counter | bytes a restore read |
qse_ | counter | bytes the writer wrote |
qse_ | counter, | files the writer wrote, by kind |
qse_ | counter, | entries the disk tier gave up, by reason (budget, floor, ttl, orphan) |
qse_ | counter, | writes not done, by reason (window: the next prefill came first; budget, floor, staging, aborted, too_big) |
qse_ | counter | restores that failed a check (checksum, layout) and took the cold path |
qse_ | gauge | the last restore’s wall time |
qse_ | counter, | stored prefixes whose 64-bit hash matched and whose tokens did not |
qse_ | counter | prompt tokens restored from a snapshot instead of forwarded |
qse_ | counter | prompt tokens run through the 64 layers |
qse_ | gauge | allocator bytes held by the engine |
qse_ | gauge | bytes the allocator has taken from the driver and not given back |
qse_ | gauge | free bytes in the board’s one memory pool |
qse_ | gauge | seconds since the server started |
qse_ | gauge, | always 1. The labels are the configuration the numbers above were produced by |
qse_ | counter, | HTTP responses by route (a fixed set) and status code |
qse_ | counter | prompt tokens restored from the state store (all of them on a response-cache replay) |
qse_ | counter | committed tokens inside the reasoning block |
qse_ | counter, | proposed blocks for which the persistent suffix store matched the context (hit) or did not (miss); counted once a block, never once a lookup |
qse_ | histogram | arrival to the engine lock, per request |
qse_ | histogram | the engine lock to the first token (template, tokenise, prefill), per request |
qse_ | histogram | per decoded request: (completion tokens - 1) / (last token - first token), the atlas row’s convention, and the predicted_ a response carries |
qse_ | histogram | per request with a forwarded prompt: forwarded tokens / prefill seconds |
qse_ | histogram | prompt tokens per request, cached ones included |
qse_ | histogram | completion tokens per request |
qse_ | gauge | token ids in the persistent suffix store |
qse_ | gauge | the state cache’s byte budget |
qse_ | gauge | resident set of the server process |
qse_ | gauge | when the server started, unix seconds |
qse_ | gauge | open /v1/dashboard/logs streams |
qse_ | counter | rows the usage ledger has written |
qse_ | counter | rows the usage ledger lost: a full queue or a failed write |
qse_ | gauge | always 1. version, the git commit, the code hash row3 records (tools/row3.py::code_hash) and a hash of the effective flags |
qse_ | counter, | requests relayed to a backend, by its id, its kind and the finish reason |
qse_ | counter, | tokens a backend reported in usage, by type (prompt, cached, completion, reasoning) |
qse_ | counter, | cost at the model’s price, micro-dollars |
qse_ | counter, | tries again before any byte reached the client, by backend and reason (429, 502, 503, connect) |
qse_ | histogram | the proxy’s own time per request: arrival to the upstream send, plus the first frame’s delay through the proxy |
qse_ | histogram | arrival at the proxy to the first token frame (the network and the backend’s queue inside) |
qse_ | gauge, | 1 when the backend’s last health check passed |
qse_ | gauge, | requests relayed to the backend now |
qse_ | gauge, | seconds since the backend’s own /metrics page was last read (vLLM, SGLang) |
qse_ | gauge, | seconds since the plugin inside the backend’s engine last posted (vLLM, SGLang with tandemllm-*-plugin) |
qse_ | gauge, | requests whose ledger row got the engine’s own numbers, by how the plugin’s item was matched (exact, approximate) or why it was not (unmatched, untagged) |
31.2 The usage ledger
server/ledger.py writes one SQLite row per request: counts, timings, the finish reason, the cache source, a client id. Never the prompt, the answer, a tool name or argument, a conversation id, the user field, an IP address or a raw token. Refused requests (503, 429) and rejected ones (400) are rows too, with null token fields, so the request count is honest.
It never slows or fails a request. A row is put on a bounded queue (10,000), a writer thread commits in batches at most every second, and a disk error drops that batch, counts it and logs it at most once a minute. Rows older than --usage-retention-days (at least 400) are deleted once a day, and once an ISO week an online backup goes beside the file, the newest 8 kept. The server takes the path from --usage-ledger only, never from the environment, so a benchmark server started with serve.env exported never writes the operator’s ledger. Migrations are forward only and in place, and keep every row.
| Column | Type, since | What it holds |
|---|---|---|
ts_ | INTEGER, | when the request arrived, ms since the epoch |
request_ | TEXT, | the response id |
model | TEXT, | the model name the client sent |
client_ | TEXT, | k: and 12 hex digits of the bearer token’s sha256, or anon |
client_ | TEXT, | read from the User-Agent: open-webui, openai-sdk, opencode, hermes, curl, dashboard, other |
endpoint | TEXT, | the route |
stream | INTEGER, | 1 for a streamed request |
status | INTEGER, | the HTTP status |
finish_ | TEXT, | stop, length, tool_calls, timeout, error, abandoned |
prompt_ | INTEGER, | prompt tokens as the client sent them |
cached_ | INTEGER, | prompt tokens served from a cache |
completion_ | INTEGER, | tokens streamed to the client |
reasoning_ | INTEGER, | visible reasoning tokens |
queue_ | REAL, | time waiting for the engine |
prompt_ | REAL, | prefill time |
ttft_ | REAL, | time to the first token |
decode_ | REAL, | time after the first token |
total_ | REAL, | arrival to the end |
decode_ | REAL, | (completion tokens - 1) / decode time; null for a replay |
prefill_ | REAL, | forwarded prompt tokens a second |
blocks | INTEGER, | verify rounds |
draft_ | INTEGER, | tokens the drafters proposed |
draft_ | INTEGER, | drafted tokens the model agreed with |
tool_ | INTEGER, | tool calls in the answer |
thinking | INTEGER, | 1 when the model thought |
cache_ | TEXT, | where the prompt resumed: resident, end, store, disk, spill, continuation, prepared, response, none |
max_ | INTEGER, | the output limit |
error_ | TEXT, | the exception class or the refusal |
engine_ | TEXT, | the VERSION file |
code_ | TEXT, | a hash over the code folders |
model_ | TEXT, | TandemLLM with the conductor on |
profile | TEXT, | the conductor profile |
profile_ | INTEGER, | its version |
conductor_ | TEXT, | raw, shadow or on |
conductor_ | INTEGER, | the session monitor’s executed acts |
conductor_ | INTEGER, | tokens the model saw that the client did not send |
conductor_ | INTEGER, | 1 when the conductor changed what the model saw or wrote |
lane | TEXT, | decode, prefill or self for a side-lane request |
backend | TEXT, | the backend id of a relayed request |
backend_ | TEXT, | its kind |
backend_ | TEXT, | the id sent upstream |
public_ | TEXT, | the id the client sent |
usage_ | TEXT, | backend or none |
cache_ | INTEGER, | tokens a provider charged as a cache write |
cost_ | INTEGER, | cost in micro-dollars at the model’s price |
proxy_ | REAL, | the proxy’s own time |
retries | INTEGER, | tries again before the first byte |
fallback_ | TEXT, | the backend tried first |
upstream_ | INTEGER, | the backend’s HTTP status |
user | TEXT, | the label of the proxy key |
preemptions | INTEGER, | preemptions (vLLM) or retractions (SGLang), from the plugin |
engine_ | TEXT, | the backend engine’s own finish reason |
corrupted | INTEGER, | vLLM’s NaN check |
l2_ | TEXT, | exact or approximate join of the plugin’s numbers |
wait_ | REAL, | ms the request waited behind the engine’s own work (the shadow reader, a summary written ahead, the conductor’s question) |
parent_ | TEXT, | the turn an internal row served (the router’s question names the turn it routes) |
slot | INTEGER, | the engine’s sequence slot with --parallel N (0 the main slot, 1 and up a side slot); null without |
engine_ | INTEGER, | the prompt tokens the engine read from a cache, the hidden notes included (cached_tokens counts the client’s among them) |
forwarded_ | INTEGER, | what the prefill forwarded, hidden tokens included |
wait_ | REAL, | ms the parallel memory admission held the request (inside queue_ms) |
sampling | TEXT, | how the request sampled: null greedy, client (its own temperature), regenerate (TandemLLM sampled a regenerate) |
The conductor’s journal shares the file: conductor_events has 53 columns (id, ts_, request_, session_, kind, task, tier, mode, sp, rule, trigger, score, decision, action, cap, tier1_, question_, probabilities, tier1_, hidden_, hidden_, tokens_, position, outcome, next_, policy_, client_, model, profile, profile_, router_, class_, confidence, source, kept, stages, classifier_, effort, budget, budget_, thinking, decisions, injected, replayed, shadow, digest, label, label_, labelled_, acted, changed, detail, extra), and conductor_texts keeps every hidden text once, by its hash.
A row’s speed figures follow the benchmark row’s formula, and a replay or a side-lane request has no decode rate. A relayed request has null queue, prefill, blocks, draft counts and cache source unless a plugin reported them, in which case l2_join says how the row was joined. A quick look:
sqlite3 ~/.qwen38-spark-engine/usage/ledger.sqlite3 \
"select datetime(ts_ms/1000,'unixepoch','localtime'), finish_reason, prompt_tokens,
completion_tokens, round(decode_tps,1), cache_source from requests order by id desc limit 10"