Estimating Costs¶
Once you have a Usage object, you can estimate what it cost against any model's
pricing.
from openai_usage import Usage
usage = Usage(requests=1, input_tokens=1000, output_tokens=2000, total_tokens=3000)
cost = usage.estimate_cost("gpt-4o")
print(f"${cost:.6f}")
Two return types¶
| Method | Returns | Use when |
|---|---|---|
estimate_cost(...) |
float |
You want a number to display or compare. |
estimate_cost_str(...) |
str |
You need exact decimal precision (it returns the raw Decimal as a string, with no float rounding). |
Choosing the model¶
The model can come from three places, in order of precedence:
- The argument:
usage.estimate_cost("anthropic/claude-3-haiku"). - The
Usage.modelfield, if the argument is omitted. - A
gpt-4o-minifallback if neither is set (a warning is logged).
usage.model = "gpt-4o"
usage.estimate_cost() # uses usage.model
usage.estimate_cost("o1") # argument wins over usage.model
Pricing data¶
Pricing comes from two catalogs, both fetched at runtime:
| Source | Covers |
|---|---|
| LiteLLM | ~3,000 models across every provider, including the embedding, image, audio and transcription models OpenRouter never proxies |
| OpenRouter | The chat models OpenRouter proxies, under vendor/model names |
A name is resolved in three steps:
- LiteLLM exact key. LiteLLM files models under their native ids
(
gpt-4o,whisper-1,claude-sonnet-4-5), so most names hit here immediately. - OpenRouter relaxed matching. Handles
vendor/modelnames, aliases, and separator differences (claude-haiku-4-5findsanthropic/claude-haiku-4.5). - LiteLLM relaxed matching, as a last resort.
You can also pass a fully resolved model object instead of a name.
Caching¶
Both catalogs are cached on disk so the download happens at most once a day.
| Setting | Default | Environment variable |
|---|---|---|
| Cache directory | ./.cache/openai-usage/ |
OPENAI_USAGE_CACHE_DIR |
| Time to live | 86400 (one day) |
OPENAI_USAGE_CACHE_TTL |
| Disable network | off | OPENAI_USAGE_OFFLINE |
Passing realtime_pricing=True ignores the TTL and re-downloads both catalogs:
When the network is unavailable¶
Nothing raises. Each step falls through to the next:
The bundled snapshot is an OpenRouter catalog shipped inside the package, so pricing keeps working offline for the models it covers. LiteLLM has no bundled snapshot; without a cache entry its lookups return nothing and resolution falls back to OpenRouter.
Set OPENAI_USAGE_OFFLINE=1 to skip the network entirely — useful in airgapped
environments and in tests. Connect and read timeouts are deliberately short, so
an unreachable network fails fast instead of stalling a caller the bundled
snapshot could have served.
A catalog that had to degrade — a failed fetch, or a fall back to the bundled snapshot — is only held in memory for a minute rather than the full day, so a process that started while the network was down picks up the real catalog once it recovers.
Cost of the first lookup¶
Catalog entries are validated one at a time, as they are looked up, rather than
all at once. A first lookup with a warm cache costs a few milliseconds and
holds only the models it actually returned; the ~3,000-entry LiteLLM catalog is
never fully materialized unless you ask for it with get_models().
How the estimate is computed¶
Tokens are split into disjoint buckets and each is priced at its own rate:
| Bucket | Rate |
|---|---|
| Cached input tokens | cache-read price |
| Cache-write input tokens | cache-creation price |
| Audio input tokens | input audio price |
| Audio output tokens | output audio price |
| Image input tokens | input image price |
| Image output tokens | output image price |
| Reasoning output tokens | reasoning price, falling back to the completion price |
| Remaining input tokens | prompt price |
| Remaining output tokens | completion price |
| Requests | per-request price |
| Seconds | per-second price |
When a usage object carries no audio, image, cached, or reasoning detail — an
ordinary chat completion, say — every special bucket is 0 and the math reduces
to the familiar prompt × input + completion × output.
Unknown models¶
By default, an unrecognized model name yields a 0.0 cost and a logged warning,
so a bad id never crashes an aggregation pipeline:
Set ignore_not_found=False to raise instead:
Limitations¶
Video tokens are billed as text
video_tokens are recorded for reporting, but neither catalog publishes a
per-token video rate, so they stay in the plain-text remainder and are
billed at the ordinary input rate. That matches how the providers reporting
them bill today, but verify it if video is a large share of your spend.
Detail buckets are assumed disjoint
Cached, cache-write, audio and image counts are subtracted from the totals to derive the plain-text remainder. If a provider reports overlapping buckets — a cached audio token counted in both — that token is subtracted twice and the estimate comes out low.
OpenRouter publishes one rate per modality
OpenRouter has a single audio and a single image rate per model, so a
model resolved through OpenRouter uses that one rate for both input and
output. LiteLLM prices them separately and is tried first, so this normally
only applies to models LiteLLM does not carry.