#[derive(Debug, Clone, Copy, Default)]
#[expect(
clippy::struct_field_names,
reason = "every field is a token count; the `_tokens` suffix is the domain vocabulary shared \
with the provider usage wire formats"
)]
pub struct CanonicalUsage {
pub input_tokens: u32,
pub output_tokens: u32,
pub cache_read_tokens: u32,
pub cache_creation_tokens: u32,
pub reasoning_tokens: u32,
pub total_tokens: u32,
}
impl CanonicalUsage {
#[must_use]
pub const fn billable_total(&self) -> u32 {
self.input_tokens
.saturating_add(self.output_tokens)
.saturating_add(self.cache_read_tokens)
.saturating_add(self.cache_creation_tokens)
}
pub fn normalise_reasoning(&mut self, provider: &str) -> bool {
let additive = self.reasoning_tokens > self.output_tokens
|| (self.reasoning_tokens > 0
&& self.total_tokens
== self.billable_total().saturating_add(self.reasoning_tokens));
if !additive {
return false;
}
let folded = self.output_tokens.saturating_add(self.reasoning_tokens);
tracing::warn!(
provider,
reasoning_tokens = self.reasoning_tokens,
reported_output_tokens = self.output_tokens,
folded_output_tokens = folded,
"provider reports reasoning tokens in addition to output tokens; folding them in so \
the thinking share is billed"
);
self.output_tokens = folded;
self.total_tokens = self.billable_total();
true
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[expect(
clippy::struct_field_names,
reason = "every field is a token count; the `_tokens` suffix is the domain vocabulary shared \
with the provider usage wire formats"
)]
pub struct CanonicalUsageUpdate {
pub input_tokens: Option<u32>,
pub output_tokens: Option<u32>,
pub cache_read_tokens: Option<u32>,
pub cache_creation_tokens: Option<u32>,
pub reasoning_tokens: Option<u32>,
pub total_tokens: Option<u32>,
}
impl CanonicalUsageUpdate {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.input_tokens.is_none()
&& self.output_tokens.is_none()
&& self.cache_read_tokens.is_none()
&& self.cache_creation_tokens.is_none()
&& self.reasoning_tokens.is_none()
&& self.total_tokens.is_none()
}
pub const fn apply_to(&self, usage: &mut CanonicalUsage) {
if let Some(v) = self.input_tokens {
usage.input_tokens = v;
}
if let Some(v) = self.output_tokens {
usage.output_tokens = v;
}
if let Some(v) = self.cache_read_tokens {
usage.cache_read_tokens = v;
}
if let Some(v) = self.cache_creation_tokens {
usage.cache_creation_tokens = v;
}
if let Some(v) = self.reasoning_tokens {
usage.reasoning_tokens = v;
}
usage.total_tokens = match self.total_tokens {
Some(v) => v,
None => usage.billable_total(),
};
}
}