use bamboo_domain::TokenBudgetUsage;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct TokenUsageRecord {
pub ts: String,
pub session_id: String,
pub model: String,
pub provider: String,
pub message_count: usize,
pub cache_creation_input_tokens: u64,
pub cache_read_input_tokens: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub thinking_tokens: u64,
pub system_tokens: u32,
pub summary_tokens: u32,
pub window_tokens: u32,
pub total_tokens: u32,
pub max_context_tokens: u32,
pub budget_limit: u32,
pub prompt_cached_tool_outputs: usize,
pub prompt_cached_tool_tokens_saved: u32,
pub truncation_occurred: bool,
pub segments_removed: usize,
}
impl TokenUsageRecord {
#[allow(clippy::too_many_arguments)]
pub fn new(
ts: String,
session_id: &str,
model: &str,
provider: &str,
message_count: usize,
usage: Option<&TokenBudgetUsage>,
cache_creation_input_tokens: u64,
cache_read_input_tokens: u64,
input_tokens: u64,
output_tokens: u64,
thinking_tokens: u64,
) -> Self {
Self {
ts,
session_id: session_id.to_string(),
model: model.to_string(),
provider: provider.to_string(),
message_count,
cache_creation_input_tokens,
cache_read_input_tokens,
input_tokens,
output_tokens,
thinking_tokens,
system_tokens: usage.map(|u| u.system_tokens).unwrap_or(0),
summary_tokens: usage.map(|u| u.summary_tokens).unwrap_or(0),
window_tokens: usage.map(|u| u.window_tokens).unwrap_or(0),
total_tokens: usage.map(|u| u.total_tokens).unwrap_or(0),
max_context_tokens: usage.map(|u| u.max_context_tokens).unwrap_or(0),
budget_limit: usage.map(|u| u.budget_limit).unwrap_or(0),
prompt_cached_tool_outputs: usage.map(|u| u.prompt_cached_tool_outputs).unwrap_or(0),
prompt_cached_tool_tokens_saved: usage
.map(|u| u.prompt_cached_tool_tokens_saved)
.unwrap_or(0),
truncation_occurred: usage.map(|u| u.truncation_occurred).unwrap_or(false),
segments_removed: usage.map(|u| u.segments_removed).unwrap_or(0),
}
}
pub fn to_json_line(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_serializes_to_single_json_line_with_cache_creation() {
let usage = TokenBudgetUsage {
system_tokens: 5000,
summary_tokens: 2000,
window_tokens: 3000,
total_tokens: 10000,
max_context_tokens: 200_000,
budget_limit: 180_000,
truncation_occurred: false,
segments_removed: 0,
prompt_cached_tool_outputs: 1,
prompt_cached_tool_tokens_saved: 42,
thinking_tokens: 7,
cache_read_input_tokens: 12_000,
};
let record = TokenUsageRecord::new(
"2026-06-15T00:00:00Z".to_string(),
"sess-1",
"claude-opus-4-8",
"anthropic",
24,
Some(&usage),
1500, 12_000,
800, 300,
7,
);
let line = record.to_json_line().expect("serializes");
assert!(!line.contains('\n'), "must be a single line");
assert!(line.contains("\"cache_creation_input_tokens\":1500"));
assert!(line.contains("\"cache_read_input_tokens\":12000"));
assert!(line.contains("\"input_tokens\":800"));
assert!(line.contains("\"session_id\":\"sess-1\""));
}
}