use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModelPricing {
pub model_name: String,
pub input_cost_per_1k: f64,
pub output_cost_per_1k: f64,
}
impl ModelPricing {
pub fn new(
model_name: impl Into<String>,
input_cost_per_1k: f64,
output_cost_per_1k: f64,
) -> Self {
Self { model_name: model_name.into(), input_cost_per_1k, output_cost_per_1k }
}
}
pub fn default_pricing() -> Vec<ModelPricing> {
vec![
ModelPricing::new("gemini-2.5-flash", 0.00015, 0.0006),
ModelPricing::new("gemini-2.5-pro", 0.00125, 0.005),
ModelPricing::new("gemini-2.0-flash", 0.0001, 0.0004),
ModelPricing::new("gemini-2.0-flash-lite", 0.000075, 0.0003),
ModelPricing::new("gpt-4o", 0.0025, 0.01),
ModelPricing::new("gpt-4o-mini", 0.00015, 0.0006),
ModelPricing::new("gpt-4-turbo", 0.01, 0.03),
ModelPricing::new("gpt-4", 0.03, 0.06),
ModelPricing::new("gpt-3.5-turbo", 0.0005, 0.0015),
ModelPricing::new("o1", 0.015, 0.06),
ModelPricing::new("o1-mini", 0.003, 0.012),
ModelPricing::new("o3-mini", 0.0011, 0.0044),
ModelPricing::new("claude-sonnet-4-20250514", 0.003, 0.015),
ModelPricing::new("claude-3.5-haiku", 0.0008, 0.004),
ModelPricing::new("claude-3-opus", 0.015, 0.075),
ModelPricing::new("claude-3-haiku", 0.00025, 0.00125),
ModelPricing::new("deepseek-chat", 0.00014, 0.00028),
ModelPricing::new("deepseek-reasoner", 0.00055, 0.0022),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_pricing_new() {
let pricing = ModelPricing::new("test-model", 0.001, 0.002);
assert_eq!(pricing.model_name, "test-model");
assert_eq!(pricing.input_cost_per_1k, 0.001);
assert_eq!(pricing.output_cost_per_1k, 0.002);
}
#[test]
fn test_default_pricing_not_empty() {
let pricing = default_pricing();
assert!(!pricing.is_empty());
}
#[test]
fn test_default_pricing_includes_gemini() {
let pricing = default_pricing();
let gemini = pricing.iter().find(|p| p.model_name == "gemini-2.5-flash");
assert!(gemini.is_some());
let gemini = gemini.unwrap();
assert!(gemini.input_cost_per_1k > 0.0);
assert!(gemini.output_cost_per_1k > 0.0);
}
#[test]
fn test_default_pricing_includes_openai() {
let pricing = default_pricing();
let gpt4o = pricing.iter().find(|p| p.model_name == "gpt-4o");
assert!(gpt4o.is_some());
let gpt4o = gpt4o.unwrap();
assert!(gpt4o.input_cost_per_1k > 0.0);
assert!(gpt4o.output_cost_per_1k > 0.0);
}
#[test]
fn test_default_pricing_includes_anthropic() {
let pricing = default_pricing();
let claude = pricing.iter().find(|p| p.model_name == "claude-sonnet-4-20250514");
assert!(claude.is_some());
let claude = claude.unwrap();
assert!(claude.input_cost_per_1k > 0.0);
assert!(claude.output_cost_per_1k > 0.0);
}
#[test]
fn test_default_pricing_all_positive_costs() {
let pricing = default_pricing();
for model in &pricing {
assert!(
model.input_cost_per_1k >= 0.0,
"Model {} has negative input cost",
model.model_name
);
assert!(
model.output_cost_per_1k >= 0.0,
"Model {} has negative output cost",
model.model_name
);
}
}
#[test]
fn test_model_pricing_serialization_roundtrip() {
let pricing = ModelPricing::new("test-model", 0.001, 0.002);
let json = serde_json::to_string(&pricing).unwrap();
let deserialized: ModelPricing = serde_json::from_str(&json).unwrap();
assert_eq!(pricing, deserialized);
}
#[test]
fn test_default_pricing_unique_model_names() {
let pricing = default_pricing();
let mut names: Vec<&str> = pricing.iter().map(|p| p.model_name.as_str()).collect();
let original_len = names.len();
names.sort();
names.dedup();
assert_eq!(names.len(), original_len, "Default pricing contains duplicate model names");
}
}