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-v4-flash", 0.00044, 0.00132),
134 ModelPricing::new("deepseek-v4-pro", 0.00132, 0.00396),
135 ]
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_model_pricing_new() {
144 let pricing = ModelPricing::new("test-model", 0.001, 0.002);
145 assert_eq!(pricing.model_name, "test-model");
146 assert_eq!(pricing.input_cost_per_1k, 0.001);
147 assert_eq!(pricing.output_cost_per_1k, 0.002);
148 }
149
150 #[test]
151 fn test_default_pricing_not_empty() {
152 let pricing = default_pricing();
153 assert!(!pricing.is_empty());
154 }
155
156 #[test]
157 fn test_default_pricing_includes_gemini() {
158 let pricing = default_pricing();
159 let gemini = pricing.iter().find(|p| p.model_name == "gemini-3.7-flash");
160 assert!(gemini.is_some());
161 let gemini = gemini.unwrap();
162 assert!(gemini.input_cost_per_1k > 0.0);
163 assert!(gemini.output_cost_per_1k > 0.0);
164 }
165
166 #[test]
167 fn test_default_pricing_includes_openai() {
168 let pricing = default_pricing();
169 let gpt = pricing.iter().find(|p| p.model_name == "gpt-5.6-terra");
170 assert!(gpt.is_some());
171 let gpt = gpt.unwrap();
172 assert!(gpt.input_cost_per_1k > 0.0);
173 assert!(gpt.output_cost_per_1k > 0.0);
174 }
175
176 #[test]
177 fn test_default_pricing_includes_anthropic() {
178 let pricing = default_pricing();
179 let claude = pricing.iter().find(|p| p.model_name == "claude-sonnet-5");
180 assert!(claude.is_some());
181 let claude = claude.unwrap();
182 assert!(claude.input_cost_per_1k > 0.0);
183 assert!(claude.output_cost_per_1k > 0.0);
184 }
185
186 #[test]
189 fn test_default_pricing_excludes_retired_models() {
190 let pricing = default_pricing();
191 for retired in [
192 "gemini-2.0-flash",
193 "gemini-2.0-flash-lite",
194 "gemini-3-pro-preview",
195 "claude-3-opus",
196 "claude-3-haiku",
197 "claude-3.5-haiku",
198 "claude-sonnet-4-20250514",
199 "deepseek-chat",
200 "deepseek-reasoner",
201 "gpt-4",
202 "gpt-4-turbo",
203 "gpt-3.5-turbo",
204 ] {
205 assert!(
206 !pricing.iter().any(|p| p.model_name == retired),
207 "{retired} is retired and must not be in the default table"
208 );
209 }
210
211 let mut names: Vec<&str> = pricing.iter().map(|p| p.model_name.as_str()).collect();
212 names.sort_unstable();
213 let count = names.len();
214 names.dedup();
215 assert_eq!(names.len(), count, "duplicate model IDs in default pricing");
216 }
217
218 #[test]
221 fn test_output_never_cheaper_than_input() {
222 for model in default_pricing() {
223 assert!(
224 model.output_cost_per_1k >= model.input_cost_per_1k,
225 "{} output {} is below input {}",
226 model.model_name,
227 model.output_cost_per_1k,
228 model.input_cost_per_1k
229 );
230 }
231 }
232
233 #[test]
234 fn test_default_pricing_all_positive_costs() {
235 let pricing = default_pricing();
236 for model in &pricing {
237 assert!(
238 model.input_cost_per_1k >= 0.0,
239 "Model {} has negative input cost",
240 model.model_name
241 );
242 assert!(
243 model.output_cost_per_1k >= 0.0,
244 "Model {} has negative output cost",
245 model.model_name
246 );
247 }
248 }
249
250 #[test]
251 fn test_model_pricing_serialization_roundtrip() {
252 let pricing = ModelPricing::new("test-model", 0.001, 0.002);
253 let json = serde_json::to_string(&pricing).unwrap();
254 let deserialized: ModelPricing = serde_json::from_str(&json).unwrap();
255 assert_eq!(pricing, deserialized);
256 }
257
258 #[test]
259 fn test_default_pricing_unique_model_names() {
260 let pricing = default_pricing();
261 let mut names: Vec<&str> = pricing.iter().map(|p| p.model_name.as_str()).collect();
262 let original_len = names.len();
263 names.sort();
264 names.dedup();
265 assert_eq!(names.len(), original_len, "Default pricing contains duplicate model names");
266 }
267}