use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
pub struct ModelPricing {
pub provider: &'static str,
pub model_pattern: &'static str,
pub input_per_million: f64,
pub output_per_million: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CostEstimate {
pub usd: f64,
pub input_rate_per_million: f64,
pub output_rate_per_million: f64,
pub model: String,
pub provider: String,
}
pub static KNOWN_PRICING: &[ModelPricing] = &[
ModelPricing {
provider: "Anthropic",
model_pattern: "claude-3-haiku",
input_per_million: 0.25,
output_per_million: 1.25,
},
ModelPricing {
provider: "Anthropic",
model_pattern: "opus-4",
input_per_million: 15.0,
output_per_million: 75.0,
},
ModelPricing {
provider: "Anthropic",
model_pattern: "sonnet-4",
input_per_million: 3.0,
output_per_million: 15.0,
},
ModelPricing {
provider: "Anthropic",
model_pattern: "haiku-4",
input_per_million: 0.8,
output_per_million: 4.0,
},
ModelPricing {
provider: "Anthropic",
model_pattern: "claude-3-5-sonnet",
input_per_million: 3.0,
output_per_million: 15.0,
},
ModelPricing {
provider: "Anthropic",
model_pattern: "claude-3-5-haiku",
input_per_million: 0.8,
output_per_million: 4.0,
},
ModelPricing {
provider: "Anthropic",
model_pattern: "claude-3-opus",
input_per_million: 15.0,
output_per_million: 75.0,
},
ModelPricing {
provider: "Anthropic",
model_pattern: "claude-3-sonnet",
input_per_million: 3.0,
output_per_million: 15.0,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "gpt-4o-mini",
input_per_million: 0.15,
output_per_million: 0.6,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "gpt-4o",
input_per_million: 2.5,
output_per_million: 10.0,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "gpt-4.1-nano",
input_per_million: 0.1,
output_per_million: 0.4,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "gpt-4.1-mini",
input_per_million: 0.4,
output_per_million: 1.6,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "gpt-4.1",
input_per_million: 2.0,
output_per_million: 8.0,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "gpt-3.5-turbo",
input_per_million: 0.5,
output_per_million: 1.5,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "gpt-4-turbo",
input_per_million: 10.0,
output_per_million: 30.0,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "gpt-4",
input_per_million: 30.0,
output_per_million: 60.0,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "o1-mini",
input_per_million: 3.0,
output_per_million: 12.0,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "o1",
input_per_million: 15.0,
output_per_million: 60.0,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "o3-mini",
input_per_million: 1.1,
output_per_million: 4.4,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "o4-mini",
input_per_million: 1.1,
output_per_million: 4.4,
},
ModelPricing {
provider: "OpenAI",
model_pattern: "o3",
input_per_million: 2.0,
output_per_million: 8.0,
},
ModelPricing {
provider: "Mistral",
model_pattern: "mistral-large",
input_per_million: 2.0,
output_per_million: 6.0,
},
ModelPricing {
provider: "Mistral",
model_pattern: "mistral-medium",
input_per_million: 2.7,
output_per_million: 8.1,
},
ModelPricing {
provider: "Mistral",
model_pattern: "mistral-small",
input_per_million: 0.2,
output_per_million: 0.6,
},
ModelPricing {
provider: "Mistral",
model_pattern: "codestral",
input_per_million: 0.3,
output_per_million: 0.9,
},
ModelPricing {
provider: "Mistral",
model_pattern: "ministral-8b",
input_per_million: 0.1,
output_per_million: 0.1,
},
ModelPricing {
provider: "Mistral",
model_pattern: "ministral-3b",
input_per_million: 0.04,
output_per_million: 0.04,
},
ModelPricing {
provider: "Mistral",
model_pattern: "pixtral-large",
input_per_million: 2.0,
output_per_million: 6.0,
},
ModelPricing {
provider: "Mistral",
model_pattern: "pixtral-12b",
input_per_million: 0.15,
output_per_million: 0.15,
},
ModelPricing {
provider: "Groq",
model_pattern: "llama-3.3-70b-specdec",
input_per_million: 0.59,
output_per_million: 0.99,
},
ModelPricing {
provider: "Groq",
model_pattern: "llama-3.3-70b",
input_per_million: 0.59,
output_per_million: 0.79,
},
ModelPricing {
provider: "Groq",
model_pattern: "llama-3.1-70b",
input_per_million: 0.59,
output_per_million: 0.79,
},
ModelPricing {
provider: "Groq",
model_pattern: "llama-3.1-8b",
input_per_million: 0.05,
output_per_million: 0.08,
},
ModelPricing {
provider: "Groq",
model_pattern: "llama3-70b",
input_per_million: 0.59,
output_per_million: 0.79,
},
ModelPricing {
provider: "Groq",
model_pattern: "llama3-8b",
input_per_million: 0.05,
output_per_million: 0.08,
},
ModelPricing {
provider: "Groq",
model_pattern: "mixtral-8x7b",
input_per_million: 0.24,
output_per_million: 0.24,
},
ModelPricing {
provider: "Groq",
model_pattern: "gemma2-9b",
input_per_million: 0.20,
output_per_million: 0.20,
},
ModelPricing {
provider: "DeepSeek",
model_pattern: "deepseek-chat",
input_per_million: 0.14,
output_per_million: 0.28,
},
ModelPricing {
provider: "DeepSeek",
model_pattern: "deepseek-reasoner",
input_per_million: 0.55,
output_per_million: 2.19,
},
ModelPricing {
provider: "DeepSeek",
model_pattern: "deepseek-coder",
input_per_million: 0.14,
output_per_million: 0.28,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-2.5-flash",
input_per_million: 0.15,
output_per_million: 0.6,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-2.5-pro",
input_per_million: 1.25,
output_per_million: 10.0,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-2.0-flash-exp",
input_per_million: 0.0,
output_per_million: 0.0,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-2.0-flash-thinking",
input_per_million: 0.0,
output_per_million: 0.0,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-2.0-flash",
input_per_million: 0.1,
output_per_million: 0.4,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-1.5-flash-8b",
input_per_million: 0.0375,
output_per_million: 0.15,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-1.5-flash",
input_per_million: 0.075,
output_per_million: 0.3,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-1.5-pro",
input_per_million: 1.25,
output_per_million: 5.0,
},
ModelPricing {
provider: "Gemini",
model_pattern: "gemini-pro",
input_per_million: 0.5,
output_per_million: 1.5,
},
ModelPricing {
provider: "xAI",
model_pattern: "grok-3-mini-fast",
input_per_million: 0.1,
output_per_million: 0.4,
},
ModelPricing {
provider: "xAI",
model_pattern: "grok-3-mini",
input_per_million: 0.3,
output_per_million: 0.5,
},
ModelPricing {
provider: "xAI",
model_pattern: "grok-3-fast",
input_per_million: 0.6,
output_per_million: 4.0,
},
ModelPricing {
provider: "xAI",
model_pattern: "grok-3",
input_per_million: 3.0,
output_per_million: 15.0,
},
ModelPricing {
provider: "xAI",
model_pattern: "grok-2",
input_per_million: 2.0,
output_per_million: 10.0,
},
];
pub fn find_pricing(model: &str) -> Option<&'static ModelPricing> {
if let Some(p) = KNOWN_PRICING.iter().find(|p| model == p.model_pattern) {
return Some(p);
}
KNOWN_PRICING
.iter()
.find(|p| model.contains(p.model_pattern))
}
pub fn estimate_cost(model: &str, input_tokens: u64, output_tokens: u64) -> Option<CostEstimate> {
let pricing = find_pricing(model)?;
let usd = (input_tokens as f64 * pricing.input_per_million
+ output_tokens as f64 * pricing.output_per_million)
/ 1_000_000.0;
Some(CostEstimate {
usd,
input_rate_per_million: pricing.input_per_million,
output_rate_per_million: pricing.output_per_million,
model: model.to_string(),
provider: pricing.provider.to_string(),
})
}
pub fn model_cost_label(model: &str) -> Option<String> {
let pricing = find_pricing(model)?;
Some(format!(
" ({} \u{00b7} ${}/{})",
pricing.provider,
format_price(pricing.input_per_million),
format_price(pricing.output_per_million),
))
}
fn format_price(price: f64) -> String {
if price >= 1.0 {
format!("{}", price)
} else {
format!("{:.2}", price)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_pricing_anthropic_sonnet() {
let p = find_pricing("claude-sonnet-4-20250514").unwrap();
assert_eq!(p.provider, "Anthropic");
assert!((p.input_per_million - 3.0).abs() < f64::EPSILON);
assert!((p.output_per_million - 15.0).abs() < f64::EPSILON);
}
#[test]
fn find_pricing_anthropic_opus() {
let p = find_pricing("claude-opus-4-20250514").unwrap();
assert_eq!(p.provider, "Anthropic");
assert!((p.input_per_million - 15.0).abs() < f64::EPSILON);
}
#[test]
fn find_pricing_openai_gpt4o() {
let p = find_pricing("gpt-4o").unwrap();
assert_eq!(p.provider, "OpenAI");
assert!((p.input_per_million - 2.5).abs() < f64::EPSILON);
}
#[test]
fn find_pricing_openai_gpt4o_mini() {
let p = find_pricing("gpt-4o-mini").unwrap();
assert_eq!(p.provider, "OpenAI");
assert!((p.input_per_million - 0.15).abs() < f64::EPSILON);
}
#[test]
fn find_pricing_anthropic_haiku() {
let p = find_pricing("claude-haiku-4-5-20251001").unwrap();
assert_eq!(p.provider, "Anthropic");
assert!((p.input_per_million - 0.8).abs() < f64::EPSILON);
let p2 = find_pricing("claude-haiku-4-5").unwrap();
assert_eq!(p2.provider, "Anthropic");
}
#[test]
fn find_pricing_unknown_returns_none() {
assert!(find_pricing("some-random-model").is_none());
}
#[test]
fn estimate_cost_sonnet_1k_tokens() {
let est = estimate_cost("claude-sonnet-4-20250514", 1000, 500).unwrap();
assert!((est.usd - 0.0105).abs() < 1e-10);
assert_eq!(est.provider, "Anthropic");
assert_eq!(est.model, "claude-sonnet-4-20250514");
}
#[test]
fn estimate_cost_unknown_returns_none() {
assert!(estimate_cost("mystery-model", 1000, 500).is_none());
}
#[test]
fn model_cost_label_sonnet_format() {
let label = model_cost_label("claude-sonnet-4-20250514").unwrap();
assert!(label.contains("Anthropic"));
assert!(label.contains("$3"));
assert!(label.contains("15"));
}
#[test]
fn model_cost_label_gpt4o_mini_format() {
let label = model_cost_label("gpt-4o-mini").unwrap();
assert!(label.contains("OpenAI"));
assert!(label.contains("$0.15"));
}
#[test]
fn model_cost_label_unknown() {
assert!(model_cost_label("random-model").is_none());
}
#[test]
fn cost_estimate_serde_roundtrip() {
let est = CostEstimate {
usd: 0.018,
input_rate_per_million: 3.0,
output_rate_per_million: 15.0,
model: "claude-sonnet-4-20250514".into(),
provider: "anthropic".into(),
};
let json = serde_json::to_string(&est).unwrap();
let back: CostEstimate = serde_json::from_str(&json).unwrap();
assert_eq!(est, back);
}
#[test]
fn known_pricing_count() {
assert!(
KNOWN_PRICING.len() >= 50,
"Expected at least 50 pricing entries, got {}",
KNOWN_PRICING.len()
);
}
#[test]
fn gpt41_variants_differentiated() {
let nano = find_pricing("gpt-4.1-nano").unwrap();
let mini = find_pricing("gpt-4.1-mini").unwrap();
let base = find_pricing("gpt-4.1").unwrap();
assert!(nano.input_per_million < mini.input_per_million);
assert!(mini.input_per_million < base.input_per_million);
}
#[test]
fn exact_match_takes_priority() {
let p = find_pricing("gpt-4o-mini").unwrap();
assert!((p.input_per_million - 0.15).abs() < f64::EPSILON);
}
#[test]
fn contains_fallback_for_dated_variants() {
let p = find_pricing("claude-sonnet-4-20250514").unwrap();
assert_eq!(p.provider, "Anthropic");
assert!((p.input_per_million - 3.0).abs() < f64::EPSILON);
}
#[test]
fn specdec_different_from_versatile() {
let specdec = find_pricing("llama-3.3-70b-specdec").unwrap();
let versatile = find_pricing("llama-3.3-70b-versatile").unwrap();
assert!(
(specdec.output_per_million - 0.99).abs() < f64::EPSILON,
"specdec should have 0.99 output"
);
assert!(
(versatile.output_per_million - 0.79).abs() < f64::EPSILON,
"versatile should have 0.79 output"
);
}
#[test]
fn claude_3_haiku_old_pricing() {
let p = find_pricing("claude-3-haiku-20240307").unwrap();
assert!((p.input_per_million - 0.25).abs() < f64::EPSILON);
}
#[test]
fn gpt4_turbo_pricing() {
let p = find_pricing("gpt-4-turbo").unwrap();
assert!((p.input_per_million - 10.0).abs() < f64::EPSILON);
}
#[test]
fn gpt35_turbo_pricing() {
let p = find_pricing("gpt-3.5-turbo").unwrap();
assert!((p.input_per_million - 0.5).abs() < f64::EPSILON);
}
#[test]
fn o1_mini_pricing() {
let p = find_pricing("o1-mini").unwrap();
assert!((p.input_per_million - 3.0).abs() < f64::EPSILON);
assert!((p.output_per_million - 12.0).abs() < f64::EPSILON);
}
#[test]
fn mistral_medium_pricing() {
let p = find_pricing("mistral-medium-latest").unwrap();
assert!((p.input_per_million - 2.7).abs() < f64::EPSILON);
}
#[test]
fn codestral_pricing() {
let p = find_pricing("codestral-latest").unwrap();
assert!((p.input_per_million - 0.3).abs() < f64::EPSILON);
}
#[test]
fn mixtral_pricing() {
let p = find_pricing("mixtral-8x7b-32768").unwrap();
assert!((p.input_per_million - 0.24).abs() < f64::EPSILON);
}
#[test]
fn deepseek_coder_pricing() {
let p = find_pricing("deepseek-coder").unwrap();
assert!((p.input_per_million - 0.14).abs() < f64::EPSILON);
}
#[test]
fn gemini_20_flash_pricing() {
let p = find_pricing("gemini-2.0-flash").unwrap();
assert!((p.input_per_million - 0.1).abs() < f64::EPSILON);
}
#[test]
fn gemini_15_pro_pricing() {
let p = find_pricing("gemini-1.5-pro").unwrap();
assert!((p.input_per_million - 1.25).abs() < f64::EPSILON);
assert!((p.output_per_million - 5.0).abs() < f64::EPSILON);
}
#[test]
fn grok_3_fast_pricing() {
let p = find_pricing("grok-3-fast").unwrap();
assert!((p.input_per_million - 0.6).abs() < f64::EPSILON);
}
#[test]
fn grok_2_pricing() {
let p = find_pricing("grok-2").unwrap();
assert!((p.input_per_million - 2.0).abs() < f64::EPSILON);
}
#[test]
fn gemini_free_preview() {
let p = find_pricing("gemini-2.0-flash-exp").unwrap();
assert!((p.input_per_million - 0.0).abs() < f64::EPSILON);
}
}