1use super::*;
2
3#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
5pub struct Response {
6 pub id: String,
8
9 pub object: String,
11
12 pub created_at: u64,
14
15 pub status: ResponseStatus,
17
18 #[serde(default)]
20 pub error: Option<ResponseError>,
21
22 #[serde(default)]
24 pub incomplete_details: Option<IncompleteDetails>,
25
26 pub model: String,
28
29 #[serde(default)]
33 pub output: Vec<OutputItem>,
34
35 #[serde(default)]
38 pub usage: Option<Usage>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub store: Option<bool>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub parallel_tool_calls: Option<bool>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub previous_response_id: Option<String>,
51}
52
53impl Response {
54 pub fn output_text(&self) -> String {
56 let mut text = String::new();
57 for item in &self.output {
58 if let OutputItem::Message { content, .. } = item {
59 for part in content {
60 if let ContentPart::OutputText {
61 text: part_text, ..
62 } = part
63 {
64 text.push_str(part_text);
65 }
66 }
67 }
68 }
69 text
70 }
71
72 pub fn reasoning_text(&self) -> String {
74 let mut text = String::new();
75 for item in &self.output {
76 if let OutputItem::Reasoning { content, .. } = item {
77 for part in content {
78 if let ContentPart::ReasoningText {
79 text: part_text, ..
80 } = part
81 {
82 text.push_str(part_text);
83 }
84 }
85 }
86 }
87 text
88 }
89}
90
91#[non_exhaustive]
93#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
94#[serde(rename_all = "snake_case")]
95pub enum ResponseStatus {
96 InProgress,
97 Completed,
98 Incomplete,
99 Failed,
100}
101
102#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
104pub struct ResponseError {
105 #[serde(default)]
106 pub code: Option<String>,
107 #[serde(default)]
108 pub message: Option<String>,
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
113pub struct IncompleteDetails {
114 pub reason: IncompleteReason,
116}
117
118#[non_exhaustive]
120#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
121#[serde(rename_all = "snake_case")]
122pub enum IncompleteReason {
123 MaxOutputTokens,
124 ContentFilter,
125}
126
127#[non_exhaustive]
129#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
130#[serde(tag = "type", rename_all = "snake_case")]
131pub enum OutputItem {
132 Message {
134 id: String,
136 #[serde(default)]
138 status: Option<OutputItemStatus>,
139 #[serde(default)]
141 role: Option<MessageRole>,
142 #[serde(default)]
144 content: Vec<ContentPart>,
145 #[serde(default, skip_serializing_if = "Vec::is_empty")]
147 annotations: Vec<serde_json::Value>,
148 },
149 Reasoning {
151 id: String,
153 #[serde(default)]
155 status: Option<OutputItemStatus>,
156 #[serde(default)]
158 content: Vec<ContentPart>,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
161 summary: Vec<serde_json::Value>,
162 },
163 FunctionCall {
165 id: String,
167 #[serde(default)]
169 call_id: Option<String>,
170 name: String,
172 arguments: String,
174 },
175 WebSearchCall {
177 id: String,
179 #[serde(default)]
181 action: Option<WebSearchAction>,
182 },
183 #[serde(other)]
185 Unknown,
186}
187
188#[non_exhaustive]
190#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
191#[serde(tag = "type", rename_all = "snake_case")]
192pub enum ContentPart {
193 OutputText {
195 text: String,
197 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 annotations: Vec<serde_json::Value>,
200 },
201 ReasoningText {
203 text: String,
205 },
206 #[serde(other)]
208 Unknown,
209}
210
211#[non_exhaustive]
213#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
214#[serde(rename_all = "snake_case")]
215pub enum OutputItemStatus {
216 InProgress,
217 Completed,
218 Incomplete,
219}
220
221#[non_exhaustive]
223#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
224#[serde(rename_all = "snake_case")]
225pub enum MessageRole {
226 Assistant,
227 #[serde(other)]
228 Unknown,
229}
230
231#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
233pub struct WebSearchAction {
234 #[serde(rename = "type")]
236 pub typ: WebSearchActionType,
237}
238
239#[non_exhaustive]
241#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
242#[serde(rename_all = "snake_case")]
243pub enum WebSearchActionType {
244 Search,
245 OpenPage,
246 FindInPage,
247 #[serde(other)]
248 Unknown,
249}
250
251#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
253pub struct Usage {
254 pub input_tokens: u64,
256
257 #[serde(default)]
259 pub input_tokens_details: Option<InputTokensDetails>,
260
261 pub output_tokens: u64,
263
264 #[serde(default)]
266 pub output_tokens_details: Option<OutputTokensDetails>,
267
268 pub total_tokens: u64,
270}
271
272#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
274pub struct InputTokensDetails {
275 #[serde(default)]
277 pub cached_tokens: Option<u64>,
278}
279
280#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
282pub struct OutputTokensDetails {
283 #[serde(default)]
285 pub reasoning_tokens: Option<u64>,
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use serde_json::json;
292
293 #[test]
294 fn deserialize_response_example() {
295 let value = json!({
296 "id": "24778070-1c36-4ae0-a4bd-870afc7fc13e",
297 "object": "response",
298 "created_at": 1753000000,
299 "status": "completed",
300 "model": "deepseek-v4-flash",
301 "output": [
302 {
303 "type": "reasoning",
304 "id": "rs_1",
305 "status": "completed",
306 "content": [
307 {"type": "reasoning_text", "text": "The user greets me. I should reply politely."}
308 ],
309 "summary": []
310 },
311 {
312 "type": "message",
313 "id": "msg_1",
314 "status": "completed",
315 "role": "assistant",
316 "content": [
317 {"type": "output_text", "text": "Hello! How can I help you today?", "annotations": []}
318 ]
319 }
320 ],
321 "usage": {
322 "input_tokens": 22,
323 "input_tokens_details": {"cached_tokens": 0},
324 "output_tokens": 29,
325 "output_tokens_details": {"reasoning_tokens": 27},
326 "total_tokens": 51
327 },
328 "store": false,
329 "parallel_tool_calls": true,
330 "previous_response_id": null,
331 "error": null,
332 "incomplete_details": null
333 });
334
335 let response: Response = serde_json::from_value(value).unwrap();
336 assert_eq!(response.id, "24778070-1c36-4ae0-a4bd-870afc7fc13e");
337 assert_eq!(response.status, ResponseStatus::Completed);
338 assert_eq!(response.output.len(), 2);
339 assert_eq!(response.output_text(), "Hello! How can I help you today?");
340 assert_eq!(
341 response.reasoning_text(),
342 "The user greets me. I should reply politely."
343 );
344 assert_eq!(response.usage.as_ref().unwrap().total_tokens, 51);
345 assert_eq!(
346 response
347 .usage
348 .as_ref()
349 .unwrap()
350 .output_tokens_details
351 .as_ref()
352 .unwrap()
353 .reasoning_tokens,
354 Some(27)
355 );
356 }
357
358 #[test]
359 fn deserialize_function_call_and_unknown_items() {
360 let value = json!({
361 "id": "r_1",
362 "object": "response",
363 "created_at": 1753000000,
364 "status": "completed",
365 "model": "deepseek-v4-flash",
366 "output": [
367 {
368 "type": "function_call",
369 "id": "fc_1",
370 "call_id": "call_1",
371 "name": "get_weather",
372 "arguments": "{\"location\":\"Hangzhou\"}"
373 },
374 {"type": "mystery_item", "id": "x_1"}
375 ],
376 "usage": {
377 "input_tokens": 10,
378 "output_tokens": 20,
379 "total_tokens": 30
380 }
381 });
382
383 let response: Response = serde_json::from_value(value).unwrap();
384 assert_eq!(response.output.len(), 2);
385 assert!(
386 matches!(&response.output[0], OutputItem::FunctionCall { name, .. } if name == "get_weather")
387 );
388 assert!(matches!(&response.output[1], OutputItem::Unknown));
389 }
390
391 #[test]
392 fn serde_roundtrip() {
393 let response = Response {
394 id: "r_1".to_string(),
395 object: "response".to_string(),
396 created_at: 1753000000,
397 status: ResponseStatus::Completed,
398 error: None,
399 incomplete_details: None,
400 model: "deepseek-v4-flash".to_string(),
401 output: vec![OutputItem::Message {
402 id: "msg_1".to_string(),
403 status: Some(OutputItemStatus::Completed),
404 role: Some(MessageRole::Assistant),
405 content: vec![ContentPart::OutputText {
406 text: "Hi".to_string(),
407 annotations: vec![],
408 }],
409 annotations: vec![],
410 }],
411 usage: Some(Usage {
412 input_tokens: 1,
413 input_tokens_details: None,
414 output_tokens: 1,
415 output_tokens_details: None,
416 total_tokens: 2,
417 }),
418 store: Some(false),
419 parallel_tool_calls: Some(true),
420 previous_response_id: None,
421 };
422
423 let value = serde_json::to_value(&response).unwrap();
424 let back: Response = serde_json::from_value(value).unwrap();
425 assert_eq!(back, response);
426 }
427}