1#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct ModelPrice {
23 pub input_usd_per_1m: f64,
25 pub output_usd_per_1m: f64,
27}
28
29const PRICE_TABLE: &[(&str, ModelPrice)] = &[
31 ("claude-3-5-sonnet", ModelPrice { input_usd_per_1m: 3.0, output_usd_per_1m: 15.0 }),
32 ("claude-3-5-haiku", ModelPrice { input_usd_per_1m: 0.80, output_usd_per_1m: 4.0 }),
33 ("claude-3-opus", ModelPrice { input_usd_per_1m: 15.0, output_usd_per_1m: 75.0 }),
34 ("claude-3-sonnet", ModelPrice { input_usd_per_1m: 3.0, output_usd_per_1m: 15.0 }),
35 ("claude-3-haiku", ModelPrice { input_usd_per_1m: 0.25, output_usd_per_1m: 1.25 }),
36 ("claude", ModelPrice { input_usd_per_1m: 3.0, output_usd_per_1m: 15.0 }),
38 ("gpt-4o-mini", ModelPrice { input_usd_per_1m: 0.15, output_usd_per_1m: 0.60 }),
39 ("gpt-4o", ModelPrice { input_usd_per_1m: 2.50, output_usd_per_1m: 10.0 }),
40 ("gpt-4-turbo", ModelPrice { input_usd_per_1m: 10.0, output_usd_per_1m: 30.0 }),
41 ("gpt-4", ModelPrice { input_usd_per_1m: 30.0, output_usd_per_1m: 60.0 }),
42 ("gpt-3.5-turbo", ModelPrice { input_usd_per_1m: 0.50, output_usd_per_1m: 1.50 }),
43 ("deepseek-reasoner", ModelPrice { input_usd_per_1m: 0.55, output_usd_per_1m: 2.19 }),
44 ("deepseek-chat", ModelPrice { input_usd_per_1m: 0.27, output_usd_per_1m: 1.10 }),
45 ("qwen", ModelPrice { input_usd_per_1m: 0.50, output_usd_per_1m: 2.0 }),
46];
47
48pub fn price_for(model: &str) -> Option<ModelPrice> {
53 let lower = model.to_ascii_lowercase();
54 PRICE_TABLE
55 .iter()
56 .find(|(pattern, _)| lower.contains(pattern))
57 .map(|(_, price)| *price)
58}
59
60pub fn estimate_cost_usd(prompt_tokens: usize, completion_tokens: usize, model: &str) -> Option<f64> {
64 let price = price_for(model)?;
65 Some(
66 prompt_tokens as f64 / 1e6 * price.input_usd_per_1m
67 + completion_tokens as f64 / 1e6 * price.output_usd_per_1m,
68 )
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn specific_beats_generic_claude() {
77 let opus = price_for("claude-3-opus-20240229").unwrap();
78 assert_eq!(opus.input_usd_per_1m, 15.0);
79 let sonnet = price_for("claude-3-sonnet-20240229").unwrap();
81 assert_eq!(sonnet.input_usd_per_1m, 3.0);
82 }
83
84 #[test]
85 fn gpt_4o_mini_beats_gpt_4o() {
86 let mini = price_for("gpt-4o-mini").unwrap();
87 assert_eq!(mini.input_usd_per_1m, 0.15);
88 let full = price_for("gpt-4o").unwrap();
89 assert_eq!(full.input_usd_per_1m, 2.50);
90 }
91
92 #[test]
93 fn unknown_model_returns_none() {
94 assert!(price_for("some-future-model").is_none());
95 assert!(estimate_cost_usd(10, 10, "unknown").is_none());
96 }
97
98 #[test]
99 fn estimate_is_per_1m_scaled() {
100 let cost = estimate_cost_usd(1_000_000, 1_000_000, "claude-3-5-sonnet").unwrap();
102 assert!((cost - 18.0).abs() < 1e-9, "got {cost}");
103 let small = estimate_cost_usd(1000, 1000, "claude-3-5-sonnet").unwrap();
105 assert!((small - 0.018).abs() < 1e-9, "got {small}");
106 }
107}