Skip to main content

llm_trait/
types.rs

1//! Usage info types.
2
3/// Token usage information for an LLM call.
4#[derive(Clone, Debug, Default)]
5pub struct UsageInfo {
6    pub prompt_tokens: Option<u32>,
7    pub completion_tokens: Option<u32>,
8    pub total_tokens: Option<u32>,
9    /// Tokens used for reasoning / thinking (DeepSeek, OpenAI o-series, etc.).
10    /// Always `None` on the Anthropic protocol: its usage object has no
11    /// thinking breakdown — thinking tokens are folded into
12    /// `completion_tokens`.
13    pub reasoning_tokens: Option<u32>,
14}
15
16impl UsageInfo {
17    /// Merge a partial usage event into `self`; the incoming `Some` field
18    /// wins over the existing value, a `None` leaves it untouched.
19    ///
20    /// Streaming protocols may split usage across several events (Anthropic
21    /// sends `prompt_tokens` in `message_start` and the final
22    /// `completion_tokens` in `message_delta`). Replacing the whole struct
23    /// on each event would silently zero out fields the last event omits,
24    /// so consumers must fold events with this method instead.
25    pub fn merge(&mut self, other: &UsageInfo) {
26        if other.prompt_tokens.is_some() {
27            self.prompt_tokens = other.prompt_tokens;
28        }
29        if other.completion_tokens.is_some() {
30            self.completion_tokens = other.completion_tokens;
31        }
32        if other.total_tokens.is_some() {
33            self.total_tokens = other.total_tokens;
34        }
35        if other.reasoning_tokens.is_some() {
36            self.reasoning_tokens = other.reasoning_tokens;
37        }
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn merge_keeps_existing_when_incoming_none() {
47        let mut acc = UsageInfo {
48            prompt_tokens: Some(5),
49            completion_tokens: Some(2),
50            total_tokens: Some(7),
51            reasoning_tokens: None,
52        };
53        // Anthropic `message_delta`: only the final output count arrives.
54        acc.merge(&UsageInfo {
55            prompt_tokens: None,
56            completion_tokens: Some(26),
57            total_tokens: None,
58            reasoning_tokens: None,
59        });
60        assert_eq!(
61            acc.prompt_tokens,
62            Some(5),
63            "prompt survives the partial event"
64        );
65        assert_eq!(acc.completion_tokens, Some(26), "later Some wins");
66        assert_eq!(acc.total_tokens, Some(7));
67    }
68
69    #[test]
70    fn merge_lets_later_some_override() {
71        // DashScope's anthropic endpoint reports the full `input_tokens` in
72        // `message_delta` (context so far), superseding the `message_start`
73        // value (latest message only) — the newer number is more useful.
74        let mut acc = UsageInfo {
75            prompt_tokens: Some(2),
76            ..Default::default()
77        };
78        acc.merge(&UsageInfo {
79            prompt_tokens: Some(63),
80            ..Default::default()
81        });
82        assert_eq!(acc.prompt_tokens, Some(63));
83    }
84}