Skip to main content

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