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