vtcode_core/open_responses/
usage.rs1use serde::{Deserialize, Deserializer, Serialize};
7
8#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct OpenUsage {
14 pub input_tokens: u64,
16
17 pub output_tokens: u64,
19
20 pub total_tokens: u64,
22
23 #[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 #[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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42pub struct InputTokensDetails {
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub cached_tokens: Option<u64>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub audio_tokens: Option<u64>,
50
51 #[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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
68pub struct OutputTokensDetails {
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub reasoning_tokens: Option<u64>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub audio_tokens: Option<u64>,
76
77 #[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 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 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 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}