use serde::{Deserialize, Serialize};
use crate::types::Usage;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ModelPricing {
pub prompt_per_1k: f64,
pub completion_per_1k: f64,
}
impl ModelPricing {
pub const fn new(prompt_per_1k: f64, completion_per_1k: f64) -> Self {
Self {
prompt_per_1k,
completion_per_1k,
}
}
pub fn estimate_cost(&self, usage: &Usage) -> f64 {
let prompt_cost = usage.prompt_tokens as f64 / 1000.0 * self.prompt_per_1k;
let completion_cost = usage.completion_tokens as f64 / 1000.0 * self.completion_per_1k;
prompt_cost + completion_cost
}
}
impl Usage {
pub fn estimated_cost(&self, pricing: &ModelPricing) -> f64 {
pricing.estimate_cost(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn usage(prompt: u64, completion: u64) -> Usage {
Usage {
prompt_tokens: prompt,
completion_tokens: completion,
total_tokens: prompt + completion,
}
}
#[test]
fn estimates_cost_from_separate_rates() {
let pricing = ModelPricing::new(0.0025, 0.01);
let cost = pricing.estimate_cost(&usage(1_000, 500));
assert!((cost - 0.0075).abs() < 1e-9, "got {cost}");
}
#[test]
fn zero_usage_is_zero_cost() {
let pricing = ModelPricing::new(0.0025, 0.01);
let cost = pricing.estimate_cost(&usage(0, 0));
assert!(cost.abs() < 1e-9, "got {cost}");
}
#[test]
fn usage_method_matches_pricing_method() {
let pricing = ModelPricing::new(0.003, 0.015);
let u = usage(1_234, 567);
let via_usage = u.estimated_cost(&pricing);
let via_pricing = pricing.estimate_cost(&u);
assert!((via_usage - via_pricing).abs() < 1e-12, "methods disagree");
}
#[test]
fn prompt_and_completion_priced_independently() {
let pricing = ModelPricing::new(0.0, 0.02);
let cost = pricing.estimate_cost(&usage(10_000, 1_000));
assert!((cost - 0.02).abs() < 1e-9, "got {cost}");
}
#[test]
fn serde_round_trips() {
let pricing = ModelPricing::new(0.0025, 0.01);
let json = serde_json::to_string(&pricing).unwrap();
let back: ModelPricing = serde_json::from_str(&json).unwrap();
assert_eq!(pricing, back);
}
}