use serde::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageRow {
pub at: String,
pub op: String,
pub unit: String,
pub returned: u64,
pub full: u64,
pub hits: u64,
pub sources: u64,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct UsageTotals {
pub ops: u64,
pub returned: u64,
pub full: u64,
pub hits: u64,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct UsageSummary {
pub since: Option<String>,
pub rows: u64,
pub recall: UsageTotals,
pub find: UsageTotals,
pub unreadable_rows: u64,
}
pub struct UsageLog {
path: PathBuf,
}
impl UsageLog {
pub fn new(root: &Path) -> Self {
Self {
path: root.join("usage.jsonl"),
}
}
pub fn append(&self, row: &UsageRow) {
let line = match serde_json::to_string(row) {
Ok(l) => l,
Err(e) => {
eprintln!("cyberbrain: usage row could not be serialised: {e}");
return;
}
};
let write = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.and_then(|mut f| writeln!(f, "{line}"));
if let Err(e) = write {
eprintln!("cyberbrain: usage log append failed: {e}");
}
}
pub fn summary(&self) -> UsageSummary {
let mut s = UsageSummary::default();
let Ok(text) = std::fs::read_to_string(&self.path) else {
return s;
};
for line in text.lines().filter(|l| !l.trim().is_empty()) {
let Ok(r) = serde_json::from_str::<UsageRow>(line) else {
s.unreadable_rows += 1;
continue;
};
s.rows += 1;
if s.since.is_none() {
s.since = Some(r.at.clone());
}
let t = match r.op.as_str() {
"find" => &mut s.find,
_ => &mut s.recall,
};
t.ops += 1;
t.returned += r.returned;
t.full += r.full;
t.hits += r.hits;
}
s
}
}
pub fn now() -> String {
let t = jiff::Timestamp::now();
t.round(jiff::Unit::Second).unwrap_or(t).to_string()
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct TaskUsage {
pub calls: u64,
pub failed: u64,
pub prompt_tokens: u64,
pub cached_prompt_tokens: u64,
pub completion_tokens: u64,
pub elapsed_ms: u64,
pub calls_without_counts: u64,
pub calls_without_cache_report: u64,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct InferenceUsage {
pub tasks: std::collections::BTreeMap<String, TaskUsage>,
pub first: Option<String>,
pub last: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct DayBucket {
pub date: String,
pub recall: UsageTotals,
pub find: UsageTotals,
pub calls: u64,
pub prompt_tokens: u64,
pub cached_prompt_tokens: u64,
pub completion_tokens: u64,
pub wall_ms: u64,
pub endpoint_cores: Option<f64>,
pub machine_cores: Option<f64>,
}
pub fn day_axis(days: usize) -> Vec<String> {
let today = jiff::Timestamp::now()
.to_zoned(jiff::tz::TimeZone::UTC)
.date();
(0..days)
.rev()
.filter_map(|i| today.checked_sub(jiff::Span::new().days(i as i64)).ok())
.map(|d| d.to_string())
.collect()
}
pub fn day_of(ts: &str) -> Option<&str> {
(ts.len() >= 10).then(|| &ts[..10])
}