Per-query token ledger for LLM pipelines: record what the API reports, answer with one GROUP BY. Zero dependencies, dashboard included.
Project description
token-ledger
The bill for an LLM system is set by total input and output tokens, summed across every call a query triggers. Not per call. Not input only. Your dashboard shows chunk math; your invoice shows the truth.
token-ledger records what the provider's API already reports, per call, tied together per user query, and answers with one GROUP BY. Zero dependencies, including the dashboard.
That screenshot is the pitch: the q_agent_loop_gone_wild row is an agent
loop budgeted for one turn that took nine. No chunk tuning finds that. The
ledger finds it in the first GROUP BY.
Install
pip install token-ledger
Record
from token_ledger import Ledger, Usage, usage_from_anthropic
ledger = Ledger("tokens.db")
with ledger.query(): # one scope per user request
resp = client.messages.create(...) # your call, unchanged
ledger.record(
model=resp.model,
usage=usage_from_anthropic(resp),
stop_reason=resp.stop_reason,
stage="answer",
)
Extractors are duck-typed and import no SDKs: usage_from_anthropic,
usage_from_openai (both Chat Completions and Responses API shapes),
usage_from_gemini. For Chat Completions streams:
from token_ledger import usage_from_openai_stream
stream = client.chat.completions.create(
..., stream=True, stream_options={"include_usage": True})
tap = usage_from_openai_stream(stream)
for chunk in tap:
... # your streaming loop, unchanged
if tap.usage: # None if include_usage was omitted
ledger.record(model, tap.usage, stage="answer")
Forget include_usage and tap.usage is None, because the provider sent no
usage at all: guard the call as above. record(None) raises rather than
logging a silent hole in the bill. For Anthropic streams,
stream.get_final_message() carries complete usage; pass it to
usage_from_anthropic as usual.
Answer
token-ledger report tokens.db # terminal rollup
token-ledger serve tokens.db # dashboard on http://127.0.0.1:8642
Costs are computed from list prices for current Claude and GPT-5 model
families. Models the price book does not know are reported as unpriced,
never silently costed at zero; add your own rates with
--prices your-prices.json:
{"qwen3-local": {"input": 0.0, "output": 0.0},
"my-fine-tune": {"input": 4.0, "output": 16.0, "cache_read": 0.4}}
The details that bite
These are the reasons this is a package and not a gist:
- Disjoint buckets by construction. OpenAI and Gemini count cached tokens inside their prompt count; Anthropic reports cache reads and writes as separate fields. The extractors normalize all three into disjoint buckets (uncached input, cache read, cache write), so cost math never double-charges a cached token.
- Fail loud, not zero. An extractor handed an object with none of the
expected fields raises, instead of recording
Usage(0, 0, 0, 0). A token meter that silently logs a zero is worse than one that stops. - Thread- and process-safe. One SQLite connection per thread, WAL mode,
and
call_indexderived atomically inside the INSERT (a correlated subquery under an immediate transaction), so two writers in the same query never collide on it, whether they are threads, twoLedgerinstances on one file, or two processes. A concurrency stress test proves it. - Query scoping that survives nesting.
ledger.query()is a context manager over a contextvar: nested scopes restore the outer query, and async tasks inherit the scope contextvars already give you. - Honest when unscoped. Calls recorded outside any
query()scope share one generated query_id per context instead of fragmenting into one-call queries or silently merging into a global bucket.
What the first week shows you
Running this on real pipelines, the surprises arrive in a reliable order: first call count (the retry someone added in March, the agent loop that averages 2.4 turns), then scaffolding (the fixed prompt overhead riding on every call, multiplied by the call count you just discovered), and only third the chunk sizes everyone tunes first.
The long version of the argument is the companion post: Your LLM bill has two sides. Build the ledger that shows both.
Layout
token_ledger/
ledger.py the table, the contextvar, the locks
extract.py duck-typed usage extractors, no SDK imports
prices.py list prices, longest-prefix matching, JSON overrides
report.py rollups per query, stage, model
dashboard.py stdlib http.server, self-contained HTML, no CDN
cli.py token-ledger report | serve
tests/ 32 tests, including the thread-safety proof
Honest limitations
- SQLite is the right tool up to serious per-host volume, not for a fleet; point the ledger at one file per service and aggregate downstream if you need more.
- Prices are list prices as of 2026-07-07, which you control via JSON; batch discounts, committed use, and provider-side price changes are your override to make.
- Anthropic cache writes are priced at the 5-minute-TTL rate (1.25x base
input). The 1-hour TTL bills at 2x base, and the ledger reads only the
aggregate
cache_creation_input_tokens, so 1h-TTL writes are undercosted 1.6x. If your workload usesttl:"1h"uniformly, overridecache_writeto 2x base via--prices. - The ledger measures cost, not quality. Cutting tokens that were buying you accuracy is a different mistake, and it will not show up here.
MIT license.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llm_token_ledger-0.1.0.tar.gz.
File metadata
- Download URL: llm_token_ledger-0.1.0.tar.gz
- Upload date:
- Size: 173.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a191d51638ddfb905f4620c04031e4dd3aa3e4882f22a7381f118dd2d2d25d2
|
|
| MD5 |
aa5c924e19c976b57a50f0ec2bfa33e6
|
|
| BLAKE2b-256 |
fe052232624e3b46ef95cf53bdbc05809637f2d9e0b46984b38e9fd4ed0d1e13
|
File details
Details for the file llm_token_ledger-0.1.0-py3-none-any.whl.
File metadata
- Download URL: llm_token_ledger-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f34728f6fd08aad8fbfd2fc243da9e44b1a1e4362f3146e7b5c26ac619f0bbed
|
|
| MD5 |
81050aa55574628384a08dd51b71672b
|
|
| BLAKE2b-256 |
1024c8e5edebd43b9943bb19b6fd2b8506702f4f972d9bc96ab89b63510c2791
|