1use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32pub struct ModelPricing {
33 pub model_name: String,
35 pub input_cost_per_1k: f64,
37 pub output_cost_per_1k: f64,
39}
40
41impl ModelPricing {
42 pub fn new(
59 model_name: impl Into<String>,
60 input_cost_per_1k: f64,
61 output_cost_per_1k: f64,
62 ) -> Self {
63 Self { model_name: model_name.into(), input_cost_per_1k, output_cost_per_1k }
64 }
65}
66
67pub fn default_pricing() -> Vec<ModelPricing> {
91 vec![
92 ModelPricing::new("gemini-3.7-flash", 0.00075, 0.00375),
95 ModelPricing::new("gemini-3.6-flash", 0.00075, 0.00375),
96 ModelPricing::new("gemini-3.5-flash", 0.0015, 0.009),
97 ModelPricing::new("gemini-3.5-flash-lite", 0.0003, 0.0025),
98 ModelPricing::new("gemini-3.1-pro-preview", 0.002, 0.012),
99 ModelPricing::new("gemini-3.1-flash-lite", 0.00025, 0.0015),
100 ModelPricing::new("gemini-3-flash-preview", 0.0005, 0.003),
101 ModelPricing::new("gemini-2.5-pro", 0.00125, 0.01),
102 ModelPricing::new("gemini-2.5-flash", 0.0003, 0.0025),
103 ModelPricing::new("gemini-2.5-flash-lite", 0.0001, 0.0004),
104 ModelPricing::new("gpt-5.6-sol", 0.004, 0.02),
106 ModelPricing::new("gpt-5.6-terra", 0.002, 0.012),
107 ModelPricing::new("gpt-5.6-luna", 0.0002, 0.0012),
108 ModelPricing::new("gpt-5.5", 0.005, 0.03),
109 ModelPricing::new("gpt-5.4", 0.0025, 0.015),
110 ModelPricing::new("gpt-5.4-mini", 0.00075, 0.0045),
111 ModelPricing::new("gpt-5.4-nano", 0.0002, 0.00125),
112 ModelPricing::new("gpt-5.3-codex", 0.00175, 0.014),
113 ModelPricing::new("gpt-5.2", 0.00175, 0.014),
114 ModelPricing::new("gpt-5.1", 0.00125, 0.01),
115 ModelPricing::new("gpt-5", 0.00125, 0.01),
116 ModelPricing::new("gpt-5-mini", 0.00025, 0.002),
117 ModelPricing::new("gpt-5-nano", 0.00005, 0.0004),
118 ModelPricing::new("gpt-4.1", 0.002, 0.008),
119 ModelPricing::new("gpt-4.1-mini", 0.0004, 0.0016),
120 ModelPricing::new("gpt-4o", 0.0025, 0.01),
121 ModelPricing::new("gpt-4o-mini", 0.00015, 0.0006),
122 ModelPricing::new("o3", 0.002, 0.008),
123 ModelPricing::new("o4-mini", 0.0011, 0.0044),
124 ModelPricing::new("claude-fable-5", 0.01, 0.05),
126 ModelPricing::new("claude-mythos-5", 0.01, 0.05),
127 ModelPricing::new("claude-opus-5", 0.005, 0.025),
128 ModelPricing::new("claude-opus-4-8", 0.005, 0.025),
129 ModelPricing::new("claude-sonnet-5", 0.002, 0.01),
130 ModelPricing::new("claude-sonnet-4-6", 0.003, 0.015),
131 ModelPricing::new("claude-haiku-4-5", 0.001, 0.005),
132 ModelPricing::new("deepseek-flash", 0.00044, 0.00132),
134 ModelPricing::new("deepseek-v4-flash", 0.00044, 0.00132),
136 ModelPricing::new("deepseek-v4-pro", 0.00132, 0.00396),
137 ]
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_model_pricing_new() {
146 let pricing = ModelPricing::new("test-model", 0.001, 0.002);
147 assert_eq!(pricing.model_name, "test-model");
148 assert_eq!(pricing.input_cost_per_1k, 0.001);
149 assert_eq!(pricing.output_cost_per_1k, 0.002);
150 }
151
152 #[test]
153 fn test_default_pricing_not_empty() {
154 let pricing = default_pricing();
155 assert!(!pricing.is_empty());
156 }
157
158 #[test]
159 fn test_default_pricing_includes_gemini() {
160 let pricing = default_pricing();
161 let gemini = pricing.iter().find(|p| p.model_name == "gemini-3.7-flash");
162 assert!(gemini.is_some());
163 let gemini = gemini.unwrap();
164 assert!(gemini.input_cost_per_1k > 0.0);
165 assert!(gemini.output_cost_per_1k > 0.0);
166 }
167
168 #[test]
169 fn test_default_pricing_includes_openai() {
170 let pricing = default_pricing();
171 let gpt = pricing.iter().find(|p| p.model_name == "gpt-5.6-terra");
172 assert!(gpt.is_some());
173 let gpt = gpt.unwrap();
174 assert!(gpt.input_cost_per_1k > 0.0);
175 assert!(gpt.output_cost_per_1k > 0.0);
176 }
177
178 #[test]
179 fn test_default_pricing_includes_anthropic() {
180 let pricing = default_pricing();
181 let claude = pricing.iter().find(|p| p.model_name == "claude-sonnet-5");
182 assert!(claude.is_some());
183 let claude = claude.unwrap();
184 assert!(claude.input_cost_per_1k > 0.0);
185 assert!(claude.output_cost_per_1k > 0.0);
186 }
187
188 #[test]
191 fn test_default_pricing_excludes_retired_models() {
192 let pricing = default_pricing();
193 for retired in [
194 "gemini-2.0-flash",
195 "gemini-2.0-flash-lite",
196 "gemini-3-pro-preview",
197 "claude-3-opus",
198 "claude-3-haiku",
199 "claude-3.5-haiku",
200 "claude-sonnet-4-20250514",
201 "deepseek-chat",
202 "deepseek-reasoner",
203 "gpt-4",
204 "gpt-4-turbo",
205 "gpt-3.5-turbo",
206 ] {
207 assert!(
208 !pricing.iter().any(|p| p.model_name == retired),
209 "{retired} is retired and must not be in the default table"
210 );
211 }
212
213 let mut names: Vec<&str> = pricing.iter().map(|p| p.model_name.as_str()).collect();
214 names.sort_unstable();
215 let count = names.len();
216 names.dedup();
217 assert_eq!(names.len(), count, "duplicate model IDs in default pricing");
218 }
219
220 #[test]
223 fn test_output_never_cheaper_than_input() {
224 for model in default_pricing() {
225 assert!(
226 model.output_cost_per_1k >= model.input_cost_per_1k,
227 "{} output {} is below input {}",
228 model.model_name,
229 model.output_cost_per_1k,
230 model.input_cost_per_1k
231 );
232 }
233 }
234
235 #[test]
236 fn test_default_pricing_all_positive_costs() {
237 let pricing = default_pricing();
238 for model in &pricing {
239 assert!(
240 model.input_cost_per_1k >= 0.0,
241 "Model {} has negative input cost",
242 model.model_name
243 );
244 assert!(
245 model.output_cost_per_1k >= 0.0,
246 "Model {} has negative output cost",
247 model.model_name
248 );
249 }
250 }
251
252 #[test]
253 fn test_model_pricing_serialization_roundtrip() {
254 let pricing = ModelPricing::new("test-model", 0.001, 0.002);
255 let json = serde_json::to_string(&pricing).unwrap();
256 let deserialized: ModelPricing = serde_json::from_str(&json).unwrap();
257 assert_eq!(pricing, deserialized);
258 }
259
260 #[test]
261 fn test_default_pricing_unique_model_names() {
262 let pricing = default_pricing();
263 let mut names: Vec<&str> = pricing.iter().map(|p| p.model_name.as_str()).collect();
264 let original_len = names.len();
265 names.sort();
266 names.dedup();
267 assert_eq!(names.len(), original_len, "Default pricing contains duplicate model names");
268 }
269}