use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
static CALIBRATION: Lazy<Mutex<HashMap<String, f64>>> = Lazy::new(|| Mutex::new(load_from_disk()));
const EMA_ALPHA: f64 = 0.3;
const MIN_OBSERVATION_TOKENS: u32 = 500;
const RATIO_MIN: f64 = 0.10;
const RATIO_MAX: f64 = 5.00;
fn calibration_path() -> PathBuf {
crate::config::opencrabs_home().join("token_calibration.json")
}
fn load_from_disk() -> HashMap<String, f64> {
let path = calibration_path();
let Ok(text) = std::fs::read_to_string(&path) else {
return HashMap::new();
};
match serde_json::from_str::<HashMap<String, f64>>(&text) {
Ok(map) => {
map.into_iter()
.filter(|(_, r)| (RATIO_MIN..=RATIO_MAX).contains(r))
.collect()
}
Err(e) => {
tracing::warn!(
"token_calibration: failed to parse {}: {} — starting fresh",
path.display(),
e
);
HashMap::new()
}
}
}
fn save_to_disk(map: &HashMap<String, f64>) {
let path = calibration_path();
let Ok(json) = serde_json::to_string_pretty(map) else {
return;
};
if let Err(e) = std::fs::write(&path, json) {
tracing::warn!(
"token_calibration: failed to persist to {}: {}",
path.display(),
e
);
}
}
pub fn get_ratio(provider: &str) -> Option<f64> {
CALIBRATION.lock().ok()?.get(provider).copied()
}
pub fn calibrate(provider: &str, raw_estimate: u32) -> Option<u32> {
get_ratio(provider).map(|ratio| ((raw_estimate as f64) * ratio).round() as u32)
}
pub fn record_observation(provider: &str, local_estimate: u32, real_input_tokens: u32) {
if local_estimate < MIN_OBSERVATION_TOKENS || real_input_tokens < MIN_OBSERVATION_TOKENS {
return;
}
let observed = real_input_tokens as f64 / local_estimate as f64;
if !(RATIO_MIN..=RATIO_MAX).contains(&observed) {
tracing::debug!(
"token_calibration: ignoring out-of-band observation for '{}' \
(local={}, real={}, ratio={:.3})",
provider,
local_estimate,
real_input_tokens,
observed
);
return;
}
let mut map = match CALIBRATION.lock() {
Ok(g) => g,
Err(_) => return,
};
let new_ratio = match map.get(provider).copied() {
None => observed,
Some(prev) => prev * (1.0 - EMA_ALPHA) + observed * EMA_ALPHA,
};
let new_ratio = new_ratio.clamp(RATIO_MIN, RATIO_MAX);
map.insert(provider.to_string(), new_ratio);
tracing::debug!(
"token_calibration: '{}' ratio updated to {:.3} (observed={:.3}, local={}, real={})",
provider,
new_ratio,
observed,
local_estimate,
real_input_tokens
);
save_to_disk(&map);
}