use crate::pricing::data::{ModelPricing, PROVIDERS};
use genai::chat::Usage;
pub fn price_it(provider_type: &str, model_name: &str, usage: &Usage) -> Option<f64> {
let provider = PROVIDERS.iter().find(|p| p.name == provider_type)?;
let mut model: Option<&ModelPricing> = None;
for m in provider.models.iter() {
if model_name.starts_with(m.name) {
let current_len = model.map(|m| m.name.len()).unwrap_or(0);
if current_len < m.name.len() {
model = Some(m)
}
}
}
let model = model?;
let prompt_tokens = usage.prompt_tokens.unwrap_or(0) as f64;
let (prompt_tokens_normal, prompt_cached_tokens, prompt_cache_creation_tokens) = match &usage.prompt_tokens_details
{
Some(details) => {
let cached = details.cached_tokens.unwrap_or(0) as f64;
let cache_creation_tokens = details.cache_creation_tokens.unwrap_or(0) as f64;
let normal = prompt_tokens - cached;
(normal, cached, cache_creation_tokens)
}
None => (prompt_tokens, 0.0, 0.0),
};
let price_prompt_normal = model.input_normal;
let price_prompt_cached = model.input_cached.unwrap_or(price_prompt_normal);
let price_prompt_cache_creation = 1.25 * price_prompt_normal;
let completion_tokens = usage.completion_tokens.unwrap_or(0) as f64;
let (completion_tokens_normal, completion_tokens_reasoning) = if let Some(reasoning_tokens) = usage
.completion_tokens_details
.as_ref()
.and_then(|v| v.reasoning_tokens.map(|v| v as f64))
{
(completion_tokens - reasoning_tokens, reasoning_tokens)
} else {
(completion_tokens, 0.)
};
let price_completion_normal = model.output_normal;
let price_completion_reasoning = model.output_reasoning.unwrap_or(price_completion_normal);
let price = (prompt_tokens_normal * price_prompt_normal)
+ (prompt_cached_tokens * price_prompt_cached)
+ (prompt_cache_creation_tokens * price_prompt_cache_creation)
+ (completion_tokens_normal * price_completion_normal)
+ (completion_tokens_reasoning * price_completion_reasoning);
let price = price / 1_000_000.0;
let price = (price * 10_000.0).round() / 10_000.0;
Some(price)
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
use genai::chat::{PromptTokensDetails, Usage};
#[test]
fn test_pricing_pricer_price_it_simple() -> Result<()> {
let usage = Usage {
prompt_tokens: Some(1000),
completion_tokens: Some(500),
prompt_tokens_details: None,
..Default::default()
};
let price = price_it("openai", "gpt-4o", &usage);
assert!(price.is_some());
let price = price.unwrap();
let expected = 0.0025 + 0.005; assert!((price - expected).abs() < f64::EPSILON);
Ok(())
}
#[test]
fn test_pricing_pricer_price_it_with_cached() -> Result<()> {
let fx_prompt_normal_tokens = 1000;
let fx_completion_tokens = 500;
let fx_cached_tokens = 400;
let usage = Usage {
prompt_tokens: Some(fx_prompt_normal_tokens + fx_cached_tokens),
completion_tokens: Some(fx_completion_tokens),
prompt_tokens_details: Some(PromptTokensDetails {
cached_tokens: Some(fx_cached_tokens),
audio_tokens: None,
cache_creation_tokens: None,
}),
..Default::default()
};
let price = price_it("openai", "gpt-4o-mini", &usage);
assert!(price.is_some());
let price = price.unwrap();
let cached = fx_cached_tokens as f64 * 0.075 / 1_000_000.0;
let prompt = fx_prompt_normal_tokens as f64 * 0.150 / 1_000_000.0;
let completion = fx_completion_tokens as f64 * 0.6 / 1_000_000.0;
let expected = cached + prompt + completion;
let expected = (expected * 10_000.0).round() / 10_000.0;
assert!((price - expected).abs() < f64::EPSILON);
Ok(())
}
#[test]
fn test_pricing_pricer_price_it_with_cached_no_cached_price() -> Result<()> {
let fx_prompt_normal_tokens = 1000;
let fx_cached_tokens = 400;
let fx_completion_tokens = 500;
let usage = Usage {
prompt_tokens: Some(fx_prompt_normal_tokens + fx_cached_tokens),
completion_tokens: Some(fx_completion_tokens),
prompt_tokens_details: Some(PromptTokensDetails {
cached_tokens: Some(fx_cached_tokens),
audio_tokens: None,
cache_creation_tokens: None,
}),
..Default::default()
};
let price = price_it("gemini", "gemini-2.5-pro", &usage);
let price = price.ok_or("Should have price")?;
let cached = fx_cached_tokens as f64 * 1.25 / 1_000_000.0;
let prompt = fx_prompt_normal_tokens as f64 * 1.25 / 1_000_000.0;
let completion = fx_completion_tokens as f64 * 10. / 1_000_000.0;
let expected = cached + prompt + completion;
let expected = (expected * 10_000.0).round() / 10_000.0;
assert!((price - expected).abs() < f64::EPSILON);
Ok(())
}
#[test]
fn test_pricing_pricer_price_it_with_cache_creation() -> Result<()> {
let fx_prompt_normal_tokens = 1000;
let fx_cached_tokens = 400;
let fx_completion_tokens = 500;
let fx_cache_creation_tokens = 200;
let usage = Usage {
prompt_tokens: Some(fx_prompt_normal_tokens + fx_cached_tokens),
completion_tokens: Some(fx_completion_tokens),
prompt_tokens_details: Some(PromptTokensDetails {
cached_tokens: Some(fx_cached_tokens),
cache_creation_tokens: Some(fx_cache_creation_tokens),
audio_tokens: None,
}),
..Default::default()
};
let price = price_it("anthropic", "claude-3-5-sonnet", &usage);
assert!(price.is_some());
let price = price.unwrap();
let cached = fx_cached_tokens as f64 * 0.3 / 1_000_000.0;
let cache_creation = fx_cache_creation_tokens as f64 * 1.25 * 3.0 / 1_000_000.0;
let prompt = fx_prompt_normal_tokens as f64 * 3.0 / 1_000_000.0;
let completion = fx_completion_tokens as f64 * 15.0 / 1_000_000.0;
let expected = cached + cache_creation + prompt + completion;
let expected = (expected * 10_000.0).round() / 10_000.0; assert!((price - expected).abs() < f64::EPSILON);
Ok(())
}
#[test]
fn test_pricing_pricer_price_it_unknown_provider() -> Result<()> {
let usage = Usage {
prompt_tokens: Some(1000),
completion_tokens: Some(500),
..Default::default()
};
let price = price_it("unknown_provider", "gpt-4o", &usage);
assert!(price.is_none());
Ok(())
}
#[test]
fn test_pricing_pricer_price_it_unknown_model() -> Result<()> {
let usage = Usage {
prompt_tokens: Some(1000),
completion_tokens: Some(500),
..Default::default()
};
let price = price_it("openai", "unknown_model", &usage);
assert!(price.is_none());
Ok(())
}
}