1use adk_core::UsageMetadata;
30use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
48pub struct UsageReport {
49 pub input_tokens: u64,
51 pub output_tokens: u64,
53 pub total_tokens: u64,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub thinking_tokens: Option<u64>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub cache_read_tokens: Option<u64>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub cache_write_tokens: Option<u64>,
64}
65
66impl UsageReport {
67 pub fn new(input_tokens: u64, output_tokens: u64) -> Self {
71 Self {
72 input_tokens,
73 output_tokens,
74 total_tokens: input_tokens + output_tokens,
75 thinking_tokens: None,
76 cache_read_tokens: None,
77 cache_write_tokens: None,
78 }
79 }
80
81 pub fn from_usage_metadata(metadata: &UsageMetadata) -> Self {
109 let input_tokens = metadata.prompt_token_count.max(0) as u64;
110 let output_tokens = metadata.candidates_token_count.max(0) as u64;
111 let total_tokens = metadata.total_token_count.max(0) as u64;
112
113 let total = if total_tokens > 0 { total_tokens } else { input_tokens + output_tokens };
116
117 let thinking_tokens =
118 metadata.thinking_token_count.and_then(|t| if t > 0 { Some(t as u64) } else { None });
119
120 let cache_read_tokens = metadata
121 .cache_read_input_token_count
122 .and_then(|t| if t > 0 { Some(t as u64) } else { None });
123
124 let cache_write_tokens = metadata
125 .cache_creation_input_token_count
126 .and_then(|t| if t > 0 { Some(t as u64) } else { None });
127
128 Self {
129 input_tokens,
130 output_tokens,
131 total_tokens: total,
132 thinking_tokens,
133 cache_read_tokens,
134 cache_write_tokens,
135 }
136 }
137
138 pub fn accumulate(&mut self, other: &UsageReport) {
142 self.input_tokens += other.input_tokens;
143 self.output_tokens += other.output_tokens;
144 self.total_tokens += other.total_tokens;
145
146 match (self.thinking_tokens, other.thinking_tokens) {
148 (Some(a), Some(b)) => self.thinking_tokens = Some(a + b),
149 (None, Some(b)) => self.thinking_tokens = Some(b),
150 _ => {}
151 }
152 match (self.cache_read_tokens, other.cache_read_tokens) {
153 (Some(a), Some(b)) => self.cache_read_tokens = Some(a + b),
154 (None, Some(b)) => self.cache_read_tokens = Some(b),
155 _ => {}
156 }
157 match (self.cache_write_tokens, other.cache_write_tokens) {
158 (Some(a), Some(b)) => self.cache_write_tokens = Some(a + b),
159 (None, Some(b)) => self.cache_write_tokens = Some(b),
160 _ => {}
161 }
162 }
163
164 pub fn is_empty(&self) -> bool {
166 self.input_tokens == 0 && self.output_tokens == 0
167 }
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct SessionUsageTracker {
176 pub cumulative: UsageReport,
178 pub turn_count: u64,
180 pub last_turn: Option<UsageReport>,
182}
183
184impl SessionUsageTracker {
185 pub fn new() -> Self {
187 Self::default()
188 }
189
190 pub fn record_turn(&mut self, turn_usage: UsageReport) {
192 self.cumulative.accumulate(&turn_usage);
193 self.turn_count += 1;
194 self.last_turn = Some(turn_usage);
195 }
196
197 pub fn total(&self) -> &UsageReport {
199 &self.cumulative
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_usage_report_new() {
209 let report = UsageReport::new(100, 50);
210 assert_eq!(report.input_tokens, 100);
211 assert_eq!(report.output_tokens, 50);
212 assert_eq!(report.total_tokens, 150);
213 assert_eq!(report.thinking_tokens, None);
214 assert_eq!(report.cache_read_tokens, None);
215 assert_eq!(report.cache_write_tokens, None);
216 }
217
218 #[test]
219 fn test_usage_report_default_is_zero() {
220 let report = UsageReport::default();
221 assert_eq!(report.input_tokens, 0);
222 assert_eq!(report.output_tokens, 0);
223 assert_eq!(report.total_tokens, 0);
224 assert!(report.is_empty());
225 }
226
227 #[test]
228 fn test_from_usage_metadata_basic() {
229 let metadata = UsageMetadata {
230 prompt_token_count: 200,
231 candidates_token_count: 100,
232 total_token_count: 300,
233 ..Default::default()
234 };
235
236 let report = UsageReport::from_usage_metadata(&metadata);
237 assert_eq!(report.input_tokens, 200);
238 assert_eq!(report.output_tokens, 100);
239 assert_eq!(report.total_tokens, 300);
240 }
241
242 #[test]
243 fn test_from_usage_metadata_with_thinking_tokens() {
244 let metadata = UsageMetadata {
245 prompt_token_count: 150,
246 candidates_token_count: 80,
247 total_token_count: 230,
248 thinking_token_count: Some(50),
249 ..Default::default()
250 };
251
252 let report = UsageReport::from_usage_metadata(&metadata);
253 assert_eq!(report.input_tokens, 150);
254 assert_eq!(report.output_tokens, 80);
255 assert_eq!(report.total_tokens, 230);
256 assert_eq!(report.thinking_tokens, Some(50));
257 }
258
259 #[test]
260 fn test_from_usage_metadata_with_cache_tokens() {
261 let metadata = UsageMetadata {
262 prompt_token_count: 100,
263 candidates_token_count: 50,
264 total_token_count: 150,
265 cache_read_input_token_count: Some(30),
266 cache_creation_input_token_count: Some(10),
267 ..Default::default()
268 };
269
270 let report = UsageReport::from_usage_metadata(&metadata);
271 assert_eq!(report.cache_read_tokens, Some(30));
272 assert_eq!(report.cache_write_tokens, Some(10));
273 }
274
275 #[test]
276 fn test_from_usage_metadata_zero_total_computes_automatically() {
277 let metadata = UsageMetadata {
278 prompt_token_count: 80,
279 candidates_token_count: 40,
280 total_token_count: 0, ..Default::default()
282 };
283
284 let report = UsageReport::from_usage_metadata(&metadata);
285 assert_eq!(report.input_tokens, 80);
286 assert_eq!(report.output_tokens, 40);
287 assert_eq!(report.total_tokens, 120); }
289
290 #[test]
291 fn test_from_usage_metadata_negative_values_clamped_to_zero() {
292 let metadata = UsageMetadata {
293 prompt_token_count: -5,
294 candidates_token_count: -10,
295 total_token_count: -15,
296 ..Default::default()
297 };
298
299 let report = UsageReport::from_usage_metadata(&metadata);
300 assert_eq!(report.input_tokens, 0);
301 assert_eq!(report.output_tokens, 0);
302 assert_eq!(report.total_tokens, 0);
303 }
304
305 #[test]
306 fn test_from_usage_metadata_zero_thinking_not_reported() {
307 let metadata = UsageMetadata {
308 prompt_token_count: 100,
309 candidates_token_count: 50,
310 total_token_count: 150,
311 thinking_token_count: Some(0),
312 ..Default::default()
313 };
314
315 let report = UsageReport::from_usage_metadata(&metadata);
316 assert_eq!(report.thinking_tokens, None); }
318
319 #[test]
320 fn test_accumulate() {
321 let mut total = UsageReport::new(100, 50);
322 let turn2 = UsageReport::new(80, 40);
323
324 total.accumulate(&turn2);
325
326 assert_eq!(total.input_tokens, 180);
327 assert_eq!(total.output_tokens, 90);
328 assert_eq!(total.total_tokens, 270);
329 }
330
331 #[test]
332 fn test_accumulate_with_optional_fields() {
333 let mut total = UsageReport {
334 input_tokens: 100,
335 output_tokens: 50,
336 total_tokens: 150,
337 thinking_tokens: Some(20),
338 cache_read_tokens: None,
339 cache_write_tokens: None,
340 };
341
342 let turn2 = UsageReport {
343 input_tokens: 80,
344 output_tokens: 40,
345 total_tokens: 120,
346 thinking_tokens: Some(15),
347 cache_read_tokens: Some(10),
348 cache_write_tokens: None,
349 };
350
351 total.accumulate(&turn2);
352
353 assert_eq!(total.thinking_tokens, Some(35));
354 assert_eq!(total.cache_read_tokens, Some(10));
355 assert_eq!(total.cache_write_tokens, None);
356 }
357
358 #[test]
359 fn test_is_empty() {
360 assert!(UsageReport::default().is_empty());
361 assert!(UsageReport::new(0, 0).is_empty());
362 assert!(!UsageReport::new(1, 0).is_empty());
363 assert!(!UsageReport::new(0, 1).is_empty());
364 }
365
366 #[test]
367 fn test_session_usage_tracker_record_turn() {
368 let mut tracker = SessionUsageTracker::new();
369 assert_eq!(tracker.turn_count, 0);
370 assert!(tracker.last_turn.is_none());
371
372 tracker.record_turn(UsageReport::new(100, 50));
373 assert_eq!(tracker.turn_count, 1);
374 assert_eq!(tracker.cumulative.input_tokens, 100);
375 assert_eq!(tracker.cumulative.output_tokens, 50);
376 assert_eq!(tracker.cumulative.total_tokens, 150);
377 assert_eq!(tracker.last_turn, Some(UsageReport::new(100, 50)));
378
379 tracker.record_turn(UsageReport::new(80, 40));
380 assert_eq!(tracker.turn_count, 2);
381 assert_eq!(tracker.cumulative.input_tokens, 180);
382 assert_eq!(tracker.cumulative.output_tokens, 90);
383 assert_eq!(tracker.cumulative.total_tokens, 270);
384 assert_eq!(tracker.last_turn, Some(UsageReport::new(80, 40)));
385 }
386
387 #[test]
388 fn test_usage_report_serialization_round_trip() {
389 let report = UsageReport {
390 input_tokens: 150,
391 output_tokens: 75,
392 total_tokens: 225,
393 thinking_tokens: Some(30),
394 cache_read_tokens: Some(20),
395 cache_write_tokens: None,
396 };
397
398 let json = serde_json::to_string(&report).unwrap();
399 let deserialized: UsageReport = serde_json::from_str(&json).unwrap();
400
401 assert_eq!(report, deserialized);
402 }
403
404 #[test]
405 fn test_usage_report_serialization_omits_none_fields() {
406 let report = UsageReport::new(100, 50);
407 let value = serde_json::to_value(&report).unwrap();
408
409 assert!(value.get("thinking_tokens").is_none());
411 assert!(value.get("cache_read_tokens").is_none());
412 assert!(value.get("cache_write_tokens").is_none());
413
414 assert_eq!(value["input_tokens"], 100);
416 assert_eq!(value["output_tokens"], 50);
417 assert_eq!(value["total_tokens"], 150);
418 }
419
420 #[test]
421 fn test_uniform_reporting_across_providers() {
422 let gemini_meta = UsageMetadata {
427 prompt_token_count: 100,
428 candidates_token_count: 50,
429 total_token_count: 150,
430 ..Default::default()
431 };
432
433 let openai_meta = UsageMetadata {
435 prompt_token_count: 100,
436 candidates_token_count: 50,
437 total_token_count: 150,
438 ..Default::default()
439 };
440
441 let anthropic_meta = UsageMetadata {
443 prompt_token_count: 100,
444 candidates_token_count: 50,
445 total_token_count: 150,
446 ..Default::default()
447 };
448
449 let gemini_report = UsageReport::from_usage_metadata(&gemini_meta);
450 let openai_report = UsageReport::from_usage_metadata(&openai_meta);
451 let anthropic_report = UsageReport::from_usage_metadata(&anthropic_meta);
452
453 assert_eq!(gemini_report, openai_report);
455 assert_eq!(openai_report, anthropic_report);
456
457 let json1 = serde_json::to_string(&gemini_report).unwrap();
459 let json2 = serde_json::to_string(&openai_report).unwrap();
460 let json3 = serde_json::to_string(&anthropic_report).unwrap();
461 assert_eq!(json1, json2);
462 assert_eq!(json2, json3);
463 }
464}