aether_core/context/
token_tracker.rs1use llm::TokenUsage;
2
3pub const DEFAULT_COMPACTION_THRESHOLD: f64 = 0.85;
5
6#[derive(Debug, Clone, Default)]
15pub struct TokenTracker {
16 total_input_tokens: u64,
17 total_output_tokens: u64,
18 total_cache_read_tokens: u64,
19 total_cache_creation_tokens: u64,
20 total_reasoning_tokens: u64,
21 last_usage: TokenUsage,
22 context_limit: Option<u32>,
23}
24
25impl TokenTracker {
26 pub fn new(context_limit: Option<u32>) -> Self {
27 Self { context_limit, ..Self::default() }
28 }
29
30 pub fn record_usage(&mut self, sample: TokenUsage) {
32 self.total_input_tokens += u64::from(sample.input_tokens);
33 self.total_output_tokens += u64::from(sample.output_tokens);
34 self.total_cache_read_tokens += u64::from(sample.cache_read_tokens.unwrap_or(0));
35 self.total_cache_creation_tokens += u64::from(sample.cache_creation_tokens.unwrap_or(0));
36 self.total_reasoning_tokens += u64::from(sample.reasoning_tokens.unwrap_or(0));
37 self.last_usage = sample;
38 }
39
40 pub fn usage_ratio(&self) -> Option<f64> {
42 let context_limit = self.context_limit?;
43 if context_limit == 0 {
44 return None;
45 }
46 Some(f64::from(self.last_usage.input_tokens) / f64::from(context_limit))
47 }
48
49 pub fn exceeds_threshold(&self, threshold: f64) -> bool {
51 self.usage_ratio().is_some_and(|ratio| ratio >= threshold)
52 }
53
54 pub fn needs_compaction(&self, estimated_tokens: u32, threshold: f64) -> bool {
56 self.context_limit.is_some_and(|limit| {
57 f64::from(self.last_usage.input_tokens.max(estimated_tokens)) >= f64::from(limit) * threshold
58 })
59 }
60
61 pub fn tokens_remaining(&self) -> Option<u32> {
63 self.context_limit.map(|context_limit| context_limit.saturating_sub(self.last_usage.input_tokens))
64 }
65
66 pub fn set_context_limit(&mut self, limit: Option<u32>) {
68 self.context_limit = limit;
69 }
70
71 pub fn context_limit(&self) -> Option<u32> {
73 self.context_limit
74 }
75
76 pub fn last_input_tokens(&self) -> u32 {
78 self.last_usage.input_tokens
79 }
80
81 pub fn last_usage(&self) -> &TokenUsage {
84 &self.last_usage
85 }
86
87 pub fn total_input_tokens(&self) -> u64 {
89 self.total_input_tokens
90 }
91
92 pub fn total_output_tokens(&self) -> u64 {
94 self.total_output_tokens
95 }
96
97 pub fn total_cache_read_tokens(&self) -> u64 {
99 self.total_cache_read_tokens
100 }
101
102 pub fn total_cache_creation_tokens(&self) -> u64 {
104 self.total_cache_creation_tokens
105 }
106
107 pub fn total_reasoning_tokens(&self) -> u64 {
109 self.total_reasoning_tokens
110 }
111
112 pub fn reset_current_usage(&mut self) {
116 self.last_usage = TokenUsage::default();
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_usage_tracking() {
126 let mut tracker = TokenTracker::new(Some(1000));
127
128 tracker.record_usage(TokenUsage::new(500, 100));
129 assert_eq!(tracker.usage_ratio(), Some(0.5));
130 assert!(!tracker.exceeds_threshold(0.85));
131
132 tracker.record_usage(TokenUsage::new(900, 50));
133 assert_eq!(tracker.usage_ratio(), Some(0.9));
134 assert!(tracker.exceeds_threshold(0.85));
135 }
136
137 #[test]
138 fn test_tokens_remaining() {
139 let mut tracker = TokenTracker::new(Some(1000));
140 tracker.record_usage(TokenUsage::new(700, 50));
141 assert_eq!(tracker.tokens_remaining(), Some(300));
142 }
143
144 #[test]
145 fn test_cumulative_totals() {
146 let mut tracker = TokenTracker::new(Some(1000));
147 tracker.record_usage(TokenUsage::new(100, 50));
148 tracker.record_usage(TokenUsage::new(200, 60));
149
150 assert_eq!(tracker.total_input_tokens(), 300);
151 assert_eq!(tracker.total_output_tokens(), 110);
152 assert_eq!(tracker.last_input_tokens(), 200);
153 }
154
155 #[test]
156 fn test_unknown_context_limit() {
157 let tracker = TokenTracker::new(None);
158 assert_eq!(tracker.usage_ratio(), None);
159 assert_eq!(tracker.tokens_remaining(), None);
160 assert!(!tracker.needs_compaction(1_000_000, 0.85));
161 }
162
163 #[test]
164 fn test_exceeds_threshold() {
165 let mut tracker = TokenTracker::new(Some(1000));
166
167 tracker.record_usage(TokenUsage::new(500, 100));
168 assert!(!tracker.exceeds_threshold(0.6));
169 assert!(tracker.exceeds_threshold(0.5));
170
171 tracker.record_usage(TokenUsage::new(850, 50));
172 assert!(tracker.exceeds_threshold(0.8));
173 assert!(tracker.exceeds_threshold(0.85));
174 }
175
176 #[test]
177 fn test_needs_compaction_from_recorded_usage() {
178 let mut tracker = TokenTracker::new(Some(10000));
179
180 tracker.record_usage(TokenUsage::new(9000, 100));
181 assert!(tracker.needs_compaction(0, 0.85));
182
183 tracker.record_usage(TokenUsage::new(7000, 100));
184 assert!(!tracker.needs_compaction(0, 0.85));
185 }
186
187 #[test]
188 fn test_needs_compaction_from_estimate_before_usage_recorded() {
189 let tracker = TokenTracker::new(Some(10000));
190
191 assert!(tracker.needs_compaction(9000, 0.85));
192 assert!(!tracker.needs_compaction(1000, 0.85));
193 }
194
195 #[test]
196 fn test_default_compaction_threshold() {
197 use super::DEFAULT_COMPACTION_THRESHOLD;
198 assert!((DEFAULT_COMPACTION_THRESHOLD - 0.85).abs() < 0.001);
199 }
200
201 #[test]
202 fn test_set_context_limit() {
203 let mut tracker = TokenTracker::new(Some(200_000));
204 assert_eq!(tracker.context_limit(), Some(200_000));
205
206 tracker.set_context_limit(Some(128_000));
207 assert_eq!(tracker.context_limit(), Some(128_000));
208
209 tracker.record_usage(TokenUsage::new(100_000, 50));
211 let expected_ratio = 100_000.0 / 128_000.0;
212 assert!((tracker.usage_ratio().unwrap_or_default() - expected_ratio).abs() < 0.001);
213 }
214
215 #[test]
216 fn test_reset_current_usage() {
217 let mut tracker = TokenTracker::new(Some(10000));
218 tracker.record_usage(TokenUsage::new(9000, 100));
219
220 assert!(tracker.needs_compaction(0, 0.85));
221
222 tracker.reset_current_usage();
223
224 assert_eq!(tracker.last_input_tokens(), 0);
225 assert!(!tracker.needs_compaction(0, 0.85));
226 assert_eq!(tracker.total_input_tokens(), 9000);
227 assert_eq!(tracker.total_output_tokens(), 100);
228 }
229
230 #[test]
231 fn test_cache_and_reasoning_totals_accumulate() {
232 let mut tracker = TokenTracker::new(Some(10000));
233
234 tracker.record_usage(TokenUsage {
235 input_tokens: 500,
236 output_tokens: 100,
237 cache_read_tokens: Some(200),
238 cache_creation_tokens: Some(50),
239 reasoning_tokens: Some(30),
240 ..TokenUsage::default()
241 });
242 tracker.record_usage(TokenUsage {
243 input_tokens: 600,
244 output_tokens: 80,
245 cache_read_tokens: Some(300),
246 cache_creation_tokens: None,
247 reasoning_tokens: Some(20),
248 ..TokenUsage::default()
249 });
250
251 assert_eq!(tracker.total_cache_read_tokens(), 500);
252 assert_eq!(tracker.total_cache_creation_tokens(), 50);
253 assert_eq!(tracker.total_reasoning_tokens(), 50);
254 }
255
256 #[test]
257 fn test_last_usage_exposes_full_token_usage() {
258 let mut tracker = TokenTracker::new(Some(10000));
259 let sample = TokenUsage {
260 input_tokens: 500,
261 output_tokens: 100,
262 cache_read_tokens: Some(200),
263 cache_creation_tokens: Some(50),
264 reasoning_tokens: Some(30),
265 input_audio_tokens: Some(5),
266 ..TokenUsage::default()
267 };
268
269 tracker.record_usage(sample);
270
271 assert_eq!(*tracker.last_usage(), sample);
272 }
273
274 #[test]
275 fn test_reset_clears_last_usage_but_keeps_cache_totals() {
276 let mut tracker = TokenTracker::new(Some(10000));
277 tracker.record_usage(TokenUsage {
278 input_tokens: 500,
279 output_tokens: 100,
280 cache_read_tokens: Some(200),
281 cache_creation_tokens: Some(50),
282 reasoning_tokens: Some(30),
283 ..TokenUsage::default()
284 });
285
286 tracker.reset_current_usage();
287
288 assert_eq!(*tracker.last_usage(), TokenUsage::default());
289 assert_eq!(tracker.total_cache_read_tokens(), 200);
290 assert_eq!(tracker.total_cache_creation_tokens(), 50);
291 assert_eq!(tracker.total_reasoning_tokens(), 30);
292 }
293}