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