Skip to main content

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}