1#[derive(Debug, Clone, Copy)]
12pub struct Price {
13 pub family: &'static str,
14 pub input: f64,
16 pub output: f64,
18}
19
20const TABLE: &[(&str, Price)] = &[
22 (
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 (
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 (
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 (
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
276pub 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
292pub 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 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 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 assert!((cost(&p, 1_000_000, 1_000_000) - 18.0).abs() < 1e-9);
344 assert_eq!(cost(&p, 0, 0), 0.0);
345 assert!(cost(&p, 1000, 500) < 0.02);
347 }
348}