embacle/pricing.rs
1// ABOUTME: Per-token price table for every provider this crate can run, keyed on LlmProvider::name()
2// ABOUTME: Longest-prefix model lookup, cache read/write multipliers, reasoning at the output rate
3//
4// SPDX-License-Identifier: Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7//! # Model Pricing
8//!
9//! Compile-time pricing data for the providers this crate runs, keyed on the
10//! exact string each one's [`LlmProvider::name`](crate::types::LlmProvider::name)
11//! reports. [`MetricsProvider`](crate::metrics::MetricsProvider) prices every
12//! call through it; a consumer that layers operator overrides on top builds
13//! its own lookup from [`lookup_pricing`], [`cost_from_pricing`] and
14//! [`zero_cost_for_unpriced`], which are public for that purpose.
15//!
16//! ## Cache accounting
17//!
18//! A prompt token can bill at three rates, so the pricing entry carries the
19//! multipliers rather than a single global discount: fresh input at 1.0×, a
20//! cache *read* at [`ModelPricing::cache_read_multiplier`] (Anthropic 0.10×,
21//! `OpenAI` 0.50×, `Gemini` 0.25×), and a cache *write* at
22//! [`ModelPricing::cache_write_multiplier`] — which is a **premium** on
23//! Anthropic (1.25×), not a discount. Reasoning tokens bill at the output
24//! rate and are additive to the completion count, because every provider that
25//! reports them separately excludes them from it.
26//!
27//! Counts arrive on [`TokenUsage`](crate::types::TokenUsage), are carried
28//! through [`TokenCounts`], and are priced by [`calculate_cost_for`].
29
30use serde::{Deserialize, Serialize};
31use tracing::{debug, info, warn};
32
33/// Default fraction of the input rate at which a context-cache *read* bills.
34///
35/// Matches `Gemini`'s advertised 25% cache-read discount, and applies to any
36/// model that does not override it via [`ModelPricing::with_cache_rates`].
37/// A provider with no cache at all never reports a read, so the multiplier
38/// is inert rather than wrong for those entries.
39pub const DEFAULT_CACHE_READ_RATE: f64 = 0.25;
40
41/// Default fraction of the input rate at which a context-cache *write* bills.
42///
43/// `1.0` — a provider that does not price cache creation separately bills
44/// those tokens as ordinary input. Anthropic is the exception and overrides
45/// it; see [`ModelPricing::with_cache_rates`].
46pub const DEFAULT_CACHE_WRITE_RATE: f64 = 1.0;
47
48/// Per-model pricing rates in USD per million tokens, plus the two
49/// cache multipliers that turn a raw token count into a billed one.
50///
51/// Cache economics are per-provider, not universal: Anthropic reads at
52/// 0.10× and *charges a premium* to write at 1.25×, `OpenAI` reads at 0.50×
53/// and does not bill writes separately, `Gemini` reads at 0.25×. A single
54/// flat discount mispriced all three, so the rates travel with the model.
55#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
56pub struct ModelPricing {
57 /// USD per 1 million input (prompt) tokens
58 pub input_per_million: f64,
59 /// USD per 1 million output (completion) tokens
60 pub output_per_million: f64,
61 /// Fraction of `input_per_million` at which cache-read tokens bill.
62 #[serde(default = "default_cache_read_rate")]
63 pub cache_read_multiplier: f64,
64 /// Fraction of `input_per_million` at which cache-write tokens bill.
65 #[serde(default = "default_cache_write_rate")]
66 pub cache_write_multiplier: f64,
67 /// A second rate card for long prompts, when the model has one: a
68 /// request whose gross prompt exceeds its threshold bills every token —
69 /// input, cache and output — at the long-prompt rates.
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub long_prompt: Option<LongPromptRates>,
72}
73
74/// The rate card a model switches to when a request's prompt is long.
75///
76/// Claude Haiku 5.5 bills \$0.10 / \$0.50 per million tokens for a prompt of
77/// 100K tokens or fewer and \$0.50 / \$2.50 above that; the cache
78/// multipliers apply to the long-prompt input rate the same way.
79#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
80pub struct LongPromptRates {
81 /// The largest gross prompt (fresh + cache read + cache write) that still
82 /// bills on the base card
83 pub above_prompt_tokens: i64,
84 /// USD per 1 million input tokens above the threshold
85 pub input_per_million: f64,
86 /// USD per 1 million output tokens above the threshold
87 pub output_per_million: f64,
88}
89
90/// Serde default for [`ModelPricing::cache_read_multiplier`], so an operator
91/// override stored before the multipliers existed still deserializes.
92fn default_cache_read_rate() -> f64 {
93 DEFAULT_CACHE_READ_RATE
94}
95
96/// Serde default for [`ModelPricing::cache_write_multiplier`], so an operator
97/// override stored before the multipliers existed still deserializes.
98fn default_cache_write_rate() -> f64 {
99 DEFAULT_CACHE_WRITE_RATE
100}
101
102impl ModelPricing {
103 /// Rates for a model that uses the default cache multipliers.
104 #[must_use]
105 pub const fn new(input_per_million: f64, output_per_million: f64) -> Self {
106 Self {
107 input_per_million,
108 output_per_million,
109 cache_read_multiplier: DEFAULT_CACHE_READ_RATE,
110 cache_write_multiplier: DEFAULT_CACHE_WRITE_RATE,
111 long_prompt: None,
112 }
113 }
114
115 /// Bill a request whose gross prompt exceeds `above_prompt_tokens` at
116 /// the given input / output rates instead of the base card.
117 #[must_use]
118 pub const fn with_long_prompt_rates(
119 mut self,
120 above_prompt_tokens: i64,
121 input_per_million: f64,
122 output_per_million: f64,
123 ) -> Self {
124 self.long_prompt = Some(LongPromptRates {
125 above_prompt_tokens,
126 input_per_million,
127 output_per_million,
128 });
129 self
130 }
131
132 /// The (input, output) rates per million tokens for a request with a gross prompt
133 /// of `prompt_tokens`: the long-prompt card above its threshold, the base
134 /// card otherwise.
135 #[must_use]
136 pub fn rates_for_prompt(&self, prompt_tokens: i64) -> (f64, f64) {
137 match self.long_prompt {
138 Some(long) if prompt_tokens > long.above_prompt_tokens => {
139 (long.input_per_million, long.output_per_million)
140 }
141 _ => (self.input_per_million, self.output_per_million),
142 }
143 }
144
145 /// Override the cache read/write multipliers for a provider that prices
146 /// them differently — Anthropic at `(0.10, 1.25)`, `OpenAI` at `(0.50, 1.0)`.
147 #[must_use]
148 pub const fn with_cache_rates(mut self, read: f64, write: f64) -> Self {
149 self.cache_read_multiplier = read;
150 self.cache_write_multiplier = write;
151 self
152 }
153}
154
155/// Providers whose per-token cost is genuinely \$0 by design, so a \$0 billed
156/// cost is *correct* rather than a missing-price undercount.
157///
158/// Two classes share this property:
159///
160/// - **Self-hosted** runtimes (Ollama / vLLM / `LocalAI` / generic local
161/// `OpenAI`-compatible endpoints): the operator pays for the hardware, not
162/// per token, so the metered per-token cost is zero.
163/// - **Flat-rate subscription** CLI/agent runners (GitHub Copilot, Cursor,
164/// `OpenCode`, Codex, Goose, Cline, Continue, Warp, Kiro, Kilo): billed by
165/// the upstream subscription, not per token.
166///
167/// Entries are the exact machine `name()` string each provider reports (see
168/// [`LlmProvider::name`](crate::types::LlmProvider::name)) — hyphenated for
169/// some runners and underscored for others. A model under one of these
170/// prefixes resolves to \$0 *without* the missing-price warning, so cost
171/// dashboards do not flag it as an undercount.
172/// `copilot_headless`, `copilot_sdk` and `claude-code` are deliberately
173/// absent here: they carry real per-token `PRICING_TABLE` entries because
174/// their Anthropic pass-through usage is metered.
175pub const NOT_PER_TOKEN_METERED_PROVIDERS: &[&str] = &[
176 // Self-hosted OpenAI-compatible runtimes.
177 "local",
178 "ollama",
179 "vllm",
180 "localai",
181 // Flat-rate subscription CLI / agent runners (machine names as reported
182 // by each runner's name()).
183 "cursor-agent",
184 "opencode",
185 "codex",
186 "goose",
187 "cline",
188 "continue",
189 "warp_cli",
190 "kiro",
191 "kilo",
192 "copilot",
193 // The browser-driven Claude.ai web UI: a subscription, not per-token.
194 "claude_web",
195];
196
197/// `OpenRouter` exposes 200+ models with per-model pricing, so a static table
198/// can never be exhaustive. An unknown `OpenRouter` model is therefore logged
199/// as *model-specific price unset* (a deliberate, non-alarming message) rather
200/// than counted as a silent undercount alongside genuinely-unexpected misses.
201const OPENROUTER_PROVIDER: &str = "openrouter";
202
203/// Compile-time pricing table: `(provider, model_prefix, pricing)`
204///
205/// The provider key is the exact `name()` string a provider reports
206/// ([`LlmProvider::name`](crate::types::LlmProvider::name)), and
207/// [`lookup_pricing`] matches it by equality — a key spelled any other way is
208/// unreachable, and its models silently bill at \$0.
209///
210/// Model matching uses prefix comparison — a model name like "gemini-2.0-flash-exp"
211/// matches the prefix "gemini-2.0-flash". When several of a provider's prefixes
212/// match, [`lookup_pricing`] takes the longest, so the most specific row wins
213/// wherever it sits in the table: `gpt-4o-mini` resolves to its own row even
214/// though `gpt-4o` is listed first and also matches it.
215///
216/// A model is priced under every id a provider reports it as, and only those:
217/// Claude Haiku 5.5 is `claude-haiku-5-5` on the Anthropic API and
218/// `claude-haiku-5.5` on Copilot, and no `claude-haiku-5` family prefix exists,
219/// so a later Haiku never inherits Haiku 5.5's card before it is priced.
220/// Opus 5 and Sonnet 5 are the exception: their exact id (`claude-opus-5`,
221/// `claude-sonnet-5`) is also the stem of every later 5.x id, so under prefix
222/// matching an unpriced Opus or Sonnet 5.x bills its 5.0 card until it gets
223/// a row of its own.
224pub const PRICING_TABLE: &[(&str, &str, ModelPricing)] = &[
225 // Gemini models (provider name matches GeminiProvider::name() = "gemini")
226 // gemini-flash-lite-latest is a Google-maintained rolling alias to the
227 // current GA flash-lite tier; prices track whatever that tier costs today.
228 (
229 "gemini",
230 "gemini-flash-lite-latest",
231 ModelPricing::new(0.10, 0.40),
232 ),
233 (
234 "gemini",
235 "gemini-2.5-flash-lite",
236 ModelPricing::new(0.10, 0.40),
237 ),
238 ("gemini", "gemini-2.5-pro", ModelPricing::new(1.25, 10.0)),
239 ("gemini", "gemini-2.5-flash", ModelPricing::new(0.15, 0.60)),
240 ("gemini", "gemini-2.0-flash", ModelPricing::new(0.075, 0.30)),
241 // Anthropic Messages API (AnthropicProvider::name() = "anthropic"; pool
242 // accounts "anthropic#N" price through pool_family). Cache reads bill at
243 // 0.10x input — 0.05x on Opus 5.5 ($0.20/MTok) — and 5-minute cache
244 // writes at 1.25x. Haiku 5.5 has two rate cards: $0.10/$0.50 for a
245 // prompt of 100K tokens or fewer, $0.50/$2.50 above that.
246 (
247 "anthropic",
248 "claude-opus-5-5",
249 ModelPricing::new(4.0, 20.0).with_cache_rates(0.05, 1.25),
250 ),
251 (
252 "anthropic",
253 "claude-sonnet-5-5",
254 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
255 ),
256 (
257 "anthropic",
258 "claude-haiku-5-5",
259 ModelPricing::new(0.10, 0.50)
260 .with_cache_rates(0.10, 1.25)
261 .with_long_prompt_rates(100_000, 0.50, 2.50),
262 ),
263 // Groq models
264 ("groq", "llama-3.3-70b", ModelPricing::new(0.59, 0.79)),
265 ("groq", "mixtral", ModelPricing::new(0.24, 0.24)),
266 ("groq", "llama-3.1-8b", ModelPricing::new(0.05, 0.08)),
267 // Copilot headless (embacle) — proxies to Anthropic Claude models
268 (
269 "copilot_headless",
270 // The Opus 4 family row, $15/$75. Opus 4.6 through 5.5 bill under
271 // their own rows below, which the longest-prefix rule picks ahead of
272 // this one; only Opus 4.5, 4.1 and 4.0 (`claude-opus-4.5`,
273 // `claude-opus-4-5`, `claude-opus-4-1`, `claude-opus-4-0`) resolve
274 // here.
275 "claude-opus-4",
276 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
277 ),
278 (
279 "copilot_headless",
280 // Version-agnostic Sonnet prefix for Sonnet 4.x (claude-sonnet-4,
281 // -4.5, -4.6) at the $3/$15 Sonnet rate, so a bump keeps shadow-COGS
282 // attributed instead of falling through to $0. Sonnet 5 ($2/$10) and
283 // Sonnet 5.5 ($2/$10) bill under their own rows, which the
284 // longest-prefix rule picks ahead of this one.
285 "claude-sonnet",
286 ModelPricing::new(3.0, 15.0).with_cache_rates(0.10, 1.25),
287 ),
288 // Claude Opus 4.6 through 5.5 and Sonnet 5, each under Copilot's dotted
289 // id and Anthropic's hyphenated one (Opus 5 and Sonnet 5 have no minor
290 // version, so one id serves both). Opus 5.5 is $4/$20 with cache reads at
291 // 0.05x; Opus 5, 4.8, 4.7 and 4.6 are $5/$25; Sonnet 5 is $2/$10.
292 // Copilot's `-fast` tiers keep their own rows below.
293 (
294 "copilot_headless",
295 "claude-opus-5.5",
296 ModelPricing::new(4.0, 20.0).with_cache_rates(0.05, 1.25),
297 ),
298 (
299 "copilot_headless",
300 "claude-opus-5-5",
301 ModelPricing::new(4.0, 20.0).with_cache_rates(0.05, 1.25),
302 ),
303 (
304 "copilot_headless",
305 "claude-opus-5",
306 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
307 ),
308 (
309 "copilot_headless",
310 "claude-opus-4.8",
311 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
312 ),
313 (
314 "copilot_headless",
315 "claude-opus-4-8",
316 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
317 ),
318 (
319 "copilot_headless",
320 "claude-opus-4.7",
321 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
322 ),
323 (
324 "copilot_headless",
325 "claude-opus-4-7",
326 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
327 ),
328 (
329 "copilot_headless",
330 "claude-opus-4.6",
331 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
332 ),
333 (
334 "copilot_headless",
335 "claude-opus-4-6",
336 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
337 ),
338 // Copilot's `-fast` tier of Opus 4.8 at its fast-mode list price,
339 // $10/$50 (2x the $5/$25 base), ahead of the base rows it would
340 // otherwise prefix-match.
341 (
342 "copilot_headless",
343 "claude-opus-4.8-fast",
344 ModelPricing::new(10.0, 50.0).with_cache_rates(0.10, 1.25),
345 ),
346 (
347 "copilot_headless",
348 "claude-opus-4-8-fast",
349 ModelPricing::new(10.0, 50.0).with_cache_rates(0.10, 1.25),
350 ),
351 // Copilot's `-fast` tiers of Opus 4.7 and 4.6. The reference gives no
352 // list price for either (Anthropic retired both), and fast mode bills at
353 // a premium over the base model ($10/$50 on Opus 4.8 and 5, 2x base), so
354 // these bill at the $15/$75 of the `claude-opus-4` family row rather
355 // than at the $5/$25 base rows they would otherwise prefix-match.
356 (
357 "copilot_headless",
358 "claude-opus-4.7-fast",
359 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
360 ),
361 (
362 "copilot_headless",
363 "claude-opus-4-7-fast",
364 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
365 ),
366 (
367 "copilot_headless",
368 "claude-opus-4.6-fast",
369 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
370 ),
371 (
372 "copilot_headless",
373 "claude-opus-4-6-fast",
374 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
375 ),
376 (
377 "copilot_headless",
378 "claude-sonnet-5",
379 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
380 ),
381 // Claude Sonnet 5.5 and Haiku 5.5, each under Copilot's dotted id and
382 // Anthropic's hyphenated one. Exact ids, never a `claude-haiku-5` family
383 // prefix; the longest-prefix rule puts the Sonnet 5.5 rows ahead of the
384 // version-agnostic `claude-sonnet` row above. Haiku 5.5 carries its base
385 // card only ($0.10/$0.50, the card for a prompt of 100K tokens or fewer):
386 // this runner reports one usage for a whole turn, summed over every model
387 // request in it, so no single request's prompt length reaches the lookup
388 // to choose the $0.50/$2.50 long-prompt card the way the `anthropic` rows do.
389 (
390 "copilot_headless",
391 "claude-sonnet-5.5",
392 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
393 ),
394 (
395 "copilot_headless",
396 "claude-sonnet-5-5",
397 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
398 ),
399 (
400 "copilot_headless",
401 "claude-haiku-5.5",
402 ModelPricing::new(0.10, 0.50).with_cache_rates(0.10, 1.25),
403 ),
404 (
405 "copilot_headless",
406 "claude-haiku-5-5",
407 ModelPricing::new(0.10, 0.50).with_cache_rates(0.10, 1.25),
408 ),
409 (
410 "copilot_headless",
411 // Claude Haiku 4.5 at its $1/$5 list price.
412 "claude-haiku-4",
413 ModelPricing::new(1.0, 5.0).with_cache_rates(0.10, 1.25),
414 ),
415 // Copilot SDK (the Rust runtime) — the same Anthropic pass-through as
416 // copilot_headless, keyed on the name CopilotSdkRunner reports.
417 (
418 "copilot_sdk",
419 // Opus 4.5, 4.1 and 4.0 only, as on the copilot_headless row.
420 "claude-opus-4",
421 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
422 ),
423 (
424 "copilot_sdk",
425 "claude-sonnet",
426 ModelPricing::new(3.0, 15.0).with_cache_rates(0.10, 1.25),
427 ),
428 // Opus 4.6 through 5.5 and Sonnet 5 under both ids, for the reasons
429 // given on the copilot_headless rows.
430 (
431 "copilot_sdk",
432 "claude-opus-5.5",
433 ModelPricing::new(4.0, 20.0).with_cache_rates(0.05, 1.25),
434 ),
435 (
436 "copilot_sdk",
437 "claude-opus-5-5",
438 ModelPricing::new(4.0, 20.0).with_cache_rates(0.05, 1.25),
439 ),
440 (
441 "copilot_sdk",
442 "claude-opus-5",
443 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
444 ),
445 (
446 "copilot_sdk",
447 "claude-opus-4.8",
448 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
449 ),
450 (
451 "copilot_sdk",
452 "claude-opus-4-8",
453 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
454 ),
455 (
456 "copilot_sdk",
457 "claude-opus-4.7",
458 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
459 ),
460 (
461 "copilot_sdk",
462 "claude-opus-4-7",
463 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
464 ),
465 (
466 "copilot_sdk",
467 "claude-opus-4.6",
468 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
469 ),
470 (
471 "copilot_sdk",
472 "claude-opus-4-6",
473 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
474 ),
475 // Copilot's `-fast` tier of Opus 4.8 at its fast-mode list price,
476 // $10/$50 (2x the $5/$25 base), ahead of the base rows it would
477 // otherwise prefix-match.
478 (
479 "copilot_sdk",
480 "claude-opus-4.8-fast",
481 ModelPricing::new(10.0, 50.0).with_cache_rates(0.10, 1.25),
482 ),
483 (
484 "copilot_sdk",
485 "claude-opus-4-8-fast",
486 ModelPricing::new(10.0, 50.0).with_cache_rates(0.10, 1.25),
487 ),
488 // Copilot's `-fast` tiers of Opus 4.7 and 4.6. The reference gives no
489 // list price for either (Anthropic retired both), and fast mode bills at
490 // a premium over the base model ($10/$50 on Opus 4.8 and 5, 2x base), so
491 // these bill at the $15/$75 of the `claude-opus-4` family row rather
492 // than at the $5/$25 base rows they would otherwise prefix-match.
493 (
494 "copilot_sdk",
495 "claude-opus-4.7-fast",
496 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
497 ),
498 (
499 "copilot_sdk",
500 "claude-opus-4-7-fast",
501 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
502 ),
503 (
504 "copilot_sdk",
505 "claude-opus-4.6-fast",
506 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
507 ),
508 (
509 "copilot_sdk",
510 "claude-opus-4-6-fast",
511 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
512 ),
513 (
514 "copilot_sdk",
515 "claude-sonnet-5",
516 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
517 ),
518 // Sonnet 5.5 and Haiku 5.5 under both ids, base card only, for the
519 // reasons given on the copilot_headless rows.
520 (
521 "copilot_sdk",
522 "claude-sonnet-5.5",
523 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
524 ),
525 (
526 "copilot_sdk",
527 "claude-sonnet-5-5",
528 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
529 ),
530 (
531 "copilot_sdk",
532 "claude-haiku-5.5",
533 ModelPricing::new(0.10, 0.50).with_cache_rates(0.10, 1.25),
534 ),
535 (
536 "copilot_sdk",
537 "claude-haiku-5-5",
538 ModelPricing::new(0.10, 0.50).with_cache_rates(0.10, 1.25),
539 ),
540 (
541 "copilot_sdk",
542 // Claude Haiku 4.5 at its $1/$5 list price.
543 "claude-haiku-4",
544 ModelPricing::new(1.0, 5.0).with_cache_rates(0.10, 1.25),
545 ),
546 // Claude Code CLI — same models as copilot_headless. Keyed "claude-code",
547 // the name ClaudeCodeRunner reports; CliRunnerType's Display prints the
548 // underscore spelling, but name() is what a caller's usage record carries.
549 (
550 "claude-code",
551 // Opus 4.5, 4.1 and 4.0 only, as on the copilot_headless row.
552 "claude-opus-4",
553 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
554 ),
555 (
556 "claude-code",
557 // Version-agnostic Sonnet prefix for Sonnet 4.x at $3/$15, mirroring
558 // the copilot_headless entry so a model bump keeps shadow-COGS
559 // attributed instead of falling through to $0. Sonnet 5 and 5.5 have
560 // their own rows, which the longest-prefix rule picks first.
561 "claude-sonnet",
562 ModelPricing::new(3.0, 15.0).with_cache_rates(0.10, 1.25),
563 ),
564 // Opus 4.6 through 5.5 and Sonnet 5 under both ids, for the reasons
565 // given on the copilot_headless rows.
566 (
567 "claude-code",
568 "claude-opus-5.5",
569 ModelPricing::new(4.0, 20.0).with_cache_rates(0.05, 1.25),
570 ),
571 (
572 "claude-code",
573 "claude-opus-5-5",
574 ModelPricing::new(4.0, 20.0).with_cache_rates(0.05, 1.25),
575 ),
576 (
577 "claude-code",
578 "claude-opus-5",
579 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
580 ),
581 (
582 "claude-code",
583 "claude-opus-4.8",
584 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
585 ),
586 (
587 "claude-code",
588 "claude-opus-4-8",
589 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
590 ),
591 (
592 "claude-code",
593 "claude-opus-4.7",
594 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
595 ),
596 (
597 "claude-code",
598 "claude-opus-4-7",
599 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
600 ),
601 (
602 "claude-code",
603 "claude-opus-4.6",
604 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
605 ),
606 (
607 "claude-code",
608 "claude-opus-4-6",
609 ModelPricing::new(5.0, 25.0).with_cache_rates(0.10, 1.25),
610 ),
611 // Copilot's `-fast` tier of Opus 4.8 at its fast-mode list price,
612 // $10/$50 (2x the $5/$25 base), ahead of the base rows it would
613 // otherwise prefix-match.
614 (
615 "claude-code",
616 "claude-opus-4.8-fast",
617 ModelPricing::new(10.0, 50.0).with_cache_rates(0.10, 1.25),
618 ),
619 (
620 "claude-code",
621 "claude-opus-4-8-fast",
622 ModelPricing::new(10.0, 50.0).with_cache_rates(0.10, 1.25),
623 ),
624 // Copilot's `-fast` tiers of Opus 4.7 and 4.6. The reference gives no
625 // list price for either (Anthropic retired both), and fast mode bills at
626 // a premium over the base model ($10/$50 on Opus 4.8 and 5, 2x base), so
627 // these bill at the $15/$75 of the `claude-opus-4` family row rather
628 // than at the $5/$25 base rows they would otherwise prefix-match.
629 (
630 "claude-code",
631 "claude-opus-4.7-fast",
632 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
633 ),
634 (
635 "claude-code",
636 "claude-opus-4-7-fast",
637 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
638 ),
639 (
640 "claude-code",
641 "claude-opus-4.6-fast",
642 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
643 ),
644 (
645 "claude-code",
646 "claude-opus-4-6-fast",
647 ModelPricing::new(15.0, 75.0).with_cache_rates(0.10, 1.25),
648 ),
649 (
650 "claude-code",
651 "claude-sonnet-5",
652 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
653 ),
654 // Sonnet 5.5 and Haiku 5.5 under both ids, base card only, for the
655 // reasons given on the copilot_headless rows.
656 (
657 "claude-code",
658 "claude-sonnet-5.5",
659 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
660 ),
661 (
662 "claude-code",
663 "claude-sonnet-5-5",
664 ModelPricing::new(2.0, 10.0).with_cache_rates(0.10, 1.25),
665 ),
666 (
667 "claude-code",
668 "claude-haiku-5.5",
669 ModelPricing::new(0.10, 0.50).with_cache_rates(0.10, 1.25),
670 ),
671 (
672 "claude-code",
673 "claude-haiku-5-5",
674 ModelPricing::new(0.10, 0.50).with_cache_rates(0.10, 1.25),
675 ),
676 (
677 "claude-code",
678 // Claude Haiku 4.5 at its $1/$5 list price.
679 "claude-haiku-4",
680 ModelPricing::new(1.0, 5.0).with_cache_rates(0.10, 1.25),
681 ),
682 // Cohere — Command A and Command R family.
683 // The longest matching prefix wins, so `command-a-reasoning` and
684 // `command-a-vision` resolve ahead of the bare `command-a` prefix and the
685 // R-family entries don't accidentally swallow R+ / R7B.
686 (
687 "cohere",
688 "command-a-reasoning",
689 ModelPricing::new(2.50, 10.0),
690 ),
691 ("cohere", "command-a-vision", ModelPricing::new(2.50, 10.0)),
692 ("cohere", "command-a", ModelPricing::new(2.50, 10.0)),
693 ("cohere", "command-r-plus", ModelPricing::new(2.50, 10.0)),
694 ("cohere", "command-r7b", ModelPricing::new(0.0375, 0.15)),
695 ("cohere", "command-r", ModelPricing::new(0.15, 0.60)),
696 // OpenAI API models
697 (
698 "openai_api",
699 "gpt-4o",
700 ModelPricing::new(2.50, 10.0).with_cache_rates(0.50, 1.00),
701 ),
702 (
703 "openai_api",
704 "gpt-4o-mini",
705 ModelPricing::new(0.15, 0.60).with_cache_rates(0.50, 1.00),
706 ),
707 // OpenRouter — the gateway passes through the underlying model's published
708 // price. These cover the curated default slugs the OpenRouter provider
709 // ships (its default model + AVAILABLE_MODELS); any other slug is logged
710 // as model-specific-unset rather than silently $0.
711 // Prefixes carry the provider/model slug so prefix matching still resolves
712 // dated or suffixed variants (e.g. `:free`, `-001`).
713 (
714 "openrouter",
715 "meta-llama/llama-3.3-70b-instruct",
716 ModelPricing::new(0.12, 0.30),
717 ),
718 (
719 "openrouter",
720 "meta-llama/llama-3.1-8b-instruct",
721 ModelPricing::new(0.02, 0.03),
722 ),
723 (
724 "openrouter",
725 "anthropic/claude-3.5-sonnet",
726 ModelPricing::new(3.0, 15.0).with_cache_rates(0.10, 1.25),
727 ),
728 (
729 "openrouter",
730 "anthropic/claude-3.5-haiku",
731 ModelPricing::new(0.80, 4.0).with_cache_rates(0.10, 1.25),
732 ),
733 (
734 "openrouter",
735 "openai/gpt-4o-mini",
736 ModelPricing::new(0.15, 0.60).with_cache_rates(0.50, 1.00),
737 ),
738 (
739 "openrouter",
740 "openai/gpt-4o",
741 ModelPricing::new(2.50, 10.0).with_cache_rates(0.50, 1.00),
742 ),
743 (
744 "openrouter",
745 "google/gemini-2.0-flash-001",
746 ModelPricing::new(0.10, 0.40),
747 ),
748 (
749 "openrouter",
750 "google/gemini-pro-1.5",
751 ModelPricing::new(1.25, 5.0),
752 ),
753 (
754 "openrouter",
755 "mistralai/mistral-large",
756 ModelPricing::new(2.0, 6.0),
757 ),
758 (
759 "openrouter",
760 "mistralai/mistral-nemo",
761 ModelPricing::new(0.03, 0.07),
762 ),
763 (
764 "openrouter",
765 "qwen/qwen-2.5-72b-instruct",
766 ModelPricing::new(0.13, 0.40),
767 ),
768];
769
770/// Look up pricing for a (provider, model) pair using prefix matching.
771///
772/// Of the provider's rows whose prefix the model starts with, the longest
773/// prefix wins, so a broad row (`gpt-4o`, `claude-sonnet`) never shadows a
774/// specific one (`gpt-4o-mini`, `claude-sonnet-5.5`) whatever the table order.
775///
776/// Public so a consumer's override layer can fall through to the table
777/// after its own lookup misses.
778#[must_use]
779pub fn lookup_pricing(provider: &str, model: &str) -> Option<ModelPricing> {
780 let provider = pool_family(provider);
781 PRICING_TABLE
782 .iter()
783 .filter(|(p, prefix, _)| *p == provider && model.starts_with(prefix))
784 .max_by_key(|(_, prefix, _)| prefix.len())
785 .map(|(_, _, pricing)| *pricing)
786}
787
788/// The provider a pooled tier prices as: `claude-code#2` is a second Claude
789/// account, billed like the first. See [`crate::pool`].
790#[must_use]
791pub fn pool_family(provider: &str) -> &str {
792 provider
793 .split_once('#')
794 .map_or(provider, |(family, _)| family)
795}
796
797/// True when a provider bills via flat-rate subscription or self-hosting, so a
798/// \$0 per-token cost is correct rather than a missing-price undercount.
799#[must_use]
800pub fn is_not_per_token_metered(provider: &str) -> bool {
801 NOT_PER_TOKEN_METERED_PROVIDERS.contains(&pool_family(provider))
802}
803
804/// Log the resolution of a (provider, model) pair that has no `PRICING_TABLE`
805/// or override entry, at a severity matching *why* it is unpriced, and return
806/// the \$0 cost all three paths fall back to.
807///
808/// - Subscription / self-hosted providers: \$0 is correct — `debug!` only, no
809/// undercount warning.
810/// - `OpenRouter`: model-specific price genuinely unset for this slug —
811/// `info!` with a distinct, non-alarming message.
812/// - Anything else: a genuinely-unexpected miss that *does* undercount —
813/// keep the `warn!`.
814///
815/// `scope` is whatever the caller keys its overrides by (an account, an
816/// organisation) and only decorates the warning.
817#[must_use]
818pub fn zero_cost_for_unpriced(provider: &str, model: &str, scope: Option<&str>) -> f64 {
819 if is_not_per_token_metered(provider) {
820 debug!(
821 provider = provider,
822 model = model,
823 "Provider is subscription/self-hosted; per-token cost is $0 by design (not an undercount)"
824 );
825 } else if provider == OPENROUTER_PROVIDER {
826 info!(
827 provider = provider,
828 model = model,
829 "OpenRouter model price is model-specific and unset for this slug; recording $0 (add a PRICING_TABLE or admin override to meter it)"
830 );
831 } else {
832 warn!(
833 provider = provider,
834 model = model,
835 scope = scope,
836 "No pricing data for model, cost will be recorded as 0.0"
837 );
838 }
839 0.0
840}
841
842/// The token counts a single LLM call reports, in the shape they bill.
843///
844/// `cached_read` and `cached_write` are both subsets of `prompt`: a provider
845/// reports the gross prompt count and then breaks out how much of it was
846/// served from cache and how much was written into it. `reasoning` is the
847/// exception — providers that report it exclude it from `completion`, so it
848/// is additive on the output side.
849#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
850pub struct TokenCounts {
851 /// Gross prompt tokens, cached portions included.
852 pub prompt: i64,
853 /// Prompt tokens served from the provider's context cache.
854 pub cached_read: i64,
855 /// Prompt tokens written into the provider's context cache this call.
856 pub cached_write: i64,
857 /// Completion tokens, excluding separately-reported reasoning tokens.
858 pub completion: i64,
859 /// Reasoning / "thought" tokens, when the provider reports them apart
860 /// from `completion`. Billed at the output rate.
861 pub reasoning: i64,
862}
863
864impl TokenCounts {
865 /// The two counts every provider reports.
866 #[must_use]
867 pub const fn new(prompt: i64, completion: i64) -> Self {
868 Self {
869 prompt,
870 cached_read: 0,
871 cached_write: 0,
872 completion,
873 reasoning: 0,
874 }
875 }
876
877 /// Attach the cache read/write split of `prompt`.
878 #[must_use]
879 pub const fn with_cache(mut self, read: i64, write: i64) -> Self {
880 self.cached_read = read;
881 self.cached_write = write;
882 self
883 }
884
885 /// Attach a separately-reported reasoning-token count.
886 #[must_use]
887 pub const fn with_reasoning(mut self, reasoning: i64) -> Self {
888 self.reasoning = reasoning;
889 self
890 }
891}
892
893/// Calculate the cost of an LLM request using compile-time pricing.
894///
895/// Returns the cost in USD. Returns 0.0 for unknown provider/model combinations
896/// (with a warning log). Treats the whole prompt as uncached and assumes no
897/// separately-reported reasoning tokens — callers holding either count must
898/// use [`calculate_cost_with_cache`] or [`calculate_cost_for`].
899#[must_use]
900pub fn calculate_cost(
901 provider: &str,
902 model: &str,
903 prompt_tokens: i64,
904 completion_tokens: i64,
905) -> f64 {
906 calculate_cost_for(
907 provider,
908 model,
909 &TokenCounts::new(prompt_tokens, completion_tokens),
910 )
911}
912
913/// Calculate the cost of an LLM request from a full [`TokenCounts`].
914///
915/// The complete entry point: every count the provider reported, priced with
916/// that model's own cache multipliers. Returns 0.0 for unpriced pairs on the
917/// same terms as [`calculate_cost_with_cache`].
918#[must_use]
919pub fn calculate_cost_for(provider: &str, model: &str, counts: &TokenCounts) -> f64 {
920 let Some(pricing) = lookup_pricing(provider, model) else {
921 return zero_cost_for_unpriced(provider, model, None);
922 };
923
924 cost_from_pricing(&pricing, counts)
925}
926
927/// Calculate the cost of an LLM request, breaking the prompt into cached + fresh tokens.
928///
929/// Cache-read tokens bill at the model's [`ModelPricing::cache_read_multiplier`]
930/// of its input rate; the remaining `(prompt_tokens - cached_tokens)` bill at
931/// the full input rate. Completion tokens always bill at the full output rate.
932/// Reports no cache *writes* — use [`calculate_cost_for`] when the provider
933/// broke those out. Returns
934/// 0.0 for unpriced (provider, model) pairs — the $0 fallback is deliberate
935/// so a missing pricing entry never blocks a chat turn. The log severity of
936/// that fallback depends on *why* the pair is unpriced (see
937/// [`zero_cost_for_unpriced`]): subscription/self-hosted providers log at
938/// debug (their $0 is correct), `OpenRouter` logs an honest model-specific
939/// notice, and only a genuinely-unexpected miss emits the undercount warning.
940#[must_use]
941pub fn calculate_cost_with_cache(
942 provider: &str,
943 model: &str,
944 prompt_tokens: i64,
945 cached_tokens: i64,
946 completion_tokens: i64,
947) -> f64 {
948 calculate_cost_for(
949 provider,
950 model,
951 &TokenCounts::new(prompt_tokens, completion_tokens).with_cache(cached_tokens, 0),
952 )
953}
954
955/// Compute the USD cost from a resolved [`ModelPricing`] + token counts.
956/// Shared by the compile-time lookup and any override path a consumer
957/// layers on top, so both sources price a call identically.
958///
959/// Four terms, because a prompt token can bill at three different rates:
960/// fresh input, a cache read at a discount, and a cache write at a premium.
961/// Reasoning tokens join completion on the output side.
962#[must_use]
963pub fn cost_from_pricing(pricing: &ModelPricing, counts: &TokenCounts) -> f64 {
964 let divisor = 1_000_000.0;
965 let prompt = counts.prompt.max(0);
966 // Reads and writes are carved out of the gross prompt in that order, so a
967 // provider that over-reports one can never push the fresh remainder below
968 // zero or bill a token twice.
969 let cached_read = counts.cached_read.clamp(0, prompt);
970 let cached_write = counts.cached_write.clamp(0, prompt - cached_read);
971 let fresh_prompt = prompt - cached_read - cached_write;
972
973 let (input, output) = pricing.rates_for_prompt(prompt);
974 let fresh_prompt_cost = fresh_prompt as f64 * input / divisor;
975 let cached_read_cost = cached_read as f64 * input * pricing.cache_read_multiplier / divisor;
976 let cached_write_cost = cached_write as f64 * input * pricing.cache_write_multiplier / divisor;
977 // Providers that report reasoning tokens exclude them from the completion
978 // count, so they are added rather than carved out.
979 let output_tokens = counts.completion.max(0) + counts.reasoning.max(0);
980 let output_cost = output_tokens as f64 * output / divisor;
981
982 fresh_prompt_cost + cached_read_cost + cached_write_cost + output_cost
983}