1use std::collections::HashMap;
14use std::sync::{Mutex, OnceLock};
15
16use serde::Serialize;
17
18use crate::core::gain::model_pricing::{ModelPricing, PricingMatchKind};
19
20#[derive(Default, Clone)]
21struct ModelAccum {
22 requests: u64,
23 tokens_saved: u64,
24 bytes_original: u64,
25 bytes_compressed: u64,
26}
27
28#[derive(Debug, Clone, Serialize, PartialEq)]
30pub struct ModelStat {
31 pub model: String,
32 pub requests: u64,
33 pub tokens_saved: u64,
34 pub usd_saved: f64,
35 pub pricing_estimated: bool,
37}
38
39const MAX_TRACKED_MODELS: usize = 256;
43
44fn store() -> &'static Mutex<HashMap<String, ModelAccum>> {
45 static STORE: OnceLock<Mutex<HashMap<String, ModelAccum>>> = OnceLock::new();
46 STORE.get_or_init(|| Mutex::new(HashMap::new()))
47}
48
49pub fn record(model: Option<&str>, tokens_saved: u64, bytes_original: u64, bytes_compressed: u64) {
54 let key = model
55 .map(str::trim)
56 .filter(|m| !m.is_empty())
57 .unwrap_or("unknown")
58 .to_string();
59
60 let mut map = store()
61 .lock()
62 .unwrap_or_else(std::sync::PoisonError::into_inner);
63 let key = if !map.contains_key(&key) && map.len() >= MAX_TRACKED_MODELS {
68 "unknown".to_string()
69 } else {
70 key
71 };
72 let acc = map.entry(key).or_default();
73 acc.requests += 1;
74 acc.tokens_saved += tokens_saved;
75 acc.bytes_original += bytes_original;
76 acc.bytes_compressed += bytes_compressed;
77}
78
79pub fn snapshot() -> Vec<ModelStat> {
81 let pricing = ModelPricing::load();
82 let map = store()
83 .lock()
84 .unwrap_or_else(std::sync::PoisonError::into_inner);
85
86 let mut stats: Vec<ModelStat> = map
87 .iter()
88 .map(|(model, acc)| {
89 let quote = pricing.quote(Some(model));
90 let usd_saved = acc.tokens_saved as f64 / 1_000_000.0 * quote.cost.input_per_m;
92 let pricing_estimated = !matches!(quote.match_kind, PricingMatchKind::Exact);
93 ModelStat {
94 model: model.clone(),
95 requests: acc.requests,
96 tokens_saved: acc.tokens_saved,
97 usd_saved,
98 pricing_estimated,
99 }
100 })
101 .collect();
102
103 stats.sort_by(|a, b| {
104 b.usd_saved
105 .partial_cmp(&a.usd_saved)
106 .unwrap_or(std::cmp::Ordering::Equal)
107 });
108 stats
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn unknown_model_buckets_and_prices_without_panic() {
117 record(None, 1000, 4000, 0);
118 record(Some(" "), 500, 2000, 0);
119 let stats = snapshot();
120 let unknown = stats.iter().find(|s| s.model == "unknown");
121 assert!(
122 unknown.is_some(),
123 "blank/None models bucket under 'unknown'"
124 );
125 assert!(unknown.unwrap().requests >= 2);
126 }
127
128 #[test]
129 fn known_model_yields_positive_usd() {
130 record(
131 Some("claude-opus-4-8-zzz-cost-test"),
132 2_000_000,
133 8_000_000,
134 100,
135 );
136 let stats = snapshot();
137 let row = stats
138 .iter()
139 .find(|s| s.model.contains("opus-4-8-zzz-cost-test"))
140 .expect("recorded model present");
141 assert!(row.usd_saved >= 0.0 && row.usd_saved.is_finite());
143 assert_eq!(row.tokens_saved, 2_000_000);
144 }
145}