Metadata-Version: 2.5
Name: tracecost
Version: 0.1.0
Summary: Recompute what your LLM traces cost, offline, from OTLP, Langfuse or Phoenix exports, with cache and reasoning tokens priced, and see where your observability tool's totals disagree.
Project-URL: Homepage, https://github.com/thanatchon36/tracecost
Project-URL: Issues, https://github.com/thanatchon36/tracecost/issues
Project-URL: Changelog, https://github.com/thanatchon36/tracecost/blob/main/CHANGELOG.md
Author: Thanatchon
License-Expression: MIT
License-File: LICENSE
Keywords: cost,genai,langfuse,llm,observability,openinference,opentelemetry,otlp,phoenix,tokens
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: typer>=0.27.3
Description-Content-Type: text/markdown

# tracecost

[![ci](https://github.com/thanatchon36/tracecost/actions/workflows/ci.yml/badge.svg)](https://github.com/thanatchon36/tracecost/actions/workflows/ci.yml)
[![coverage](https://codecov.io/gh/thanatchon36/tracecost/graph/badge.svg)](https://codecov.io/gh/thanatchon36/tracecost)
[![PyPI](https://img.shields.io/pypi/v/tracecost)](https://pypi.org/project/tracecost/)
[![Python](https://img.shields.io/pypi/pyversions/tracecost)](https://pypi.org/project/tracecost/)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/thanatchon36/tracecost/blob/main/LICENSE)

Recompute what your LLM traces cost, offline, from an OpenTelemetry, Langfuse or Phoenix
export, with cache and reasoning tokens priced, and see where your observability tool's
totals disagree.

## The problem

> "the **Cost ($)** column shows the observation-level cost of the root span, which is always
> **$0.00** since spans cannot carry usage"
> — [langfuse#16077](https://github.com/langfuse/langfuse/issues/16077), still open

The dashboards that show you LLM spend compute it once, at ingest, from a price table you don't
control. Root spans show $0.00 because only the child calls carry usage
([langfuse#16077](https://github.com/langfuse/langfuse/issues/16077)); the per-user total sums
only the top 20 users ([#18210](https://github.com/langfuse/langfuse/issues/18210)); the OpenAI
priority tier is billed at the default rate
([#13780](https://github.com/langfuse/langfuse/issues/13780)); cache tokens get dropped on the
way in ([mlflow#22745](https://github.com/mlflow/mlflow/issues/22745)) or never reported
([ollama#15758](https://github.com/ollama/ollama/issues/15758)); and a cost you computed
yourself is re-derived and overwritten
([phoenix#13655](https://github.com/Arize-ai/phoenix/issues/13655)). When the invoice arrives,
nothing lets you re-price the traces and find the gap.

## Demo

![tracecost pricing a six-turn support-agent export by trace and by user](https://raw.githubusercontent.com/thanatchon36/tracecost/main/docs/demo.gif)

```console
$ tracecost cost examples/demo/traces.jsonl --by trace      # in a clone of this repository
tracecost 0.1.0 · 12 generations · 6 traces · prices 2026-10-13 (bundled)

by trace                              calls       input  cache-read    output        cost
support-turn …f4ae25                      2      35,200       6,000     1,900     $0.1060
support-turn …f4ab6e                      2      33,800      24,000     1,900     $0.0675
support-turn …f4a8b7                      2      32,400      24,000     1,900     $0.0515
support-turn …f4a600                      2      31,000      24,000     1,900     $0.0480
support-turn …f4b393                      2      38,000      24,000     1,900     $0.0265
support-turn …f4b0dc                      2      36,600      24,000     1,900     $0.0135
total                                    12     207,000     126,000    11,400     $0.3131

⚠ 1 generation on model 'acme-support-7b' has no price (counted as $0). Add it with --prices.
⚠ 1 generation from anthropic reports no cache attributes although the model bills cached tokens; zero cache may mean 'not reported' (ollama#15758). Verify the exporter.
⚠ 1 generation uses a supplied cost [vendor_invoice (1)] totalling $0.0125; the token table says $0.0515. Use --recompute to price from tokens.
```

Six conversation turns of a support agent: each root `invoke_agent` span has a `gpt-5` planner
call (reasoning tokens; one turn on the priority tier), a tool call, and a `claude-sonnet-4-5`
answer with prompt caching. The warnings are the point: one turn ran on a self-hosted model the
table doesn't know, one exporter dropped the cache attributes, and one call carries a cost from
a vendor invoice that is a quarter of what the token table says. The data is synthetic and
generated by `examples/demo/make_demo.py`.

## Install

```bash
uvx tracecost cost traces.jsonl     # run without installing
pip install tracecost
```

Python 3.11+. No network access at runtime, no API keys, no telemetry.

## Features

- **Three input formats, one model.** OTLP JSON/JSONL with OpenTelemetry GenAI (`gen_ai.*`) or
  OpenInference (`llm.*`) attributes, Phoenix `get_spans_dataframe` exports, and Langfuse
  `observations` + `traces` API responses load into the same `Generation` record.
- **Cache and reasoning tokens priced correctly.** Cache read, cache write and reasoning tokens
  get their own rates; Langfuse's exclusive buckets are converted to OpenTelemetry's inclusive
  counts so nothing is double-counted ([ADR-0004](docs/decisions/ADR-0004-inclusive-token-semantics.md)).
- **A dated price table you can see and override.** A bundled snapshot of
  [pydantic/genai-prices](https://github.com/pydantic/genai-prices) with tiers, dated price
  changes (selected by each span's start time) and OpenAI service tiers; `--prices` adds your
  own models; `tracecost prices gpt-5` shows exactly what applied.
- **Rollups with no cutoff.** `--by root|trace|session|user|model`; root spans get the sum of
  everything beneath them, however deep.
- **Supplied costs honoured, then audited.** `llm.cost.total`, `cost.amount`/`cost.source` and
  Langfuse `costDetails` win over the table and are labelled; `--recompute` shows the gap.
- **Diff two runs.** `tracecost diff before.jsonl after.jsonl` matches root spans by name and
  shows what moved in calls, tokens, cost and model mix; `--fail-over` turns it into a CI gate.

## Compared with

| | tracecost | [Langfuse](https://github.com/langfuse/langfuse) / [Phoenix](https://github.com/Arize-ai/phoenix) | [genai-prices](https://github.com/pydantic/genai-prices) | [tokencost](https://github.com/AgentOps-AI/tokencost) | OTel Collector [`gen_ai_normalizer`](https://opentelemetry.io/docs/collector/components/processor/) |
|---|---|---|---|---|---|
| Reads exported trace files | OTLP, Phoenix, Langfuse | their own store | no (a price library) | no | no (live pipeline) |
| Cross-dialect (`gen_ai.*`, `llm.*`, Langfuse) | yes | each ingests its own + some conversion | n/a | n/a | OpenInference/OpenLLMetry → `gen_ai.*` |
| Cache read/write, reasoning, service tiers | yes | partly, varies by version | yes (data source) | no cache tiers | n/a |
| Re-price history after a price change | yes, offline | no, computed at ingest | yes (library) | yes | no |
| Supplied cost honoured and compared | yes | Langfuse yes, Phoenix re-derives | n/a | n/a | n/a |
| Rollups: root, trace, session, user, model | yes | yes, with the bugs cited above | no | no | no |
| Diff two runs | yes | no | no | no | no |
| Dashboards, traces UI, evals, alerts | **no** | **yes** | no | no | no |
| Live prices | no (dated snapshot) | yes | yes | stale since 2025-09 | n/a |

Langfuse and Phoenix are far more than this tool; use them to look at traces. tracecost is the
offline second opinion on their numbers. genai-prices is where tracecost's prices come from.

## How it works

```text
export files ──► readers ──► Generation records ──► prices ──► rollup / diff ──► table · JSON
```

1. **Read.** Each file is sniffed (`resourceSpans` → OTLP, `context.span_id` → Phoenix,
   `traceId` → Langfuse) and every span becomes a `Span`; spans that carry a model or token usage
   also become a `Generation`. Attribute names are tables, not code
   ([`readers/dialects.py`](src/tracecost/readers/dialects.py)).
2. **Price.** The model name is matched against the snapshot's match clauses (provider hint
   first), the rates in force at the span's start time are resolved (dated prices, tiers,
   service-tier variants), and `uncached_input × input + cache_read × cache_read + cache_write ×
   cache_write + output × output + reasoning × (reasoning or output)` is computed per generation.
   A supplied cost wins unless `--recompute`; the table figure is kept beside it.
3. **Roll up.** Every span's root is found by walking parent ids within the file; generations
   are summed per root, trace, session, user and model, and tool spans are counted per root.
4. **Report.** A fixed-width table with aggregated warnings, or JSON with one row per generation
   for DuckDB and pandas.

Details: [docs/concepts.md](docs/concepts.md) · [docs/api.md](docs/api.md) ·
[docs/quickstart.md](docs/quickstart.md) · [docs/faq.md](docs/faq.md).

## Limitations

- **Prices are a snapshot.** The header prints its date. Regenerate with
  `scripts/update_prices.py` or override with `--prices`; a provider's surprise price change is
  not known until then.
- **JSON-encoded OTLP only.** Protobuf-binary and gzip exports are on the roadmap.
- **Files, not servers.** v0.1 reads what you export; it doesn't call the Langfuse or Phoenix
  API.
- **Token semantics follow the OTel registry.** If an exporter writes `gen_ai.usage.input_tokens`
  *excluding* cache tokens (Anthropic's raw `input_tokens`), cache will be under-priced; compare
  the per-generation JSON with your raw usage once. See [docs/faq.md](docs/faq.md).
- **Non-token costs on tool spans** (OCR, GPU time on a retriever) are read only when attached
  to an LLM span; generic span costs are planned.
- **Estimates, not invoices.** Provider rounding, minimum charges, batch discounts and free
  tiers are not modelled.

## Roadmap

- Read directly from the Langfuse public API and a Phoenix server (paginated), so the audit
  needs no manual export.
- Protobuf-binary and gzip OTLP input.
- `reconcile`: compare totals with a provider's usage/billing CSV for the same period.
- Cost attributes on tool and retriever spans in the same rollups.
- A GitHub Action wrapping `--fail-over` for recorded eval runs.

## Contributing

Bug reports with a small export that gives a wrong number are the most useful thing you can
send. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup (`uv sync`, `uv run pytest`), how to add
an input format, and how to refresh the price snapshot.

## License

[MIT](LICENSE). The bundled price data is from
[pydantic/genai-prices](https://github.com/pydantic/genai-prices), also MIT.
