A ledger for the part of the bill you cannot see.
Providers tell you what you spent this month. They rarely tell you which feature, model, or experiment spent it. burn sits between your SDK and the provider and writes one local row per request, so the answer is a query instead of an estimate.
Cost visibility should not require a platform.
A provider dashboard shows a monthly total. That is enough to notice a bill and not enough to explain it. The useful question is smaller: what did this feature, this model, or this evaluation run cost?
burn makes the answer local. Your SDK talks to a proxy on your machine. The proxy forwards the request unchanged, reads the usage that the provider already sends back, prices it, and writes a row. There is no hosted service, no account, and no new place for prompts to accumulate.
It is deliberately a proxy and not an SDK wrapper. If your code can change a base URL, it can be measured. The same setup works for OpenAI, Anthropic, and anything that speaks one of those formats.
Request in. Metadata out.
The request body is forwarded, not copied into the database. The response stream is passed through as it arrives; usage parsing happens on the side.
Forward
The proxy resolves the first path segment to a provider URL, strips hop-by-hop headers and x-burn-tag, and forwards the request. It does not rewrite your payload, except to add stream_options.include_usage for OpenAI chat streams when usage was not requested.
Read
A JSON response is parsed after it is forwarded. An SSE stream is parsed as chunks pass through. If no usage is present, no row is written. burn never estimates tokens from text.
Price
The model is matched against a provider-scoped price list. A matching override wins. Cache and reasoning tokens use their own rates when available. An unknown model is stored with a null cost.
Six routes. Two formats. Room for more.
A provider is a URL and a format. The built-ins cover the common APIs; burn provider add covers a private gateway, a local model server, or another hosted provider.
| Provider | Local route | Upstream URL | Format |
|---|---|---|---|
| openai | /openai | https://api.openai.com | openai |
| anthropic | /anthropic | https://api.anthropic.com | anthropic |
| openrouter | /openrouter | https://openrouter.ai/api | openai |
| deepseek | /deepseek | https://api.deepseek.com | openai |
| groq | /groq | https://api.groq.com/openai | openai |
| gemini | /gemini | https://generativelanguage.googleapis.com/v1beta/openai | openai |
| custom | /<name> | anything http(s) | openai or anthropic |
Routes are path prefixes. The rest of the path and the query string are preserved when forwarding.
Exactly what is in the row.
One SQLite table, one row per request where usage was reported. The schema is in the source, and this is the complete list.
Never stored
- Prompt text
- Response text
- Request bodies
- Response bodies
- API keys or authorization headers
- Cookies
- Client IP addresses
- User identifiers
| Column | Type | Source |
|---|---|---|
| ts | integer | Request start time, epoch milliseconds |
| provider | text | The route prefix, such as openai or anthropic |
| model | text | Reported by the provider response, or the request body |
| tag | text | Optional X-Burn-Tag header, stripped before forwarding |
| input_tokens | integer | Input tokens excluding cache reads |
| output_tokens | integer | Output tokens |
| cache_read_tokens | integer | Tokens served from a provider cache |
| cache_write_tokens | integer | Tokens written to a provider cache |
| reasoning_tokens | integer | Reasoning tokens when the provider reports them |
| cost | real, nullable | Computed at request time. Null when no price is known |
| latency_ms | integer | Time to upstream response headers |
| status | integer | Upstream HTTP status |
| stream | integer | 1 when the response was an SSE stream |
A public list, a local cache, no guesses.
Full pricing guide- Source
- The CloudPrice LiteLLM-compatible map, filtered to chat models and stored as USD per 1 million tokens.
- Cache
- prices.json is reused for 24 hours. A stale copy triggers a background refresh, so a proxied request is never delayed by the download. A failed refresh leaves the cached copy in place.
- Fallback
- About 30 common models are bundled for a first run or an offline machine. The full price list is not bundled with the package.
- Match
- Provider-scoped exact key, exact model, suffix-stripped model, then longest prefix. A direct OpenAI request is never priced from a Bedrock or Azure key.
- Cache tokens
- Cache reads and writes use explicit rates when the list has them. Otherwise burn applies the OpenAI or Claude fallback multipliers. Reasoning tokens use their own rate when available.
- Unknown models
- Stored with a null cost and counted as unpriced. burn never substitutes a similar model. Update the list and run recalc, or set an override.
Local by default, and honest about it.
burn is a development tool, not a gateway. The security model follows from that.
- The proxy and dashboard bind to 127.0.0.1. They are not reachable from another machine unless you deliberately change that.
- There is no authentication on either port. Anyone who can reach the port can use your upstream credentials or read the ledger. Keep it local.
- Prompt and response bodies are never written to the database. The schema has no column for them, and the code does not log them.
- The x-burn-tag header is stored truncated to 200 characters and stripped before the request is forwarded.
- burn does not add telemetry, identifiers, or usage uploads. The only outbound call it makes itself is the price list download.
- BURN_OFFLINE=1 disables the price download. The proxy still works from the cached or bundled list.
- API keys and authorization headers are forwarded upstream, not stored. The database holds neither.
What burn cannot see.
- Only traffic routed through the proxy. Direct provider calls are invisible.
- Only requests where the provider reported usage. Errors, blocked calls, and cancelled streams are not logged.
- Only usage shapes burn knows: OpenAI chat completions, the OpenAI responses API, and Anthropic messages, as JSON or SSE. Unknown shapes are forwarded but not logged.
- Costs are computed when the request finishes. A later price change does not rewrite history; recalc only fills rows that had no cost.
- The price list can lag a provider change, and it cannot model every billing rule. Treat the number as an estimate.
- Budgets compare against proxied traffic in local time. They are not a provider-side cap.
Tracked in the open.
There is no formal roadmap document. Feature requests, bugs, and design discussion live in GitHub issues, and the changelog records what actually shipped.
If you need a provider, a report, or a pricing rule that is missing, open an issue with the concrete case. A small tool stays useful by staying small.