use std::sync::{Mutex, OnceLock};
use crate::models::Usage;
static PENDING: OnceLock<Mutex<f64>> = OnceLock::new();
fn cell() -> &'static Mutex<f64> {
PENDING.get_or_init(|| Mutex::new(0.0))
}
pub fn report(model: &str, usage: &Usage) {
let Some(cost) = crate::pricing::calculate_turn_cost_from_usage(model, usage) else {
return;
};
if cost <= 0.0 {
return;
}
if let Ok(mut pending) = cell().lock() {
*pending += cost;
}
}
pub fn drain() -> f64 {
let Ok(mut pending) = cell().lock() else {
return 0.0;
};
std::mem::replace(&mut *pending, 0.0)
}
#[cfg(test)]
pub fn reset_for_tests() {
if let Ok(mut pending) = cell().lock() {
*pending = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn small_usage() -> Usage {
Usage {
input_tokens: 1_000,
output_tokens: 500,
..Default::default()
}
}
fn serial_lock() -> std::sync::MutexGuard<'static, ()> {
static M: OnceLock<Mutex<()>> = OnceLock::new();
M.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
#[test]
fn report_adds_to_pool_and_drain_returns_then_resets() {
let _g = serial_lock();
reset_for_tests();
report("deepseek-v4-flash", &small_usage());
let first = drain();
assert!(first > 0.0, "expected positive cost, got {first}");
let second = drain();
assert_eq!(second, 0.0, "drain must zero the pool");
}
#[test]
fn report_skips_unknown_models() {
let _g = serial_lock();
reset_for_tests();
report("deepseek-ai/deepseek-v4-pro", &small_usage());
assert_eq!(drain(), 0.0);
}
#[test]
fn report_accumulates_across_multiple_calls() {
let _g = serial_lock();
reset_for_tests();
report("deepseek-v4-flash", &small_usage());
report("deepseek-v4-flash", &small_usage());
let total = drain();
let single =
crate::pricing::calculate_turn_cost_from_usage("deepseek-v4-flash", &small_usage())
.unwrap();
assert!((total - 2.0 * single).abs() < 1e-12);
}
}