agent_teams/models/
token.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct TokenUsage {
13 pub input_tokens: u64,
15 pub output_tokens: u64,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub cache_read_tokens: Option<u64>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub cache_write_tokens: Option<u64>,
23}
24
25impl TokenUsage {
26 pub fn total(&self) -> u64 {
28 self.input_tokens + self.output_tokens
29 }
30
31 pub fn merge(&mut self, other: &TokenUsage) {
33 self.input_tokens += other.input_tokens;
34 self.output_tokens += other.output_tokens;
35 match (self.cache_read_tokens, other.cache_read_tokens) {
36 (Some(a), Some(b)) => self.cache_read_tokens = Some(a + b),
37 (None, Some(b)) => self.cache_read_tokens = Some(b),
38 _ => {}
39 }
40 match (self.cache_write_tokens, other.cache_write_tokens) {
41 (Some(a), Some(b)) => self.cache_write_tokens = Some(a + b),
42 (None, Some(b)) => self.cache_write_tokens = Some(b),
43 _ => {}
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct ToolCallRecord {
52 pub tool_name: String,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub input_summary: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub timestamp: Option<DateTime<Utc>>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct CostSummary {
66 pub total_usage: TokenUsage,
68 pub session_count: usize,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub per_agent: Vec<AgentTokenUsage>,
73 pub estimated_cost_usd: f64,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct AgentTokenUsage {
81 pub agent_name: String,
83 pub usage: TokenUsage,
85}
86
87pub const MAX_PROMPT_SUMMARY_LEN: usize = 2000;
89
90pub const MAX_TOOL_INPUT_SUMMARY_LEN: usize = 200;
92
93pub fn truncate_string(s: &str, max_len: usize) -> String {
95 if s.len() <= max_len {
96 s.to_string()
97 } else {
98 let target = max_len.saturating_sub(3);
100 let boundary = s[..=target.min(s.len().saturating_sub(1))]
101 .char_indices()
102 .map(|(i, _)| i)
103 .last()
104 .unwrap_or(0);
105 let truncated = &s[..boundary];
106 format!("{truncated}...")
107 }
108}
109
110const INPUT_PRICE_PER_M: f64 = 3.0;
112const OUTPUT_PRICE_PER_M: f64 = 15.0;
113const CACHE_READ_PRICE_PER_M: f64 = 0.30;
114const CACHE_WRITE_PRICE_PER_M: f64 = 3.75;
115
116pub fn estimate_cost(usage: &TokenUsage) -> f64 {
118 let input_cost = usage.input_tokens as f64 / 1_000_000.0 * INPUT_PRICE_PER_M;
119 let output_cost = usage.output_tokens as f64 / 1_000_000.0 * OUTPUT_PRICE_PER_M;
120 let cache_read_cost = usage.cache_read_tokens.unwrap_or(0) as f64 / 1_000_000.0 * CACHE_READ_PRICE_PER_M;
121 let cache_write_cost = usage.cache_write_tokens.unwrap_or(0) as f64 / 1_000_000.0 * CACHE_WRITE_PRICE_PER_M;
122 input_cost + output_cost + cache_read_cost + cache_write_cost
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn truncate_string_works() {
131 assert_eq!(truncate_string("hello", 10), "hello");
132 assert_eq!(truncate_string("hello world", 8), "hello...");
133 assert_eq!(truncate_string("", 5), "");
134 assert_eq!(truncate_string("ab", 2), "ab");
135 }
136
137 #[test]
138 fn token_usage_total() {
139 let usage = TokenUsage {
140 input_tokens: 1000,
141 output_tokens: 500,
142 cache_read_tokens: None,
143 cache_write_tokens: None,
144 };
145 assert_eq!(usage.total(), 1500);
146 }
147
148 #[test]
149 fn token_usage_merge() {
150 let mut a = TokenUsage {
151 input_tokens: 1000,
152 output_tokens: 500,
153 cache_read_tokens: Some(200),
154 cache_write_tokens: None,
155 };
156 let b = TokenUsage {
157 input_tokens: 2000,
158 output_tokens: 300,
159 cache_read_tokens: Some(100),
160 cache_write_tokens: Some(50),
161 };
162 a.merge(&b);
163 assert_eq!(a.input_tokens, 3000);
164 assert_eq!(a.output_tokens, 800);
165 assert_eq!(a.cache_read_tokens, Some(300));
166 assert_eq!(a.cache_write_tokens, Some(50));
167 }
168
169 #[test]
170 fn estimate_cost_basic() {
171 let usage = TokenUsage {
172 input_tokens: 1_000_000,
173 output_tokens: 1_000_000,
174 cache_read_tokens: None,
175 cache_write_tokens: None,
176 };
177 let cost = estimate_cost(&usage);
178 assert!((cost - 18.0).abs() < 0.01);
180 }
181}