Skip to main content

serializer/
llm_models.rs

1//! LLM Model Definitions with Pricing
2//!
3//! Comprehensive list of LLM models with their pricing per 1M tokens.
4//! Data sourced from official provider pricing pages (January 2026).
5
6use std::fmt;
7
8/// LLM Provider
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Provider {
11    /// OpenAI or Azure OpenAI served model.
12    OpenAI,
13    /// Anthropic served model.
14    Anthropic,
15    /// Google served model.
16    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/// LLM Model definition with pricing
30#[derive(Debug, Clone)]
31pub struct LlmModel {
32    /// Model name (e.g., "GPT-5.2", "Claude Sonnet 4")
33    pub name: &'static str,
34    /// Provider
35    pub provider: Provider,
36    /// Context window size (e.g., "400K", "200K", "1M", "2M")
37    pub context_window: &'static str,
38    /// Price per 1M input tokens in USD
39    pub input_per_1m: f64,
40    /// Price per 1M cached input tokens in USD
41    pub input_cached_per_1m: f64,
42    /// Price per 1M output tokens in USD
43    pub output_per_1m: f64,
44    /// Characters per token ratio (for estimation)
45    pub chars_per_token: f64,
46}
47
48/// All supported LLM models with current pricing (January 2026)
49pub static LLM_MODELS: &[LlmModel] = &[
50    // OpenAI/Azure Models
51    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    // Anthropic Models
79    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    // Google Models
134    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    /// Estimate token count for given text
201    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    /// Calculate input cost for given token count
207    pub fn calculate_input_cost(&self, tokens: usize) -> f64 {
208        (tokens as f64 / 1_000_000.0) * self.input_per_1m
209    }
210
211    /// Calculate cached input cost for given token count
212    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    /// Calculate output cost for given token count
217    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/// Token analysis result for a single model
223#[derive(Debug, Clone)]
224pub struct TokenAnalysis {
225    /// Model name used for this token estimate.
226    pub model_name: &'static str,
227    /// Provider that serves the model.
228    pub provider: Provider,
229    /// Published context-window label.
230    pub context_window: &'static str,
231    /// Estimated token count for the analyzed text.
232    pub tokens: usize,
233    /// Estimated uncached input cost in USD.
234    pub input_cost: f64,
235    /// Estimated cached-input cost in USD.
236    pub cached_cost: f64,
237    /// Estimated output cost in USD for the same token count.
238    pub output_cost: f64,
239}
240
241/// Analyze text for all LLM models
242pub 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
260/// Format cost as string
261pub 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        // Use 4 decimal places for costs under $1
268        format!("${:.4}", cost)
269    } else {
270        format!("${:.2}", cost)
271    }
272}
273
274/// Format token count
275pub 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
285/// Get models grouped by provider
286pub 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!"; // 13 chars
313        let gpt5 = &LLM_MODELS[0]; // GPT-5.2, 4.0 chars/token
314        assert_eq!(gpt5.estimate_tokens(text), 4); // ceil(13/4.0) = 4
315    }
316
317    #[test]
318    fn test_cost_calculation() {
319        let gpt5_mini = &LLM_MODELS[2]; // GPT-5 mini
320        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}