Contents TandemLLM Docs
v0.12.0
Star58

Part VI, Reference

32Metrics and the ledger

32.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:

text
- 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.

Table 32.1 Prometheus families registered in the code
MetricType, labelsWhat it counts
qse_requests_totalcounter, finish_reasongenerations that finished, by the reason they finished
qse_request_success_totalcountergenerations that finished on the model’s own stop token or the caller’s token budget
qse_prompt_tokens_totalcounterprompt tokens accepted, including tokens served from a cache
qse_generation_tokens_totalcountertokens written by the engine
qse_errors_totalcounter, typeexceptions that ended a generation, by exception class
qse_images_left_out_totalcountera conversation’s earlier images left out of a request over the image caps (server/images.py)
qse_time_to_first_token_secondshistogramarrival of the request to its first token, queue wait included
qse_time_per_output_token_secondshistogramgap between two tokens of one generation.
qse_e2e_request_latency_secondshistogramarrival of the request to the end of its stream, queue wait included
qse_spec_decode_num_accepted_tokens_totalcounterdrafted 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_spec_decode_num_draft_tokens_totalcountertokens proposed by the drafter, anchor excluded
qse_spec_decode_num_drafts_totalcounterblocks the drafter proposed
qse_spec_accept_per_blockhistogramtokens committed by one verified block, the target’s own token included
qse_draft_width_chosen_totalcounter, widthblocks proposed at each width, anchor included.
qse_verify_secondshistogramone verify pass over a draft block, as the decode loop paid for it
qse_draft_secondshistogramone call to the drafter, lookup and block drafter together
qse_conductor_decisions_totalcounter, task, tier, mode, decisionconductor decisions, passes included: the monitor’s tasks and the router’s class (task route)
qse_conductor_actions_totalcounter, task, actiondecisions the conductor executed, by task and action
qse_conductor_hidden_tokens_totalcountertokens the model saw that the client did not send or receive (hidden closes, hidden turns, reminders, router notes new and replayed)
qse_conductor_gave_up_totalcounterturns the conductor ended with its GIVE_UP sentence
qse_conductor_tier1_secondshistogramone tier 1 question (a classifier stage: the decider or Qwen), answered or not
qse_conductor_tier1_failures_totalcounter, reasontier 1 questions without a usable answer, by reason
qse_conductor_reasoning_tokens_cut_totalcounterreasoning tokens written before a hidden close cut the block (the size of the blocks the conductor closed)
qse_requests_runninggaugegenerations the engine is working on
qse_requests_waitinggaugerequests holding a socket and waiting for the engine
qse_requests_refused_totalcounter, reasonrequests turned away with a Retry-After before the engine saw them
qse_cache_misses_totalcounter, cacheprefix lookups that found nothing
qse_cache_evictions_totalcounter, cacheentries dropped to stay inside the byte budget
qse_cache_bytesgauge, cachebytes held by each cache
qse_memory_bytesgauge, partthe engine’s memory against its limits, by part
qse_memory_shed_totalcounter, levelcaches given back to keep the engine under its ceiling or its peak, by level
qse_cache_entriesgauge, cacheentries held by each cache
qse_disk_cache_bytesgauge, partthe 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_disk_cache_entriesgauge, kindthe disk tier’s files: anchors (one state each) and chunks (1,024 tokens of rows each)
qse_disk_cache_lookups_totalcounterprefills the disk tier could have shortened by a chunk or more
qse_disk_cache_hits_totalcounterprefills that started from the disk tier
qse_disk_cache_misses_totalcounterlookups the disk tier did not answer
qse_disk_cache_tokens_restored_totalcounterprompt tokens read back from the disk instead of prefilled
qse_disk_cache_read_bytes_totalcounterbytes a restore read
qse_disk_cache_written_bytes_totalcounterbytes the writer wrote
qse_disk_cache_writes_totalcounter, kindfiles the writer wrote, by kind
qse_disk_cache_evictions_totalcounter, reasonentries the disk tier gave up, by reason (budget, floor, ttl, orphan)
qse_disk_cache_dropped_totalcounter, reasonwrites not done, by reason (window: the next prefill came first; budget, floor, staging, aborted, too_big)
qse_disk_cache_failed_restores_totalcounterrestores that failed a check (checksum, layout) and took the cold path
qse_disk_cache_last_restore_secondsgaugethe last restore’s wall time
qse_cache_collisions_totalcounter, cachestored prefixes whose 64-bit hash matched and whose tokens did not
qse_prefill_tokens_reused_totalcounterprompt tokens restored from a snapshot instead of forwarded
qse_prefill_tokens_forwarded_totalcounterprompt tokens run through the 64 layers
qse_gpu_memory_used_bytesgaugeallocator bytes held by the engine
qse_gpu_memory_reserved_bytesgaugebytes the allocator has taken from the driver and not given back
qse_unified_memory_free_bytesgaugefree bytes in the board’s one memory pool
qse_engine_uptime_secondsgaugeseconds since the server started
qse_engine_infogauge, version, model, drafter, width, tree, nvfp4, fp8_head, max_len, cachesalways 1. The labels are the configuration the numbers above were produced by
qse_http_requests_totalcounter, route, codeHTTP responses by route (a fixed set) and status code
qse_prompt_tokens_cached_totalcounterprompt tokens restored from the state store (all of them on a response-cache replay)
qse_reasoning_tokens_totalcountercommitted tokens inside the reasoning block
qse_suffix_store_drafts_totalcounter, outcomeproposed blocks for which the persistent suffix store matched the context (hit) or did not (miss); counted once a block, never once a lookup
qse_request_queue_secondshistogramarrival to the engine lock, per request
qse_request_prefill_secondshistogramthe engine lock to the first token (template, tokenise, prefill), per request
qse_request_decode_tokens_per_secondhistogramper decoded request: (completion tokens - 1) / (last token - first token), the atlas row’s convention, and the predicted_per_second a response carries
qse_request_prefill_tokens_per_secondhistogramper request with a forwarded prompt: forwarded tokens / prefill seconds
qse_request_prompt_tokenshistogramprompt tokens per request, cached ones included
qse_request_completion_tokenshistogramcompletion tokens per request
qse_suffix_store_tokensgaugetoken ids in the persistent suffix store
qse_state_store_budget_bytesgaugethe state cache’s byte budget
qse_process_resident_memory_bytesgaugeresident set of the server process
qse_engine_start_time_secondsgaugewhen the server started, unix seconds
qse_log_subscribersgaugeopen /v1/dashboard/logs streams
qse_usage_ledger_rows_totalcounterrows the usage ledger has written
qse_usage_ledger_dropped_totalcounterrows the usage ledger lost: a full queue or a failed write
qse_build_infogaugealways 1. version, the git commit, the code hash row3 records (tools/row3.py::code_hash) and a hash of the effective flags
qse_proxy_requests_totalcounter, backend, kind, finish_reasonrequests relayed to a backend, by its id, its kind and the finish reason
qse_proxy_tokens_totalcounter, backend, typetokens a backend reported in usage, by type (prompt, cached, completion, reasoning)
qse_proxy_cost_micros_totalcounter, backendcost at the model’s price, micro-dollars
qse_upstream_retries_totalcounter, backend, reasontries again before any byte reached the client, by backend and reason (429, 502, 503, connect)
qse_proxy_overhead_mshistogramthe proxy’s own time per request: arrival to the upstream send, plus the first frame’s delay through the proxy
qse_proxy_ttft_secondshistogramarrival at the proxy to the first token frame (the network and the backend’s queue inside)
qse_backend_upgauge, backend1 when the backend’s last health check passed
qse_backend_inflightgauge, backendrequests relayed to the backend now
qse_backend_scrape_age_secondsgauge, backendseconds since the backend’s own /metrics page was last read (vLLM, SGLang)
qse_backend_plugin_age_secondsgauge, backendseconds since the plugin inside the backend’s engine last posted (vLLM, SGLang with tandemllm-*-plugin)
qse_backend_plugin_joinsgauge, backend, joinrequests 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)

