1use serde::Serialize;
4use std::collections::HashMap;
5use std::sync::Mutex;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8#[derive(Default)]
10pub struct UsageStats {
11 pub total_requests: AtomicU64,
13 pub success_requests: AtomicU64,
15 pub failure_requests: AtomicU64,
17 pub input_tokens: AtomicU64,
19 pub output_tokens: AtomicU64,
21 model_counts: Mutex<HashMap<String, ModelStats>>,
23}
24
25#[derive(Default, Clone, Serialize)]
27pub struct ModelStats {
28 pub requests: u64,
29 pub success: u64,
30 pub failure: u64,
31 pub input_tokens: u64,
32 pub output_tokens: u64,
33}
34
35#[derive(Serialize)]
37pub struct UsageSnapshot {
38 pub total_requests: u64,
39 pub success_requests: u64,
40 pub failure_requests: u64,
41 pub input_tokens: u64,
42 pub output_tokens: u64,
43 pub models: HashMap<String, ModelStats>,
44}
45
46impl UsageStats {
47 #[must_use]
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn record_success(&self, model: &str, input_tokens: u64, output_tokens: u64) {
55 self.total_requests.fetch_add(1, Ordering::Relaxed);
56 self.success_requests.fetch_add(1, Ordering::Relaxed);
57 self.input_tokens.fetch_add(input_tokens, Ordering::Relaxed);
58 self.output_tokens
59 .fetch_add(output_tokens, Ordering::Relaxed);
60
61 if let Ok(mut map) = self.model_counts.lock() {
62 let entry = map.entry(model.to_string()).or_default();
63 entry.requests += 1;
64 entry.success += 1;
65 entry.input_tokens += input_tokens;
66 entry.output_tokens += output_tokens;
67 }
68 }
69
70 pub fn record_failure(&self, model: &str) {
72 self.total_requests.fetch_add(1, Ordering::Relaxed);
73 self.failure_requests.fetch_add(1, Ordering::Relaxed);
74
75 if let Ok(mut map) = self.model_counts.lock() {
76 let entry = map.entry(model.to_string()).or_default();
77 entry.requests += 1;
78 entry.failure += 1;
79 }
80 }
81
82 #[must_use]
84 pub fn snapshot(&self) -> UsageSnapshot {
85 let models = self
86 .model_counts
87 .lock()
88 .map(|m| m.clone())
89 .unwrap_or_default();
90 UsageSnapshot {
91 total_requests: self.total_requests.load(Ordering::Relaxed),
92 success_requests: self.success_requests.load(Ordering::Relaxed),
93 failure_requests: self.failure_requests.load(Ordering::Relaxed),
94 input_tokens: self.input_tokens.load(Ordering::Relaxed),
95 output_tokens: self.output_tokens.load(Ordering::Relaxed),
96 models,
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_record_success() {
107 let stats = UsageStats::new();
108 stats.record_success("claude-opus-4-5", 100, 200);
109 stats.record_success("claude-opus-4-5", 50, 100);
110 stats.record_success("gpt-4o", 80, 150);
111
112 let snap = stats.snapshot();
113 assert_eq!(snap.total_requests, 3);
114 assert_eq!(snap.success_requests, 3);
115 assert_eq!(snap.failure_requests, 0);
116 assert_eq!(snap.input_tokens, 230);
117 assert_eq!(snap.output_tokens, 450);
118
119 let claude = &snap.models["claude-opus-4-5"];
120 assert_eq!(claude.requests, 2);
121 assert_eq!(claude.success, 2);
122 assert_eq!(claude.input_tokens, 150);
123 assert_eq!(claude.output_tokens, 300);
124 }
125
126 #[test]
127 fn test_record_failure() {
128 let stats = UsageStats::new();
129 stats.record_failure("gpt-4o");
130 stats.record_success("gpt-4o", 10, 20);
131
132 let snap = stats.snapshot();
133 assert_eq!(snap.total_requests, 2);
134 assert_eq!(snap.success_requests, 1);
135 assert_eq!(snap.failure_requests, 1);
136
137 let model = &snap.models["gpt-4o"];
138 assert_eq!(model.requests, 2);
139 assert_eq!(model.failure, 1);
140 assert_eq!(model.success, 1);
141 }
142
143 #[test]
144 fn test_snapshot_empty() {
145 let stats = UsageStats::new();
146 let snap = stats.snapshot();
147 assert_eq!(snap.total_requests, 0);
148 assert!(snap.models.is_empty());
149 }
150}