Cost Estimation
Cost tracking tells you what a call did cost. Estimation tells you what it
will cost, before you send it, so you can decide whether to allow it (reserve
budget, gate a request, refuse an over-priced job). Enable the estimate
feature:
minillmlib = { version = "0.5", features = ["estimate"] }
One call on the generator
The GeneratorInfo you already send completions with answers directly:
#![allow(unused)] fn main() { use minillmlib::{ChatNode, CompletionParameters, GeneratorInfo}; let generator = GeneratorInfo::openrouter("anthropic/claude-haiku-4.5"); let params = CompletionParameters::new().with_max_tokens(1024); let root = ChatNode::root("You are terse."); let prompt = root.add_user("Name three primary colours."); let usd = generator.estimate_cost_usd(&prompt.thread(), ¶ms).await?; println!("this call will cost at most ${usd:.6}"); }
If you want the raw rates instead (to price several prompts, or to combine with
cost tracking's token buckets), use
generator.model_rates(), which returns the per-million-token prices and the
model's limits.
Where the prices come from
OpenRouter's catalog is the one public price sheet, and it lists the first-party vendors (OpenAI, Anthropic) at their own standard rates. So the model is looked up by its OpenRouter id:
- An OpenRouter generator's model id already is a catalog id. Nothing to set.
- A direct-vendor generator whose id differs sets the catalog id explicitly; that is what unlocks estimation:
#![allow(unused)] fn main() { let generator = GeneratorInfo::anthropic("claude-haiku-4-5-20251001") .with_openrouter_name("anthropic/claude-haiku-4.5"); }
The only failure is a model the catalog does not know at all. Everything else produces a number.
Whose rates: the provider question
One model is served by many providers at different prices (some models span a 5x range). The rates used are:
- the serving provider's own, when the generator's provider knows its catalog slug (the built-in Anthropic and OpenAI providers do: a direct call is billed by the vendor itself);
- otherwise the dearest rate any provider of the model charges, taken bucket by bucket. That is the only figure that is a ceiling wherever OpenRouter's routing lands, and routing really does land on expensive endpoints: a real request was once billed at nearly 4x the advertised rate.
If a request pins one provider through routing settings (a single-entry order
with fallbacks off), ProviderSettings::billing_provider() yields its slug and
generator.model_rates_served_by(Some(&slug)) prices at exactly that provider.
What the figure means
The estimate is deliberately high, never a best guess: only the low side lets you overspend. It is still an estimate (tokenizers differ across model families), so treat it as a strong ceiling to reserve against, not a guarantee, and replace it with the tracked real cost once the call returns. It assumes:
- no prompt caching (caching only ever lowers the real cost);
- the largest completion the request permits, including any reasoning budget,
which providers bill on top of
max_tokens; - a minute of media for a clip whose length you did not state. Set the real
length with
AudioData::with_duration/VideoData::with_durationwhenever your prompt carries audio or video: the one-minute assumption overshoots short clips by a lot (measured live: 5-7x on an 8-second clip) and undershoots anything longer than a minute, which is the one way the estimate can come in below the real cost. With the length declared the media estimate is tight (the video token model matched Gemini's real billing within 1%). Audio bills by the second, at up to 1000x the text rate on some models, so this is where the money is.
There is no error case beyond an uncatalogued model: a prompt counted larger than the model accepts is priced as the largest input the model does accept, so you always get a number.
Keep your generators alive
Each GeneratorInfo caches the prices it fetches for an hour, and clones share
the cache. Reuse the same generator for both completions and estimates: only the
first estimate in an hour touches the network, and concurrent estimates for the
same model share a single fetch. Recreating the generator per call throws the
cache away and refetches the price sheet every time. (A generator whose model
you change notices and refetches; it never serves another model's prices.)
The library deliberately holds no registry of generators; pooling is yours to
do, and only worth doing if you estimate costs. For code that multiplexes many
models, generator.pricing_key() gives you the map key: the catalog model id
plus the provider slug, exactly the pair that determines the price. Build a
generator on a miss, reuse it on a hit:
#![allow(unused)] fn main() { use std::collections::HashMap; struct Generators(HashMap<String, GeneratorInfo>); impl Generators { fn for_model(&mut self, model: &str) -> &GeneratorInfo { let generator = GeneratorInfo::openrouter(model); self.0.entry(generator.pricing_key()).or_insert(generator) } } }
If you never estimate costs, ignore all of this: the cache sits unused and costs nothing.