32.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.

Table 32.2 The usage ledger’s requests table (schema version 10)
ColumnType, sinceWhat it holds
ts_msINTEGER, v1when the request arrived, ms since the epoch
request_idTEXT, v1the response id
modelTEXT, v1the model name the client sent
client_idTEXT, v1k: and 12 hex digits of the bearer token’s sha256, or anon
client_kindTEXT, v1read from the User-Agent: open-webui, openai-sdk, opencode, hermes, curl, dashboard, other
endpointTEXT, v1the route
streamINTEGER, v11 for a streamed request
statusINTEGER, v1the HTTP status
finish_reasonTEXT, v1stop, length, tool_calls, timeout, error, abandoned
prompt_tokensINTEGER, v1prompt tokens as the client sent them
cached_tokensINTEGER, v1prompt tokens served from a cache
completion_tokensINTEGER, v1tokens streamed to the client
reasoning_tokensINTEGER, v1visible reasoning tokens
queue_msREAL, v1time waiting for the engine
prompt_msREAL, v1prefill time
ttft_msREAL, v1time to the first token
decode_msREAL, v1time after the first token
total_msREAL, v1arrival to the end
decode_tpsREAL, v1(completion tokens - 1) / decode time; null for a replay
prefill_tpsREAL, v1forwarded prompt tokens a second
blocksINTEGER, v1verify rounds
draft_tokensINTEGER, v1tokens the drafters proposed
draft_acceptedINTEGER, v1drafted tokens the model agreed with
tool_callsINTEGER, v1tool calls in the answer
thinkingINTEGER, v11 when the model thought
cache_sourceTEXT, v1where the prompt resumed: resident, end, store, disk, spill, continuation, prepared, response, none
max_tokensINTEGER, v1the output limit
error_typeTEXT, v1the exception class or the refusal
engine_versionTEXT, v1the VERSION file
code_shaTEXT, v1a hash over the code folders
model_servedTEXT, v2TandemLLM with the conductor on
profileTEXT, v2the conductor profile
profile_versionINTEGER, v2its version
conductor_modeTEXT, v2raw, shadow or on
conductor_actsINTEGER, v2the session monitor’s executed acts
conductor_hidden_tokensINTEGER, v2tokens the model saw that the client did not send
conductor_actedINTEGER, v21 when the conductor changed what the model saw or wrote
laneTEXT, v2decode, prefill or self for a side-lane request
backendTEXT, v3the backend id of a relayed request
backend_kindTEXT, v3its kind
backend_modelTEXT, v3the id sent upstream
public_modelTEXT, v3the id the client sent
usage_sourceTEXT, v3backend or none
cache_write_tokensINTEGER, v3tokens a provider charged as a cache write
cost_microsINTEGER, v3cost in micro-dollars at the model’s price
proxy_msREAL, v3the proxy’s own time
retriesINTEGER, v3tries again before the first byte
fallback_fromTEXT, v3the backend tried first
upstream_statusINTEGER, v3the backend’s HTTP status
userTEXT, v3the label of the proxy key
preemptionsINTEGER, v4preemptions (vLLM) or retractions (SGLang), from the plugin
engine_finishTEXT, v4the backend engine’s own finish reason
corruptedINTEGER, v4vLLM’s NaN check
l2_joinTEXT, v4exact or approximate join of the plugin’s numbers
wait_own_msREAL, v5ms the request waited behind the engine’s own work (the shadow reader, a summary written ahead, the conductor’s question)
parent_requestTEXT, v6the turn an internal row served (the router’s question names the turn it routes)
slotINTEGER, v7the engine’s sequence slot with --parallel N (0 the main slot, 1 and up a side slot); null without
engine_cachedINTEGER, v8the prompt tokens the engine read from a cache, the hidden notes included (cached_tokens counts the client’s among them)
forwarded_tokensINTEGER, v8what the prefill forwarded, hidden tokens included
wait_mem_msREAL, v8ms the parallel memory admission held the request (inside queue_ms)
samplingTEXT, v9how the request sampled: null greedy, client (its own temperature), regenerate (TandemLLM sampled a regenerate)
system_fingerprintTEXT, v9

The conductor’s journal shares the file: conductor_events has 53 columns (id, ts_ms, request_id, session_key, kind, task, tier, mode, sp, rule, trigger, score, decision, action, cap, tier1_model, question_id, probabilities, tier1_ms, hidden_text_id, hidden_tokens, tokens_at_cut, position, outcome, next_checker, policy_version, client_kind, model, profile, profile_version, router_version, class_id, confidence, source, kept, stages, classifier_ms, effort, budget, budget_source, thinking, decisions, injected, replayed, shadow, digest, label, label_note, labelled_at, 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:

shell
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"