#![deny(missing_docs)]
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy)]
pub struct Call<'a> {
pub provider: &'a str,
pub model: &'a str,
pub input_tokens: u64,
pub output_tokens: u64,
pub cost_usd: f64,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Bucket {
pub calls: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub cost_usd: f64,
}
impl Bucket {
fn add_call(&mut self, c: &Call<'_>) {
self.calls += 1;
self.input_tokens += c.input_tokens;
self.output_tokens += c.output_tokens;
self.cost_usd += c.cost_usd;
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Snapshot {
pub total_calls: u64,
pub total_input_tokens: u64,
pub total_output_tokens: u64,
pub total_cost_usd: f64,
}
#[derive(Debug, Default)]
pub struct Meter {
by_pm: BTreeMap<(String, String), Bucket>,
snap: Snapshot,
}
impl Meter {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, c: Call<'_>) {
let key = (c.provider.to_string(), c.model.to_string());
let bucket = self.by_pm.entry(key).or_default();
bucket.add_call(&c);
self.snap.total_calls += 1;
self.snap.total_input_tokens += c.input_tokens;
self.snap.total_output_tokens += c.output_tokens;
self.snap.total_cost_usd += c.cost_usd;
}
pub fn snapshot(&self) -> Snapshot {
self.snap.clone()
}
pub fn by_provider(&self) -> Vec<(String, Bucket)> {
let mut acc: BTreeMap<String, Bucket> = BTreeMap::new();
for ((provider, _), b) in self.by_pm.iter() {
let target = acc.entry(provider.clone()).or_default();
target.calls += b.calls;
target.input_tokens += b.input_tokens;
target.output_tokens += b.output_tokens;
target.cost_usd += b.cost_usd;
}
let mut v: Vec<(String, Bucket)> = acc.into_iter().collect();
v.sort_by(|a, b| {
b.1.cost_usd
.partial_cmp(&a.1.cost_usd)
.unwrap_or(std::cmp::Ordering::Equal)
});
v
}
pub fn by_model(&self) -> Vec<((String, String), Bucket)> {
let mut v: Vec<((String, String), Bucket)> =
self.by_pm.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
v.sort_by(|a, b| {
b.1.cost_usd
.partial_cmp(&a.1.cost_usd)
.unwrap_or(std::cmp::Ordering::Equal)
});
v
}
pub fn reset(&mut self) {
self.by_pm.clear();
self.snap = Snapshot::default();
}
}