1use std::sync::atomic::{AtomicU64, Ordering};
2
3use serde_json::Value;
4
5use crate::USD_PER_INPUT_MTOK;
6
7#[derive(Debug, Default)]
16pub struct Usage {
17 requests: AtomicU64,
18 retries: AtomicU64,
19 input_tokens: AtomicU64,
20 output_tokens: AtomicU64,
21}
22
23impl Usage {
24 pub fn record(&self, input_tokens: u64, output_tokens: u64) {
26 self.requests.fetch_add(1, Ordering::Relaxed);
27 self.input_tokens.fetch_add(input_tokens, Ordering::Relaxed);
28 self.output_tokens.fetch_add(output_tokens, Ordering::Relaxed);
29 }
30
31 pub fn record_retry(&self) {
34 self.retries.fetch_add(1, Ordering::Relaxed);
35 }
36
37 pub(crate) fn record_reported(&self, usage: Option<&Value>) {
40 let field = |k: &str| usage.and_then(|u| u.get(k)).and_then(Value::as_u64).unwrap_or(0);
41 self.record(field("input_tokens"), field("output_tokens"));
42 }
43
44 pub fn requests(&self) -> u64 {
46 self.requests.load(Ordering::Relaxed)
47 }
48
49 pub fn retries(&self) -> u64 {
51 self.retries.load(Ordering::Relaxed)
52 }
53
54 pub fn input_tokens(&self) -> u64 {
56 self.input_tokens.load(Ordering::Relaxed)
57 }
58
59 pub fn output_tokens(&self) -> u64 {
61 self.output_tokens.load(Ordering::Relaxed)
62 }
63
64 pub fn cost_usd(&self) -> f64 {
67 self.input_tokens() as f64 / 1_000_000.0 * USD_PER_INPUT_MTOK
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use serde_json::json;
75
76 #[test]
77 fn counts_requests_retries_and_tokens() {
78 let usage = Usage::default();
79 assert_eq!((usage.requests(), usage.retries(), usage.input_tokens(), usage.output_tokens()), (0, 0, 0, 0));
80 usage.record(100, 10);
81 usage.record_retry();
82 usage.record_reported(Some(&json!({"input_tokens": 50, "output_tokens": 5})));
83 usage.record_reported(Some(&json!({"input_tokens": "not a number"})));
84 usage.record_reported(None);
85 assert_eq!((usage.requests(), usage.retries(), usage.input_tokens(), usage.output_tokens()), (4, 1, 150, 15));
86 assert!((usage.cost_usd() - 150.0 / 1_000_000.0 * USD_PER_INPUT_MTOK).abs() < 1e-15);
87 }
88}