1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0
/// Tracks token usage and cache hit statistics.
///
/// Note: `last_cache` is only populated by Claude and `OpenAI` providers.
/// Ollama and Gemini only use `last_usage`.
///
/// `last_reasoning` stores reasoning tokens, which are a **subset** of completion tokens
/// (`OpenAI` o-series only). Never add reasoning tokens to cost separately.
#[derive(Debug, Default)]
#[allow(clippy::struct_field_names)] // all fields are `last_*` by design — they track the last seen value
pub(crate) struct UsageTracker {
last_usage: std::sync::Mutex<Option<(u64, u64)>>,
last_cache: std::sync::Mutex<Option<(u64, u64)>>,
last_reasoning: std::sync::Mutex<Option<u64>>,
}
impl UsageTracker {
pub(crate) fn record_usage(&self, input: u64, output: u64) {
if let Ok(mut g) = self.last_usage.lock() {
*g = Some((input, output));
}
}
pub(crate) fn record_cache(&self, creation: u64, read: u64) {
if let Ok(mut g) = self.last_cache.lock() {
*g = Some((creation, read));
}
}
/// Record reasoning tokens from the last response.
///
/// Reasoning tokens are a **subset** of completion tokens; callers must not add them
/// to cost calculations.
pub(crate) fn record_reasoning(&self, tokens: u64) {
if let Ok(mut g) = self.last_reasoning.lock() {
*g = Some(tokens);
}
}
pub(crate) fn last_usage(&self) -> Option<(u64, u64)> {
self.last_usage.lock().ok().and_then(|g| *g)
}
pub(crate) fn last_cache_usage(&self) -> Option<(u64, u64)> {
self.last_cache.lock().ok().and_then(|g| *g)
}
/// Returns reasoning tokens from the last response, or `None` if the provider
/// does not report them.
pub(crate) fn last_reasoning(&self) -> Option<u64> {
self.last_reasoning.lock().ok().and_then(|g| *g)
}
}
impl Clone for UsageTracker {
fn clone(&self) -> Self {
Self::default()
}
}