1use std::sync::atomic::{AtomicU64, Ordering};
2
3use crate::{TokenUsage, USD_PER_INPUT_MTOK};
4
5#[derive(Debug, Default)]
14pub struct Usage {
15 requests: AtomicU64,
16 retries: AtomicU64,
17 input_tokens: AtomicU64,
18 output_tokens: AtomicU64,
19}
20
21impl Usage {
22 pub fn record(&self, input_tokens: u64, output_tokens: u64) {
24 self.requests.fetch_add(1, Ordering::Relaxed);
25 self.input_tokens.fetch_add(input_tokens, Ordering::Relaxed);
26 self.output_tokens.fetch_add(output_tokens, Ordering::Relaxed);
27 }
28
29 pub fn record_retry(&self) {
32 self.retries.fetch_add(1, Ordering::Relaxed);
33 }
34
35 pub(crate) fn record_reported(&self, usage: TokenUsage) {
37 self.record(usage.input_tokens.unwrap_or(0), usage.output_tokens.unwrap_or(0));
38 }
39
40 pub fn requests(&self) -> u64 {
42 self.requests.load(Ordering::Relaxed)
43 }
44
45 pub fn retries(&self) -> u64 {
47 self.retries.load(Ordering::Relaxed)
48 }
49
50 pub fn input_tokens(&self) -> u64 {
52 self.input_tokens.load(Ordering::Relaxed)
53 }
54
55 pub fn output_tokens(&self) -> u64 {
57 self.output_tokens.load(Ordering::Relaxed)
58 }
59
60 pub fn cost_usd(&self) -> f64 {
63 self.input_tokens() as f64 / 1_000_000.0 * USD_PER_INPUT_MTOK
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn counts_requests_retries_and_tokens() {
73 let usage = Usage::default();
74 assert_eq!((usage.requests(), usage.retries(), usage.input_tokens(), usage.output_tokens()), (0, 0, 0, 0));
75 usage.record(100, 10);
76 usage.record_retry();
77 let reported = |text: &str| serde_json::from_str::<TokenUsage>(text).unwrap();
78 usage.record_reported(reported(r#"{"input_tokens": 50, "output_tokens": 5}"#));
79 usage.record_reported(reported(r#"{"output_tokens": null}"#));
80 usage.record_reported(TokenUsage::default());
81 assert_eq!((usage.requests(), usage.retries(), usage.input_tokens(), usage.output_tokens()), (4, 1, 150, 15));
82 assert!((usage.cost_usd() - 150.0 / 1_000_000.0 * USD_PER_INPUT_MTOK).abs() < 1e-15);
83 }
84}