use serde::{Deserialize, Serialize};
use crate::conversation::token_usage::Usage;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Modality {
Text,
Image,
Audio,
Video,
Pdf,
}
fn deserialize_modalities<'de, D>(deserializer: D) -> Result<Vec<Modality>, D::Error>
where
D: serde::Deserializer<'de>,
{
let strings: Vec<String> = Vec::deserialize(deserializer)?;
Ok(strings
.into_iter()
.filter_map(|s| serde_json::from_value(serde_json::Value::String(s)).ok())
.collect())
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Modalities {
#[serde(default, deserialize_with = "deserialize_modalities")]
pub input: Vec<Modality>,
#[serde(default, deserialize_with = "deserialize_modalities")]
pub output: Vec<Modality>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Pricing {
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_write: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tiers: Vec<PricingTier>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PricingTier {
pub above_input_tokens: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_write: Option<f64>,
}
impl Pricing {
pub fn has_no_usable_rate(&self) -> bool {
fn is_gap(price: Option<f64>) -> bool {
matches!(price, None | Some(0.0))
}
is_gap(self.input) || is_gap(self.output)
}
pub fn estimate_cost(&self, usage: &Usage) -> Option<f64> {
let prompt_tokens = usage.input_tokens.unwrap_or(0).max(0) as u64;
let tier = self
.tiers
.iter()
.filter(|tier| prompt_tokens > tier.above_input_tokens)
.max_by_key(|tier| tier.above_input_tokens);
let input_price = tier.and_then(|t| t.input).or(self.input)?;
let output_price = tier.and_then(|t| t.output).or(self.output)?;
let cache_read_price = tier
.and_then(|t| t.cache_read)
.or(self.cache_read)
.unwrap_or(input_price);
let cache_write_price = tier
.and_then(|t| t.cache_write)
.or(self.cache_write)
.unwrap_or(input_price);
let input_tokens = prompt_tokens as f64;
let output_tokens = usage.output_tokens.unwrap_or(0).max(0) as f64;
let cache_read_tokens = usage.cache_read_input_tokens.unwrap_or(0).max(0) as f64;
let cache_write_tokens = usage.cache_write_input_tokens.unwrap_or(0).max(0) as f64;
let uncached_input_tokens =
(input_tokens - cache_read_tokens - cache_write_tokens).max(0.0);
Some(
(uncached_input_tokens * input_price
+ cache_read_tokens * cache_read_price
+ cache_write_tokens * cache_write_price
+ output_tokens * output_price)
/ 1_000_000.0,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Limit {
pub context: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingMode {
Enabled,
Adaptive,
AlwaysOnAdaptive,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanonicalModel {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachment: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_efforts: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_mode: Option<ThinkingMode>,
#[serde(default)]
pub tool_call: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub knowledge: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub release_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_updated: Option<String>,
#[serde(default)]
pub modalities: Modalities,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_weights: Option<bool>,
#[serde(default)]
pub cost: Pricing,
#[serde(default)]
pub limit: Limit,
}
#[cfg(test)]
mod tests {
use super::*;
fn pricing(
input: Option<f64>,
output: Option<f64>,
cache_read: Option<f64>,
cache_write: Option<f64>,
) -> Pricing {
Pricing {
input,
output,
cache_read,
cache_write,
tiers: Vec::new(),
}
}
#[test]
fn estimate_cost_prices_cache_tokens_at_cache_rates() {
let pricing = pricing(Some(5.0), Some(25.0), Some(0.5), Some(6.25));
let usage =
Usage::new(Some(10_000), Some(1_000), None).with_cache_tokens(Some(8_000), Some(1_000));
let cost = pricing.estimate_cost(&usage).unwrap();
let expected =
(1_000.0 * 5.0 + 8_000.0 * 0.5 + 1_000.0 * 6.25 + 1_000.0 * 25.0) / 1_000_000.0;
assert!((cost - expected).abs() < f64::EPSILON);
}
#[test]
fn estimate_cost_handles_missing_prices() {
let usage =
Usage::new(Some(1_000), Some(100), None).with_cache_tokens(Some(600), Some(200));
let cost = pricing(Some(2.0), Some(10.0), None, None)
.estimate_cost(&usage)
.unwrap();
assert_eq!(cost, (1_000.0 * 2.0 + 100.0 * 10.0) / 1_000_000.0);
assert!(pricing(None, Some(10.0), None, None)
.estimate_cost(&usage)
.is_none());
assert!(pricing(Some(2.0), None, None, None)
.estimate_cost(&usage)
.is_none());
}
#[test]
fn has_no_usable_rate_covers_absent_zero_and_asymmetric_prices() {
assert!(pricing(None, None, None, None).has_no_usable_rate());
assert!(pricing(Some(0.0), Some(0.0), None, None).has_no_usable_rate());
assert!(pricing(Some(0.0), Some(5.0), None, None).has_no_usable_rate());
assert!(pricing(Some(5.0), Some(0.0), None, None).has_no_usable_rate());
assert!(pricing(None, Some(5.0), None, None).has_no_usable_rate());
assert!(!pricing(Some(1.25), Some(10.0), None, None).has_no_usable_rate());
assert!(!pricing(Some(1.25), Some(10.0), Some(0.0), Some(0.0)).has_no_usable_rate());
}
#[test]
fn estimate_cost_clamps_cache_tokens_exceeding_input() {
let pricing = pricing(Some(5.0), Some(25.0), Some(0.5), Some(6.25));
let usage = Usage::new(Some(100), Some(10), None).with_cache_tokens(Some(150), Some(50));
let cost = pricing.estimate_cost(&usage).unwrap();
let expected = (150.0 * 0.5 + 50.0 * 6.25 + 10.0 * 25.0) / 1_000_000.0;
assert!((cost - expected).abs() < f64::EPSILON);
}
}