Skip to main content

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

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llm_token_ledger-0.1.0.tar.gz (173.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llm_token_ledger-0.1.0-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

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

Hashes for llm_token_ledger-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7a191d51638ddfb905f4620c04031e4dd3aa3e4882f22a7381f118dd2d2d25d2
MD5 aa5c924e19c976b57a50f0ec2bfa33e6
BLAKE2b-256 fe052232624e3b46ef95cf53bdbc05809637f2d9e0b46984b38e9fd4ed0d1e13

See more details on using hashes here.

File details

Details for the file llm_token_ledger-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for llm_token_ledger-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f34728f6fd08aad8fbfd2fc243da9e44b1a1e4362f3146e7b5c26ac619f0bbed
MD5 81050aa55574628384a08dd51b71672b
BLAKE2b-256 1024c8e5edebd43b9943bb19b6fd2b8506702f4f972d9bc96ab89b63510c2791

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page