use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub cache_read_tokens: u32,
pub cache_creation_tokens: u32,
pub cost_usd: Option<f64>,
}
impl Usage {
pub fn new(
prompt_tokens: u32,
completion_tokens: u32,
cache_read_tokens: u32,
cache_creation_tokens: u32,
) -> Self {
Self {
prompt_tokens,
completion_tokens,
cache_read_tokens,
cache_creation_tokens,
cost_usd: None,
}
}
pub fn total_tokens(&self) -> u32 {
self.prompt_tokens.saturating_add(self.completion_tokens)
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct PromptTokensDetails {
#[serde(default)]
pub cached_tokens: u32,
#[serde(default)]
pub cache_write_tokens: u32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UsageBlock {
#[serde(default)]
pub prompt_tokens: u32,
#[serde(default)]
pub completion_tokens: u32,
#[serde(default)]
pub total_tokens: u32,
#[serde(default)]
pub cache_read_input_tokens: u32,
#[serde(default)]
pub cache_creation_input_tokens: u32,
#[serde(default)]
pub prompt_tokens_details: Option<PromptTokensDetails>,
#[serde(default)]
pub cost: Option<f64>,
}
impl UsageBlock {
pub fn into_usage(self) -> Usage {
let (details_read, details_write) = self
.prompt_tokens_details
.map(|d| (d.cached_tokens, d.cache_write_tokens))
.unwrap_or((0, 0));
let cache_read = if self.cache_read_input_tokens != 0 {
self.cache_read_input_tokens
} else {
details_read
};
let cache_creation = if self.cache_creation_input_tokens != 0 {
self.cache_creation_input_tokens
} else {
details_write
};
let mut usage = Usage::new(
self.prompt_tokens,
self.completion_tokens,
cache_read,
cache_creation,
);
usage.cost_usd = self.cost;
usage
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usage_total_tokens() {
let u = Usage::new(100, 50, 20, 10);
assert_eq!(u.total_tokens(), 150);
assert_eq!(u.cost_usd, None);
}
#[test]
fn usage_block_maps_flat_fields() {
let block = UsageBlock {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
cache_read_input_tokens: 20,
cache_creation_input_tokens: 10,
prompt_tokens_details: None,
cost: None,
};
let u = block.into_usage();
assert_eq!(u.prompt_tokens, 100);
assert_eq!(u.completion_tokens, 50);
assert_eq!(u.cache_read_tokens, 20);
assert_eq!(u.cache_creation_tokens, 10);
}
#[test]
fn usage_block_maps_openrouter_details() {
let fixture = r#"{
"prompt_tokens": 1200,
"completion_tokens": 80,
"total_tokens": 1280,
"prompt_tokens_details": {"cached_tokens": 900, "cache_write_tokens": 300}
}"#;
let block: UsageBlock = serde_json::from_str(fixture).expect("deserialise");
let u = block.into_usage();
assert_eq!(u.cache_read_tokens, 900);
assert_eq!(u.cache_creation_tokens, 300);
}
#[test]
fn usage_block_prefers_authoritative_cost() {
let fixture = r#"{"prompt_tokens": 10, "completion_tokens": 2, "cost": 0.001234}"#;
let block: UsageBlock = serde_json::from_str(fixture).expect("deserialise");
assert_eq!(block.into_usage().cost_usd, Some(0.001234));
}
#[test]
fn default_usage_block_is_zero() {
let u = UsageBlock::default().into_usage();
assert_eq!(u.total_tokens(), 0);
assert_eq!(u.cost_usd, None);
}
}