Skip to main content

vtcode_core/open_responses/
response.rs

1//! Response object for Open Responses.
2//!
3//! The Response is the top-level object returned by the API,
4//! containing output items, usage statistics, and metadata.
5
6use serde::{Deserialize, Serialize};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use super::{OpenResponseError, OpenUsage, OutputItem};
10use crate::llm::provider::ToolDefinition;
11
12/// Unique identifier for a response.
13pub type ResponseId = String;
14
15/// Status of a response.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
17#[serde(rename_all = "snake_case")]
18pub enum ResponseStatus {
19    /// Response is queued for processing.
20    Queued,
21
22    /// Response is currently being processed.
23    #[default]
24    InProgress,
25
26    /// Response completed successfully.
27    Completed,
28
29    /// Response failed with an error.
30    Failed,
31
32    /// Response is incomplete (e.g., token limit reached).
33    Incomplete,
34}
35
36impl ResponseStatus {
37    /// Returns true if this is a terminal status.
38    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/// Details about why a response was incomplete.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct IncompleteDetails {
58    /// The reason the response could not be completed.
59    pub reason: IncompleteReason,
60}
61
62/// Reasons why a response may be incomplete.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum IncompleteReason {
66    /// Maximum output tokens reached.
67    MaxOutputTokens,
68
69    /// Maximum tool calls reached.
70    MaxToolCalls,
71
72    /// Content filter triggered.
73    ContentFilter,
74
75    /// User cancelled the request.
76    Cancelled,
77}
78
79/// The main response object per the Open Responses specification.
80///
81/// This is the top-level object returned by the API, containing all
82/// output items, usage statistics, and metadata about the response.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct Response {
85    /// Unique ID of the response.
86    pub id: ResponseId,
87
88    /// Object type, always "response".
89    pub object: String,
90
91    /// Unix timestamp (seconds) when the response was created.
92    pub created_at: u64,
93
94    /// Unix timestamp (seconds) when the response was completed, if applicable.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub completed_at: Option<u64>,
97
98    /// Current status of the response.
99    pub status: ResponseStatus,
100
101    /// Details about why the response was incomplete, if applicable.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub incomplete_details: Option<IncompleteDetails>,
104
105    /// The model that generated this response.
106    pub model: String,
107
108    /// ID of the previous response in the chain, if any.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub previous_response_id: Option<String>,
111
112    /// Additional instructions used to guide the model.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub instructions: Option<String>,
115
116    /// Output items generated by the model.
117    pub output: Vec<OutputItem>,
118
119    /// Error that occurred, if the response failed.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub error: Option<Box<OpenResponseError>>,
122
123    /// Tools that were available to the model.
124    pub tools: Option<Vec<ToolDefinition>>,
125
126    /// Token usage statistics.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub usage: Option<Box<OpenUsage>>,
129
130    /// Whether parallel tool calls were allowed.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub parallel_tool_calls: Option<bool>,
133
134    /// Maximum output tokens allowed.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub max_output_tokens: Option<u64>,
137
138    /// Maximum tool calls allowed.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub max_tool_calls: Option<u64>,
141
142    /// Sampling temperature used.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub temperature: Option<f64>,
145
146    /// Nucleus sampling parameter used.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub top_p: Option<f64>,
149
150    /// Whether this response was stored.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub store: Option<bool>,
153
154    /// Whether this request ran in the background.
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub background: Option<bool>,
157
158    /// Service tier used for this response.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub service_tier: Option<String>,
161
162    /// Developer-defined metadata.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub metadata: Option<serde_json::Value>,
165}
166
167impl Response {
168    /// Creates a new response with the given ID and model.
169    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    /// Adds an output item to the response.
202    pub fn add_output(&mut self, item: OutputItem) {
203        self.output.push(item);
204    }
205
206    /// Marks the response as completed.
207    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    /// Marks the response as failed with the given error.
218    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    /// Marks the response as incomplete with the given reason.
230    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    /// Sets the usage statistics.
242    pub fn with_usage(mut self, usage: OpenUsage) -> Self {
243        self.usage = Some(usage.into());
244        self
245    }
246
247    /// Sets the available tools.
248    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
260/// Generates a unique response ID.
261pub 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
274/// Generates a unique output item ID.
275pub 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/// Generates a unique content part ID.
284#[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}