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    /// 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}