use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenUsage {
pub input_tokens: u64,
pub output_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_write_tokens: Option<u64>,
}
impl TokenUsage {
pub fn total(&self) -> u64 {
self.input_tokens + self.output_tokens
}
pub fn merge(&mut self, other: &TokenUsage) {
self.input_tokens += other.input_tokens;
self.output_tokens += other.output_tokens;
match (self.cache_read_tokens, other.cache_read_tokens) {
(Some(a), Some(b)) => self.cache_read_tokens = Some(a + b),
(None, Some(b)) => self.cache_read_tokens = Some(b),
_ => {}
}
match (self.cache_write_tokens, other.cache_write_tokens) {
(Some(a), Some(b)) => self.cache_write_tokens = Some(a + b),
(None, Some(b)) => self.cache_write_tokens = Some(b),
_ => {}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCallRecord {
pub tool_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CostSummary {
pub total_usage: TokenUsage,
pub session_count: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub per_agent: Vec<AgentTokenUsage>,
pub estimated_cost_usd: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTokenUsage {
pub agent_name: String,
pub usage: TokenUsage,
}
pub const MAX_PROMPT_SUMMARY_LEN: usize = 2000;
pub const MAX_TOOL_INPUT_SUMMARY_LEN: usize = 200;
pub fn truncate_string(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
let target = max_len.saturating_sub(3);
let boundary = s[..=target.min(s.len().saturating_sub(1))]
.char_indices()
.map(|(i, _)| i)
.last()
.unwrap_or(0);
let truncated = &s[..boundary];
format!("{truncated}...")
}
}
const INPUT_PRICE_PER_M: f64 = 3.0;
const OUTPUT_PRICE_PER_M: f64 = 15.0;
const CACHE_READ_PRICE_PER_M: f64 = 0.30;
const CACHE_WRITE_PRICE_PER_M: f64 = 3.75;
pub fn estimate_cost(usage: &TokenUsage) -> f64 {
let input_cost = usage.input_tokens as f64 / 1_000_000.0 * INPUT_PRICE_PER_M;
let output_cost = usage.output_tokens as f64 / 1_000_000.0 * OUTPUT_PRICE_PER_M;
let cache_read_cost = usage.cache_read_tokens.unwrap_or(0) as f64 / 1_000_000.0 * CACHE_READ_PRICE_PER_M;
let cache_write_cost = usage.cache_write_tokens.unwrap_or(0) as f64 / 1_000_000.0 * CACHE_WRITE_PRICE_PER_M;
input_cost + output_cost + cache_read_cost + cache_write_cost
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_string_works() {
assert_eq!(truncate_string("hello", 10), "hello");
assert_eq!(truncate_string("hello world", 8), "hello...");
assert_eq!(truncate_string("", 5), "");
assert_eq!(truncate_string("ab", 2), "ab");
}
#[test]
fn token_usage_total() {
let usage = TokenUsage {
input_tokens: 1000,
output_tokens: 500,
cache_read_tokens: None,
cache_write_tokens: None,
};
assert_eq!(usage.total(), 1500);
}
#[test]
fn token_usage_merge() {
let mut a = TokenUsage {
input_tokens: 1000,
output_tokens: 500,
cache_read_tokens: Some(200),
cache_write_tokens: None,
};
let b = TokenUsage {
input_tokens: 2000,
output_tokens: 300,
cache_read_tokens: Some(100),
cache_write_tokens: Some(50),
};
a.merge(&b);
assert_eq!(a.input_tokens, 3000);
assert_eq!(a.output_tokens, 800);
assert_eq!(a.cache_read_tokens, Some(300));
assert_eq!(a.cache_write_tokens, Some(50));
}
#[test]
fn estimate_cost_basic() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
cache_read_tokens: None,
cache_write_tokens: None,
};
let cost = estimate_cost(&usage);
assert!((cost - 18.0).abs() < 0.01);
}
}