1use serde::{Deserialize, Serialize};
4
5pub type RunId = uuid::Uuid;
7pub type AgentId = uuid::Uuid;
9pub type PhaseId = u32;
11
12#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(default)]
15pub struct TokenUsage {
16 pub input: u64,
17 pub output: u64,
18 pub cache_read: u64,
19 pub cache_write: u64,
20}
21
22impl std::ops::Add for TokenUsage {
23 type Output = Self;
24 fn add(self, o: Self) -> Self {
25 Self {
26 input: self.input + o.input,
27 output: self.output + o.output,
28 cache_read: self.cache_read + o.cache_read,
29 cache_write: self.cache_write + o.cache_write,
30 }
31 }
32}
33
34impl std::ops::AddAssign for TokenUsage {
35 fn add_assign(&mut self, o: Self) {
36 *self = *self + o;
37 }
38}
39
40impl TokenUsage {
41 pub fn total(&self) -> u64 {
43 self.input + self.output
44 }
45
46 pub fn display_total(&self) -> String {
48 fmt_tokens(self.total())
49 }
50
51 pub fn display_split(&self) -> String {
53 let mut parts = vec![
54 format!("↑{}", fmt_tokens(self.input)),
55 format!("↓{}", fmt_tokens(self.output)),
56 ];
57 if self.cache_read > 0 {
58 parts.push(format!("{} cached", fmt_tokens(self.cache_read)));
59 }
60 parts.join(" ")
61 }
62}
63
64pub fn fmt_tokens(n: u64) -> String {
74 if n < 1_000 {
75 return n.to_string();
76 }
77 let (divisor, suffix) = if n < 1_000_000 {
78 (1_000_u64, "k")
79 } else if n < 1_000_000_000 {
80 (1_000_000_u64, "M")
81 } else {
82 (1_000_000_000_u64, "B")
83 };
84 let v = n as f64 / divisor as f64;
85 if v >= 999.95 {
87 let next_divisor = divisor * 1000;
88 let next_suffix = match suffix {
89 "k" => "M",
90 "M" => "B",
91 "B" => "T",
92 _ => unreachable!("unexpected suffix {suffix}"),
93 };
94 let v = n as f64 / next_divisor as f64;
95 fmt_suffix(v, next_suffix)
96 } else {
97 fmt_suffix(v, suffix)
98 }
99}
100
101fn fmt_suffix(v: f64, suffix: &str) -> String {
102 let s = format!("{:.1}", v);
103 let s = s.trim_end_matches(".0");
104 format!("{}{}", s, suffix)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_default() {
113 let t = TokenUsage::default();
114 assert_eq!(t.input, 0);
115 assert_eq!(t.output, 0);
116 assert_eq!(t.cache_read, 0);
117 assert_eq!(t.cache_write, 0);
118 }
119
120 #[test]
121 fn test_total_basic() {
122 let t = TokenUsage {
123 input: 10,
124 output: 20,
125 cache_read: 5,
126 cache_write: 3,
127 };
128 assert_eq!(t.total(), 30);
129 }
130
131 #[test]
132 fn test_total_zero() {
133 let t = TokenUsage::default();
134 assert_eq!(t.total(), 0);
135 }
136
137 #[test]
138 fn test_total_only_input() {
139 let t = TokenUsage {
140 input: 100,
141 output: 0,
142 cache_read: 0,
143 cache_write: 0,
144 };
145 assert_eq!(t.total(), 100);
146 }
147
148 #[test]
149 fn test_total_only_output() {
150 let t = TokenUsage {
151 input: 0,
152 output: 200,
153 cache_read: 0,
154 cache_write: 0,
155 };
156 assert_eq!(t.total(), 200);
157 }
158
159 #[test]
160 fn test_add() {
161 let a = TokenUsage {
162 input: 10,
163 output: 20,
164 cache_read: 5,
165 cache_write: 3,
166 };
167 let b = TokenUsage {
168 input: 3,
169 output: 7,
170 cache_read: 2,
171 cache_write: 1,
172 };
173 let result = a + b;
174 assert_eq!(result.input, 13);
175 assert_eq!(result.output, 27);
176 assert_eq!(result.cache_read, 7);
177 assert_eq!(result.cache_write, 4);
178 }
179
180 #[test]
181 fn test_add_zero() {
182 let a = TokenUsage {
183 input: 10,
184 output: 20,
185 cache_read: 5,
186 cache_write: 3,
187 };
188 let zero = TokenUsage::default();
189 let result = a + zero;
190 assert_eq!(result.input, 10);
191 assert_eq!(result.output, 20);
192 assert_eq!(result.cache_read, 5);
193 assert_eq!(result.cache_write, 3);
194 }
195
196 #[test]
197 fn test_add_large() {
198 let a = TokenUsage {
199 input: u64::MAX,
200 output: 0,
201 cache_read: 0,
202 cache_write: 0,
203 };
204 let b = TokenUsage {
205 input: 0,
206 output: u64::MAX,
207 cache_read: 0,
208 cache_write: 0,
209 };
210 let result = a + b;
211 assert_eq!(result.input, u64::MAX);
212 assert_eq!(result.output, u64::MAX);
213 }
214
215 #[test]
216 fn test_add_assign() {
217 let mut a = TokenUsage {
218 input: 10,
219 output: 20,
220 cache_read: 5,
221 cache_write: 3,
222 };
223 let b = TokenUsage {
224 input: 3,
225 output: 7,
226 cache_read: 2,
227 cache_write: 1,
228 };
229 a += b;
230 assert_eq!(a.input, 13);
231 assert_eq!(a.output, 27);
232 assert_eq!(a.cache_read, 7);
233 assert_eq!(a.cache_write, 4);
234 }
235
236 #[test]
237 fn test_add_assign_zero() {
238 let mut a = TokenUsage {
239 input: 10,
240 output: 20,
241 cache_read: 5,
242 cache_write: 3,
243 };
244 a += TokenUsage::default();
245 assert_eq!(a.input, 10);
246 assert_eq!(a.output, 20);
247 assert_eq!(a.cache_read, 5);
248 assert_eq!(a.cache_write, 3);
249 }
250
251 #[test]
252 fn test_add_assign_chained() {
253 let mut a = TokenUsage {
254 input: 1,
255 output: 2,
256 cache_read: 3,
257 cache_write: 4,
258 };
259 let b = TokenUsage {
260 input: 10,
261 output: 20,
262 cache_read: 30,
263 cache_write: 40,
264 };
265 let c = TokenUsage {
266 input: 100,
267 output: 200,
268 cache_read: 300,
269 cache_write: 400,
270 };
271 a += b;
272 a += c;
273 assert_eq!(a.input, 111);
274 assert_eq!(a.output, 222);
275 assert_eq!(a.cache_read, 333);
276 assert_eq!(a.cache_write, 444);
277 }
278
279 #[test]
280 fn test_serialize_roundtrip() {
281 let t = TokenUsage {
282 input: 1,
283 output: 2,
284 cache_read: 3,
285 cache_write: 4,
286 };
287 let json = serde_json::to_string(&t).unwrap();
288 let deserialized: TokenUsage = serde_json::from_str(&json).unwrap();
289 assert_eq!(deserialized, t);
290 }
291
292 #[test]
293 fn test_serialize_default() {
294 let t = TokenUsage::default();
295 let json = serde_json::to_string(&t).unwrap();
296 assert_eq!(
297 json,
298 r#"{"input":0,"output":0,"cache_read":0,"cache_write":0}"#
299 );
300 let deserialized: TokenUsage = serde_json::from_str(&json).unwrap();
301 assert_eq!(deserialized, t);
302 }
303
304 #[test]
305 fn test_debug_format() {
306 let t = TokenUsage {
307 input: 1,
308 output: 2,
309 cache_read: 3,
310 cache_write: 4,
311 };
312 let debug = format!("{:?}", t);
313 assert!(debug.contains("input: 1"));
314 assert!(debug.contains("output: 2"));
315 assert!(debug.contains("cache_read: 3"));
316 assert!(debug.contains("cache_write: 4"));
317 }
318
319 #[test]
320 fn test_clone() {
321 let t = TokenUsage {
322 input: 10,
323 output: 20,
324 cache_read: 5,
325 cache_write: 3,
326 };
327 let cloned = t;
328 assert_eq!(cloned, t);
329 }
330
331 #[test]
332 fn test_copy() {
333 let t = TokenUsage {
334 input: 10,
335 output: 20,
336 cache_read: 5,
337 cache_write: 3,
338 };
339 let copied = t;
340 let also_t = t; assert_eq!(copied, also_t);
342 }
343
344 #[test]
345 fn test_add_commutative() {
346 let a = TokenUsage {
347 input: 5,
348 output: 10,
349 cache_read: 2,
350 cache_write: 1,
351 };
352 let b = TokenUsage {
353 input: 3,
354 output: 7,
355 cache_read: 4,
356 cache_write: 6,
357 };
358 assert_eq!(a + b, b + a);
359 }
360
361 #[test]
362 fn test_total_excludes_cache() {
363 let t = TokenUsage {
364 input: 10,
365 output: 20,
366 cache_read: 100,
367 cache_write: 200,
368 };
369 assert_eq!(t.total(), 30);
370 }
371
372 #[test]
373 fn test_add_assign_identity() {
374 let mut a = TokenUsage {
375 input: 5,
376 output: 5,
377 cache_read: 5,
378 cache_write: 5,
379 };
380 a += TokenUsage::default();
381 assert_eq!(
382 a,
383 TokenUsage {
384 input: 5,
385 output: 5,
386 cache_read: 5,
387 cache_write: 5
388 }
389 );
390 }
391
392 #[test]
393 fn test_type_aliases() {
394 let _run_id = RunId::nil();
396 let _agent_id = AgentId::nil();
397 let _phase_id: PhaseId = 42;
398 }
399
400 #[test]
403 fn fmt_tokens_zero() {
404 assert_eq!(fmt_tokens(0), "0");
405 }
406
407 #[test]
408 fn fmt_tokens_small() {
409 assert_eq!(fmt_tokens(1), "1");
410 assert_eq!(fmt_tokens(999), "999");
411 }
412
413 #[test]
414 fn fmt_tokens_exactly_1k() {
415 assert_eq!(fmt_tokens(1_000), "1k");
416 }
417
418 #[test]
419 fn fmt_tokens_k_with_decimal() {
420 assert_eq!(fmt_tokens(1_200), "1.2k");
421 assert_eq!(fmt_tokens(12_345), "12.3k");
422 }
423
424 #[test]
425 fn fmt_tokens_k_whole_no_decimal() {
426 assert_eq!(fmt_tokens(12_000), "12k");
427 }
428
429 #[test]
430 fn fmt_tokens_exactly_1m() {
431 assert_eq!(fmt_tokens(1_000_000), "1M");
432 }
433
434 #[test]
435 fn fmt_tokens_m_with_decimal() {
436 assert_eq!(fmt_tokens(1_500_000), "1.5M");
437 assert_eq!(fmt_tokens(2_300_000), "2.3M");
438 }
439
440 #[test]
441 fn fmt_tokens_m_whole_no_decimal() {
442 assert_eq!(fmt_tokens(10_000_000), "10M");
443 }
444
445 #[test]
446 fn fmt_tokens_border_999999() {
447 assert_eq!(fmt_tokens(999_999), "1M");
448 }
449
450 #[test]
451 fn fmt_tokens_border_999_999_999() {
452 assert_eq!(fmt_tokens(999_999_999), "1B");
453 }
454
455 #[test]
456 fn fmt_tokens_exactly_1b() {
457 assert_eq!(fmt_tokens(1_000_000_000), "1B");
458 }
459
460 #[test]
461 fn fmt_tokens_b_with_decimal() {
462 assert_eq!(fmt_tokens(1_500_000_000), "1.5B");
463 assert_eq!(fmt_tokens(2_300_000_000), "2.3B");
464 }
465
466 #[test]
467 fn fmt_tokens_b_whole_no_decimal() {
468 assert_eq!(fmt_tokens(10_000_000_000), "10B");
469 }
470
471 #[test]
472 fn display_total_matches_fmt_tokens() {
473 let t = TokenUsage {
474 input: 5_000,
475 output: 7_345,
476 cache_read: 0,
477 cache_write: 0,
478 };
479 assert_eq!(t.display_total(), fmt_tokens(12_345));
480 assert_eq!(t.display_total(), "12.3k");
481 }
482
483 #[test]
484 fn display_split_basic() {
485 let t = TokenUsage {
486 input: 5_000,
487 output: 7_345,
488 cache_read: 0,
489 cache_write: 0,
490 };
491 assert_eq!(t.display_split(), "↑5k ↓7.3k");
492 }
493
494 #[test]
495 fn display_split_with_cache() {
496 let t = TokenUsage {
497 input: 1_200,
498 output: 3_400,
499 cache_read: 800,
500 cache_write: 0,
501 };
502 assert_eq!(t.display_split(), "↑1.2k ↓3.4k 800 cached");
503 }
504
505 #[test]
506 fn display_split_zero() {
507 let t = TokenUsage::default();
508 assert_eq!(t.display_split(), "↑0 ↓0");
509 }
510}