mod bills;
pub use bills::{Scope, StepBill, bills, total};
const STEPS_DIR: &str = "steps";
const RESPONSE_FILE: &str = "response.json";
const STEP_SEQ_WIDTH: usize = 3;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BudgetSpend {
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_read_tokens: u64,
pub cache_write_tokens: u64,
}
impl BudgetSpend {
pub fn total_tokens(&self) -> u64 {
self.input_tokens + self.output_tokens + self.cache_read_tokens + self.cache_write_tokens
}
fn add(&mut self, other: BudgetSpend) {
self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
self.cache_read_tokens = self
.cache_read_tokens
.saturating_add(other.cache_read_tokens);
self.cache_write_tokens = self
.cache_write_tokens
.saturating_add(other.cache_write_tokens);
}
}
pub fn spend_from_bytes(bytes: &[u8]) -> BudgetSpend {
let mut total = BudgetSpend::default();
for line in bytes.split(|b| *b == b'\n') {
if let Some(spend) = usage_line(line) {
total.add(spend);
}
}
total
}
pub fn last_usage(bytes: &[u8]) -> BudgetSpend {
bytes
.split(|b| *b == b'\n')
.filter_map(usage_line)
.next_back()
.unwrap_or_default()
}
fn usage_line(line: &[u8]) -> Option<BudgetSpend> {
let value: serde_json::Value = serde_json::from_slice(line).ok()?;
if value.get("type")?.as_str()? != "usage" {
return None;
}
Some(BudgetSpend {
input_tokens: counter(&value, "input_tokens"),
output_tokens: counter(&value, "output_tokens"),
cache_read_tokens: counter(&value, "cache_read_tokens"),
cache_write_tokens: counter(&value, "cache_write_tokens"),
})
}
fn counter(value: &serde_json::Value, key: &str) -> u64 {
value
.get(key)
.and_then(serde_json::Value::as_u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn folds_every_segment_and_ignores_everything_else() {
let jsonl = [
r#"{"type":"usage","input_tokens":10,"output_tokens":5,"cache_read_tokens":2,"cache_write_tokens":1}"#,
r#"{"type":"content_delta","index":0,"delta":{"text_delta":"hi"}}"#,
r#"{"type":"usage","input_tokens":3,"output_tokens":null}"#,
"not json",
r#"{"no_type":1}"#,
r#"{"type":123}"#,
]
.join("\n");
let s = spend_from_bytes(jsonl.as_bytes());
assert_eq!(s.input_tokens, 13);
assert_eq!(s.output_tokens, 5);
assert_eq!(s.cache_read_tokens, 2);
assert_eq!(s.cache_write_tokens, 1);
assert_eq!(s.total_tokens(), 21);
}
#[test]
fn empty_bytes_are_zero_spend() {
assert_eq!(spend_from_bytes(b""), BudgetSpend::default());
}
#[test]
fn last_usage_takes_the_final_segment_not_the_fold() {
let jsonl = [
r#"{"type":"usage","input_tokens":10,"cache_read_tokens":90}"#,
r#"{"type":"content_delta","index":0,"delta":{"text_delta":"hi"}}"#,
r#"{"type":"usage","input_tokens":4,"cache_read_tokens":120}"#,
]
.join("\n");
let last = last_usage(jsonl.as_bytes());
assert_eq!(last.input_tokens, 4);
assert_eq!(last.cache_read_tokens, 120);
assert_eq!(spend_from_bytes(jsonl.as_bytes()).input_tokens, 14);
}
#[test]
fn a_payload_with_no_usage_line_has_no_last_segment() {
assert_eq!(last_usage(b"{\"type\":\"end\"}"), BudgetSpend::default());
}
}