use std::collections::BTreeMap;
use rust_decimal::Decimal;
use crate::connection::StreamFraming;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TokenUsage {
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub cached_input_tokens: Option<u64>,
pub cache_creation: Vec<CacheCreation>,
pub reasoning_tokens: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CacheCreation {
pub tokens: u64,
pub ttl_seconds: Option<u64>,
}
impl TokenUsage {
pub fn cache_creation_tokens(&self) -> u64 {
self.cache_creation
.iter()
.fold(0, |sum, entry| sum.saturating_add(entry.tokens))
}
pub fn add_cache_creation(&mut self, tokens: Option<u64>, ttl_seconds: Option<u64>) {
let Some(tokens) = tokens else { return };
if let Some(entry) = self
.cache_creation
.iter_mut()
.find(|entry| entry.ttl_seconds == ttl_seconds)
{
entry.tokens = entry.tokens.saturating_add(tokens);
} else {
self.cache_creation.push(CacheCreation {
tokens,
ttl_seconds,
});
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum UsageCompleteness {
Complete,
Partial,
#[default]
Unknown,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NormalizedUsage {
pub tokens: TokenUsage,
pub metrics: BTreeMap<String, Decimal>,
pub dimensions: BTreeMap<String, String>,
pub actual_service_tier: Option<String>,
pub completeness: UsageCompleteness,
pub attempts: Vec<UsageAttempt>,
pub responses: Vec<ResponseUsage>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResponseUsage {
pub id: String,
pub usage: Box<NormalizedUsage>,
}
impl NormalizedUsage {
pub fn aggregate<'a>(values: impl IntoIterator<Item = &'a NormalizedUsage>) -> Self {
let mut total = Self {
completeness: UsageCompleteness::Complete,
..Default::default()
};
let mut first = true;
fn add(target: &mut Option<u64>, value: Option<u64>) {
if let Some(v) = value {
*target = Some(target.unwrap_or(0).saturating_add(v));
}
}
for value in values {
add(&mut total.tokens.input_tokens, value.tokens.input_tokens);
add(&mut total.tokens.output_tokens, value.tokens.output_tokens);
add(
&mut total.tokens.cached_input_tokens,
value.tokens.cached_input_tokens,
);
add(
&mut total.tokens.reasoning_tokens,
value.tokens.reasoning_tokens,
);
for entry in &value.tokens.cache_creation {
total
.tokens
.add_cache_creation(Some(entry.tokens), entry.ttl_seconds);
}
for (key, count) in &value.metrics {
let sum = total.metrics.entry(key.clone()).or_default();
*sum = sum.saturating_add(*count);
}
if first {
total.dimensions = value.dimensions.clone();
total.actual_service_tier = value.actual_service_tier.clone();
first = false;
} else {
total
.dimensions
.retain(|key, v| value.dimensions.get(key) == Some(v));
if total.actual_service_tier != value.actual_service_tier {
total.actual_service_tier = None;
}
}
if value.completeness != UsageCompleteness::Complete {
total.completeness = UsageCompleteness::Partial;
}
}
total
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsageAttempt {
pub operation: Option<crate::Operation>,
pub model: String,
pub usage: Box<NormalizedUsage>,
pub billable: Option<bool>,
pub started_at_ms: Option<i64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UsageTransport {
Http { framing: Option<StreamFraming> },
WebSocket,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UsageStreamEnd {
Complete,
Interrupted,
}