Metadata-Version: 2.4
Name: llm-token-ledger
Version: 0.1.0
Summary: Per-query token ledger for LLM pipelines: record what the API reports, answer with one GROUP BY. Zero dependencies, dashboard included.
Project-URL: Homepage, https://github.com/vinimabreu/token-ledger
Project-URL: Issues, https://github.com/vinimabreu/token-ledger/issues
Author: Vinicius Pereira
License: MIT
License-File: LICENSE
Keywords: anthropic,cost,gemini,llm,observability,openai,tokens
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# token-ledger

<p>
<img src="https://img.shields.io/badge/license-MIT-6E56CF?style=flat-square" alt="MIT license">
<img src="https://img.shields.io/badge/python-3.10%2B-6E56CF?style=flat-square&logo=python&logoColor=white" alt="Python 3.10+">
<img src="https://img.shields.io/badge/tests-32%20passing-6E56CF?style=flat-square&logo=pytest&logoColor=white" alt="32 tests passing">
<img src="https://img.shields.io/badge/dependencies-zero-6E56CF?style=flat-square" alt="zero dependencies">
<img src="https://github.com/vinimabreu/token-ledger/actions/workflows/ci.yml/badge.svg" alt="CI">
</p>

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.

<img src="https://raw.githubusercontent.com/vinimabreu/token-ledger/main/assets/dashboard.png" width="100%" alt="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

```bash
pip install token-ledger
```

## Record

```python
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:

```python
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

```bash
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`:

```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.](https://dev.to/vinimabreu/your-llm-bill-has-two-sides-build-the-ledger-that-shows-both-p54)

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