Skip to main content

deepseek_sdk/responses/
response.rs

1use super::*;
2
3/// A response object in the OpenAI Responses API format.
4#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
5pub struct Response {
6    /// A unique identifier for the response.
7    pub id: String,
8
9    /// The object type, which is always `response`.
10    pub object: String,
11
12    /// The Unix timestamp (in seconds) of when the response was created.
13    pub created_at: u64,
14
15    /// The status of the response.
16    pub status: ResponseStatus,
17
18    /// The error object when the response failed, with `code` and `message` fields.
19    #[serde(default)]
20    pub error: Option<ResponseError>,
21
22    /// The details about why the response is incomplete.
23    #[serde(default)]
24    pub incomplete_details: Option<IncompleteDetails>,
25
26    /// The model used for the response.
27    pub model: String,
28
29    /// The list of output items generated by the model. In thinking mode, the chain-of-thought
30    /// is returned as a `reasoning` item before the `message` item. Function calls are returned
31    /// as `function_call` items, and server-side web search actions as `web_search_call` items.
32    #[serde(default)]
33    pub output: Vec<OutputItem>,
34
35    /// Token usage statistics for the response.
36    /// Present on final events and non-streaming responses; `null` on intermediate stream events.
37    #[serde(default)]
38    pub usage: Option<Usage>,
39
40    /// Whether the response is stored on the server. DeepSeek is stateless and always returns `false`.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub store: Option<bool>,
43
44    /// Whether the model may call multiple tools in parallel. DeepSeek always returns `true`.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub parallel_tool_calls: Option<bool>,
47
48    /// The ID of the previous response. DeepSeek is stateless and always returns `null`.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub previous_response_id: Option<String>,
51}
52
53impl Response {
54    /// Concatenate the plain-text output of all `output_text` content parts of `message` items.
55    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    /// Concatenate the chain-of-thought text of all `reasoning_text` content parts of `reasoning` items.
73    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/// The status of a response.
92#[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/// Error details of a failed response.
103#[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/// The details about why a response is incomplete.
112#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
113pub struct IncompleteDetails {
114    /// The reason the response is incomplete.
115    pub reason: IncompleteReason,
116}
117
118/// Reason why a response is incomplete.
119#[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/// An output item generated by the model.
128#[non_exhaustive]
129#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
130#[serde(tag = "type", rename_all = "snake_case")]
131pub enum OutputItem {
132    /// A message item. The role is always `assistant`.
133    Message {
134        /// The unique ID of the output item.
135        id: String,
136        /// The status of the output item.
137        #[serde(default)]
138        status: Option<OutputItemStatus>,
139        /// The role of the author of this message. Always `assistant`.
140        #[serde(default)]
141        role: Option<MessageRole>,
142        /// A list of `output_text` content parts.
143        #[serde(default)]
144        content: Vec<ContentPart>,
145        /// Annotations attached to the content parts.
146        #[serde(default, skip_serializing_if = "Vec::is_empty")]
147        annotations: Vec<serde_json::Value>,
148    },
149    /// A reasoning (chain-of-thought) item.
150    Reasoning {
151        /// The unique ID of the output item.
152        id: String,
153        /// The status of the output item.
154        #[serde(default)]
155        status: Option<OutputItemStatus>,
156        /// A list of `reasoning_text` content parts carrying the chain-of-thought in plain text.
157        #[serde(default)]
158        content: Vec<ContentPart>,
159        /// An optional summary of the reasoning, currently not generated by DeepSeek.
160        #[serde(default, skip_serializing_if = "Vec::is_empty")]
161        summary: Vec<serde_json::Value>,
162    },
163    /// A function call item.
164    FunctionCall {
165        /// The unique ID of the output item.
166        id: String,
167        /// An identifier used when passing the function output back to the API.
168        #[serde(default)]
169        call_id: Option<String>,
170        /// The name of the function to call.
171        name: String,
172        /// The arguments to call the function with, as generated by the model in JSON format.
173        arguments: String,
174    },
175    /// A server-side web search call item.
176    WebSearchCall {
177        /// The unique ID of the output item.
178        id: String,
179        /// An object describing the search action executed on the server side.
180        #[serde(default)]
181        action: Option<WebSearchAction>,
182    },
183    /// An unrecognized output item type, tolerated during deserialization.
184    #[serde(other)]
185    Unknown,
186}
187
188/// A content part of an output item.
189#[non_exhaustive]
190#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
191#[serde(tag = "type", rename_all = "snake_case")]
192pub enum ContentPart {
193    /// Visible output text of a `message` item.
194    OutputText {
195        /// The text content.
196        text: String,
197        /// Annotations attached to the text.
198        #[serde(default, skip_serializing_if = "Vec::is_empty")]
199        annotations: Vec<serde_json::Value>,
200    },
201    /// Chain-of-thought text of a `reasoning` item.
202    ReasoningText {
203        /// The text content.
204        text: String,
205    },
206    /// An unrecognized content part type, tolerated during deserialization.
207    #[serde(other)]
208    Unknown,
209}
210
211/// The status of an output item.
212#[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/// Role of a message output item.
222#[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/// A server-side web search action.
232#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
233pub struct WebSearchAction {
234    /// The type of the search action.
235    #[serde(rename = "type")]
236    pub typ: WebSearchActionType,
237}
238
239/// The type of a server-side web search action.
240#[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/// Token usage statistics for a response.
252#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
253pub struct Usage {
254    /// Number of input tokens.
255    pub input_tokens: u64,
256
257    /// Breakdown of the input tokens.
258    #[serde(default)]
259    pub input_tokens_details: Option<InputTokensDetails>,
260
261    /// Number of output tokens.
262    pub output_tokens: u64,
263
264    /// Breakdown of the output tokens.
265    #[serde(default)]
266    pub output_tokens_details: Option<OutputTokensDetails>,
267
268    /// Total number of tokens used in the request (input + output).
269    pub total_tokens: u64,
270}
271
272/// Breakdown of the input tokens.
273#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
274pub struct InputTokensDetails {
275    /// Number of input tokens that hit the context cache.
276    #[serde(default)]
277    pub cached_tokens: Option<u64>,
278}
279
280/// Breakdown of the output tokens.
281#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
282pub struct OutputTokensDetails {
283    /// Number of reasoning (chain-of-thought) tokens generated by the model.
284    #[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}