openai_compat/types/responses/
response.rs1use serde::{Deserialize, Serialize};
7
8use crate::pagination::HasId;
9
10#[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#[derive(Debug, Clone, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct ResponseError {
27 pub code: String,
28 pub message: String,
29}
30
31#[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#[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#[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#[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(untagged)]
111pub enum OutputItem {
112 Known(KnownOutputItem),
113 Other(serde_json::Value),
114}
115
116#[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 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 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#[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}