1pub mod cancel;
47pub mod create;
48pub mod delete;
49pub mod input_items;
50pub mod retrieve;
51
52use std::collections::HashMap;
53
54use serde::Deserialize;
55
56use crate::chat::ServiceTier;
57use crate::responses::create::request::Truncation;
58
59#[derive(Debug, Deserialize, Clone)]
62pub struct Response {
63 pub id: String,
65
66 pub created_at: u64,
68
69 #[serde(default)]
71 pub error: Option<ResponseError>,
72
73 #[serde(default)]
75 pub incomplete_details: Option<IncompleteDetails>,
76
77 #[serde(default)]
80 pub instructions: Option<String>,
81
82 #[serde(default)]
85 pub metadata: Option<HashMap<String, String>>,
86
87 pub model: String,
89
90 pub object: ResponseObject,
92
93 #[serde(default)]
96 pub output: Vec<ResponseOutputItem>,
97
98 #[serde(default)]
100 pub parallel_tool_calls: Option<bool>,
101
102 #[serde(default)]
105 pub previous_response_id: Option<String>,
106
107 #[serde(default)]
109 pub temperature: Option<f32>,
110
111 #[serde(default)]
113 pub top_p: Option<f32>,
114
115 #[serde(default)]
117 pub store: Option<bool>,
118
119 #[serde(default)]
121 pub status: Option<ResponseStatus>,
122
123 #[serde(default)]
125 pub usage: Option<ResponseUsage>,
126
127 #[serde(default)]
130 pub background: Option<bool>,
131
132 #[serde(default)]
135 pub completed_at: Option<u64>,
136
137 #[serde(default)]
140 pub max_output_tokens: Option<u32>,
141
142 #[serde(default)]
145 pub max_tool_calls: Option<u32>,
146
147 #[serde(default)]
150 pub top_logprobs: Option<u32>,
151
152 #[serde(default)]
155 pub truncation: Option<Truncation>,
156
157 #[serde(default)]
160 pub service_tier: Option<ServiceTier>,
161
162 #[serde(default)]
164 pub user: Option<String>,
165
166 #[serde(default)]
169 pub safety_identifier: Option<String>,
170
171 #[serde(default)]
173 pub prompt_cache_key: Option<String>,
174}
175
176impl Response {
177 pub fn is_completed(&self) -> bool {
179 matches!(self.status, Some(ResponseStatus::Completed))
180 }
181
182 pub fn output_text(&self) -> String {
186 let mut text = String::new();
187 for item in &self.output {
188 if let ResponseOutputItem::Message(message) = item {
189 for part in &message.content {
190 if let OutputContent::OutputText { text: t, .. } = part {
191 text.push_str(t);
192 }
193 }
194 }
195 }
196 text
197 }
198
199 pub fn reasoning_text(&self) -> String {
204 let mut text = String::new();
205 for item in &self.output {
206 if let ResponseOutputItem::Reasoning(reasoning) = item {
207 for part in reasoning.content.as_deref().unwrap_or_default() {
208 let ReasoningContent::ReasoningText { text: t } = part;
209 text.push_str(t);
210 }
211 }
212 }
213 text
214 }
215}
216
217#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
219pub enum ResponseObject {
220 #[serde(rename = "response")]
221 Response,
222}
223
224#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
226#[serde(rename_all = "snake_case")]
227pub enum ResponseStatus {
228 InProgress,
229 Completed,
230 Incomplete,
231 Failed,
232 Cancelled,
233 Queued,
234}
235
236#[derive(Debug, Deserialize, Clone)]
238pub struct ResponseError {
239 #[serde(default)]
241 pub code: Option<String>,
242 #[serde(default)]
244 pub message: Option<String>,
245}
246
247#[derive(Debug, Deserialize, Clone)]
249pub struct IncompleteDetails {
250 #[serde(default)]
252 pub reason: Option<IncompleteReason>,
253}
254
255#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
257#[serde(rename_all = "snake_case")]
258pub enum IncompleteReason {
259 MaxOutputTokens,
260 MaxMessages,
261 ContentFilter,
262}
263
264#[derive(Debug, Deserialize, Clone)]
270#[serde(tag = "type", rename_all = "snake_case")]
271pub enum ResponseOutputItem {
272 Message(ResponseOutputMessage),
274 Reasoning(ResponseReasoningItem),
276 FunctionCall(ResponseFunctionCall),
278 WebSearchCall(ResponseWebSearchCall),
280 #[cfg(feature = "qwen")]
282 McpCall(McpCall),
283 #[cfg(feature = "qwen")]
285 FileSearchCall(FileSearchCall),
286 #[cfg(feature = "qwen")]
288 CodeInterpreterCall(CodeInterpreterCall),
289 #[cfg(feature = "qwen")]
291 WebExtractorCall(WebExtractorCall),
292 #[cfg(feature = "qwen")]
294 WebSearchImageCall(WebSearchImageCall),
295 #[cfg(feature = "qwen")]
297 ImageSearchCall(ImageSearchCall),
298 #[serde(other)]
300 Other,
301}
302
303#[derive(Debug, Deserialize, Clone)]
305pub struct ResponseOutputMessage {
306 #[serde(default)]
308 pub id: Option<String>,
309 #[serde(default)]
311 pub role: Option<String>,
312 #[serde(default)]
314 pub content: Vec<OutputContent>,
315 #[serde(default)]
317 pub status: Option<ItemStatus>,
318}
319
320#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
322#[serde(rename_all = "snake_case")]
323pub enum ItemStatus {
324 InProgress,
325 Completed,
326 Incomplete,
327}
328
329#[derive(Debug, Deserialize, Clone)]
331#[serde(tag = "type", rename_all = "snake_case")]
332pub enum OutputContent {
333 OutputText {
335 text: String,
337 #[serde(default)]
339 annotations: Vec<Annotation>,
340 },
341 Refusal {
343 refusal: String,
345 },
346}
347
348#[derive(Debug, Deserialize, Clone)]
350#[serde(tag = "type", rename_all = "snake_case")]
351pub enum Annotation {
352 UrlCitation {
354 url: String,
356 #[serde(default)]
358 title: Option<String>,
359 #[serde(default)]
361 url_citation_index: Option<u32>,
362 #[serde(default)]
364 start_index: Option<u32>,
365 #[serde(default)]
367 end_index: Option<u32>,
368 },
369}
370
371#[derive(Debug, Deserialize, Clone)]
373pub struct ResponseFunctionCall {
374 #[serde(default)]
376 pub id: Option<String>,
377 #[serde(default)]
380 pub call_id: Option<String>,
381 #[serde(default)]
383 pub name: Option<String>,
384 #[serde(default)]
386 pub arguments: Option<String>,
387 #[serde(default)]
389 pub status: Option<ItemStatus>,
390}
391
392#[derive(Debug, Deserialize, Clone)]
394pub struct ResponseReasoningItem {
395 #[serde(default)]
397 pub id: Option<String>,
398 #[serde(default)]
402 pub content: Option<Vec<ReasoningContent>>,
403 #[serde(default)]
405 pub summary: Vec<ReasoningSummary>,
406 #[serde(default)]
408 pub status: Option<ItemStatus>,
409}
410
411#[derive(Debug, Deserialize, Clone)]
417#[serde(tag = "type", rename_all = "snake_case")]
418pub enum ReasoningContent {
419 ReasoningText {
421 text: String,
423 },
424}
425
426#[derive(Debug, Deserialize, Clone)]
428pub struct ReasoningSummary {
429 #[serde(rename = "type")]
431 pub summary_type: Option<String>,
432 #[serde(default)]
434 pub text: Option<String>,
435}
436
437#[derive(Debug, Deserialize, Clone)]
439pub struct ResponseWebSearchCall {
440 #[serde(default)]
442 pub id: Option<String>,
443 #[serde(default)]
445 pub status: Option<ItemStatus>,
446 #[serde(default)]
448 pub action: Option<WebSearchAction>,
449}
450
451#[derive(Debug, Deserialize, Clone)]
453pub struct WebSearchAction {
454 #[serde(rename = "type")]
456 pub action_type: Option<String>,
457 #[serde(default)]
459 pub query: Option<String>,
460 #[serde(default)]
462 pub sources: Vec<WebSearchSource>,
463}
464
465#[derive(Debug, Deserialize, Clone)]
467pub struct WebSearchSource {
468 #[serde(rename = "type")]
470 pub source_type: Option<String>,
471 #[serde(default)]
473 pub url: Option<String>,
474}
475
476#[cfg(feature = "qwen")]
478#[derive(Debug, Deserialize, Clone)]
479pub struct McpCall {
480 #[serde(default)]
482 pub id: Option<String>,
483 #[serde(default)]
485 pub name: Option<String>,
486 #[serde(default)]
488 pub server_label: Option<String>,
489 #[serde(default)]
491 pub arguments: Option<String>,
492 #[serde(default)]
494 pub output: Option<String>,
495 #[serde(default)]
497 pub status: Option<ItemStatus>,
498}
499
500#[cfg(feature = "qwen")]
502#[derive(Debug, Deserialize, Clone)]
503pub struct FileSearchCall {
504 #[serde(default)]
506 pub id: Option<String>,
507 #[serde(default)]
509 pub queries: Vec<String>,
510 #[serde(default)]
512 pub results: Vec<serde_json::Value>,
513 #[serde(default)]
515 pub status: Option<ItemStatus>,
516}
517
518#[cfg(feature = "qwen")]
520#[derive(Debug, Deserialize, Clone)]
521pub struct CodeInterpreterCall {
522 #[serde(default)]
524 pub id: Option<String>,
525 #[serde(default)]
527 pub code: Option<String>,
528 #[serde(default)]
530 pub outputs: Vec<serde_json::Value>,
531 #[serde(default)]
533 pub container_id: Option<String>,
534 #[serde(default)]
536 pub status: Option<ItemStatus>,
537}
538
539#[cfg(feature = "qwen")]
541#[derive(Debug, Deserialize, Clone)]
542pub struct WebExtractorCall {
543 #[serde(default)]
545 pub id: Option<String>,
546 #[serde(default)]
548 pub goal: Option<String>,
549 #[serde(default)]
551 pub output: Option<String>,
552 #[serde(default)]
554 pub urls: Vec<String>,
555 #[serde(default)]
557 pub status: Option<ItemStatus>,
558}
559
560#[cfg(feature = "qwen")]
562#[derive(Debug, Deserialize, Clone)]
563pub struct WebSearchImageCall {
564 #[serde(default)]
566 pub id: Option<String>,
567 #[serde(default)]
569 pub name: Option<String>,
570 #[serde(default)]
572 pub arguments: Option<String>,
573 #[serde(default)]
575 pub output: Option<String>,
576 #[serde(default)]
578 pub status: Option<ItemStatus>,
579}
580
581#[cfg(feature = "qwen")]
583#[derive(Debug, Deserialize, Clone)]
584pub struct ImageSearchCall {
585 #[serde(default)]
587 pub id: Option<String>,
588 #[serde(default)]
590 pub name: Option<String>,
591 #[serde(default)]
594 pub arguments: Option<String>,
595 #[serde(default)]
597 pub output: Option<String>,
598 #[serde(default)]
600 pub status: Option<ItemStatus>,
601}
602
603#[derive(Debug, Deserialize, Clone)]
605pub struct ResponseUsage {
606 pub input_tokens: usize,
608 #[serde(default)]
610 pub input_tokens_details: Option<InputTokensDetails>,
611 pub output_tokens: usize,
613 #[serde(default)]
615 pub output_tokens_details: Option<OutputTokensDetails>,
616 pub total_tokens: usize,
618 #[cfg(feature = "qwen")]
621 #[serde(default)]
622 pub x_details: Option<Vec<serde_json::Value>>,
623 #[cfg(feature = "qwen")]
626 #[serde(default)]
627 pub x_tools: Option<serde_json::Value>,
628}
629
630#[derive(Debug, Deserialize, Clone)]
632pub struct InputTokensDetails {
633 #[serde(default)]
635 pub cached_tokens: Option<usize>,
636 #[serde(default)]
638 pub cache_write_tokens: Option<usize>,
639}
640
641#[derive(Debug, Deserialize, Clone)]
643pub struct OutputTokensDetails {
644 #[serde(default)]
646 pub reasoning_tokens: Option<usize>,
647}
648
649crate::impl_from_str!(Response);
650
651#[cfg(test)]
652mod tests {
653 use std::str::FromStr;
657
658 use super::*;
659
660 const DEEPSEEK_RESPONSE_JSON: &str = r#"
663 {
664 "id": "24778070-1c36-4ae0-a4bd-870afc7fc13e",
665 "object": "response",
666 "created_at": 1753000000,
667 "status": "completed",
668 "model": "deepseek-v4-flash",
669 "output": [
670 {
671 "type": "reasoning",
672 "id": "rs_1",
673 "status": "completed",
674 "content": [
675 {
676 "type": "reasoning_text",
677 "text": "The user greets me. I should reply politely."
678 }
679 ],
680 "summary": []
681 },
682 {
683 "type": "message",
684 "id": "msg_1",
685 "status": "completed",
686 "role": "assistant",
687 "content": [
688 {
689 "type": "output_text",
690 "text": "Hello! How can I help you today?",
691 "annotations": []
692 }
693 ]
694 }
695 ],
696 "usage": {
697 "input_tokens": 22,
698 "input_tokens_details": { "cached_tokens": 0 },
699 "output_tokens": 29,
700 "output_tokens_details": { "reasoning_tokens": 27 },
701 "total_tokens": 51
702 },
703 "store": false,
704 "parallel_tool_calls": true,
705 "previous_response_id": null,
706 "error": null,
707 "incomplete_details": null
708 }"#;
709
710 #[test]
711 fn parses_documented_deepseek_response() {
712 let response = Response::from_str(DEEPSEEK_RESPONSE_JSON).unwrap();
713 assert_eq!(response.object, ResponseObject::Response);
714 assert_eq!(response.model, "deepseek-v4-flash");
715 assert!(response.is_completed());
716 assert_eq!(response.store, Some(false));
717 assert_eq!(response.parallel_tool_calls, Some(true));
718 assert_eq!(response.output.len(), 2);
719 assert_eq!(response.output_text(), "Hello! How can I help you today?");
720 assert_eq!(
721 response.reasoning_text(),
722 "The user greets me. I should reply politely."
723 );
724 let usage = response.usage.unwrap();
725 assert_eq!(usage.total_tokens, 51);
726 assert_eq!(usage.input_tokens_details.unwrap().cached_tokens, Some(0));
727 assert_eq!(
728 usage.output_tokens_details.unwrap().reasoning_tokens,
729 Some(27)
730 );
731 }
732
733 #[test]
734 fn unknown_output_item_type_is_preserved() {
735 let json = r#"{
736 "id": "resp_1",
737 "object": "response",
738 "created_at": 0,
739 "status": "completed",
740 "model": "m",
741 "output": [{"type": "something_future"}]
742 }"#;
743 let response = Response::from_str(json).unwrap();
744 assert!(matches!(
745 response.output.first(),
746 Some(ResponseOutputItem::Other)
747 ));
748 }
749}
750
751#[cfg(test)]
752mod integration {
753 use futures_util::StreamExt;
761
762 use crate::responses::create::request::{Input, RequestBody};
763 use crate::responses::create::response::ResponseStreamEvent;
764 use crate::rest::{
765 default_client,
766 delete::DeleteNoStream,
767 get::GetNoStream,
768 post::{PostNoStream, PostStream},
769 };
770
771 use super::{Response, ResponseObject};
772
773 const DEEPSEEK_URL: &str = "https://api.deepseek.com";
774 const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
775
776 const QWEN_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
777 const QWEN_MODEL: &str = "qwen3-max";
778
779 fn deepseek_api_key() -> Option<String> {
780 std::env::var("DEEPSEEK_API_KEY")
781 .ok()
782 .map(|key| key.trim().to_string())
783 .filter(|key| !key.is_empty())
784 }
785
786 fn qwen_api_key() -> Option<String> {
787 std::env::var("QWEN_API_KEY")
788 .ok()
789 .map(|key| key.trim().to_string())
790 .filter(|key| !key.is_empty())
791 }
792
793 async fn run_stream(
794 request: &RequestBody,
795 client: &reqwest::Client,
796 base_url: &str,
797 api_key: &str,
798 ) -> anyhow::Result<Response> {
799 let mut stream = request
800 .get_stream_response(client, base_url, api_key)
801 .await?;
802
803 let mut final_response: Option<Response> = None;
804 while let Some(event) = stream.next().await {
805 let event: ResponseStreamEvent = event?;
806 println!(
807 "responses stream event: {:?}",
808 std::mem::discriminant(&event)
809 );
810 if let Some(response) = event.final_response() {
811 final_response = Some(response.clone());
812 }
813 }
814
815 let response =
816 final_response.expect("the stream should end with a terminal response event");
817 Ok(response)
818 }
819
820 #[tokio::test]
823 async fn test_deepseek_response_no_stream() -> Result<(), anyhow::Error> {
824 let Some(api_key) = deepseek_api_key() else {
825 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
826 return Ok(());
827 };
828
829 let request = RequestBody {
830 model: DEEPSEEK_MODEL.to_string(),
831 input: Input::Text("用一句话介绍你自己。".to_string()),
832 instructions: Some("You are a helpful assistant.".to_string()),
833 ..Default::default()
834 };
835
836 let response = request
837 .get_response(&default_client(), DEEPSEEK_URL, &api_key)
838 .await?;
839
840 println!("deepseek responses no-stream: {response:#?}");
841 assert_eq!(response.object, ResponseObject::Response);
842 assert!(response.is_completed());
843 assert_eq!(response.store, Some(false));
845 assert!(!response.output_text().is_empty());
846 Ok(())
847 }
848
849 #[tokio::test]
852 async fn test_deepseek_response_stream() -> Result<(), anyhow::Error> {
853 let Some(api_key) = deepseek_api_key() else {
854 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
855 return Ok(());
856 };
857
858 let request = RequestBody {
859 model: DEEPSEEK_MODEL.to_string(),
860 input: Input::Text("用一句话介绍你自己。".to_string()),
861 stream: Some(true),
862 ..Default::default()
863 };
864
865 let response = run_stream(&request, &default_client(), DEEPSEEK_URL, &api_key).await?;
866
867 println!("deepseek responses stream final: {response:#?}");
868 assert!(response.is_completed());
869 assert!(!response.output_text().is_empty());
870 Ok(())
871 }
872
873 #[tokio::test]
876 async fn test_qwen_response_no_stream() -> Result<(), anyhow::Error> {
877 let Some(api_key) = qwen_api_key() else {
878 println!("Skipping: set QWEN_API_KEY to run this test");
879 return Ok(());
880 };
881
882 let request = RequestBody {
883 model: QWEN_MODEL.to_string(),
884 input: Input::Text("用一句话介绍你自己。".to_string()),
885 instructions: Some("You are a helpful assistant.".to_string()),
886 store: Some(false),
889 ..Default::default()
890 };
891
892 let response = request
893 .get_response(&default_client(), QWEN_URL, &api_key)
894 .await?;
895
896 println!("qwen responses no-stream: {response:#?}");
897 assert_eq!(response.object, ResponseObject::Response);
898 assert!(response.is_completed());
899 assert!(!response.output_text().is_empty());
900 Ok(())
901 }
902
903 #[tokio::test]
905 async fn test_qwen_response_stream() -> Result<(), anyhow::Error> {
906 let Some(api_key) = qwen_api_key() else {
907 println!("Skipping: set QWEN_API_KEY to run this test");
908 return Ok(());
909 };
910
911 let request = RequestBody {
912 model: QWEN_MODEL.to_string(),
913 input: Input::Text("用一句话介绍你自己。".to_string()),
914 stream: Some(true),
915 store: Some(false),
916 ..Default::default()
917 };
918
919 let response = run_stream(&request, &default_client(), QWEN_URL, &api_key).await?;
920
921 println!("qwen responses stream final: {response:#?}");
922 assert!(response.is_completed());
923 assert!(!response.output_text().is_empty());
924 Ok(())
925 }
926
927 #[tokio::test]
929 async fn test_qwen_response_previous_response_id() -> Result<(), anyhow::Error> {
930 let Some(api_key) = qwen_api_key() else {
931 println!("Skipping: set QWEN_API_KEY to run this test");
932 return Ok(());
933 };
934
935 let client = default_client();
936
937 let first = RequestBody {
940 model: QWEN_MODEL.to_string(),
941 input: Input::Text("法国的首都是哪里?".to_string()),
942 ..Default::default()
943 }
944 .get_response(&client, QWEN_URL, &api_key)
945 .await?;
946 assert!(first.is_completed());
947
948 let second = RequestBody {
949 model: QWEN_MODEL.to_string(),
950 input: Input::Text("它的人口大约是多少?".to_string()),
951 previous_response_id: Some(first.id.clone()),
952 store: Some(false),
953 ..Default::default()
954 }
955 .get_response(&client, QWEN_URL, &api_key)
956 .await?;
957
958 println!("qwen responses second turn: {second:#?}");
959 assert!(second.is_completed());
960 assert_eq!(
961 second.previous_response_id.as_deref(),
962 Some(first.id.as_str())
963 );
964 assert!(!second.output_text().is_empty());
965 Ok(())
966 }
967
968 #[tokio::test]
970 async fn test_qwen_response_store_retrieve_delete() -> Result<(), anyhow::Error> {
971 let Some(api_key) = qwen_api_key() else {
972 println!("Skipping: set QWEN_API_KEY to run this test");
973 return Ok(());
974 };
975
976 let client = default_client();
977
978 let created = RequestBody {
979 model: QWEN_MODEL.to_string(),
980 input: Input::Text("说一个法语单词。".to_string()),
981 store: Some(true),
982 ..Default::default()
983 }
984 .get_response(&client, QWEN_URL, &api_key)
985 .await?;
986 assert!(created.is_completed());
987
988 let retrieved = crate::responses::retrieve::RetrieveRequest {
990 response_id: &created.id,
991 }
992 .get_response(&client, QWEN_URL, &api_key)
993 .await?;
994 assert_eq!(retrieved.id, created.id);
995 assert_eq!(retrieved.output_text(), created.output_text());
996
997 let deleted = crate::responses::delete::DeleteRequest {
999 response_id: &created.id,
1000 }
1001 .get_response(&client, QWEN_URL, &api_key)
1002 .await?;
1003 assert_eq!(deleted.id, created.id);
1004 assert!(deleted.deleted);
1005
1006 Ok(())
1007 }
1008}