Skip to main content

adk_managed/
usage.rs

1//! Uniform usage reporting across all providers.
2//!
3//! This module provides the [`UsageReport`] type that normalizes token usage
4//! from all LLM providers into a consistent format. After each turn, the
5//! session loop extracts `input_tokens` and `output_tokens` from the
6//! [`UsageMetadata`] in the LLM response and produces
7//! a uniform `UsageReport`.
8//!
9//! # Provider Parity (Requirement 5.3)
10//!
11//! > Streaming token output and usage metadata (input/output tokens) SHALL be
12//! > reported uniformly so the platform can meter cost per provider.
13//!
14//! Different providers report usage with different field names:
15//! - Gemini: `prompt_token_count` / `candidates_token_count`
16//! - OpenAI: `prompt_tokens` / `completion_tokens`
17//! - Anthropic: `input_tokens` / `output_tokens`
18//!
19//! All of these are normalized into `adk-core`'s [`UsageMetadata`] by each
20//! provider's client. This module further simplifies into `UsageReport` for
21//! the managed runtime's uniform reporting.
22//!
23//! # Integration
24//!
25//! The session loop calls [`UsageReport::from_usage_metadata`] after each turn
26//! to extract usage information. The platform can then use this for billing,
27//! monitoring, and cost tracking.
28
29use adk_core::UsageMetadata;
30use serde::{Deserialize, Serialize};
31
32/// Uniform usage report emitted after each turn.
33///
34/// Normalizes provider-specific token counts into a simple, consistent
35/// structure that the platform uses for metering and billing.
36///
37/// # Example
38///
39/// ```rust
40/// use adk_managed::usage::UsageReport;
41///
42/// let report = UsageReport::new(100, 50);
43/// assert_eq!(report.input_tokens, 100);
44/// assert_eq!(report.output_tokens, 50);
45/// assert_eq!(report.total_tokens, 150);
46/// ```
47#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
48pub struct UsageReport {
49    /// Number of tokens in the input/prompt.
50    pub input_tokens: u64,
51    /// Number of tokens generated in the output/response.
52    pub output_tokens: u64,
53    /// Total tokens (input + output). Always equals `input_tokens + output_tokens`.
54    pub total_tokens: u64,
55    /// Tokens consumed by thinking/reasoning (if applicable).
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub thinking_tokens: Option<u64>,
58    /// Tokens read from cache (if provider supports caching).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub cache_read_tokens: Option<u64>,
61    /// Tokens written to cache (if provider supports caching).
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub cache_write_tokens: Option<u64>,
64}
65
66impl UsageReport {
67    /// Create a new usage report with the given input and output token counts.
68    ///
69    /// Total is computed automatically as `input_tokens + output_tokens`.
70    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    /// Create a `UsageReport` from `adk-core`'s [`UsageMetadata`].
82    ///
83    /// This is the primary conversion used by the session loop after each turn.
84    /// It normalizes the provider-specific field names into the uniform format.
85    ///
86    /// # Arguments
87    ///
88    /// * `metadata` - The raw usage metadata from the LLM response.
89    ///
90    /// # Example
91    ///
92    /// ```rust
93    /// use adk_core::UsageMetadata;
94    /// use adk_managed::usage::UsageReport;
95    ///
96    /// let metadata = UsageMetadata {
97    ///     prompt_token_count: 150,
98    ///     candidates_token_count: 75,
99    ///     total_token_count: 225,
100    ///     ..Default::default()
101    /// };
102    ///
103    /// let report = UsageReport::from_usage_metadata(&metadata);
104    /// assert_eq!(report.input_tokens, 150);
105    /// assert_eq!(report.output_tokens, 75);
106    /// assert_eq!(report.total_tokens, 225);
107    /// ```
108    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        // Use the metadata's total if it's provided and non-zero,
114        // otherwise compute it ourselves.
115        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    /// Accumulate another report into this one (for multi-turn aggregation).
139    ///
140    /// This is useful for tracking total usage across an entire session.
141    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        // Accumulate optional fields
147        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    /// Returns true if this report has zero usage (no tokens consumed).
165    pub fn is_empty(&self) -> bool {
166        self.input_tokens == 0 && self.output_tokens == 0
167    }
168}
169
170/// Accumulated usage tracking for an entire session.
171///
172/// The session loop maintains one of these and calls `record_turn` after
173/// each turn completes. The platform can read the cumulative usage at any time.
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct SessionUsageTracker {
176    /// Cumulative usage across all turns.
177    pub cumulative: UsageReport,
178    /// Number of turns completed.
179    pub turn_count: u64,
180    /// Usage from the most recent turn (for per-turn billing).
181    pub last_turn: Option<UsageReport>,
182}
183
184impl SessionUsageTracker {
185    /// Create a new empty tracker.
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// Record usage from a completed turn.
191    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    /// Get the cumulative usage report.
198    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, // Provider didn't report total
281            ..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); // Computed: 80 + 40
288    }
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); // Zero thinking is not reported
317    }
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        // Optional None fields should not appear in JSON
410        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        // Required fields must appear
415        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        // Simulate usage from different providers all going through UsageMetadata.
423        // The key guarantee: regardless of provider, the UsageReport looks the same.
424
425        // Gemini response
426        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        // OpenAI response (same tokens, different internal naming)
434        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        // Anthropic response (same tokens)
442        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        // All reports should be identical
454        assert_eq!(gemini_report, openai_report);
455        assert_eq!(openai_report, anthropic_report);
456
457        // And serialization should be byte-identical
458        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}