Skip to main content

openai_compat/types/responses/
response.rs

1//! The `Response` object and output items, mirroring
2//! `openai-python/src/openai/types/responses/response.py` and
3//! `response_output_item.py` (v1 subset: message / function_call / reasoning
4//! output items — exotic ones round-trip through `Other`).
5
6use serde::{Deserialize, Serialize};
7
8use crate::pagination::HasId;
9
10/// Lifecycle status of a response.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13#[non_exhaustive]
14pub enum ResponseStatus {
15    Completed,
16    Failed,
17    InProgress,
18    Cancelled,
19    Queued,
20    Incomplete,
21}
22
23/// Error returned when the model fails to generate a response.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct ResponseError {
27    pub code: String,
28    pub message: String,
29}
30
31/// Why an otherwise-successful response is incomplete.
32#[derive(Debug, Clone, Default, Serialize, Deserialize)]
33#[non_exhaustive]
34pub struct IncompleteDetails {
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub reason: Option<String>,
37}
38
39/// Token usage for a response, mirroring `ResponseUsage`.
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41#[non_exhaustive]
42pub struct ResponseUsage {
43    #[serde(default)]
44    pub input_tokens: u64,
45    #[serde(default)]
46    pub output_tokens: u64,
47    #[serde(default)]
48    pub total_tokens: u64,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub input_tokens_details: Option<serde_json::Value>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub output_tokens_details: Option<serde_json::Value>,
53}
54
55/// One part of an output message's content.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(tag = "type", rename_all = "snake_case")]
58#[non_exhaustive]
59pub enum OutputContent {
60    OutputText {
61        text: String,
62        #[serde(default)]
63        annotations: Vec<serde_json::Value>,
64    },
65    Refusal {
66        refusal: String,
67    },
68}
69
70/// The typed output item variants this crate models directly. Anything else
71/// (computer_call, file_search_call, web_search_call, mcp_call, ...) falls
72/// back to [`OutputItem::Other`].
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(tag = "type", rename_all = "snake_case")]
75#[non_exhaustive]
76pub enum KnownOutputItem {
77    Message {
78        id: String,
79        role: String,
80        content: Vec<OutputContent>,
81        #[serde(default, skip_serializing_if = "Option::is_none")]
82        status: Option<String>,
83    },
84    FunctionCall {
85        id: String,
86        call_id: String,
87        name: String,
88        /// JSON-encoded arguments string (may be invalid JSON — validate
89        /// before use).
90        arguments: String,
91        #[serde(default, skip_serializing_if = "Option::is_none")]
92        status: Option<String>,
93    },
94    Reasoning {
95        id: String,
96        summary: Vec<serde_json::Value>,
97        #[serde(default, skip_serializing_if = "Option::is_none")]
98        content: Option<Vec<serde_json::Value>>,
99        #[serde(default, skip_serializing_if = "Option::is_none")]
100        encrypted_content: Option<String>,
101    },
102}
103
104/// One item in `Response.output`, mirroring `ResponseOutputItem`.
105///
106/// Unrecognized `type` values deserialize into [`OutputItem::Other`] and
107/// round-trip without data loss (object key order is normalized through
108/// `serde_json::Value`, but no field is dropped).
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(untagged)]
111pub enum OutputItem {
112    Known(KnownOutputItem),
113    Other(serde_json::Value),
114}
115
116/// Response from `POST /responses`, `GET /responses/{id}`, and the terminal
117/// stream events (`Completed`/`Failed`/`Incomplete`).
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[non_exhaustive]
120pub struct Response {
121    pub id: String,
122    pub created_at: f64,
123    #[serde(default)]
124    pub object: String,
125    #[serde(default)]
126    pub status: Option<ResponseStatus>,
127    pub model: String,
128    #[serde(default)]
129    pub output: Vec<OutputItem>,
130    #[serde(default)]
131    pub error: Option<ResponseError>,
132    #[serde(default)]
133    pub incomplete_details: Option<IncompleteDetails>,
134    #[serde(default)]
135    pub usage: Option<ResponseUsage>,
136    #[serde(default)]
137    pub metadata: Option<std::collections::HashMap<String, String>>,
138    #[serde(default)]
139    pub previous_response_id: Option<String>,
140    #[serde(default)]
141    pub temperature: Option<f64>,
142    #[serde(default)]
143    pub top_p: Option<f64>,
144}
145
146impl Response {
147    /// Concatenated text of every `output_text` content part across all
148    /// output messages — the common case for reading a response's answer.
149    /// Mirrors the Python SDK's `Response.output_text` computed property.
150    pub fn output_text(&self) -> String {
151        let mut text = String::new();
152        for item in &self.output {
153            if let OutputItem::Known(KnownOutputItem::Message { content, .. }) = item {
154                for part in content {
155                    if let OutputContent::OutputText { text: part_text, .. } = part {
156                        text.push_str(part_text);
157                    }
158                }
159            }
160        }
161        text
162    }
163
164    /// Function calls the model made in this response, if any.
165    pub fn function_calls(&self) -> Vec<&KnownOutputItem> {
166        self.output
167            .iter()
168            .filter_map(|item| match item {
169                OutputItem::Known(known @ KnownOutputItem::FunctionCall { .. }) => Some(known),
170                _ => None,
171            })
172            .collect()
173    }
174}
175
176/// One item returned by `GET /responses/{id}/input_items`. Kept as raw JSON
177/// since input items may be any of the input or output item shapes.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(transparent)]
180pub struct ResponseItem(pub serde_json::Value);
181
182impl HasId for ResponseItem {
183    fn id(&self) -> Option<&str> {
184        self.0.get("id").and_then(serde_json::Value::as_str)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn sample_response_json() -> serde_json::Value {
193        serde_json::json!({
194            "id": "resp_123",
195            "object": "response",
196            "created_at": 1728933352.0,
197            "status": "completed",
198            "model": "gpt-5",
199            "output": [{
200                "type": "message",
201                "id": "msg_1",
202                "role": "assistant",
203                "content": [{"type": "output_text", "text": "Hello there!", "annotations": []}]
204            }],
205            "usage": {
206                "input_tokens": 10,
207                "output_tokens": 5,
208                "total_tokens": 15,
209                "input_tokens_details": {"cached_tokens": 0},
210                "output_tokens_details": {"reasoning_tokens": 0}
211            }
212        })
213    }
214
215    #[test]
216    fn deserializes_real_response() {
217        let response: Response = serde_json::from_value(sample_response_json()).unwrap();
218        assert_eq!(response.status, Some(ResponseStatus::Completed));
219        assert_eq!(response.usage.as_ref().unwrap().total_tokens, 15);
220    }
221
222    #[test]
223    fn output_text_concatenates() {
224        let response: Response = serde_json::from_value(sample_response_json()).unwrap();
225        assert_eq!(response.output_text(), "Hello there!");
226    }
227
228    #[test]
229    fn function_call_output_item_parses() {
230        let json = serde_json::json!({
231            "id": "resp_1", "object": "response", "created_at": 1.0, "model": "gpt-5",
232            "output": [{
233                "type": "function_call",
234                "id": "fc_1",
235                "call_id": "call_1",
236                "name": "get_weather",
237                "arguments": "{\"city\":\"Hanoi\"}"
238            }]
239        });
240        let response: Response = serde_json::from_value(json).unwrap();
241        let calls = response.function_calls();
242        assert_eq!(calls.len(), 1);
243        match calls[0] {
244            KnownOutputItem::FunctionCall { name, .. } => assert_eq!(name, "get_weather"),
245            _ => panic!("expected FunctionCall"),
246        }
247    }
248
249    #[test]
250    fn unknown_output_item_preserved_as_other() {
251        let json = serde_json::json!({
252            "type": "web_search_call",
253            "id": "ws_1",
254            "status": "completed"
255        });
256        let item: OutputItem = serde_json::from_value(json.clone()).unwrap();
257        assert!(matches!(item, OutputItem::Other(_)));
258        assert_eq!(serde_json::to_value(&item).unwrap(), json);
259    }
260}