use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunUsage {
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_read_tokens: u64,
pub cache_creation_tokens: u64,
#[serde(default)]
pub reasoning_tokens: u64,
#[serde(default)]
pub thoughts_tokens: u64,
#[serde(default, skip_serializing_if = "is_zero")]
pub model_responses: u32,
}
impl RunUsage {
pub const fn total_tokens(&self) -> u64 {
self.input_tokens + self.output_tokens
}
#[must_use]
pub const fn plus(self, other: Self) -> Self {
Self {
input_tokens: self.input_tokens + other.input_tokens,
output_tokens: self.output_tokens + other.output_tokens,
cache_read_tokens: self.cache_read_tokens + other.cache_read_tokens,
cache_creation_tokens: self.cache_creation_tokens + other.cache_creation_tokens,
reasoning_tokens: self.reasoning_tokens + other.reasoning_tokens,
thoughts_tokens: self.thoughts_tokens + other.thoughts_tokens,
model_responses: self.model_responses.saturating_add(other.model_responses),
}
}
pub(crate) fn recording_agent(self, event: &mentra::agent::AgentEvent) -> Self {
let mentra::agent::AgentEvent::UsageReport {
input_tokens,
output_tokens,
cache_read_tokens,
cache_creation_tokens,
reasoning_tokens,
thoughts_tokens,
} = event
else {
return self;
};
Self {
input_tokens: self.input_tokens + *input_tokens,
output_tokens: self.output_tokens + *output_tokens,
cache_read_tokens: self.cache_read_tokens + *cache_read_tokens,
cache_creation_tokens: self.cache_creation_tokens + *cache_creation_tokens,
reasoning_tokens: self.reasoning_tokens + *reasoning_tokens,
thoughts_tokens: self.thoughts_tokens + *thoughts_tokens,
model_responses: self.model_responses.saturating_add(1),
}
}
}
fn is_zero(value: &u32) -> bool {
*value == 0
}
#[cfg(test)]
mod tests {
use super::*;
fn usage_report(input: u64, output: u64) -> mentra::agent::AgentEvent {
mentra::agent::AgentEvent::UsageReport {
input_tokens: input,
output_tokens: output,
cache_read_tokens: 1,
cache_creation_tokens: 2,
reasoning_tokens: 3,
thoughts_tokens: 4,
}
}
#[test]
fn usage_is_summed_over_the_rounds_of_a_turn() {
let usage = RunUsage::default()
.recording_agent(&usage_report(100, 20))
.recording_agent(&usage_report(150, 30));
assert_eq!(usage.input_tokens, 250);
assert_eq!(usage.output_tokens, 50);
assert_eq!(usage.cache_read_tokens, 2);
assert_eq!(usage.cache_creation_tokens, 4);
assert_eq!(usage.reasoning_tokens, 6);
assert_eq!(usage.thoughts_tokens, 8);
assert_eq!(usage.model_responses, 2);
assert_eq!(usage.total_tokens(), 300, "the budget counts these two");
}
#[test]
fn an_event_that_reports_no_usage_changes_nothing() {
let counted = RunUsage::default().recording_agent(&usage_report(10, 5));
let after = counted.recording_agent(&mentra::agent::AgentEvent::RunFinished);
assert_eq!(after, counted);
}
#[test]
fn a_turn_that_reported_nothing_reports_zero() {
let usage = RunUsage::default();
assert_eq!(usage.total_tokens(), 0);
}
#[test]
fn usage_adds_up_across_runs() {
let one = RunUsage::default().recording_agent(&usage_report(100, 10));
let two = RunUsage::default().recording_agent(&usage_report(200, 20));
assert_eq!(one.plus(two).total_tokens(), 330);
assert_eq!(one.total_tokens(), 110, "the originals are untouched");
}
}