use std::sync::atomic::{AtomicU64, Ordering};
use serde_json::Value;
use crate::USD_PER_INPUT_MTOK;
#[derive(Debug, Default)]
pub struct Usage {
requests: AtomicU64,
retries: AtomicU64,
input_tokens: AtomicU64,
output_tokens: AtomicU64,
}
impl Usage {
pub fn record(&self, input_tokens: u64, output_tokens: u64) {
self.requests.fetch_add(1, Ordering::Relaxed);
self.input_tokens.fetch_add(input_tokens, Ordering::Relaxed);
self.output_tokens.fetch_add(output_tokens, Ordering::Relaxed);
}
pub fn record_retry(&self) {
self.retries.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_reported(&self, usage: Option<&Value>) {
let field = |k: &str| usage.and_then(|u| u.get(k)).and_then(Value::as_u64).unwrap_or(0);
self.record(field("input_tokens"), field("output_tokens"));
}
pub fn requests(&self) -> u64 {
self.requests.load(Ordering::Relaxed)
}
pub fn retries(&self) -> u64 {
self.retries.load(Ordering::Relaxed)
}
pub fn input_tokens(&self) -> u64 {
self.input_tokens.load(Ordering::Relaxed)
}
pub fn output_tokens(&self) -> u64 {
self.output_tokens.load(Ordering::Relaxed)
}
pub fn cost_usd(&self) -> f64 {
self.input_tokens() as f64 / 1_000_000.0 * USD_PER_INPUT_MTOK
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn counts_requests_retries_and_tokens() {
let usage = Usage::default();
assert_eq!((usage.requests(), usage.retries(), usage.input_tokens(), usage.output_tokens()), (0, 0, 0, 0));
usage.record(100, 10);
usage.record_retry();
usage.record_reported(Some(&json!({"input_tokens": 50, "output_tokens": 5})));
usage.record_reported(Some(&json!({"input_tokens": "not a number"})));
usage.record_reported(None);
assert_eq!((usage.requests(), usage.retries(), usage.input_tokens(), usage.output_tokens()), (4, 1, 150, 15));
assert!((usage.cost_usd() - 150.0 / 1_000_000.0 * USD_PER_INPUT_MTOK).abs() < 1e-15);
}
}