vtcode_llm/open_responses/
response.rs1use serde::{Deserialize, Serialize};
7use std::time::{SystemTime, UNIX_EPOCH};
8use vtcode_macros::StringNewtype;
9
10use super::{OpenResponseError, OpenUsage, OutputItem};
11use crate::provider::ToolDefinition;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, StringNewtype)]
15#[serde(transparent)]
16pub struct ResponseId(String);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
20#[serde(rename_all = "snake_case")]
21pub enum ResponseStatus {
22 Queued,
24
25 #[default]
27 InProgress,
28
29 Completed,
31
32 Failed,
34
35 Incomplete,
37}
38
39impl ResponseStatus {
40 pub(crate) fn is_terminal(&self) -> bool {
42 matches!(self, Self::Completed | Self::Failed | Self::Incomplete)
43 }
44}
45
46impl std::fmt::Display for ResponseStatus {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Queued => write!(f, "queued"),
50 Self::InProgress => write!(f, "in_progress"),
51 Self::Completed => write!(f, "completed"),
52 Self::Failed => write!(f, "failed"),
53 Self::Incomplete => write!(f, "incomplete"),
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct IncompleteDetails {
61 pub reason: IncompleteReason,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum IncompleteReason {
69 MaxOutputTokens,
71
72 MaxToolCalls,
74
75 ContentFilter,
77
78 Cancelled,
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct Response {
88 pub(crate) id: ResponseId,
90
91 pub object: String,
93
94 pub created_at: u64,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub completed_at: Option<u64>,
100
101 pub status: ResponseStatus,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub incomplete_details: Option<IncompleteDetails>,
107
108 pub model: String,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 previous_response_id: Option<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 instructions: Option<String>,
118
119 pub output: Vec<OutputItem>,
121
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 error: Option<Box<OpenResponseError>>,
125
126 tools: Option<Vec<ToolDefinition>>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub(crate) usage: Option<Box<OpenUsage>>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 parallel_tool_calls: Option<bool>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 max_output_tokens: Option<u64>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 max_tool_calls: Option<u64>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
147 temperature: Option<f64>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
151 top_p: Option<f64>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
155 store: Option<bool>,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
159 background: Option<bool>,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 service_tier: Option<String>,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
167 metadata: Option<serde_json::Value>,
168}
169
170impl Response {
171 pub fn new(id: impl Into<ResponseId>, model: impl Into<String>) -> Self {
173 let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).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(crate) 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(SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0));
210 }
211
212 pub(crate) fn fail(&mut self, error: OpenResponseError) {
214 self.status = ResponseStatus::Failed;
215 self.error = Some(error.into());
216 self.completed_at = Some(SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0));
217 }
218
219 pub fn incomplete(&mut self, reason: IncompleteReason) {
221 self.status = ResponseStatus::Incomplete;
222 self.incomplete_details = Some(IncompleteDetails { reason });
223 self.completed_at = Some(SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0));
224 }
225
226 pub fn with_usage(mut self, usage: OpenUsage) -> Self {
228 self.usage = Some(usage.into());
229 self
230 }
231
232 pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
234 self.tools = Some(tools);
235 self
236 }
237}
238
239impl Default for Response {
240 fn default() -> Self {
241 Self::new(generate_response_id(), "unknown")
242 }
243}
244
245pub fn generate_response_id() -> String {
247 use std::sync::atomic::{AtomicU64, Ordering};
248 static COUNTER: AtomicU64 = AtomicU64::new(0);
249
250 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0);
251 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
252
253 format!("resp_{timestamp:x}_{count:04x}")
254}
255
256pub fn generate_item_id() -> String {
258 use std::sync::atomic::{AtomicU64, Ordering};
259 static COUNTER: AtomicU64 = AtomicU64::new(0);
260
261 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
262 format!("item_{count}")
263}
264
265#[expect(
267 dead_code,
268 reason = "Intentional compatibility, platform, test, or API-shape suppression."
269)]
270pub fn generate_content_part_id() -> String {
271 use std::sync::atomic::{AtomicU64, Ordering};
272 static COUNTER: AtomicU64 = AtomicU64::new(0);
273
274 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
275 format!("cp_{count}")
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_response_creation() {
284 let response = Response::new("resp_123", "gpt-5");
285 assert_eq!(response.id.as_str(), "resp_123");
286 assert_eq!(response.model, "gpt-5");
287 assert_eq!(response.object, "response");
288 assert_eq!(response.status, ResponseStatus::InProgress);
289 }
290
291 #[test]
292 fn test_response_complete() {
293 let mut response = Response::new("resp_123", "gpt-5");
294 response.complete();
295 assert_eq!(response.status, ResponseStatus::Completed);
296 assert!(response.completed_at.is_some());
297 }
298
299 #[test]
300 fn test_response_fail() {
301 let mut response = Response::new("resp_123", "gpt-5");
302 response.fail(OpenResponseError::server_error("Test error"));
303 assert_eq!(response.status, ResponseStatus::Failed);
304 assert!(response.error.is_some());
305 }
306
307 #[test]
308 fn test_id_generation() {
309 let id1 = generate_response_id();
310 let id2 = generate_response_id();
311 assert_ne!(id1, id2);
312 assert!(id1.starts_with("resp_"));
313 }
314
315 #[test]
316 fn boxed_sparse_fields_are_smaller_than_inline_options() {
317 use std::mem::size_of;
318
319 assert!(size_of::<Option<Box<OpenUsage>>>() < size_of::<Option<OpenUsage>>());
320 assert!(size_of::<Option<Box<OpenResponseError>>>() < size_of::<Option<OpenResponseError>>());
321 }
322}