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 (
32 "claude-3-5-sonnet",
33 ModelPrice {
34 input_usd_per_1m: 3.0,
35 output_usd_per_1m: 15.0,
36 },
37 ),
38 (
39 "claude-3-5-haiku",
40 ModelPrice {
41 input_usd_per_1m: 0.80,
42 output_usd_per_1m: 4.0,
43 },
44 ),
45 (
46 "claude-3-opus",
47 ModelPrice {
48 input_usd_per_1m: 15.0,
49 output_usd_per_1m: 75.0,
50 },
51 ),
52 (
53 "claude-3-sonnet",
54 ModelPrice {
55 input_usd_per_1m: 3.0,
56 output_usd_per_1m: 15.0,
57 },
58 ),
59 (
60 "claude-3-haiku",
61 ModelPrice {
62 input_usd_per_1m: 0.25,
63 output_usd_per_1m: 1.25,
64 },
65 ),
66 (
68 "claude",
69 ModelPrice {
70 input_usd_per_1m: 3.0,
71 output_usd_per_1m: 15.0,
72 },
73 ),
74 (
75 "gpt-4o-mini",
76 ModelPrice {
77 input_usd_per_1m: 0.15,
78 output_usd_per_1m: 0.60,
79 },
80 ),
81 (
82 "gpt-4o",
83 ModelPrice {
84 input_usd_per_1m: 2.50,
85 output_usd_per_1m: 10.0,
86 },
87 ),
88 (
89 "gpt-4-turbo",
90 ModelPrice {
91 input_usd_per_1m: 10.0,
92 output_usd_per_1m: 30.0,
93 },
94 ),
95 (
96 "gpt-4",
97 ModelPrice {
98 input_usd_per_1m: 30.0,
99 output_usd_per_1m: 60.0,
100 },
101 ),
102 (
103 "gpt-3.5-turbo",
104 ModelPrice {
105 input_usd_per_1m: 0.50,
106 output_usd_per_1m: 1.50,
107 },
108 ),
109 (
110 "deepseek-reasoner",
111 ModelPrice {
112 input_usd_per_1m: 0.55,
113 output_usd_per_1m: 2.19,
114 },
115 ),
116 (
117 "deepseek-chat",
118 ModelPrice {
119 input_usd_per_1m: 0.27,
120 output_usd_per_1m: 1.10,
121 },
122 ),
123 (
124 "qwen",
125 ModelPrice {
126 input_usd_per_1m: 0.50,
127 output_usd_per_1m: 2.0,
128 },
129 ),
130];
131
132pub fn price_for(model: &str) -> Option<ModelPrice> {
137 let lower = model.to_ascii_lowercase();
138 PRICE_TABLE
139 .iter()
140 .find(|(pattern, _)| lower.contains(pattern))
141 .map(|(_, price)| *price)
142}
143
144pub fn estimate_cost_usd(
148 prompt_tokens: usize,
149 completion_tokens: usize,
150 model: &str,
151) -> Option<f64> {
152 let price = price_for(model)?;
153 Some(
154 prompt_tokens as f64 / 1e6 * price.input_usd_per_1m
155 + completion_tokens as f64 / 1e6 * price.output_usd_per_1m,
156 )
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn specific_beats_generic_claude() {
165 let opus = price_for("claude-3-opus-20240229").unwrap();
166 assert_eq!(opus.input_usd_per_1m, 15.0);
167 let sonnet = price_for("claude-3-sonnet-20240229").unwrap();
169 assert_eq!(sonnet.input_usd_per_1m, 3.0);
170 }
171
172 #[test]
173 fn gpt_4o_mini_beats_gpt_4o() {
174 let mini = price_for("gpt-4o-mini").unwrap();
175 assert_eq!(mini.input_usd_per_1m, 0.15);
176 let full = price_for("gpt-4o").unwrap();
177 assert_eq!(full.input_usd_per_1m, 2.50);
178 }
179
180 #[test]
181 fn unknown_model_returns_none() {
182 assert!(price_for("some-future-model").is_none());
183 assert!(estimate_cost_usd(10, 10, "unknown").is_none());
184 }
185
186 #[test]
187 fn estimate_is_per_1m_scaled() {
188 let cost = estimate_cost_usd(1_000_000, 1_000_000, "claude-3-5-sonnet").unwrap();
190 assert!((cost - 18.0).abs() < 1e-9, "got {cost}");
191 let small = estimate_cost_usd(1000, 1000, "claude-3-5-sonnet").unwrap();
193 assert!((small - 0.018).abs() < 1e-9, "got {small}");
194 }
195}