use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum UsageCoverage {
Exact,
Partial,
Absent,
}
impl UsageCoverage {
pub fn as_str(self) -> &'static str {
match self {
UsageCoverage::Exact => "exact",
UsageCoverage::Partial => "partial",
UsageCoverage::Absent => "absent",
}
}
}
impl std::fmt::Display for UsageCoverage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Usage {
pub input_tokens: Option<u64>,
pub cache_read_tokens: Option<u64>,
pub cache_write_tokens: Option<u64>,
pub output_tokens: Option<u64>,
}
impl Usage {
pub fn absent() -> Self {
Self::default()
}
#[must_use]
pub fn with_input(mut self, tokens: u64) -> Self {
self.input_tokens = Some(tokens);
self
}
#[must_use]
pub fn with_cache_read(mut self, tokens: u64) -> Self {
self.cache_read_tokens = Some(tokens);
self
}
#[must_use]
pub fn with_cache_write(mut self, tokens: u64) -> Self {
self.cache_write_tokens = Some(tokens);
self
}
#[must_use]
pub fn with_output(mut self, tokens: u64) -> Self {
self.output_tokens = Some(tokens);
self
}
pub fn embedding(input_tokens: u64) -> Self {
Usage {
input_tokens: Some(input_tokens),
cache_read_tokens: Some(0),
cache_write_tokens: Some(0),
output_tokens: Some(0),
}
}
pub fn coverage(&self) -> UsageCoverage {
let fields = [
self.input_tokens,
self.cache_read_tokens,
self.cache_write_tokens,
self.output_tokens,
];
let present = fields.iter().filter(|f| f.is_some()).count();
match present {
0 => UsageCoverage::Absent,
n if n == fields.len() => UsageCoverage::Exact,
_ => UsageCoverage::Partial,
}
}
pub fn prompt_tokens(&self) -> Option<u64> {
let parts = [
self.input_tokens,
self.cache_read_tokens,
self.cache_write_tokens,
];
if parts.iter().all(Option::is_none) {
return None;
}
Some(parts.iter().map(|p| p.unwrap_or(0)).sum())
}
pub fn merge(self, other: Usage) -> Usage {
fn add(a: Option<u64>, b: Option<u64>) -> Option<u64> {
match (a, b) {
(None, None) => None,
(a, b) => Some(a.unwrap_or(0).saturating_add(b.unwrap_or(0))),
}
}
Usage {
input_tokens: add(self.input_tokens, other.input_tokens),
cache_read_tokens: add(self.cache_read_tokens, other.cache_read_tokens),
cache_write_tokens: add(self.cache_write_tokens, other.cache_write_tokens),
output_tokens: add(self.output_tokens, other.output_tokens),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn full() -> Usage {
Usage {
input_tokens: Some(100),
cache_read_tokens: Some(900),
cache_write_tokens: Some(50),
output_tokens: Some(20),
}
}
#[test]
fn nothing_reported_is_absent_rather_than_zero() {
let nothing = Usage::absent();
assert_eq!(nothing.coverage(), UsageCoverage::Absent);
assert_eq!(nothing.prompt_tokens(), None);
assert_eq!(nothing.output_tokens, None);
}
#[test]
fn some_fields_reported_is_partial() {
let partial = Usage {
output_tokens: Some(20),
..Usage::absent()
};
assert_eq!(partial.coverage(), UsageCoverage::Partial);
}
#[test]
fn every_field_reported_is_exact() {
assert_eq!(full().coverage(), UsageCoverage::Exact);
}
#[test]
fn the_prompt_total_counts_the_cached_part() {
assert_eq!(full().prompt_tokens(), Some(1_050));
}
#[test]
fn merging_two_absent_usages_stays_absent() {
let merged = Usage::absent().merge(Usage::absent());
assert_eq!(merged.coverage(), UsageCoverage::Absent);
}
#[test]
fn merging_a_reported_call_with_an_unreported_one_keeps_the_number() {
let merged = full().merge(Usage::absent());
assert_eq!(merged.output_tokens, Some(20));
assert_eq!(merged.coverage(), UsageCoverage::Exact);
}
#[test]
fn merging_adds_field_by_field() {
let merged = full().merge(full());
assert_eq!(merged.input_tokens, Some(200));
assert_eq!(merged.output_tokens, Some(40));
}
#[test]
fn an_embedding_call_is_measured_exactly_rather_than_partly() {
let usage = Usage::embedding(1_500);
assert_eq!(usage.coverage(), UsageCoverage::Exact);
assert_eq!(usage.prompt_tokens(), Some(1_500));
assert_eq!(usage.output_tokens, Some(0));
}
#[test]
fn an_embedding_nobody_measured_is_still_absent() {
assert_eq!(Usage::absent().coverage(), UsageCoverage::Absent);
assert_eq!(Usage::embedding(0).coverage(), UsageCoverage::Exact);
}
}