vtcode_core/open_responses/
response.rs1use serde::{Deserialize, Serialize};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use super::{OpenResponseError, OpenUsage, OutputItem};
10use crate::llm::provider::ToolDefinition;
11
12pub type ResponseId = String;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
17#[serde(rename_all = "snake_case")]
18pub enum ResponseStatus {
19 Queued,
21
22 #[default]
24 InProgress,
25
26 Completed,
28
29 Failed,
31
32 Incomplete,
34}
35
36impl ResponseStatus {
37 pub fn is_terminal(&self) -> bool {
39 matches!(self, Self::Completed | Self::Failed | Self::Incomplete)
40 }
41}
42
43impl std::fmt::Display for ResponseStatus {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::Queued => write!(f, "queued"),
47 Self::InProgress => write!(f, "in_progress"),
48 Self::Completed => write!(f, "completed"),
49 Self::Failed => write!(f, "failed"),
50 Self::Incomplete => write!(f, "incomplete"),
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct IncompleteDetails {
58 pub reason: IncompleteReason,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum IncompleteReason {
66 MaxOutputTokens,
68
69 MaxToolCalls,
71
72 ContentFilter,
74
75 Cancelled,
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct Response {
85 pub id: ResponseId,
87
88 pub object: String,
90
91 pub created_at: u64,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub completed_at: Option<u64>,
97
98 pub status: ResponseStatus,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub incomplete_details: Option<IncompleteDetails>,
104
105 pub model: String,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub previous_response_id: Option<String>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub instructions: Option<String>,
115
116 pub output: Vec<OutputItem>,
118
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub error: Option<Box<OpenResponseError>>,
122
123 pub tools: Option<Vec<ToolDefinition>>,
125
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub usage: Option<Box<OpenUsage>>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub parallel_tool_calls: Option<bool>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub max_output_tokens: Option<u64>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub max_tool_calls: Option<u64>,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub temperature: Option<f64>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub top_p: Option<f64>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub store: Option<bool>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub background: Option<bool>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub service_tier: Option<String>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub metadata: Option<serde_json::Value>,
165}
166
167impl Response {
168 pub fn new(id: impl Into<String>, model: impl Into<String>) -> Self {
170 let now = SystemTime::now()
171 .duration_since(UNIX_EPOCH)
172 .map(|d| d.as_secs())
173 .unwrap_or(0);
174
175 Self {
176 id: id.into(),
177 object: "response".to_string(),
178 created_at: now,
179 completed_at: None,
180 status: ResponseStatus::InProgress,
181 incomplete_details: None,
182 model: model.into(),
183 previous_response_id: None,
184 instructions: None,
185 output: Vec::new(),
186 error: None,
187 tools: None,
188 usage: None,
189 parallel_tool_calls: None,
190 max_output_tokens: None,
191 max_tool_calls: None,
192 temperature: None,
193 top_p: None,
194 store: None,
195 background: None,
196 service_tier: None,
197 metadata: None,
198 }
199 }
200
201 pub fn add_output(&mut self, item: OutputItem) {
203 self.output.push(item);
204 }
205
206 pub fn complete(&mut self) {
208 self.status = ResponseStatus::Completed;
209 self.completed_at = Some(
210 SystemTime::now()
211 .duration_since(UNIX_EPOCH)
212 .map(|d| d.as_secs())
213 .unwrap_or(0),
214 );
215 }
216
217 pub fn fail(&mut self, error: OpenResponseError) {
219 self.status = ResponseStatus::Failed;
220 self.error = Some(error.into());
221 self.completed_at = Some(
222 SystemTime::now()
223 .duration_since(UNIX_EPOCH)
224 .map(|d| d.as_secs())
225 .unwrap_or(0),
226 );
227 }
228
229 pub fn incomplete(&mut self, reason: IncompleteReason) {
231 self.status = ResponseStatus::Incomplete;
232 self.incomplete_details = Some(IncompleteDetails { reason });
233 self.completed_at = Some(
234 SystemTime::now()
235 .duration_since(UNIX_EPOCH)
236 .map(|d| d.as_secs())
237 .unwrap_or(0),
238 );
239 }
240
241 pub fn with_usage(mut self, usage: OpenUsage) -> Self {
243 self.usage = Some(usage.into());
244 self
245 }
246
247 pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
249 self.tools = Some(tools);
250 self
251 }
252}
253
254impl Default for Response {
255 fn default() -> Self {
256 Self::new(generate_response_id(), "unknown")
257 }
258}
259
260pub fn generate_response_id() -> String {
262 use std::sync::atomic::{AtomicU64, Ordering};
263 static COUNTER: AtomicU64 = AtomicU64::new(0);
264
265 let timestamp = SystemTime::now()
266 .duration_since(UNIX_EPOCH)
267 .map(|d| d.as_millis())
268 .unwrap_or(0);
269 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
270
271 format!("resp_{:x}_{:04x}", timestamp, count)
272}
273
274pub fn generate_item_id() -> String {
276 use std::sync::atomic::{AtomicU64, Ordering};
277 static COUNTER: AtomicU64 = AtomicU64::new(0);
278
279 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
280 format!("item_{count}")
281}
282
283#[expect(dead_code)]
285pub fn generate_content_part_id() -> String {
286 use std::sync::atomic::{AtomicU64, Ordering};
287 static COUNTER: AtomicU64 = AtomicU64::new(0);
288
289 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
290 format!("cp_{count}")
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_response_creation() {
299 let response = Response::new("resp_123", "gpt-5");
300 assert_eq!(response.id, "resp_123");
301 assert_eq!(response.model, "gpt-5");
302 assert_eq!(response.object, "response");
303 assert_eq!(response.status, ResponseStatus::InProgress);
304 }
305
306 #[test]
307 fn test_response_complete() {
308 let mut response = Response::new("resp_123", "gpt-5");
309 response.complete();
310 assert_eq!(response.status, ResponseStatus::Completed);
311 assert!(response.completed_at.is_some());
312 }
313
314 #[test]
315 fn test_response_fail() {
316 let mut response = Response::new("resp_123", "gpt-5");
317 response.fail(OpenResponseError::server_error("Test error"));
318 assert_eq!(response.status, ResponseStatus::Failed);
319 assert!(response.error.is_some());
320 }
321
322 #[test]
323 fn test_id_generation() {
324 let id1 = generate_response_id();
325 let id2 = generate_response_id();
326 assert_ne!(id1, id2);
327 assert!(id1.starts_with("resp_"));
328 }
329
330 #[test]
331 fn boxed_sparse_fields_are_smaller_than_inline_options() {
332 use std::mem::size_of;
333
334 assert!(size_of::<Option<Box<OpenUsage>>>() < size_of::<Option<OpenUsage>>());
335 assert!(
336 size_of::<Option<Box<OpenResponseError>>>() < size_of::<Option<OpenResponseError>>()
337 );
338 }
339}