API reference

Estimate

What one call costs, and why. Every line of the breakdown names the literal dataset key it was billed with, every rate that could not be found is listed instead of being silently treated as zero, and every judgement call the engine made comes back in resolution.

Request

POST /v1/estimate

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 }
  }
}'
FieldTypeDefaultNotes
modelstringrequiredA dataset id. Resolved exactly first, then case-insensitively.
providerstringDisambiguates an id that matches several entries. See 409.
usageobjectrequiredWhat was consumed. All 33 fields below. May be empty, which prices to 0.
service_tierstandard | batch | priority | flexstandardSee below.
region_processingglobal | eu | usglobalSee below.
optionsobject{}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

FieldBase rate keyNotes
input_tokensinput_cost_per_token
output_tokensoutput_cost_per_tokenReasoning tokens are subtracted from it first.
reasoning_tokensoutput_cost_per_reasoning_tokenA 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_tokenscache_read_input_token_cost
cache_creation_tokenscache_creation_input_token_costAdded to the 5m bucket. See below.
cache_creation_tokens_by_ttlsame, with an _above_1hr axis{"5m"?: n, "1h"?: n}. The 1h bucket gets its own line.

Audio, image and video

FieldBase rate key
input_audio_tokensinput_cost_per_audio_token
output_audio_tokensoutput_cost_per_audio_token
cache_read_audio_tokenscache_read_input_audio_token_cost
cache_creation_audio_tokenscache_creation_input_audio_token_cost
input_image_tokensinput_cost_per_image_token
output_image_tokensoutput_cost_per_image_token
output_video_tokensoutput_cost_per_video_token

Non-token units

FieldBase rate keyNotes
input_characters / output_charactersinput_cost_per_character / output_cost_per_characterSome TTS models price per character.
audio_secondsinput_cost_per_secondTranscription. Also bills output_cost_per_second — see transcription_billing.
output_audio_secondsoutput_cost_per_secondUsed as the output quantity when audio_seconds is present.
video_seconds / output_video_secondsinput_cost_per_video_per_second / output_cost_per_video_per_second
input_images / output_imagesinput_cost_per_image / output_cost_per_imageImage generation is usually priced per image.
input_pixels / output_pixelsinput_cost_per_pixel / output_cost_per_pixel
image_dimensionsinput_cost_per_pixel{width, height, count?}. Multiplied into input_pixels when that is not given explicitly.
pagesocr_cost_per_page
annotated_pagesannotation_cost_per_page
requestsinput_cost_per_request
queriesinput_cost_per_query
code_interpreter_sessionscode_interpreter_cost_per_session
web_search, search_resultsnoneAccepted 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.

The tier is a flag, not a marginal band. If the request crosses the threshold, the whole request is repriced — output included. The clearest evidence is that the dataset defines 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

OptionValuesDefaultEffect
tier_policyflag | marginalflagflag reprices the whole request once a threshold is crossed. marginal is not implemented and falls through to the base rate.
threshold_basisinput_only | input_plus_cacheinput_plus_cacheWhich tokens count toward the context threshold.
cache_tokens_included_in_inputbooleanfalseWhen true, cache_read_tokens is subtracted from input_tokens before billing.
limit_policyerror | warn | ignorewarnWhat to do when usage exceeds a limit the model declares. error turns it into a 422.
transcription_billinginput_only | bothbothWhether 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_tointeger 0–2010Decimals 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)."]
}
FieldMeaning
rate_keyThe 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.
rateThe rate as an exact decimal string, exactly as the catalog serialized it.
rate_per_unitsHow many units one rate covers. Always 1 except for the _per_1k_* keys, where it is 1000.
categoryinput, output, cache_read, cache_write, surcharge, other.
unittoken, second, character, image, pixel, page, query, request, session, credit.
notesPresent 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.

CodeMeaning
SERVICE_TIER_NOT_PRICEDThe requested tier has no rate for this key; standard was used instead.
CONTEXT_TIER_NOT_PRICEDThe threshold was crossed but this particular key has no long-context variant; the base rate was used.
RATE_FALLBACK_APPLIEDA semantic fallback was taken from an explicit table — most often output_cost_per_reasoning_tokenoutput_cost_per_token. Also emitted for a regional uplift.
ALIAS_KEY_USEDA legacy key name was matched, e.g. input_cost_per_token_cache_hit for a cache read.
EXCEEDS_MAX_INPUT_TOKENSInput exceeds the model's declared max_input_tokens. Still priced — under limit_policy: "error" it becomes a 422.
EXCEEDS_MAX_OUTPUT_TOKENSSame for max_output_tokens.
SUSPICIOUS_RATE_MAGNITUDEA rate above roughly $500/MTok. Not corrected — flagged, because a handful of upstream entries are implausible and a few are genuinely that expensive.
DEPRECATED_MODELThe model declares a deprecation_date in the past.
CACHE_BELOW_MIN_TOKENSThe cache write is below prompt_cache_min_tokens; the provider would not have cached it.
MIXED_CURRENCY_TOTALSRates in more than one currency, so the totals are not summable. See the DBU limitation.
TIERED_PRICING_APPLIEDDeclared 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"] }
]
A total of 0 with a non-empty 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

StatusSlugCause
400invalid-requestSchema validation failed. The body carries errors[] with a path and a message per issue.
400invalid-requestreasoning_tokens exceeds output_tokens. Checked after the schema, so it carries a detail instead of errors[].
404model-not-foundNo such id. Unlike the GET route, this one carries no suggestions.
409ambiguous-modelThe id matches several entries. candidates here is a list of {id, provider} objects — pass provider to resolve it.
422model-not-pricedThe model exists but has no pricing key at all (97 entries). Carries model: {id, provider, mode}.
422limits-exceededOnly with limit_policy: "error". Carries the limit and what you provided.
503not-readyThe 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.

GapWhat actually happens
usage.web_search and usage.search_resultsValidated 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_pricingNot 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 ratesinput_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 inputcitation_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 kBAnswers 500 internal-error rather than 400 / 413.

If one of these blocks you, the issue tracker is the place to say so.