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> {
83 vec![
84 ModelPricing::new("gemini-2.5-flash", 0.00015, 0.0006),
86 ModelPricing::new("gemini-2.5-pro", 0.00125, 0.005),
87 ModelPricing::new("gemini-2.0-flash", 0.0001, 0.0004),
88 ModelPricing::new("gemini-2.0-flash-lite", 0.000075, 0.0003),
89 ModelPricing::new("gpt-4o", 0.0025, 0.01),
91 ModelPricing::new("gpt-4o-mini", 0.00015, 0.0006),
92 ModelPricing::new("gpt-4-turbo", 0.01, 0.03),
93 ModelPricing::new("gpt-4", 0.03, 0.06),
94 ModelPricing::new("gpt-3.5-turbo", 0.0005, 0.0015),
95 ModelPricing::new("o1", 0.015, 0.06),
96 ModelPricing::new("o1-mini", 0.003, 0.012),
97 ModelPricing::new("o3-mini", 0.0011, 0.0044),
98 ModelPricing::new("claude-sonnet-4-20250514", 0.003, 0.015),
100 ModelPricing::new("claude-3.5-haiku", 0.0008, 0.004),
101 ModelPricing::new("claude-3-opus", 0.015, 0.075),
102 ModelPricing::new("claude-3-haiku", 0.00025, 0.00125),
103 ModelPricing::new("deepseek-chat", 0.00014, 0.00028),
105 ModelPricing::new("deepseek-reasoner", 0.00055, 0.0022),
106 ]
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_model_pricing_new() {
115 let pricing = ModelPricing::new("test-model", 0.001, 0.002);
116 assert_eq!(pricing.model_name, "test-model");
117 assert_eq!(pricing.input_cost_per_1k, 0.001);
118 assert_eq!(pricing.output_cost_per_1k, 0.002);
119 }
120
121 #[test]
122 fn test_default_pricing_not_empty() {
123 let pricing = default_pricing();
124 assert!(!pricing.is_empty());
125 }
126
127 #[test]
128 fn test_default_pricing_includes_gemini() {
129 let pricing = default_pricing();
130 let gemini = pricing.iter().find(|p| p.model_name == "gemini-2.5-flash");
131 assert!(gemini.is_some());
132 let gemini = gemini.unwrap();
133 assert!(gemini.input_cost_per_1k > 0.0);
134 assert!(gemini.output_cost_per_1k > 0.0);
135 }
136
137 #[test]
138 fn test_default_pricing_includes_openai() {
139 let pricing = default_pricing();
140 let gpt4o = pricing.iter().find(|p| p.model_name == "gpt-4o");
141 assert!(gpt4o.is_some());
142 let gpt4o = gpt4o.unwrap();
143 assert!(gpt4o.input_cost_per_1k > 0.0);
144 assert!(gpt4o.output_cost_per_1k > 0.0);
145 }
146
147 #[test]
148 fn test_default_pricing_includes_anthropic() {
149 let pricing = default_pricing();
150 let claude = pricing.iter().find(|p| p.model_name == "claude-sonnet-4-20250514");
151 assert!(claude.is_some());
152 let claude = claude.unwrap();
153 assert!(claude.input_cost_per_1k > 0.0);
154 assert!(claude.output_cost_per_1k > 0.0);
155 }
156
157 #[test]
158 fn test_default_pricing_all_positive_costs() {
159 let pricing = default_pricing();
160 for model in &pricing {
161 assert!(
162 model.input_cost_per_1k >= 0.0,
163 "Model {} has negative input cost",
164 model.model_name
165 );
166 assert!(
167 model.output_cost_per_1k >= 0.0,
168 "Model {} has negative output cost",
169 model.model_name
170 );
171 }
172 }
173
174 #[test]
175 fn test_model_pricing_serialization_roundtrip() {
176 let pricing = ModelPricing::new("test-model", 0.001, 0.002);
177 let json = serde_json::to_string(&pricing).unwrap();
178 let deserialized: ModelPricing = serde_json::from_str(&json).unwrap();
179 assert_eq!(pricing, deserialized);
180 }
181
182 #[test]
183 fn test_default_pricing_unique_model_names() {
184 let pricing = default_pricing();
185 let mut names: Vec<&str> = pricing.iter().map(|p| p.model_name.as_str()).collect();
186 let original_len = names.len();
187 names.sort();
188 names.dedup();
189 assert_eq!(names.len(), original_len, "Default pricing contains duplicate model names");
190 }
191}