use leviath_core::run_archive::{InferenceKind, RunRecord};
use crate::persistence::{RunMetadata, TokenTotals};
use crate::persistence_bridge::PersistMsg;
use crate::pipeline::PersistenceStage;
pub struct CallUsage<'a> {
pub kind: InferenceKind,
pub stage: &'a str,
pub iteration: usize,
pub provider: &'a str,
pub model: &'a str,
pub usage: &'a leviath_providers::TokenUsage,
}
pub fn record_call(
totals: Option<&mut TokenTotals>,
persist: Option<&PersistenceStage>,
metadata: Option<&RunMetadata>,
call: &CallUsage<'_>,
) {
if let Some(totals) = totals {
totals.add_usage(call.usage);
}
let (Some(persist), Some(md)) = (persist, metadata) else {
return;
};
let record = RunRecord::InferenceUsage {
kind: call.kind,
stage: call.stage.to_string(),
iteration: call.iteration,
provider: call.provider.to_string(),
model: call.model.to_string(),
prompt_tokens: call.usage.prompt_tokens,
completion_tokens: call.usage.completion_tokens,
cached_tokens: call.usage.cached_tokens,
cache_write_tokens: call.usage.cache_write_tokens,
at: chrono::Utc::now().timestamp(),
};
let _ = persist.0.send(PersistMsg::Append {
run_id: md.run_id.clone(),
record: Box::new(record),
ack: None,
});
}
#[cfg(test)]
mod tests {
use super::*;
fn usage() -> leviath_providers::TokenUsage {
leviath_providers::TokenUsage {
prompt_tokens: 100,
completion_tokens: 20,
cached_tokens: 3,
cache_write_tokens: 4,
total_tokens: 120,
}
}
fn metadata() -> RunMetadata {
RunMetadata {
run_id: "run-u".to_string(),
agent_name: "a".to_string(),
agent_path: "/a".to_string(),
task: "t".to_string(),
model: None,
workdir: "/w".to_string(),
num_stages: 1,
started_at: 0,
parent_run_id: None,
metadata: Default::default(),
callback_url: None,
callback_secret: None,
title: None,
unattended: false,
read_paths: None,
output_request: None,
}
}
fn call(kind: InferenceKind, u: &leviath_providers::TokenUsage) -> CallUsage<'_> {
CallUsage {
kind,
stage: "plan",
iteration: 2,
provider: "anthropic",
model: "claude-sonnet-5",
usage: u,
}
}
#[test]
fn counting_and_journaling_are_independent() {
let u = usage();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let tx_for_noise = tx.clone();
let mut totals = TokenTotals::default();
record_call(
Some(&mut totals),
Some(&PersistenceStage(tx)),
Some(&metadata()),
&call(InferenceKind::Compaction, &u),
);
assert_eq!(totals.prompt_tokens, 100);
let _ = tx_for_noise.send(crate::persistence_bridge::PersistMsg::StageLines {
run_id: "run-u".to_string(),
output_appends: vec![],
log_appends: vec![],
});
let mut appended: Vec<(String, RunRecord)> = Vec::new();
while let Ok(msg) = rx.try_recv() {
if let crate::persistence_bridge::PersistMsg::Append { run_id, record, .. } = msg {
appended.push((run_id, *record));
}
}
assert_eq!(appended.len(), 1, "one call, one record");
let (run_id, record) = appended.remove(0);
assert_eq!(run_id, "run-u");
let mut value = serde_json::to_value(&record).unwrap();
let fields = value["InferenceUsage"].as_object_mut().unwrap();
assert!(fields.remove("at").is_some(), "a call is stamped");
assert_eq!(
value,
serde_json::json!({
"InferenceUsage": {
"kind": "compaction",
"stage": "plan",
"iteration": 2,
"provider": "anthropic",
"model": "claude-sonnet-5",
"prompt_tokens": 100,
"completion_tokens": 20,
"cached_tokens": 3,
"cache_write_tokens": 4,
}
})
);
let mut totals = TokenTotals::default();
record_call(
Some(&mut totals),
None,
Some(&metadata()),
&call(InferenceKind::Stage, &u),
);
assert_eq!(totals.prompt_tokens, 100);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let mut totals = TokenTotals::default();
record_call(
Some(&mut totals),
Some(&PersistenceStage(tx)),
None,
&call(InferenceKind::Title, &u),
);
assert_eq!(totals.prompt_tokens, 100);
assert!(rx.try_recv().is_err());
record_call(None, None, None, &call(InferenceKind::Routing, &u));
}
#[test]
fn repeated_calls_accumulate() {
let u = usage();
let mut totals = TokenTotals::default();
for _ in 0..3 {
record_call(
Some(&mut totals),
None,
None,
&call(InferenceKind::Stage, &u),
);
}
assert_eq!(totals.prompt_tokens, 300);
assert_eq!(totals.completion_tokens, 60);
assert_eq!(totals.cached_tokens, 9);
assert_eq!(totals.cache_write_tokens, 12);
}
}