use genai::chat::Usage;
use serde::{Deserialize, Serialize};
#[cfg(feature = "server")]
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(feature = "server", derive(ToSchema))]
#[allow(clippy::struct_field_names)]
pub struct TokenUsage {
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub total_tokens: u64,
}
impl TokenUsage {
#[must_use]
pub const fn new() -> Self {
Self {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
}
}
pub const fn accumulate(
&mut self,
other: &Self,
) {
self.prompt_tokens = self.prompt_tokens.saturating_add(other.prompt_tokens);
self.completion_tokens = self.completion_tokens.saturating_add(other.completion_tokens);
self.total_tokens = self.total_tokens.saturating_add(other.total_tokens);
}
pub fn add_genai_usage(
&mut self,
usage: &Usage,
) {
self.accumulate(&Self::from(usage));
}
}
fn clamp(value: Option<i32>) -> u64 {
value.map_or(0, |v| u64::try_from(v).unwrap_or(0))
}
impl From<&Usage> for TokenUsage {
fn from(usage: &Usage) -> Self {
let prompt_tokens = clamp(usage.prompt_tokens);
let completion_tokens = clamp(usage.completion_tokens);
let total_tokens = usage
.total_tokens
.map_or_else(|| prompt_tokens.saturating_add(completion_tokens), |t| clamp(Some(t)));
Self {
prompt_tokens,
completion_tokens,
total_tokens,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn usage(
prompt: Option<i32>,
completion: Option<i32>,
total: Option<i32>,
) -> Usage {
Usage {
prompt_tokens: prompt,
completion_tokens: completion,
total_tokens: total,
prompt_tokens_details: None,
completion_tokens_details: None,
}
}
#[test]
fn from_genai_usage_uses_reported_total() {
let token_usage = TokenUsage::from(&usage(Some(10), Some(5), Some(15)));
assert_eq!(token_usage.prompt_tokens, 10);
assert_eq!(token_usage.completion_tokens, 5);
assert_eq!(token_usage.total_tokens, 15);
}
#[test]
fn from_genai_usage_falls_back_to_sum_when_total_missing() {
let token_usage = TokenUsage::from(&usage(Some(10), Some(5), None));
assert_eq!(token_usage.total_tokens, 15);
}
#[test]
fn from_genai_usage_treats_none_and_negative_as_zero() {
let token_usage = TokenUsage::from(&usage(None, Some(-7), None));
assert_eq!(token_usage.prompt_tokens, 0);
assert_eq!(token_usage.completion_tokens, 0);
assert_eq!(token_usage.total_tokens, 0);
}
#[test]
fn accumulate_sums_all_fields() {
let mut acc = TokenUsage::new();
acc.add_genai_usage(&usage(Some(10), Some(5), Some(15)));
acc.add_genai_usage(&usage(Some(3), Some(2), Some(5)));
assert_eq!(acc.prompt_tokens, 13);
assert_eq!(acc.completion_tokens, 7);
assert_eq!(acc.total_tokens, 20);
}
#[test]
fn accumulate_saturates_on_overflow() {
let mut acc = TokenUsage {
prompt_tokens: u64::MAX,
completion_tokens: 0,
total_tokens: 0,
};
acc.accumulate(&TokenUsage {
prompt_tokens: 1,
completion_tokens: 0,
total_tokens: 0,
});
assert_eq!(acc.prompt_tokens, u64::MAX);
}
}