Skip to content

vinimabreu/token-ledger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

token-ledger

MIT license Python 3.10+ 32 tests passing zero dependencies CI

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.

token-ledger dashboard: the 9-call agent loop query costing 10x the median, an unpriced local model honestly flagged

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 llm-token-ledger

The distribution is llm-token-ledger (PyPI would not take the shorter name); you import it as 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_index derived 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, two Ledger instances 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 uses ttl:"1h" uniformly, override cache_write to 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.

About

Per-query token ledger for LLM pipelines: record what the API reports, answer with one GROUP BY. Zero dependencies, dashboard included.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages