Skip to main content

deepseek_sdk/responses/
stream.rs

1//! Responses API streaming and request implementation for `/responses`.
2use crate::DeepSeekRequest;
3use crate::error::DeepSeekError;
4use crate::{api_post, api_request_stream, consume_sse, spawn_blocking_stream};
5
6use super::request::*;
7use super::response::*;
8use reqwest::Method;
9use serde::{Deserialize, Serialize};
10use tokio::sync::mpsc;
11
12/// Stream item produced by Responses API streaming.
13pub type ResponsesStreamItem = Result<ResponsesStreamEvent, DeepSeekError>;
14
15/// Blocking iterator over streamed Responses API events.
16pub struct ResponsesStreamBlocking {
17    pub rx: std::sync::mpsc::Receiver<ResponsesStreamItem>,
18}
19
20impl Iterator for ResponsesStreamBlocking {
21    type Item = ResponsesStreamItem;
22
23    fn next(&mut self) -> Option<Self::Item> {
24        self.rx.recv().ok()
25    }
26}
27
28/// A semantic server-sent event emitted by the Responses API stream.
29///
30/// Each event carries a `sequence_number`. The final event is `response.completed` /
31/// `response.incomplete` / `response.failed`; there is no `data: [DONE]` message.
32#[non_exhaustive]
33#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
34#[serde(tag = "type")]
35pub enum ResponsesStreamEvent {
36    /// The first event; the response has been created with status `in_progress`.
37    #[serde(rename = "response.created")]
38    ResponseCreated {
39        sequence_number: u64,
40        response: Response,
41    },
42    /// The response is being generated.
43    #[serde(rename = "response.in_progress")]
44    ResponseInProgress {
45        sequence_number: u64,
46        response: Response,
47    },
48    /// An output item starts.
49    #[serde(rename = "response.output_item.added")]
50    ResponseOutputItemAdded {
51        sequence_number: u64,
52        output_index: u64,
53        item: OutputItem,
54    },
55    /// An output item completes.
56    #[serde(rename = "response.output_item.done")]
57    ResponseOutputItemDone {
58        sequence_number: u64,
59        output_index: u64,
60        item: OutputItem,
61    },
62    /// A content part within an output item starts.
63    #[serde(rename = "response.content_part.added")]
64    ResponseContentPartAdded {
65        sequence_number: u64,
66        item_id: String,
67        output_index: u64,
68        content_index: u64,
69        part: ContentPart,
70    },
71    /// A content part within an output item completes.
72    #[serde(rename = "response.content_part.done")]
73    ResponseContentPartDone {
74        sequence_number: u64,
75        item_id: String,
76        output_index: u64,
77        content_index: u64,
78        part: ContentPart,
79    },
80    /// Incremental chain-of-thought text.
81    #[serde(rename = "response.reasoning_text.delta")]
82    ResponseReasoningTextDelta {
83        sequence_number: u64,
84        item_id: String,
85        output_index: u64,
86        content_index: u64,
87        delta: String,
88    },
89    /// The full chain-of-thought text.
90    #[serde(rename = "response.reasoning_text.done")]
91    ResponseReasoningTextDone {
92        sequence_number: u64,
93        item_id: String,
94        output_index: u64,
95        content_index: u64,
96        text: String,
97    },
98    /// Incremental output text.
99    #[serde(rename = "response.output_text.delta")]
100    ResponseOutputTextDelta {
101        sequence_number: u64,
102        item_id: String,
103        output_index: u64,
104        content_index: u64,
105        delta: String,
106    },
107    /// The full output text.
108    #[serde(rename = "response.output_text.done")]
109    ResponseOutputTextDone {
110        sequence_number: u64,
111        item_id: String,
112        output_index: u64,
113        content_index: u64,
114        text: String,
115    },
116    /// Incremental function call arguments.
117    #[serde(rename = "response.function_call_arguments.delta")]
118    ResponseFunctionCallArgumentsDelta {
119        sequence_number: u64,
120        item_id: String,
121        output_index: u64,
122        delta: String,
123    },
124    /// The full function call arguments.
125    #[serde(rename = "response.function_call_arguments.done")]
126    ResponseFunctionCallArgumentsDone {
127        sequence_number: u64,
128        item_id: String,
129        output_index: u64,
130        arguments: String,
131    },
132    /// Incremental custom tool call (`apply_patch`) input.
133    #[serde(rename = "response.custom_tool_call_input.delta")]
134    ResponseCustomToolCallInputDelta {
135        sequence_number: u64,
136        item_id: String,
137        output_index: u64,
138        delta: String,
139    },
140    /// The full custom tool call (`apply_patch`) input.
141    #[serde(rename = "response.custom_tool_call_input.done")]
142    ResponseCustomToolCallInputDone {
143        sequence_number: u64,
144        item_id: String,
145        output_index: u64,
146        input: String,
147    },
148    /// A server-side web search tool call is starting.
149    #[serde(rename = "response.web_search_call.in_progress")]
150    ResponseWebSearchCallInProgress {
151        sequence_number: u64,
152        item_id: String,
153        output_index: u64,
154    },
155    /// A server-side web search tool call is searching.
156    #[serde(rename = "response.web_search_call.searching")]
157    ResponseWebSearchCallSearching {
158        sequence_number: u64,
159        item_id: String,
160        output_index: u64,
161    },
162    /// A server-side web search tool call completed.
163    #[serde(rename = "response.web_search_call.completed")]
164    ResponseWebSearchCallCompleted {
165        sequence_number: u64,
166        item_id: String,
167        output_index: u64,
168    },
169    /// The final event when the response completes normally, carrying the full response object.
170    #[serde(rename = "response.completed")]
171    ResponseCompleted {
172        sequence_number: u64,
173        response: Response,
174    },
175    /// The final event when the response is truncated, carrying the full response object.
176    #[serde(rename = "response.incomplete")]
177    ResponseIncomplete {
178        sequence_number: u64,
179        response: Response,
180    },
181    /// The final event when the response fails, carrying the full response object with error details.
182    #[serde(rename = "response.failed")]
183    ResponseFailed {
184        sequence_number: u64,
185        response: Response,
186    },
187    /// An unrecognized event type, tolerated during deserialization.
188    #[serde(other)]
189    Unknown,
190}
191
192impl ResponsesStreamEvent {
193    /// Whether this event is one of the terminal events (`response.completed` /
194    /// `response.incomplete` / `response.failed`).
195    #[allow(clippy::match_like_matches_macro)] // non_exhaustive enum requires a catch-all arm
196    pub fn is_terminal(&self) -> bool {
197        match self {
198            ResponsesStreamEvent::ResponseCompleted { .. }
199            | ResponsesStreamEvent::ResponseIncomplete { .. }
200            | ResponsesStreamEvent::ResponseFailed { .. } => true,
201            _ => false,
202        }
203    }
204
205    /// The full `response` object carried by created / in-progress / terminal events.
206    pub fn response(&self) -> Option<&Response> {
207        match self {
208            ResponsesStreamEvent::ResponseCreated { response, .. }
209            | ResponsesStreamEvent::ResponseInProgress { response, .. }
210            | ResponsesStreamEvent::ResponseCompleted { response, .. }
211            | ResponsesStreamEvent::ResponseIncomplete { response, .. }
212            | ResponsesStreamEvent::ResponseFailed { response, .. } => Some(response),
213            _ => None,
214        }
215    }
216
217    /// The incremental delta text carried by `*_text.delta` / `*_arguments.delta` events.
218    pub fn delta(&self) -> Option<&str> {
219        match self {
220            ResponsesStreamEvent::ResponseOutputTextDelta { delta, .. }
221            | ResponsesStreamEvent::ResponseReasoningTextDelta { delta, .. }
222            | ResponsesStreamEvent::ResponseFunctionCallArgumentsDelta { delta, .. }
223            | ResponsesStreamEvent::ResponseCustomToolCallInputDelta { delta, .. } => Some(delta),
224            _ => None,
225        }
226    }
227}
228
229impl DeepSeekRequest for ResponsesRequest {
230    type Response = Response;
231    type StreamItem = ResponsesStreamItem;
232    type BlockingStream = ResponsesStreamBlocking;
233
234    async fn send(self) -> Result<Response, DeepSeekError> {
235        let client = self.client.clone();
236        api_post("/responses", &self, client).await
237    }
238
239    async fn stream(self) -> Result<mpsc::Receiver<ResponsesStreamItem>, DeepSeekError> {
240        let mut request = self;
241        request.stream = Some(true);
242
243        let client = request.client.clone();
244        let event_source = api_request_stream(
245            Method::POST,
246            "/responses",
247            |builder| builder.json(&request),
248            client,
249        )
250        .await?;
251
252        Ok(consume_sse(event_source, |data| {
253            serde_json::from_str::<ResponsesStreamEvent>(&data)
254                .map(Some)
255                .map_err(|err| DeepSeekError::decode(err.to_string(), data))
256        }))
257    }
258
259    fn stream_blocking(self) -> Result<ResponsesStreamBlocking, DeepSeekError> {
260        let rx = spawn_blocking_stream(self.stream())?;
261        Ok(ResponsesStreamBlocking { rx })
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use crate::{DEFAULT_BASE_URL, DeepSeekClient};
269
270    #[test]
271    fn deserialize_stream_event_dotted_names() {
272        let created = serde_json::from_str::<ResponsesStreamEvent>(
273            r#"{"type":"response.created","sequence_number":0,"response":{"id":"r_1","object":"response","created_at":1753000000,"status":"in_progress","model":"deepseek-v4-flash","output":[],"usage":null}}"#,
274        )
275        .unwrap();
276        assert!(matches!(
277            created,
278            ResponsesStreamEvent::ResponseCreated { response, .. }
279                if response.usage.is_none() && response.status == ResponseStatus::InProgress
280        ));
281
282        let delta = serde_json::from_str::<ResponsesStreamEvent>(
283            r#"{"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_1","output_index":1,"content_index":0,"delta":"Hello"}"#,
284        )
285        .unwrap();
286        assert!(matches!(
287            delta,
288            ResponsesStreamEvent::ResponseOutputTextDelta { delta, .. } if delta == "Hello"
289        ));
290
291        let done = serde_json::from_str::<ResponsesStreamEvent>(
292            r#"{"type":"response.output_item.done","sequence_number":20,"output_index":1,"item":{"type":"message","id":"msg_1","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hello"}]}}"#,
293        )
294        .unwrap();
295        assert!(matches!(
296            done,
297            ResponsesStreamEvent::ResponseOutputItemDone { .. }
298        ));
299
300        let unknown = serde_json::from_str::<ResponsesStreamEvent>(
301            r#"{"type":"something.new","sequence_number":99}"#,
302        )
303        .unwrap();
304        assert!(matches!(unknown, ResponsesStreamEvent::Unknown));
305    }
306
307    #[test]
308    fn stream_event_serializes_dotted_names() {
309        let evt = ResponsesStreamEvent::ResponseCompleted {
310            sequence_number: 20,
311            response: Response {
312                id: "r_1".to_string(),
313                object: "response".to_string(),
314                created_at: 1753000000,
315                status: ResponseStatus::Completed,
316                error: None,
317                incomplete_details: None,
318                model: "deepseek-v4-flash".to_string(),
319                output: vec![],
320                usage: None,
321                store: None,
322                parallel_tool_calls: None,
323                previous_response_id: None,
324            },
325        };
326        let value = serde_json::to_value(evt).unwrap();
327        assert_eq!(
328            value.get("type"),
329            Some(&serde_json::json!("response.completed"))
330        );
331    }
332
333    fn get_client() -> DeepSeekClient {
334        DeepSeekClient::new(
335            std::env::var("DEEPSEEK_API_KEY").expect("DEEPSEEK_API_KEY is not set"),
336            DEFAULT_BASE_URL.clone(),
337        )
338    }
339
340    fn get_builder() -> ResponsesRequestBuilder {
341        ResponsesRequestBuilder::default()
342            .client(get_client())
343            .model("deepseek-v4-flash")
344            .instructions("You are a helpful assistant.")
345            .reasoning(Reasoning::new(ReasoningEffort::None))
346    }
347
348    #[tokio::test]
349    async fn responses_basic() {
350        let req = get_builder()
351            .input("Reply with exactly: OK")
352            .max_output_tokens(64_u32)
353            .build()
354            .unwrap();
355        let response = req.send().await.unwrap();
356        println!("{:#?}", response);
357        assert_eq!(response.object, "response");
358        assert_eq!(response.status, ResponseStatus::Completed);
359        assert!(!response.output_text().is_empty());
360        assert!(response.usage.is_some());
361    }
362
363    #[tokio::test]
364    async fn responses_thinking_mode() {
365        let req = get_builder()
366            .reasoning(Reasoning::new(ReasoningEffort::Low))
367            .input("What is 2+2? Reply briefly.")
368            .max_output_tokens(256_u32)
369            .build()
370            .unwrap();
371        let response = req.send().await.unwrap();
372        println!("{:#?}", response);
373        let has_reasoning = response
374            .output
375            .iter()
376            .any(|item| matches!(item, OutputItem::Reasoning { .. }));
377        assert!(
378            has_reasoning,
379            "expected a reasoning output item in thinking mode"
380        );
381        assert!(!response.output_text().is_empty());
382    }
383
384    #[tokio::test]
385    async fn responses_json_object() {
386        let req = get_builder()
387            .text(Text::new(TextFormat::json_object()))
388            .input("Return a JSON object with a `city` field set to Hangzhou.")
389            .max_output_tokens(128_u32)
390            .build()
391            .unwrap();
392        let response = req.send().await.unwrap();
393        println!("{:#?}", response);
394        let parsed: serde_json::Value = serde_json::from_str(&response.output_text())
395            .expect("output text should be valid JSON");
396        assert_eq!(
397            parsed.get("city").and_then(|c| c.as_str()),
398            Some("Hangzhou")
399        );
400    }
401
402    #[tokio::test]
403    async fn responses_json_schema() {
404        let req = get_builder()
405            .text(Text::new(TextFormat::json_schema(
406                "city_response",
407                serde_json::json!({
408                    "type": "object",
409                    "properties": {
410                        "city": {"type": "string"},
411                        "temperature": {"type": "number"}
412                    },
413                    "required": ["city", "temperature"]
414                }),
415            )))
416            .input("What is the weather in Hangzhou? Say 24 degrees.")
417            .max_output_tokens(128_u32)
418            .build()
419            .unwrap();
420        let response = req.send().await.unwrap();
421        println!("{:#?}", response);
422        let parsed: serde_json::Value = serde_json::from_str(&response.output_text())
423            .expect("output text should be valid JSON");
424        assert_eq!(
425            parsed.get("city").and_then(|c| c.as_str()),
426            Some("Hangzhou")
427        );
428        assert!(parsed.get("temperature").and_then(|t| t.as_f64()).is_some());
429    }
430
431    #[tokio::test]
432    async fn responses_input_item_list() {
433        let req = get_builder()
434            .input(vec![
435                InputItem::user("Remember my name is Alice."),
436                InputItem::assistant("Got it, Alice!"),
437                InputItem::user("What is my name?"),
438            ])
439            .max_output_tokens(64_u32)
440            .build()
441            .unwrap();
442        let response = req.send().await.unwrap();
443        println!("{:#?}", response);
444        assert!(response.output_text().contains("Alice"));
445    }
446
447    #[tokio::test]
448    async fn responses_tool_call() {
449        let tool = Tool::function(
450            "get_weather",
451            "Get the weather of a location.",
452            Some(serde_json::json!({
453                "type": "object",
454                "properties": {
455                    "location": {"type": "string"}
456                },
457                "required": ["location"]
458            })),
459        );
460        let req = get_builder()
461            .input("What is the weather in Hangzhou?")
462            .tool(tool)
463            .tool_choice(ToolChoice::named("get_weather"))
464            .max_output_tokens(128_u32)
465            .build()
466            .unwrap();
467        let response = req.send().await.unwrap();
468        println!("{:#?}", response);
469        let function_call = response
470            .output
471            .iter()
472            .find(|item| matches!(item, OutputItem::FunctionCall { .. }))
473            .expect("expected a function_call output item");
474        let OutputItem::FunctionCall {
475            name, arguments, ..
476        } = function_call
477        else {
478            unreachable!()
479        };
480        assert_eq!(name, "get_weather");
481        let parsed: serde_json::Value =
482            serde_json::from_str(arguments).expect("arguments should be valid JSON");
483        assert_eq!(
484            parsed.get("location").and_then(|l| l.as_str()),
485            Some("Hangzhou")
486        );
487    }
488
489    #[tokio::test]
490    async fn responses_multi_turn_tool_call() {
491        let tool = Tool::function(
492            "get_weather",
493            "Get the weather of a location.",
494            Some(serde_json::json!({
495                "type": "object",
496                "properties": {
497                    "location": {"type": "string"}
498                },
499                "required": ["location"]
500            })),
501        );
502
503        let first = get_builder()
504            .input("What is the weather in Hangzhou?")
505            .tool(tool.clone())
506            .tool_choice(ToolChoice::named("get_weather"))
507            .max_output_tokens(128_u32)
508            .build()
509            .unwrap();
510        let first_resp = first.send().await.unwrap();
511        let function_call = first_resp
512            .output
513            .iter()
514            .find(|item| matches!(item, OutputItem::FunctionCall { .. }))
515            .expect("expected a function_call output item");
516        let OutputItem::FunctionCall {
517            call_id,
518            name,
519            arguments,
520            ..
521        } = function_call
522        else {
523            unreachable!()
524        };
525        let call_id = call_id.clone().expect("function_call carries a call_id");
526        let name = name.clone();
527        let arguments = arguments.clone();
528
529        let second = get_builder()
530            .input(vec![
531                InputItem::user("What is the weather in Hangzhou?"),
532                InputItem::function_call(call_id.clone(), name, arguments),
533                InputItem::function_call_output(call_id, "24°C, clear sky"),
534            ])
535            .tool(tool)
536            .tool_choice(ToolChoice::auto())
537            .max_output_tokens(128_u32)
538            .build()
539            .unwrap();
540        let second_resp = second.send().await.unwrap();
541        println!("{:#?}", second_resp);
542        assert!(second_resp.output_text().contains("24°C"));
543    }
544
545    #[tokio::test]
546    async fn responses_web_search() {
547        let req = get_builder()
548            .tool(Tool::web_search())
549            .tool_choice(ToolChoice::web_search())
550            .input("Search the web and report the weather in Hangzhou.")
551            .max_output_tokens(256_u32)
552            .build()
553            .unwrap();
554        let response = req.send().await.unwrap();
555        println!("{:#?}", response);
556        assert_eq!(response.status, ResponseStatus::Completed);
557        // Forcing web_search executes the search on the server and returns the
558        // `web_search_call` items; a final assistant message is not guaranteed.
559        assert!(
560            response
561                .output
562                .iter()
563                .any(|item| matches!(item, OutputItem::WebSearchCall { .. })),
564            "expected web_search_call output items"
565        );
566    }
567
568    #[tokio::test]
569    async fn responses_stream_async() {
570        let req = get_builder()
571            .input("Count from 1 to 5.")
572            .max_output_tokens(128_u32)
573            .build()
574            .unwrap();
575
576        let mut rx = req.stream().await.unwrap();
577        let mut text = String::new();
578        let mut saw_terminal = false;
579        while let Some(item) = rx.recv().await {
580            match item {
581                Ok(evt) => {
582                    if let Some(delta) = evt.delta() {
583                        text.push_str(delta);
584                    }
585                    if evt.is_terminal() {
586                        saw_terminal = true;
587                        let response = evt.response().expect("terminal event carries the response");
588                        assert!(response.usage.is_some());
589                    }
590                }
591                Err(err) => eprintln!("Error>\t {err:?}"),
592            }
593        }
594        println!("Model>\t {text}");
595        assert!(!text.is_empty());
596        assert!(saw_terminal, "expected a terminal stream event");
597    }
598
599    #[test]
600    fn responses_stream_blocking() {
601        let req = get_builder()
602            .input("Count from 1 to 5.")
603            .max_output_tokens(128_u32)
604            .build()
605            .unwrap();
606
607        let stream = req.stream_blocking().unwrap();
608        let mut text = String::new();
609        for item in stream.take(500) {
610            match item {
611                Ok(evt) => {
612                    if let Some(delta) = evt.delta() {
613                        text.push_str(delta);
614                    }
615                }
616                Err(err) => eprintln!("Error>\t {err:?}"),
617            }
618        }
619        println!("Model>\t {text}");
620        assert!(!text.is_empty());
621    }
622}