1use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Provider {
11 OpenAI,
13 Anthropic,
15 Google,
17}
18
19impl fmt::Display for Provider {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 Provider::OpenAI => write!(f, "OpenAI/Azure"),
23 Provider::Anthropic => write!(f, "Anthropic"),
24 Provider::Google => write!(f, "Google"),
25 }
26 }
27}
28
29#[derive(Debug, Clone)]
31pub struct LlmModel {
32 pub name: &'static str,
34 pub provider: Provider,
36 pub context_window: &'static str,
38 pub input_per_1m: f64,
40 pub input_cached_per_1m: f64,
42 pub output_per_1m: f64,
44 pub chars_per_token: f64,
46}
47
48pub static LLM_MODELS: &[LlmModel] = &[
50 LlmModel {
52 name: "GPT-5.2",
53 provider: Provider::OpenAI,
54 context_window: "400K",
55 input_per_1m: 1.75,
56 input_cached_per_1m: 0.175,
57 output_per_1m: 14.0,
58 chars_per_token: 4.0,
59 },
60 LlmModel {
61 name: "GPT-5.2 pro",
62 provider: Provider::OpenAI,
63 context_window: "400K",
64 input_per_1m: 21.0,
65 input_cached_per_1m: 0.0,
66 output_per_1m: 168.0,
67 chars_per_token: 4.0,
68 },
69 LlmModel {
70 name: "GPT-5 mini",
71 provider: Provider::OpenAI,
72 context_window: "400K",
73 input_per_1m: 0.25,
74 input_cached_per_1m: 0.025,
75 output_per_1m: 2.0,
76 chars_per_token: 4.0,
77 },
78 LlmModel {
80 name: "Claude Opus 4.5",
81 provider: Provider::Anthropic,
82 context_window: "200K",
83 input_per_1m: 5.0,
84 input_cached_per_1m: 0.5,
85 output_per_1m: 25.0,
86 chars_per_token: 3.8,
87 },
88 LlmModel {
89 name: "Claude Opus 4",
90 provider: Provider::Anthropic,
91 context_window: "200K",
92 input_per_1m: 15.0,
93 input_cached_per_1m: 1.5,
94 output_per_1m: 75.0,
95 chars_per_token: 3.8,
96 },
97 LlmModel {
98 name: "Claude Sonnet 4.5",
99 provider: Provider::Anthropic,
100 context_window: "200K",
101 input_per_1m: 3.0,
102 input_cached_per_1m: 0.3,
103 output_per_1m: 15.0,
104 chars_per_token: 3.8,
105 },
106 LlmModel {
107 name: "Claude Sonnet 4",
108 provider: Provider::Anthropic,
109 context_window: "200K",
110 input_per_1m: 3.0,
111 input_cached_per_1m: 0.3,
112 output_per_1m: 15.0,
113 chars_per_token: 3.8,
114 },
115 LlmModel {
116 name: "Claude Haiku 4.5",
117 provider: Provider::Anthropic,
118 context_window: "200K",
119 input_per_1m: 1.0,
120 input_cached_per_1m: 0.1,
121 output_per_1m: 5.0,
122 chars_per_token: 3.8,
123 },
124 LlmModel {
125 name: "Claude Haiku 3.5",
126 provider: Provider::Anthropic,
127 context_window: "200K",
128 input_per_1m: 0.8,
129 input_cached_per_1m: 0.08,
130 output_per_1m: 4.0,
131 chars_per_token: 3.8,
132 },
133 LlmModel {
135 name: "Gemini 3 Pro (Preview)",
136 provider: Provider::Google,
137 context_window: "1M",
138 input_per_1m: 2.0,
139 input_cached_per_1m: 0.2,
140 output_per_1m: 12.0,
141 chars_per_token: 4.2,
142 },
143 LlmModel {
144 name: "Gemini 3 Flash (Preview)",
145 provider: Provider::Google,
146 context_window: "1M",
147 input_per_1m: 0.5,
148 input_cached_per_1m: 0.05,
149 output_per_1m: 3.0,
150 chars_per_token: 4.2,
151 },
152 LlmModel {
153 name: "Gemini 2.5 Pro",
154 provider: Provider::Google,
155 context_window: "2M",
156 input_per_1m: 1.25,
157 input_cached_per_1m: 0.125,
158 output_per_1m: 10.0,
159 chars_per_token: 4.2,
160 },
161 LlmModel {
162 name: "Gemini 2.5 Flash",
163 provider: Provider::Google,
164 context_window: "1M",
165 input_per_1m: 0.3,
166 input_cached_per_1m: 0.03,
167 output_per_1m: 2.5,
168 chars_per_token: 4.2,
169 },
170 LlmModel {
171 name: "Gemini 2.5 Flash-Lite",
172 provider: Provider::Google,
173 context_window: "1M",
174 input_per_1m: 0.1,
175 input_cached_per_1m: 0.01,
176 output_per_1m: 0.4,
177 chars_per_token: 4.2,
178 },
179 LlmModel {
180 name: "Gemini 2.0 Flash",
181 provider: Provider::Google,
182 context_window: "1M",
183 input_per_1m: 0.1,
184 input_cached_per_1m: 0.025,
185 output_per_1m: 0.4,
186 chars_per_token: 4.2,
187 },
188 LlmModel {
189 name: "Gemini 2.0 Flash-Lite",
190 provider: Provider::Google,
191 context_window: "1M",
192 input_per_1m: 0.075,
193 input_cached_per_1m: 0.0,
194 output_per_1m: 0.3,
195 chars_per_token: 4.2,
196 },
197];
198
199impl LlmModel {
200 pub fn estimate_tokens(&self, text: &str) -> usize {
202 let char_count = text.chars().count();
203 ((char_count as f64) / self.chars_per_token).ceil() as usize
204 }
205
206 pub fn calculate_input_cost(&self, tokens: usize) -> f64 {
208 (tokens as f64 / 1_000_000.0) * self.input_per_1m
209 }
210
211 pub fn calculate_cached_cost(&self, tokens: usize) -> f64 {
213 (tokens as f64 / 1_000_000.0) * self.input_cached_per_1m
214 }
215
216 pub fn calculate_output_cost(&self, tokens: usize) -> f64 {
218 (tokens as f64 / 1_000_000.0) * self.output_per_1m
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct TokenAnalysis {
225 pub model_name: &'static str,
227 pub provider: Provider,
229 pub context_window: &'static str,
231 pub tokens: usize,
233 pub input_cost: f64,
235 pub cached_cost: f64,
237 pub output_cost: f64,
239}
240
241pub fn analyze_all_models(text: &str) -> Vec<TokenAnalysis> {
243 LLM_MODELS
244 .iter()
245 .map(|model| {
246 let tokens = model.estimate_tokens(text);
247 TokenAnalysis {
248 model_name: model.name,
249 provider: model.provider,
250 context_window: model.context_window,
251 tokens,
252 input_cost: model.calculate_input_cost(tokens),
253 cached_cost: model.calculate_cached_cost(tokens),
254 output_cost: model.calculate_output_cost(tokens),
255 }
256 })
257 .collect()
258}
259
260pub fn format_cost(cost: f64) -> String {
262 if cost == 0.0 {
263 "$0.0000".to_string()
264 } else if cost < 0.0001 {
265 "<$0.0001".to_string()
266 } else if cost < 1.0 {
267 format!("${:.4}", cost)
269 } else {
270 format!("${:.2}", cost)
271 }
272}
273
274pub fn format_tokens(tokens: usize) -> String {
276 if tokens >= 1_000_000 {
277 format!("{:.2}M", tokens as f64 / 1_000_000.0)
278 } else if tokens >= 1_000 {
279 format!("{:.1}K", tokens as f64 / 1_000.0)
280 } else {
281 tokens.to_string()
282 }
283}
284
285pub fn models_by_provider() -> Vec<(Provider, Vec<&'static LlmModel>)> {
287 let mut openai = Vec::new();
288 let mut anthropic = Vec::new();
289 let mut google = Vec::new();
290
291 for model in LLM_MODELS {
292 match model.provider {
293 Provider::OpenAI => openai.push(model),
294 Provider::Anthropic => anthropic.push(model),
295 Provider::Google => google.push(model),
296 }
297 }
298
299 vec![
300 (Provider::OpenAI, openai),
301 (Provider::Anthropic, anthropic),
302 (Provider::Google, google),
303 ]
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_token_estimation() {
312 let text = "Hello, world!"; let gpt5 = &LLM_MODELS[0]; assert_eq!(gpt5.estimate_tokens(text), 4); }
316
317 #[test]
318 fn test_cost_calculation() {
319 let gpt5_mini = &LLM_MODELS[2]; let tokens = 1_000_000;
321 assert!((gpt5_mini.calculate_input_cost(tokens) - 0.25).abs() < 0.001);
322 }
323
324 #[test]
325 fn test_format_cost() {
326 assert_eq!(format_cost(0.0), "$0.0000");
327 assert_eq!(format_cost(0.00001), "<$0.0001");
328 assert_eq!(format_cost(0.0012), "$0.0012");
329 assert_eq!(format_cost(1.5), "$1.50");
330 }
331
332 #[test]
333 fn test_format_tokens() {
334 assert_eq!(format_tokens(500), "500");
335 assert_eq!(format_tokens(1500), "1.5K");
336 assert_eq!(format_tokens(1_500_000), "1.50M");
337 }
338}