use super::call_log::ModelUsage;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelCallUsage {
pub model: String,
pub usage: AttributedUsage,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributedUsage {
Llm {
input_tokens: Option<u64>,
output_tokens: Option<u64>,
},
Embedding {
input_tokens: Option<u64>,
},
}
impl ModelCallUsage {
pub(crate) fn llm(model: impl Into<String>, usage: ModelUsage) -> Self {
Self {
model: model.into(),
usage: AttributedUsage::Llm {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
},
}
}
pub(crate) fn embedding(model: impl Into<String>, input_tokens: Option<u64>) -> Self {
Self {
model: model.into(),
usage: AttributedUsage::Embedding { input_tokens },
}
}
pub fn summed_by_model(calls: &[Self]) -> Vec<Self> {
let mut totals = Vec::new();
for call in calls {
if let Some(total) = totals.iter_mut().find(|total| same_group(total, call)) {
add_same_kind(&mut total.usage, &call.usage);
} else {
totals.push(call.clone());
}
}
totals
}
}
fn same_group(left: &ModelCallUsage, right: &ModelCallUsage) -> bool {
left.model == right.model && same_kind(&left.usage, &right.usage)
}
fn same_kind(left: &AttributedUsage, right: &AttributedUsage) -> bool {
matches!(
(left, right),
(AttributedUsage::Llm { .. }, AttributedUsage::Llm { .. })
| (
AttributedUsage::Embedding { .. },
AttributedUsage::Embedding { .. }
)
)
}
fn add_same_kind(total: &mut AttributedUsage, next: &AttributedUsage) {
match (total, next) {
(
AttributedUsage::Llm {
input_tokens,
output_tokens,
},
AttributedUsage::Llm {
input_tokens: next_input,
output_tokens: next_output,
},
) => {
*input_tokens = add_count(*input_tokens, *next_input);
*output_tokens = add_count(*output_tokens, *next_output);
}
(
AttributedUsage::Embedding { input_tokens },
AttributedUsage::Embedding {
input_tokens: next_input,
},
) => *input_tokens = add_count(*input_tokens, *next_input),
_ => {}
}
}
fn add_count(left: Option<u64>, right: Option<u64>) -> Option<u64> {
Some(left?.saturating_add(right?))
}
#[cfg(test)]
mod tests {
use super::*;
fn llm(model: &str, input: Option<u64>, output: Option<u64>) -> ModelCallUsage {
ModelCallUsage::llm(
model,
ModelUsage {
input_tokens: input,
output_tokens: output,
},
)
}
#[test]
fn summed_by_model_adds_a_model_and_keeps_a_missing_count() {
let calls = [
llm("haiku", Some(10), Some(1)),
llm("sonnet", Some(3), Some(4)),
llm("haiku", None, Some(2)),
];
assert_eq!(
ModelCallUsage::summed_by_model(&calls),
vec![llm("haiku", None, Some(3)), llm("sonnet", Some(3), Some(4)),]
);
}
#[test]
fn summed_by_model_does_not_merge_llm_and_embedding() {
let calls = [
llm("haiku", Some(10), Some(1)),
ModelCallUsage::embedding("haiku", Some(7)),
ModelCallUsage::embedding("haiku", None),
];
assert_eq!(
ModelCallUsage::summed_by_model(&calls),
vec![
llm("haiku", Some(10), Some(1)),
ModelCallUsage::embedding("haiku", None),
]
);
}
}