Skip to main content

llm_verify/
pricing.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Built-in list price table, USD per million tokens.
3//!
4//! Used to turn a token-count discrepancy into the number a buyer actually
5//! cares about: how much extra they were charged. Matching is by longest
6//! substring so `claude-opus-4-5-20251101` and `anthropic/claude-opus-4.5`
7//! both resolve, while an unknown model resolves to `None` — the audit then
8//! reports the token ratio and explicitly says no price was applied, rather
9//! than inventing a dollar figure.
10
11#[derive(Debug, Clone, Copy)]
12pub struct Price {
13    pub family: &'static str,
14    /// USD per million input tokens.
15    pub input: f64,
16    /// USD per million output tokens.
17    pub output: f64,
18}
19
20/// Longest pattern wins, so `gpt-4o-mini` never matches the `gpt-4o` row.
21const TABLE: &[(&str, Price)] = &[
22    // Anthropic
23    (
24        "claude-opus",
25        Price {
26            family: "Claude Opus",
27            input: 15.0,
28            output: 75.0,
29        },
30    ),
31    (
32        "claude-sonnet",
33        Price {
34            family: "Claude Sonnet",
35            input: 3.0,
36            output: 15.0,
37        },
38    ),
39    (
40        "claude-haiku",
41        Price {
42            family: "Claude Haiku",
43            input: 0.80,
44            output: 4.0,
45        },
46    ),
47    (
48        "claude-3-5-sonnet",
49        Price {
50            family: "Claude 3.5 Sonnet",
51            input: 3.0,
52            output: 15.0,
53        },
54    ),
55    (
56        "claude-3-5-haiku",
57        Price {
58            family: "Claude 3.5 Haiku",
59            input: 0.80,
60            output: 4.0,
61        },
62    ),
63    (
64        "claude-3-opus",
65        Price {
66            family: "Claude 3 Opus",
67            input: 15.0,
68            output: 75.0,
69        },
70    ),
71    // OpenAI
72    (
73        "gpt-4o-mini",
74        Price {
75            family: "GPT-4o mini",
76            input: 0.15,
77            output: 0.60,
78        },
79    ),
80    (
81        "gpt-4o",
82        Price {
83            family: "GPT-4o",
84            input: 2.50,
85            output: 10.0,
86        },
87    ),
88    (
89        "gpt-4.1-mini",
90        Price {
91            family: "GPT-4.1 mini",
92            input: 0.40,
93            output: 1.60,
94        },
95    ),
96    (
97        "gpt-4.1-nano",
98        Price {
99            family: "GPT-4.1 nano",
100            input: 0.10,
101            output: 0.40,
102        },
103    ),
104    (
105        "gpt-4.1",
106        Price {
107            family: "GPT-4.1",
108            input: 2.0,
109            output: 8.0,
110        },
111    ),
112    (
113        "gpt-4-turbo",
114        Price {
115            family: "GPT-4 Turbo",
116            input: 10.0,
117            output: 30.0,
118        },
119    ),
120    (
121        "gpt-3.5-turbo",
122        Price {
123            family: "GPT-3.5 Turbo",
124            input: 0.50,
125            output: 1.50,
126        },
127    ),
128    (
129        "o3-mini",
130        Price {
131            family: "o3-mini",
132            input: 1.10,
133            output: 4.40,
134        },
135    ),
136    (
137        "o4-mini",
138        Price {
139            family: "o4-mini",
140            input: 1.10,
141            output: 4.40,
142        },
143    ),
144    (
145        "o3",
146        Price {
147            family: "o3",
148            input: 2.0,
149            output: 8.0,
150        },
151    ),
152    (
153        "o1-mini",
154        Price {
155            family: "o1-mini",
156            input: 1.10,
157            output: 4.40,
158        },
159    ),
160    (
161        "o1",
162        Price {
163            family: "o1",
164            input: 15.0,
165            output: 60.0,
166        },
167    ),
168    // Google
169    (
170        "gemini-2.5-pro",
171        Price {
172            family: "Gemini 2.5 Pro",
173            input: 1.25,
174            output: 10.0,
175        },
176    ),
177    (
178        "gemini-2.5-flash",
179        Price {
180            family: "Gemini 2.5 Flash",
181            input: 0.30,
182            output: 2.50,
183        },
184    ),
185    (
186        "gemini-2.0-flash",
187        Price {
188            family: "Gemini 2.0 Flash",
189            input: 0.10,
190            output: 0.40,
191        },
192    ),
193    (
194        "gemini-1.5-pro",
195        Price {
196            family: "Gemini 1.5 Pro",
197            input: 1.25,
198            output: 5.0,
199        },
200    ),
201    // DeepSeek / Moonshot / Zhipu / Alibaba
202    (
203        "deepseek-reasoner",
204        Price {
205            family: "DeepSeek Reasoner",
206            input: 0.55,
207            output: 2.19,
208        },
209    ),
210    (
211        "deepseek-chat",
212        Price {
213            family: "DeepSeek Chat",
214            input: 0.27,
215            output: 1.10,
216        },
217    ),
218    (
219        "deepseek",
220        Price {
221            family: "DeepSeek",
222            input: 0.27,
223            output: 1.10,
224        },
225    ),
226    (
227        "kimi",
228        Price {
229            family: "Kimi",
230            input: 0.60,
231            output: 2.50,
232        },
233    ),
234    (
235        "moonshot",
236        Price {
237            family: "Moonshot",
238            input: 0.60,
239            output: 2.50,
240        },
241    ),
242    (
243        "glm-4",
244        Price {
245            family: "GLM-4",
246            input: 0.60,
247            output: 0.60,
248        },
249    ),
250    (
251        "glm",
252        Price {
253            family: "GLM",
254            input: 0.60,
255            output: 0.60,
256        },
257    ),
258    (
259        "qwen-max",
260        Price {
261            family: "Qwen Max",
262            input: 1.60,
263            output: 6.40,
264        },
265    ),
266    (
267        "qwen",
268        Price {
269            family: "Qwen",
270            input: 0.40,
271            output: 1.20,
272        },
273    ),
274];
275
276/// List price for a model ID, or `None` when we have no entry.
277pub fn lookup(model: &str) -> Option<Price> {
278    let m = model.trim().to_ascii_lowercase().replace('.', "-");
279    let mut best: Option<(usize, Price)> = None;
280    for (pat, price) in TABLE {
281        let pat_norm = pat.replace('.', "-");
282        if m.contains(&pat_norm) {
283            let len = pat_norm.len();
284            if best.map(|(l, _)| len > l).unwrap_or(true) {
285                best = Some((len, *price));
286            }
287        }
288    }
289    best.map(|(_, p)| p)
290}
291
292/// USD for a given token split. Callers must have a `Price` in hand, which is
293/// only obtainable from `lookup`, so there is no path to a fabricated cost.
294pub fn cost(price: &Price, input_tokens: u32, output_tokens: u32) -> f64 {
295    (input_tokens as f64 * price.input + output_tokens as f64 * price.output) / 1_000_000.0
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn longest_pattern_wins() {
304        // The killer case: gpt-4o-mini must not be priced as gpt-4o.
305        assert_eq!(lookup("gpt-4o-mini").unwrap().family, "GPT-4o mini");
306        assert_eq!(lookup("gpt-4o").unwrap().family, "GPT-4o");
307        assert_eq!(lookup("o3-mini").unwrap().family, "o3-mini");
308        assert_eq!(
309            lookup("deepseek-reasoner").unwrap().family,
310            "DeepSeek Reasoner"
311        );
312    }
313
314    #[test]
315    fn resolves_dated_and_prefixed_ids() {
316        assert_eq!(
317            lookup("claude-opus-4-5-20251101").unwrap().family,
318            "Claude Opus"
319        );
320        assert_eq!(
321            lookup("anthropic/claude-sonnet-4.5").unwrap().family,
322            "Claude Sonnet"
323        );
324        // Dot and dash version styles must land on the same row.
325        assert_eq!(lookup("gpt-4.1-mini").unwrap().family, "GPT-4.1 mini");
326        assert_eq!(lookup("gpt-4-1-mini").unwrap().family, "GPT-4.1 mini");
327    }
328
329    #[test]
330    fn unknown_models_yield_no_price_rather_than_a_guess() {
331        assert!(lookup("some-private-finetune-v3").is_none());
332        assert!(lookup("").is_none());
333    }
334
335    #[test]
336    fn cost_is_per_million_tokens() {
337        let p = Price {
338            family: "t",
339            input: 3.0,
340            output: 15.0,
341        };
342        // 1M input + 1M output at 3 + 15.
343        assert!((cost(&p, 1_000_000, 1_000_000) - 18.0).abs() < 1e-9);
344        assert_eq!(cost(&p, 0, 0), 0.0);
345        // A realistic small call stays in the sub-cent range.
346        assert!(cost(&p, 1000, 500) < 0.02);
347    }
348}