submilli_engine/runtime/
model_calls.rs1use super::call_log::ModelUsage;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ModelCallUsage {
17 pub model: String,
18 pub usage: AttributedUsage,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum AttributedUsage {
27 Llm {
28 input_tokens: Option<u64>,
29 output_tokens: Option<u64>,
30 },
31 Embedding {
32 input_tokens: Option<u64>,
33 },
34}
35
36impl ModelCallUsage {
37 pub(crate) fn llm(model: impl Into<String>, usage: ModelUsage) -> Self {
38 Self {
39 model: model.into(),
40 usage: AttributedUsage::Llm {
41 input_tokens: usage.input_tokens,
42 output_tokens: usage.output_tokens,
43 },
44 }
45 }
46
47 pub(crate) fn embedding(model: impl Into<String>, input_tokens: Option<u64>) -> Self {
48 Self {
49 model: model.into(),
50 usage: AttributedUsage::Embedding { input_tokens },
51 }
52 }
53
54 pub fn summed_by_model(calls: &[Self]) -> Vec<Self> {
59 let mut totals = Vec::new();
60 for call in calls {
61 if let Some(total) = totals.iter_mut().find(|total| same_group(total, call)) {
62 add_same_kind(&mut total.usage, &call.usage);
63 } else {
64 totals.push(call.clone());
65 }
66 }
67 totals
68 }
69}
70
71fn same_group(left: &ModelCallUsage, right: &ModelCallUsage) -> bool {
72 left.model == right.model && same_kind(&left.usage, &right.usage)
73}
74
75fn same_kind(left: &AttributedUsage, right: &AttributedUsage) -> bool {
76 matches!(
77 (left, right),
78 (AttributedUsage::Llm { .. }, AttributedUsage::Llm { .. })
79 | (
80 AttributedUsage::Embedding { .. },
81 AttributedUsage::Embedding { .. }
82 )
83 )
84}
85
86fn add_same_kind(total: &mut AttributedUsage, next: &AttributedUsage) {
89 match (total, next) {
90 (
91 AttributedUsage::Llm {
92 input_tokens,
93 output_tokens,
94 },
95 AttributedUsage::Llm {
96 input_tokens: next_input,
97 output_tokens: next_output,
98 },
99 ) => {
100 *input_tokens = add_count(*input_tokens, *next_input);
101 *output_tokens = add_count(*output_tokens, *next_output);
102 }
103 (
104 AttributedUsage::Embedding { input_tokens },
105 AttributedUsage::Embedding {
106 input_tokens: next_input,
107 },
108 ) => *input_tokens = add_count(*input_tokens, *next_input),
109 _ => {}
110 }
111}
112
113fn add_count(left: Option<u64>, right: Option<u64>) -> Option<u64> {
114 Some(left?.saturating_add(right?))
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn llm(model: &str, input: Option<u64>, output: Option<u64>) -> ModelCallUsage {
122 ModelCallUsage::llm(
123 model,
124 ModelUsage {
125 input_tokens: input,
126 output_tokens: output,
127 },
128 )
129 }
130
131 #[test]
132 fn summed_by_model_adds_a_model_and_keeps_a_missing_count() {
133 let calls = [
134 llm("haiku", Some(10), Some(1)),
135 llm("sonnet", Some(3), Some(4)),
136 llm("haiku", None, Some(2)),
137 ];
138 assert_eq!(
139 ModelCallUsage::summed_by_model(&calls),
140 vec![llm("haiku", None, Some(3)), llm("sonnet", Some(3), Some(4)),]
141 );
142 }
143
144 #[test]
145 fn summed_by_model_does_not_merge_llm_and_embedding() {
146 let calls = [
147 llm("haiku", Some(10), Some(1)),
148 ModelCallUsage::embedding("haiku", Some(7)),
149 ModelCallUsage::embedding("haiku", None),
150 ];
151 assert_eq!(
152 ModelCallUsage::summed_by_model(&calls),
153 vec![
154 llm("haiku", Some(10), Some(1)),
155 ModelCallUsage::embedding("haiku", None),
156 ]
157 );
158 }
159}