use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TokenUsage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_cached_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_cache_write_tokens: Option<u64>,
}
impl TokenUsage {
pub fn is_reported(&self) -> bool {
self.input_tokens.is_some()
|| self.output_tokens.is_some()
|| self.input_cached_tokens.is_some()
|| self.input_cache_write_tokens.is_some()
}
pub fn merge(&mut self, next: &Self) {
self.input_tokens = next.input_tokens.or(self.input_tokens);
self.output_tokens = next.output_tokens.or(self.output_tokens);
self.input_cached_tokens = next.input_cached_tokens.or(self.input_cached_tokens);
self.input_cache_write_tokens = next
.input_cache_write_tokens
.or(self.input_cache_write_tokens);
}
pub fn total_tokens(&self) -> Option<u64> {
self.input_tokens?.checked_add(self.output_tokens?)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TokenUsageCoverage {
pub input_tokens: u64,
pub output_tokens: u64,
pub input_cached_tokens: u64,
pub input_cache_write_tokens: u64,
}
impl crate::ModelUsageSummary {
pub fn observe_usage(&mut self, usage: &TokenUsage) {
if let Some(tokens) = usage.input_tokens {
self.input_tokens = self.input_tokens.saturating_add(tokens);
self.usage_coverage.input_tokens += 1;
}
if let Some(tokens) = usage.output_tokens {
self.output_tokens = self.output_tokens.saturating_add(tokens);
self.usage_coverage.output_tokens += 1;
}
self.usage_coverage.input_cached_tokens += u64::from(usage.input_cached_tokens.is_some());
self.usage_coverage.input_cache_write_tokens +=
u64::from(usage.input_cache_write_tokens.is_some());
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn missing_zero_and_partial_counters_survive_serialization_and_merge() {
let mut usage: TokenUsage =
serde_json::from_value(json!({"inputTokens":10,"inputCachedTokens":6})).unwrap();
assert_eq!(usage.total_tokens(), None);
usage.merge(&serde_json::from_value(json!({"outputTokens":4})).unwrap());
usage.merge(&TokenUsage::default());
assert_eq!(usage.total_tokens(), Some(14));
assert_eq!(
serde_json::to_value(&usage).unwrap(),
json!({"inputTokens":10,"outputTokens":4,"inputCachedTokens":6})
);
usage.merge(&serde_json::from_value(json!({"inputCachedTokens":0})).unwrap());
assert_eq!(usage.input_cached_tokens, Some(0));
assert!(!TokenUsage::default().is_reported());
assert_eq!(
serde_json::to_value(TokenUsage::default()).unwrap(),
json!({})
);
}
#[test]
fn summaries_count_only_observed_fields_and_total_overflow_is_absent() {
let mut summary = crate::ModelUsageSummary::default();
summary.observe_usage(&TokenUsage::default());
summary.observe_usage(&TokenUsage {
input_tokens: Some(0),
input_cached_tokens: Some(0),
..Default::default()
});
assert_eq!(summary.usage_coverage.input_tokens, 1);
assert_eq!(summary.usage_coverage.output_tokens, 0);
assert_eq!(summary.usage_coverage.input_cached_tokens, 1);
assert_eq!(
TokenUsage {
input_tokens: Some(u64::MAX),
output_tokens: Some(1),
..Default::default()
}
.total_tokens(),
None
);
}
}