codetether_agent/telemetry/tokens/snapshot.rs
1//! Read-only snapshots of token usage.
2//!
3//! Snapshots are plain data — they don't hold locks and can be freely cloned
4//! across threads or serialized for the TUI / API.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use super::totals::TokenTotals;
10
11/// A point-in-time view of the global [`super::AtomicTokenCounter`].
12///
13/// # Examples
14///
15/// ```rust
16/// use codetether_agent::telemetry::GlobalTokenSnapshot;
17///
18/// let s = GlobalTokenSnapshot::new(1_000, 500, 1_500);
19/// assert_eq!(s.totals.total(), 1_500);
20/// assert!(s.summary().contains("1500"));
21/// ```
22#[derive(Debug, Clone, Default)]
23pub struct GlobalTokenSnapshot {
24 /// Cumulative prompt tokens.
25 pub input: u64,
26 /// Cumulative completion tokens.
27 pub output: u64,
28 /// Deprecated duplicate of [`Self::totals`] kept for API compatibility.
29 pub total: TokenTotals,
30 /// Preferred aggregate view.
31 pub totals: TokenTotals,
32 /// Number of completions recorded since startup.
33 pub request_count: u64,
34}
35
36impl GlobalTokenSnapshot {
37 /// Build a snapshot from raw counts (the third arg is ignored and kept
38 /// for API compatibility).
39 pub fn new(input: u64, output: u64, _total: u64) -> Self {
40 Self {
41 input,
42 output,
43 total: TokenTotals::new(input, output),
44 totals: TokenTotals::new(input, output),
45 request_count: 0,
46 }
47 }
48
49 /// Human-readable one-liner, e.g. `"1500 total tokens (1000 input, 500 output)"`.
50 pub fn summary(&self) -> String {
51 format!(
52 "{} total tokens ({} input, {} output)",
53 self.totals.total(),
54 self.input,
55 self.output
56 )
57 }
58}
59
60/// Per-model token snapshot. Instances are stamped with [`Utc::now`] when
61/// produced by [`super::AtomicTokenCounter::model_snapshots`].
62///
63/// # Examples
64///
65/// ```rust
66/// use codetether_agent::telemetry::TokenUsageSnapshot;
67///
68/// // `current()` pulls from the global counter and never panics.
69/// let s = TokenUsageSnapshot::current();
70/// assert_eq!(s.name, "global");
71/// ```
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TokenUsageSnapshot {
74 /// Scope name (model id, or `"global"` for the whole process).
75 pub name: String,
76 /// Prompt tokens observed in this scope.
77 pub prompt_tokens: u64,
78 /// Completion tokens observed in this scope.
79 pub completion_tokens: u64,
80 /// `prompt_tokens + completion_tokens`.
81 pub total_tokens: u64,
82 /// Convenience aggregate.
83 pub totals: TokenTotals,
84 /// When this snapshot was produced.
85 pub timestamp: DateTime<Utc>,
86 /// Number of requests rolled into this snapshot (0 if unknown).
87 pub request_count: u64,
88}
89
90impl TokenUsageSnapshot {
91 /// Sample the global [`super::super::TOKEN_USAGE`] counter right now.
92 pub fn current() -> Self {
93 let (prompt, comp, total) = super::super::TOKEN_USAGE.get();
94 Self {
95 name: "global".to_string(),
96 prompt_tokens: prompt,
97 completion_tokens: comp,
98 total_tokens: total,
99 totals: TokenTotals::new(prompt, comp),
100 timestamp: Utc::now(),
101 request_count: 0,
102 }
103 }
104
105 /// Human-readable one-liner.
106 pub fn summary(&self) -> String {
107 format!(
108 "{} total tokens ({} input, {} output)",
109 self.totals.total(),
110 self.prompt_tokens,
111 self.completion_tokens
112 )
113 }
114}