1use 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
12pub type ResponsesStreamItem = Result<ResponsesStreamEvent, DeepSeekError>;
14
15pub 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#[non_exhaustive]
33#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
34#[serde(tag = "type")]
35pub enum ResponsesStreamEvent {
36 #[serde(rename = "response.created")]
38 ResponseCreated {
39 sequence_number: u64,
40 response: Response,
41 },
42 #[serde(rename = "response.in_progress")]
44 ResponseInProgress {
45 sequence_number: u64,
46 response: Response,
47 },
48 #[serde(rename = "response.output_item.added")]
50 ResponseOutputItemAdded {
51 sequence_number: u64,
52 output_index: u64,
53 item: OutputItem,
54 },
55 #[serde(rename = "response.output_item.done")]
57 ResponseOutputItemDone {
58 sequence_number: u64,
59 output_index: u64,
60 item: OutputItem,
61 },
62 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[serde(rename = "response.web_search_call.in_progress")]
150 ResponseWebSearchCallInProgress {
151 sequence_number: u64,
152 item_id: String,
153 output_index: u64,
154 },
155 #[serde(rename = "response.web_search_call.searching")]
157 ResponseWebSearchCallSearching {
158 sequence_number: u64,
159 item_id: String,
160 output_index: u64,
161 },
162 #[serde(rename = "response.web_search_call.completed")]
164 ResponseWebSearchCallCompleted {
165 sequence_number: u64,
166 item_id: String,
167 output_index: u64,
168 },
169 #[serde(rename = "response.completed")]
171 ResponseCompleted {
172 sequence_number: u64,
173 response: Response,
174 },
175 #[serde(rename = "response.incomplete")]
177 ResponseIncomplete {
178 sequence_number: u64,
179 response: Response,
180 },
181 #[serde(rename = "response.failed")]
183 ResponseFailed {
184 sequence_number: u64,
185 response: Response,
186 },
187 #[serde(other)]
189 Unknown,
190}
191
192impl ResponsesStreamEvent {
193 #[allow(clippy::match_like_matches_macro)] 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 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 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 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}