Skip to main content

vtcode_core/open_responses/
usage.rs

1//! Token usage statistics for Open Responses.
2//!
3//! Provides a unified usage model that can bridge from VT Code's internal
4//! usage tracking to the Open Responses specification.
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8/// Token usage statistics for a response.
9///
10/// This struct follows the Open Responses specification for usage reporting
11/// and can be converted from VT Code's internal usage types.
12#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct OpenUsage {
14    /// Number of input tokens processed.
15    pub input_tokens: u64,
16
17    /// Number of output tokens generated.
18    pub output_tokens: u64,
19
20    /// Total number of tokens used (input + output).
21    pub total_tokens: u64,
22
23    /// Detailed breakdown of input token usage.
24    #[serde(
25        default,
26        skip_serializing_if = "Option::is_none",
27        deserialize_with = "deserialize_boxed_input_tokens_details_opt"
28    )]
29    pub input_tokens_details: Option<Box<InputTokensDetails>>,
30
31    /// Detailed breakdown of output token usage.
32    #[serde(
33        default,
34        skip_serializing_if = "Option::is_none",
35        deserialize_with = "deserialize_boxed_output_tokens_details_opt"
36    )]
37    pub output_tokens_details: Option<Box<OutputTokensDetails>>,
38}
39
40/// Detailed breakdown of input token usage.
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42pub struct InputTokensDetails {
43    /// Number of cached tokens reused from previous requests.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub cached_tokens: Option<u64>,
46
47    /// Number of tokens used for audio input.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub audio_tokens: Option<u64>,
50
51    /// Number of tokens used for text input.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub text_tokens: Option<u64>,
54}
55
56impl InputTokensDetails {
57    fn is_empty(&self) -> bool {
58        self.cached_tokens.is_none() && self.audio_tokens.is_none() && self.text_tokens.is_none()
59    }
60
61    fn into_boxed_if_non_empty(self) -> Option<Box<Self>> {
62        (!self.is_empty()).then_some(Box::new(self))
63    }
64}
65
66/// Detailed breakdown of output token usage.
67#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
68pub struct OutputTokensDetails {
69    /// Number of tokens used for reasoning/thinking.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub reasoning_tokens: Option<u64>,
72
73    /// Number of tokens used for audio output.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub audio_tokens: Option<u64>,
76
77    /// Number of tokens used for text output.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub text_tokens: Option<u64>,
80}
81
82impl OutputTokensDetails {
83    fn is_empty(&self) -> bool {
84        self.reasoning_tokens.is_none() && self.audio_tokens.is_none() && self.text_tokens.is_none()
85    }
86
87    fn into_boxed_if_non_empty(self) -> Option<Box<Self>> {
88        (!self.is_empty()).then_some(Box::new(self))
89    }
90}
91
92impl OpenUsage {
93    /// Creates a new usage instance with the given token counts.
94    pub fn new(input_tokens: u64, output_tokens: u64) -> Self {
95        Self {
96            input_tokens,
97            output_tokens,
98            total_tokens: input_tokens + output_tokens,
99            input_tokens_details: None,
100            output_tokens_details: None,
101        }
102    }
103
104    /// Creates usage from VT Code's internal LLM usage type.
105    pub fn from_llm_usage(usage: &crate::llm::provider::Usage) -> Self {
106        let mut details = InputTokensDetails::default();
107        let cached = usage.cache_read_tokens_or_fallback();
108        if cached > 0 {
109            details.cached_tokens = Some(cached as u64);
110        }
111
112        Self {
113            input_tokens: usage.prompt_tokens as u64,
114            output_tokens: usage.completion_tokens as u64,
115            total_tokens: usage.total_tokens as u64,
116            input_tokens_details: if details.cached_tokens.is_some() {
117                Some(Box::new(details))
118            } else {
119                None
120            },
121            output_tokens_details: None,
122        }
123    }
124
125    /// Creates usage from VT Code's exec events usage type.
126    pub fn from_exec_usage(usage: &vtcode_exec_events::Usage) -> Self {
127        let input_details = if usage.cached_input_tokens > 0 {
128            Some(Box::new(InputTokensDetails {
129                cached_tokens: Some(usage.cached_input_tokens),
130                audio_tokens: None,
131                text_tokens: None,
132            }))
133        } else {
134            None
135        };
136
137        Self {
138            input_tokens: usage.input_tokens,
139            output_tokens: usage.output_tokens,
140            total_tokens: usage.input_tokens + usage.output_tokens,
141            input_tokens_details: input_details,
142            output_tokens_details: None,
143        }
144    }
145}
146
147fn deserialize_boxed_input_tokens_details_opt<'de, D>(
148    deserializer: D,
149) -> Result<Option<Box<InputTokensDetails>>, D::Error>
150where
151    D: Deserializer<'de>,
152{
153    Option::<InputTokensDetails>::deserialize(deserializer)
154        .map(|value| value.and_then(InputTokensDetails::into_boxed_if_non_empty))
155}
156
157fn deserialize_boxed_output_tokens_details_opt<'de, D>(
158    deserializer: D,
159) -> Result<Option<Box<OutputTokensDetails>>, D::Error>
160where
161    D: Deserializer<'de>,
162{
163    Option::<OutputTokensDetails>::deserialize(deserializer)
164        .map(|value| value.and_then(OutputTokensDetails::into_boxed_if_non_empty))
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_usage_new() {
173        let usage = OpenUsage::new(100, 50);
174        assert_eq!(usage.input_tokens, 100);
175        assert_eq!(usage.output_tokens, 50);
176        assert_eq!(usage.total_tokens, 150);
177    }
178
179    #[test]
180    fn test_from_exec_usage() {
181        let exec_usage = vtcode_exec_events::Usage {
182            input_tokens: 1000,
183            cached_input_tokens: 500,
184            cache_creation_tokens: 0,
185            output_tokens: 200,
186        };
187        let usage = OpenUsage::from_exec_usage(&exec_usage);
188        assert_eq!(usage.input_tokens, 1000);
189        assert_eq!(usage.output_tokens, 200);
190        assert_eq!(usage.total_tokens, 1200);
191        assert_eq!(usage.input_tokens_details.unwrap().cached_tokens, Some(500));
192    }
193
194    #[test]
195    fn test_from_llm_usage_falls_back_to_cached_prompt_tokens() {
196        let usage = OpenUsage::from_llm_usage(&crate::llm::provider::Usage {
197            prompt_tokens: 1000,
198            completion_tokens: 250,
199            total_tokens: 1250,
200            cached_prompt_tokens: Some(400),
201            cache_creation_tokens: None,
202            cache_read_tokens: None,
203        });
204
205        assert_eq!(usage.input_tokens, 1000);
206        assert_eq!(usage.output_tokens, 250);
207        assert_eq!(
208            usage
209                .input_tokens_details
210                .and_then(|details| details.cached_tokens),
211            Some(400)
212        );
213    }
214
215    #[test]
216    fn empty_details_deserialize_to_none() {
217        let usage: OpenUsage = serde_json::from_str(
218            r#"{
219                "input_tokens": 1,
220                "output_tokens": 2,
221                "total_tokens": 3,
222                "input_tokens_details": {},
223                "output_tokens_details": {}
224            }"#,
225        )
226        .unwrap();
227
228        assert!(usage.input_tokens_details.is_none());
229        assert!(usage.output_tokens_details.is_none());
230    }
231
232    #[test]
233    fn boxed_details_are_smaller_than_inline_options() {
234        use std::mem::size_of;
235
236        assert!(
237            size_of::<Option<Box<InputTokensDetails>>>() < size_of::<Option<InputTokensDetails>>()
238        );
239        assert!(
240            size_of::<Option<Box<OutputTokensDetails>>>()
241                < size_of::<Option<OutputTokensDetails>>()
242        );
243    }
244}