Request
A JSON body with a model and the usage you want priced. Everything else has a default. The schema is
strict: an unrecognized key is a 400, not a silently ignored field — a typo
in a usage field would otherwise quietly under-bill.
curl -X POST localhost:8080/v1/estimate -H 'content-type: application/json' -d '{
"model": "claude-sonnet-4-5",
"usage": {
"input_tokens": 190000,
"output_tokens": 4000,
"cache_read_tokens": 20000,
"cache_creation_tokens_by_ttl": { "1h": 8000 }
}
}'
| Field | Type | Default | Notes |
|---|---|---|---|
model | string | required | A dataset id. Resolved exactly first, then case-insensitively. |
provider | string | — | Disambiguates an id that matches several entries. See 409. |
usage | object | required | What was consumed. All 33 fields below. May be empty, which prices to 0. |
service_tier | standard | batch | priority | flex | standard | See below. |
region_processing | global | eu | us | global | See below. |
options | object | {} | Six switches over how the arithmetic is done. See below. |
Every numeric usage value must be finite and >= 0. The body limit is 256 kB.
Usage fields
Each field maps to one base rate key. If the model does not declare that key — or a variant of it that
fits the tier and TTL — the quantity lands in unpriced[], never in
the total at a made-up rate.
Text tokens
| Field | Base rate key | Notes |
|---|---|---|
input_tokens | input_cost_per_token | — |
output_tokens | output_cost_per_token | Reasoning tokens are subtracted from it first. |
reasoning_tokens | output_cost_per_reasoning_token | A subset of output_tokens, billed as its own line. Almost no model declares a distinct rate, so it usually falls back to the output rate with a warning. Exceeding output_tokens is a 400. |
cache_read_tokens | cache_read_input_token_cost | — |
cache_creation_tokens | cache_creation_input_token_cost | Added to the 5m bucket. See below. |
cache_creation_tokens_by_ttl | same, with an _above_1hr axis | {"5m"?: n, "1h"?: n}. The 1h bucket gets its own line. |
Audio, image and video
| Field | Base rate key |
|---|---|
input_audio_tokens | input_cost_per_audio_token |
output_audio_tokens | output_cost_per_audio_token |
cache_read_audio_tokens | cache_read_input_audio_token_cost |
cache_creation_audio_tokens | cache_creation_input_audio_token_cost |
input_image_tokens | input_cost_per_image_token |
output_image_tokens | output_cost_per_image_token |
output_video_tokens | output_cost_per_video_token |
Non-token units
| Field | Base rate key | Notes |
|---|---|---|
input_characters / output_characters | input_cost_per_character / output_cost_per_character | Some TTS models price per character. |
audio_seconds | input_cost_per_second | Transcription. Also bills output_cost_per_second — see transcription_billing. |
output_audio_seconds | output_cost_per_second | Used as the output quantity when audio_seconds is present. |
video_seconds / output_video_seconds | input_cost_per_video_per_second / output_cost_per_video_per_second | — |
input_images / output_images | input_cost_per_image / output_cost_per_image | Image generation is usually priced per image. |
input_pixels / output_pixels | input_cost_per_pixel / output_cost_per_pixel | — |
image_dimensions | → input_cost_per_pixel | {width, height, count?}. Multiplied into input_pixels when that is not given explicitly. |
pages | ocr_cost_per_page | — |
annotated_pages | annotation_cost_per_page | — |
requests | input_cost_per_request | — |
queries | input_cost_per_query | — |
code_interpreter_sessions | code_interpreter_cost_per_session | — |
web_search, search_results | none | Accepted and validated, but never billed. See limitations. |
Examples across modalities
# Embeddings: input only — 2.5M tokens of text-embedding-3-small
{ "model": "text-embedding-3-small", "usage": { "input_tokens": 2500000 } }
# -> 0.05 USD [input.text 2500000 x 2e-8]
# Transcription priced per second of audio
{ "model": "assemblyai/best", "usage": { "audio_seconds": 3600 } }
# -> 0.119988 USD [input.audio_seconds 3600 x 0.00003333, plus output.audio_seconds at 0]
# Image generation, priced per image
{ "model": "dall-e-3", "usage": { "input_images": 4 } }
# -> 0.16 USD [input.images 4 x 0.04] + DEPRECATED_MODEL warning
# OCR, priced per page
{ "model": "azure_ai/mistral-document-ai-2505", "usage": { "pages": 320 } }
# -> 0.96 USD [other.pages 320 x 0.003, rate_key ocr_cost_per_page]
Service tiers
batch, priority and flex select a rate variant —
input_cost_per_token_batches, ..._priority, ..._flex. OpenAI's batch
tier is the familiar case: half price.
{ "model": "gpt-4o", "usage": { "input_tokens": 500000, "output_tokens": 50000 }, "service_tier": "batch" }
input.text 500000 x 0.00000125 = 0.625 [input_cost_per_token_batches]
output.text 50000 x 0.000005 = 0.25 [output_cost_per_token_batches]
-----
0.875 USD
When the model has no rate for the tier you asked for, the estimator degrades to standard and says
so — it does not fail, and it does not pretend the discount applied. Compare
service_tier_requested with service_tier_applied in
resolution, or look for
SERVICE_TIER_NOT_PRICED in the warnings:
{ "model": "gpt-4o", "usage": { "input_tokens": 1000, "output_tokens": 100 }, "service_tier": "flex" }
"resolution": { "service_tier_requested": "flex", "service_tier_applied": "standard", … }
"warnings": [
{ "code": "SERVICE_TIER_NOT_PRICED",
"message": "The model has no \"flex\" rate for input_cost_per_token; falling back to standard.",
"field": "input_tokens" }, … ]
Context tiers
Several providers charge more once a prompt crosses a threshold — Anthropic and Google at 200k tokens, for
instance. The catalog encodes it as parallel keys: input_cost_per_token_above_200k_tokens.
output_cost_per_token_above_200k_tokens: an output price gated on an input threshold
only makes sense as a flag. Of the 117 threshold pairs in the catalog, 116 have a ratio of exactly 2.0, and
Anthropic, Google and OpenAI all document the same scheme.
Which tokens count toward the threshold is options.threshold_basis. By default cache reads
count too (input_plus_cache), because the provider still has to hold that context — that is
why the example below reports 218,000 considered tokens for 190,000 input tokens.
{ "model": "claude-sonnet-4-5",
"usage": { "input_tokens": 190000, "output_tokens": 4000,
"cache_read_tokens": 20000, "cache_creation_tokens_by_ttl": { "1h": 8000 } } }
"resolution": {
"threshold_basis": "input_plus_cache",
"threshold_tokens_considered": 218000,
"context_tier_applied": "above_200k_tokens",
"context_tier_thresholds_available": [200000]
}
Set threshold_basis: "input_only" to leave cache out of the comparison.
Cache and TTL
Cache writes are priced by how long the entry lives. _above_1hr is not a
volume threshold — it is Anthropic's 1 hour TTL, and it composes with the context tier, which is how
a single line ends up billed with a quadruple key:
cache_write.1h 8000 x 0.000012 = 0.096 [cache_creation_input_token_cost_above_1hr_above_200k_tokens]
cache_creation_tokens and cache_creation_tokens_by_ttl["5m"] are added together
into one 5-minute line; the "1h" bucket always gets its own.
If the model declares prompt_cache_min_tokens and your cache write is below it, the write is
still billed — but you get a CACHE_BELOW_MIN_TOKENS warning saying the provider would not have
cached it at all.
If your provider reports cache reads inside input_tokens rather than alongside them,
set options.cache_tokens_included_in_input: true and the overlap is subtracted instead of
billed twice.
Regional uplift
18 models declare regional_processing_uplift_multiplier_eu / _us. Setting
region_processing applies it as a separate surcharge line over the USD subtotal,
rather than by multiplying each rate — so the base rates in the breakdown stay comparable to the catalog.
{ "model": "gpt-5.6", "usage": { "input_tokens": 10000, "output_tokens": 1000 }, "region_processing": "eu" }
input.text 10000 token x 0.000004 = 0.04 [input_cost_per_token]
output.text 1000 token x 0.00002 = 0.02 [output_cost_per_token]
surcharge.region_eu 1 request x 0.1 = 0.006 [regional_processing_uplift_multiplier_eu]
-----
0.066 USD
A model with no uplift key is unaffected — no line, no warning, same total as global.
Options
| Option | Values | Default | Effect |
|---|---|---|---|
tier_policy | flag | marginal | flag | flag reprices the whole request once a threshold is crossed. marginal is not implemented and falls through to the base rate. |
threshold_basis | input_only | input_plus_cache | input_plus_cache | Which tokens count toward the context threshold. |
cache_tokens_included_in_input | boolean | false | When true, cache_read_tokens is subtracted from input_tokens before billing. |
limit_policy | error | warn | ignore | warn | What to do when usage exceeds a limit the model declares. error turns it into a 422. |
transcription_billing | input_only | both | both | Whether audio_seconds also bills the model's per-second output rate. whisper-1 declares both, and assemblyai/best spelling out output_cost_per_second: 0.0 suggests they are meant to be summed. |
round_to | integer 0–20 | 10 | Decimals in totals[currency].rounded. exact is never rounded. |
Response shape
{
"model": { "requested": "claude-sonnet-4-5", "resolved_key": "claude-sonnet-4-5",
"provider": "anthropic", "mode": "chat" },
"dataset": { "sha256": "a5682d2f…", "loaded_at": "2026-08-27T06:23:42.602Z" },
"totals": { "USD": { "exact": "1.338", "rounded": "1.3380000000", "decimals": 10 } },
"lines": [ … ],
"subtotals": { "input": "1.14", "output": "0.09", "cache_read": "0.012", "cache_write": "0.096" },
"resolution": { … },
"warnings": [ … ],
"unpriced": [ … ]
}
dataset.sha256 is the digest of the snapshot that produced this answer — pin it if you need
two estimates to be comparable. Every monetary value is a string, because these are exact
decimals and JSON numbers are not.
Totals
totals is keyed by currency, not a single number, because the catalog carries rates
in Databricks DBU alongside USD. exact is the unrounded decimal; rounded honours
round_to.
subtotals groups the lines by category: input, output, cache_read, cache_write, surcharge, other.
Cost lines
{
"id": "cache_write.1h",
"label": "Cache write",
"category": "cache_write",
"currency": "USD",
"quantity": 8000,
"unit": "token",
"rate": "0.000012",
"rate_key": "cache_creation_input_token_cost_above_1hr_above_200k_tokens",
"rate_per_units": 1,
"amount": "0.096",
"notes": ["Cache write with a 1 hour TTL.", "Long-context rate: 218000 > 200000 tokens (flag tier)."]
}
| Field | Meaning |
|---|---|
rate_key | The literal key in the dataset. This is the field that makes the total checkable — look it up in /v1/models/{id} and you will find that exact number. |
rate | The rate as an exact decimal string, exactly as the catalog serialized it. |
rate_per_units | How many units one rate covers. Always 1 except for the _per_1k_* keys, where it is 1000. |
category | input, output, cache_read, cache_write, surcharge, other. |
unit | token, second, character, image, pixel, page, query, request, session, credit. |
notes | Present only when something non-obvious happened to this line: a TTL axis, a long-context reprice, a transcription output pairing. |
Resolution
Every judgement call, in one object — this is what you inspect when a total surprises you.
"resolution": {
"service_tier_requested": "standard",
"service_tier_applied": "standard",
"tier_policy": "flag",
"threshold_basis": "input_plus_cache",
"threshold_tokens_considered": 218000,
"context_tier_applied": "above_200k_tokens",
"context_tier_thresholds_available": [200000],
"pricing_mechanism": "flat_keys",
"rate_keys_used": [
"input_cost_per_token_above_200k_tokens",
"output_cost_per_token_above_200k_tokens",
"cache_read_input_token_cost_above_200k_tokens",
"cache_creation_input_token_cost_above_1hr_above_200k_tokens"
]
}
context_tier_applied is null when no threshold was crossed, and
context_tier_thresholds_available tells you whether the model has any thresholds at all.
pricing_mechanism is always "flat_keys" today —
tiered_pricing is not read.
Warnings
Warnings never change the status code. They are how the estimator tells you it made a decision you might disagree with, or that the number is right but the request is not.
| Code | Meaning |
|---|---|
SERVICE_TIER_NOT_PRICED | The requested tier has no rate for this key; standard was used instead. |
CONTEXT_TIER_NOT_PRICED | The threshold was crossed but this particular key has no long-context variant; the base rate was used. |
RATE_FALLBACK_APPLIED | A semantic fallback was taken from an explicit table — most often output_cost_per_reasoning_token → output_cost_per_token. Also emitted for a regional uplift. |
ALIAS_KEY_USED | A legacy key name was matched, e.g. input_cost_per_token_cache_hit for a cache read. |
EXCEEDS_MAX_INPUT_TOKENS | Input exceeds the model's declared max_input_tokens. Still priced — under limit_policy: "error" it becomes a 422. |
EXCEEDS_MAX_OUTPUT_TOKENS | Same for max_output_tokens. |
SUSPICIOUS_RATE_MAGNITUDE | A rate above roughly $500/MTok. Not corrected — flagged, because a handful of upstream entries are implausible and a few are genuinely that expensive. |
DEPRECATED_MODEL | The model declares a deprecation_date in the past. |
CACHE_BELOW_MIN_TOKENS | The cache write is below prompt_cache_min_tokens; the provider would not have cached it. |
MIXED_CURRENCY_TOTALS | Rates in more than one currency, so the totals are not summable. See the DBU limitation. |
TIERED_PRICING_APPLIED | Declared in the type but never emitted, since tiered_pricing is not read. |
# wandb/zai-org/GLM-4.5 at $55,000/MTok — served as-is, flagged loudly
"warnings": [
{ "code": "SUSPICIOUS_RATE_MAGNITUDE",
"message": "Rate input_cost_per_token=0.055 implies $55000/MTok. Likely an upstream dataset error." } ]
o1-pro at $600/MTok is genuinely that expensive and sits in an allowlist, so it produces no
warning.
Unpriced usage
The rule that shapes this endpoint: a missing rate is never billed as 0. 258 models
legitimately declare output_cost_per_token: 0, so "free" and "unknown" have to stay
distinguishable. Usage the catalog cannot price is listed here, with the keys that were tried.
{ "model": "dashscope/qwen-flash", "usage": { "input_tokens": 1000, "output_tokens": 100 } }
"totals": { "USD": { "exact": "0", … } },
"lines": [],
"unpriced": [
{ "usage_field": "input_tokens", "quantity": 1000, "reason": "NO_RATE_KEY", "candidate_keys_checked": ["input_cost_per_token"] },
{ "usage_field": "output_tokens", "quantity": 100, "reason": "NO_RATE_KEY", "candidate_keys_checked": ["output_cost_per_token"] }
]
unpriced[] does not mean the call is free. It
means nothing could be priced. Check unpriced before trusting any total — that model prices
only through tiered_pricing, which is not read.
reason is one of NO_RATE_KEY, MODE_MISMATCH or NON_MONETARY_UNIT; in practice only the first is emitted.
Errors
| Status | Slug | Cause |
|---|---|---|
| 400 | invalid-request | Schema validation failed. The body carries errors[] with a path and a message per issue. |
| 400 | invalid-request | reasoning_tokens exceeds output_tokens. Checked after the schema, so it carries a detail instead of errors[]. |
| 404 | model-not-found | No such id. Unlike the GET route, this one carries no suggestions. |
| 409 | ambiguous-model | The id matches several entries. candidates here is a list of {id, provider} objects — pass provider to resolve it. |
| 422 | model-not-priced | The model exists but has no pricing key at all (97 entries). Carries model: {id, provider, mode}. |
| 422 | limits-exceeded | Only with limit_policy: "error". Carries the limit and what you provided. |
| 503 | not-ready | The dataset is still loading. |
# A typo in a usage field is a 400, not a silent zero — the schema is strict
{ "model": "gpt-4o", "usage": { "input_tokens": 1000, "input_toknes": 500 } }
{ "type": "about:blank#invalid-request", "title": "The request is not valid.", "status": 400,
"errors": [{ "path": "usage", "message": "Unrecognized key(s) in object: 'input_toknes'" }] }
# limit_policy: "error" turns the limit warning into a refusal
{ "type": "about:blank#limits-exceeded", "title": "The limits declared by the model are exceeded.",
"status": 422, "detail": "5000000 input tokens exceed max_input_tokens=128000.",
"max_input_tokens": 128000, "provided": 5000000 }
# The model exists, but the catalog publishes no rate for it
{ "type": "about:blank#model-not-priced", "title": "The model exists but carries no pricing data.",
"status": 422, "detail": "The dataset publishes no rate for this entry.",
"model": { "id": "github_copilot/claude-opus-4.5", "provider": "github_copilot", "mode": "chat" } }
What the estimator cannot price
These are accepted inputs or catalog features that currently produce no cost line. They are written down rather than hidden, because a silently missing charge is worse than a documented one.
| Gap | What actually happens |
|---|---|
usage.web_search and usage.search_results | Validated and accepted, then ignored. search_context_cost_per_query — present on 282 models, described in the cost catalog — is never read. The response is 200, the search costs nothing, and it does not even appear in unpriced[]. |
options.tier_policy: "marginal" | Does not compute per-band pricing. It resolves to no threshold at all, so the base rate applies and the result equals a request that never crossed the threshold. Only flag is real today. |
tiered_pricing | Not read. The 21 models priced only through it pass the "is it billable" check and answer 200 with a total of 0 and all usage in unpriced[]. |
| Databricks DBU rates | input_dbu_cost_per_token and output_dbu_cost_per_token (47 models) are described in the catalog, but no usage field maps to them, so a DBU total is never produced and MIXED_CURRENCY_TOTALS never fires. totals is keyed by currency in anticipation of it. |
| Other catalog keys with no input | citation_cost_per_token, ocr_cost_per_credit and file_search_cost_per_1k_calls are described but have no corresponding usage field. |
| Malformed JSON, or over 256 kB | Answers 500 internal-error rather than 400 / 413. |
If one of these blocks you, the issue tracker is the place to say so.