1pub mod cancel;
52pub mod create;
53pub mod delete;
54pub mod input_items;
55pub mod retrieve;
56
57use std::collections::HashMap;
58
59use serde::Deserialize;
60
61use crate::chat::ServiceTier;
62use crate::responses::create::request::Truncation;
63
64#[derive(Debug, Deserialize, Clone)]
67pub struct Response {
68 pub id: String,
70
71 pub created_at: u64,
73
74 #[serde(default)]
76 pub error: Option<ResponseError>,
77
78 #[serde(default)]
80 pub incomplete_details: Option<IncompleteDetails>,
81
82 #[serde(default)]
85 pub instructions: Option<String>,
86
87 #[serde(default)]
90 pub metadata: Option<HashMap<String, String>>,
91
92 pub model: String,
94
95 pub object: ResponseObject,
97
98 #[serde(default)]
101 pub output: Vec<ResponseOutputItem>,
102
103 #[serde(default)]
105 pub parallel_tool_calls: Option<bool>,
106
107 #[serde(default)]
110 pub previous_response_id: Option<String>,
111
112 #[serde(default)]
114 pub temperature: Option<f32>,
115
116 #[serde(default)]
118 pub top_p: Option<f32>,
119
120 #[serde(default)]
122 pub store: Option<bool>,
123
124 #[serde(default)]
126 pub status: Option<ResponseStatus>,
127
128 #[serde(default)]
130 pub usage: Option<ResponseUsage>,
131
132 #[serde(default)]
135 pub background: Option<bool>,
136
137 #[serde(default)]
140 pub completed_at: Option<u64>,
141
142 #[serde(default)]
145 pub max_output_tokens: Option<u32>,
146
147 #[serde(default)]
150 pub max_tool_calls: Option<u32>,
151
152 #[serde(default)]
155 pub top_logprobs: Option<u32>,
156
157 #[serde(default)]
160 pub truncation: Option<Truncation>,
161
162 #[serde(default)]
165 pub service_tier: Option<ServiceTier>,
166
167 #[serde(default)]
169 pub user: Option<String>,
170
171 #[serde(default)]
174 pub safety_identifier: Option<String>,
175
176 #[serde(default)]
178 pub prompt_cache_key: Option<String>,
179}
180
181impl Response {
182 pub fn is_completed(&self) -> bool {
184 matches!(self.status, Some(ResponseStatus::Completed))
185 }
186
187 pub fn output_text(&self) -> String {
191 let mut text = String::new();
192 for item in &self.output {
193 if let ResponseOutputItem::Message(message) = item {
194 for part in &message.content {
195 if let OutputContent::OutputText { text: t, .. } = part {
196 text.push_str(t);
197 }
198 }
199 }
200 }
201 text
202 }
203
204 pub fn reasoning_text(&self) -> String {
209 let mut text = String::new();
210 for item in &self.output {
211 if let ResponseOutputItem::Reasoning(reasoning) = item {
212 for part in reasoning.content.as_deref().unwrap_or_default() {
213 let ReasoningContent::ReasoningText { text: t } = part;
214 text.push_str(t);
215 }
216 }
217 }
218 text
219 }
220}
221
222#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
224pub enum ResponseObject {
225 #[serde(rename = "response")]
226 Response,
227}
228
229#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
231#[serde(rename_all = "snake_case")]
232pub enum ResponseStatus {
233 InProgress,
234 Completed,
235 Incomplete,
236 Failed,
237 Cancelled,
238 Queued,
239}
240
241#[derive(Debug, Deserialize, Clone)]
243pub struct ResponseError {
244 #[serde(default)]
246 pub code: Option<String>,
247 #[serde(default)]
249 pub message: Option<String>,
250}
251
252#[derive(Debug, Deserialize, Clone)]
254pub struct IncompleteDetails {
255 #[serde(default)]
257 pub reason: Option<IncompleteReason>,
258}
259
260#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
262#[serde(rename_all = "snake_case")]
263pub enum IncompleteReason {
264 MaxOutputTokens,
265 MaxMessages,
266 ContentFilter,
267}
268
269#[derive(Debug, Deserialize, Clone)]
275#[serde(tag = "type", rename_all = "snake_case")]
276pub enum ResponseOutputItem {
277 Message(ResponseOutputMessage),
279 Reasoning(ResponseReasoningItem),
281 FunctionCall(ResponseFunctionCall),
283 WebSearchCall(ResponseWebSearchCall),
285 #[cfg(feature = "qwen")]
287 McpCall(McpCall),
288 #[cfg(feature = "qwen")]
290 FileSearchCall(FileSearchCall),
291 #[cfg(feature = "qwen")]
293 CodeInterpreterCall(CodeInterpreterCall),
294 #[cfg(feature = "qwen")]
296 WebExtractorCall(WebExtractorCall),
297 #[cfg(feature = "qwen")]
299 WebSearchImageCall(WebSearchImageCall),
300 #[cfg(feature = "qwen")]
302 ImageSearchCall(ImageSearchCall),
303 #[serde(other)]
305 Other,
306}
307
308#[derive(Debug, Deserialize, Clone)]
310pub struct ResponseOutputMessage {
311 #[serde(default)]
313 pub id: Option<String>,
314 #[serde(default)]
316 pub role: Option<String>,
317 #[serde(default)]
319 pub content: Vec<OutputContent>,
320 #[serde(default)]
322 pub status: Option<ItemStatus>,
323}
324
325#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
327#[serde(rename_all = "snake_case")]
328pub enum ItemStatus {
329 InProgress,
330 Completed,
331 Incomplete,
332}
333
334#[derive(Debug, Deserialize, Clone)]
336#[serde(tag = "type", rename_all = "snake_case")]
337pub enum OutputContent {
338 OutputText {
340 text: String,
342 #[serde(default)]
344 annotations: Vec<Annotation>,
345 },
346 Refusal {
348 refusal: String,
350 },
351}
352
353#[derive(Debug, Deserialize, Clone)]
355#[serde(tag = "type", rename_all = "snake_case")]
356pub enum Annotation {
357 UrlCitation {
359 url: String,
361 #[serde(default)]
363 title: Option<String>,
364 #[serde(default)]
366 url_citation_index: Option<u32>,
367 #[serde(default)]
369 start_index: Option<u32>,
370 #[serde(default)]
372 end_index: Option<u32>,
373 },
374}
375
376#[derive(Debug, Deserialize, Clone)]
378pub struct ResponseFunctionCall {
379 #[serde(default)]
381 pub id: Option<String>,
382 #[serde(default)]
385 pub call_id: Option<String>,
386 #[serde(default)]
388 pub name: Option<String>,
389 #[serde(default)]
391 pub arguments: Option<String>,
392 #[serde(default)]
394 pub status: Option<ItemStatus>,
395}
396
397#[derive(Debug, Deserialize, Clone)]
399pub struct ResponseReasoningItem {
400 #[serde(default)]
402 pub id: Option<String>,
403 #[serde(default)]
407 pub content: Option<Vec<ReasoningContent>>,
408 #[serde(default)]
410 pub summary: Vec<ReasoningSummary>,
411 #[serde(default)]
413 pub status: Option<ItemStatus>,
414}
415
416#[derive(Debug, Deserialize, Clone)]
422#[serde(tag = "type", rename_all = "snake_case")]
423pub enum ReasoningContent {
424 ReasoningText {
426 text: String,
428 },
429}
430
431#[derive(Debug, Deserialize, Clone)]
433pub struct ReasoningSummary {
434 #[serde(rename = "type")]
436 pub summary_type: Option<String>,
437 #[serde(default)]
439 pub text: Option<String>,
440}
441
442#[derive(Debug, Deserialize, Clone)]
444pub struct ResponseWebSearchCall {
445 #[serde(default)]
447 pub id: Option<String>,
448 #[serde(default)]
450 pub status: Option<ItemStatus>,
451 #[serde(default)]
453 pub action: Option<WebSearchAction>,
454}
455
456#[derive(Debug, Deserialize, Clone)]
458pub struct WebSearchAction {
459 #[serde(rename = "type")]
461 pub action_type: Option<String>,
462 #[serde(default)]
464 pub query: Option<String>,
465 #[serde(default)]
467 pub sources: Vec<WebSearchSource>,
468}
469
470#[derive(Debug, Deserialize, Clone)]
472pub struct WebSearchSource {
473 #[serde(rename = "type")]
475 pub source_type: Option<String>,
476 #[serde(default)]
478 pub url: Option<String>,
479}
480
481#[cfg(feature = "qwen")]
483#[derive(Debug, Deserialize, Clone)]
484pub struct McpCall {
485 #[serde(default)]
487 pub id: Option<String>,
488 #[serde(default)]
490 pub name: Option<String>,
491 #[serde(default)]
493 pub server_label: Option<String>,
494 #[serde(default)]
496 pub arguments: Option<String>,
497 #[serde(default)]
499 pub output: Option<String>,
500 #[serde(default)]
502 pub status: Option<ItemStatus>,
503}
504
505#[cfg(feature = "qwen")]
507#[derive(Debug, Deserialize, Clone)]
508pub struct FileSearchCall {
509 #[serde(default)]
511 pub id: Option<String>,
512 #[serde(default)]
514 pub queries: Vec<String>,
515 #[serde(default)]
517 pub results: Vec<serde_json::Value>,
518 #[serde(default)]
520 pub status: Option<ItemStatus>,
521}
522
523#[cfg(feature = "qwen")]
525#[derive(Debug, Deserialize, Clone)]
526pub struct CodeInterpreterCall {
527 #[serde(default)]
529 pub id: Option<String>,
530 #[serde(default)]
532 pub code: Option<String>,
533 #[serde(default)]
535 pub outputs: Vec<serde_json::Value>,
536 #[serde(default)]
538 pub container_id: Option<String>,
539 #[serde(default)]
541 pub status: Option<ItemStatus>,
542}
543
544#[cfg(feature = "qwen")]
546#[derive(Debug, Deserialize, Clone)]
547pub struct WebExtractorCall {
548 #[serde(default)]
550 pub id: Option<String>,
551 #[serde(default)]
553 pub goal: Option<String>,
554 #[serde(default)]
556 pub output: Option<String>,
557 #[serde(default)]
559 pub urls: Vec<String>,
560 #[serde(default)]
562 pub status: Option<ItemStatus>,
563}
564
565#[cfg(feature = "qwen")]
567#[derive(Debug, Deserialize, Clone)]
568pub struct WebSearchImageCall {
569 #[serde(default)]
571 pub id: Option<String>,
572 #[serde(default)]
574 pub name: Option<String>,
575 #[serde(default)]
577 pub arguments: Option<String>,
578 #[serde(default)]
580 pub output: Option<String>,
581 #[serde(default)]
583 pub status: Option<ItemStatus>,
584}
585
586#[cfg(feature = "qwen")]
588#[derive(Debug, Deserialize, Clone)]
589pub struct ImageSearchCall {
590 #[serde(default)]
592 pub id: Option<String>,
593 #[serde(default)]
595 pub name: Option<String>,
596 #[serde(default)]
599 pub arguments: Option<String>,
600 #[serde(default)]
602 pub output: Option<String>,
603 #[serde(default)]
605 pub status: Option<ItemStatus>,
606}
607
608#[derive(Debug, Deserialize, Clone)]
610pub struct ResponseUsage {
611 pub input_tokens: usize,
613 #[serde(default)]
615 pub input_tokens_details: Option<InputTokensDetails>,
616 pub output_tokens: usize,
618 #[serde(default)]
620 pub output_tokens_details: Option<OutputTokensDetails>,
621 pub total_tokens: usize,
623 #[cfg(feature = "qwen")]
626 #[serde(default)]
627 pub x_details: Option<Vec<serde_json::Value>>,
628 #[cfg(feature = "qwen")]
631 #[serde(default)]
632 pub x_tools: Option<serde_json::Value>,
633}
634
635#[derive(Debug, Deserialize, Clone)]
637pub struct InputTokensDetails {
638 #[serde(default)]
640 pub cached_tokens: Option<usize>,
641 #[serde(default)]
643 pub cache_write_tokens: Option<usize>,
644}
645
646#[derive(Debug, Deserialize, Clone)]
648pub struct OutputTokensDetails {
649 #[serde(default)]
651 pub reasoning_tokens: Option<usize>,
652}
653
654crate::impl_from_str!(Response);
655
656#[cfg(test)]
657mod tests {
658 use std::str::FromStr;
662
663 use super::*;
664
665 const DEEPSEEK_RESPONSE_JSON: &str = r#"
668 {
669 "id": "24778070-1c36-4ae0-a4bd-870afc7fc13e",
670 "object": "response",
671 "created_at": 1753000000,
672 "status": "completed",
673 "model": "deepseek-v4-flash",
674 "output": [
675 {
676 "type": "reasoning",
677 "id": "rs_1",
678 "status": "completed",
679 "content": [
680 {
681 "type": "reasoning_text",
682 "text": "The user greets me. I should reply politely."
683 }
684 ],
685 "summary": []
686 },
687 {
688 "type": "message",
689 "id": "msg_1",
690 "status": "completed",
691 "role": "assistant",
692 "content": [
693 {
694 "type": "output_text",
695 "text": "Hello! How can I help you today?",
696 "annotations": []
697 }
698 ]
699 }
700 ],
701 "usage": {
702 "input_tokens": 22,
703 "input_tokens_details": { "cached_tokens": 0 },
704 "output_tokens": 29,
705 "output_tokens_details": { "reasoning_tokens": 27 },
706 "total_tokens": 51
707 },
708 "store": false,
709 "parallel_tool_calls": true,
710 "previous_response_id": null,
711 "error": null,
712 "incomplete_details": null
713 }"#;
714
715 #[test]
716 fn parses_documented_deepseek_response() {
717 let response = Response::from_str(DEEPSEEK_RESPONSE_JSON).unwrap();
718 assert_eq!(response.object, ResponseObject::Response);
719 assert_eq!(response.model, "deepseek-v4-flash");
720 assert!(response.is_completed());
721 assert_eq!(response.store, Some(false));
722 assert_eq!(response.parallel_tool_calls, Some(true));
723 assert_eq!(response.output.len(), 2);
724 assert_eq!(response.output_text(), "Hello! How can I help you today?");
725 assert_eq!(
726 response.reasoning_text(),
727 "The user greets me. I should reply politely."
728 );
729 let usage = response.usage.unwrap();
730 assert_eq!(usage.total_tokens, 51);
731 assert_eq!(usage.input_tokens_details.unwrap().cached_tokens, Some(0));
732 assert_eq!(
733 usage.output_tokens_details.unwrap().reasoning_tokens,
734 Some(27)
735 );
736 }
737
738 #[test]
739 fn unknown_output_item_type_is_preserved() {
740 let json = r#"{
741 "id": "resp_1",
742 "object": "response",
743 "created_at": 0,
744 "status": "completed",
745 "model": "m",
746 "output": [{"type": "something_future"}]
747 }"#;
748 let response = Response::from_str(json).unwrap();
749 assert!(matches!(
750 response.output.first(),
751 Some(ResponseOutputItem::Other)
752 ));
753 }
754}
755
756#[cfg(test)]
757mod integration {
758 use futures_util::StreamExt;
766
767 use crate::responses::create::request::{Input, RequestBody};
768 use crate::responses::create::response::ResponseStreamEvent;
769 use crate::rest::{
770 RequestOptions, default_client,
771 delete::DeleteNoStream,
772 get::GetNoStream,
773 post::{PostNoStream, PostStream},
774 };
775
776 use super::{Response, ResponseObject};
777
778 const DEEPSEEK_URL: &str = "https://api.deepseek.com";
779 const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
780
781 const QWEN_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
782 const QWEN_MODEL: &str = "qwen3-max";
783
784 fn deepseek_api_key() -> Option<String> {
785 std::env::var("DEEPSEEK_API_KEY")
786 .ok()
787 .map(|key| key.trim().to_string())
788 .filter(|key| !key.is_empty())
789 }
790
791 fn qwen_api_key() -> Option<String> {
792 std::env::var("QWEN_API_KEY")
793 .ok()
794 .map(|key| key.trim().to_string())
795 .filter(|key| !key.is_empty())
796 }
797
798 async fn run_stream(
799 request: &RequestBody,
800 client: &reqwest::Client,
801 base_url: &str,
802 options: &RequestOptions,
803 ) -> anyhow::Result<Response> {
804 let mut stream = request
805 .get_stream_response(client, base_url, options)
806 .await?;
807
808 let mut final_response: Option<Response> = None;
809 while let Some(event) = stream.next().await {
810 let event: ResponseStreamEvent = event?;
811 println!(
812 "responses stream event: {:?}",
813 std::mem::discriminant(&event)
814 );
815 if let Some(response) = event.final_response() {
816 final_response = Some(response.clone());
817 }
818 }
819
820 let response =
821 final_response.expect("the stream should end with a terminal response event");
822 Ok(response)
823 }
824
825 #[tokio::test]
828 async fn test_deepseek_response_no_stream() -> Result<(), anyhow::Error> {
829 let Some(api_key) = deepseek_api_key() else {
830 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
831 return Ok(());
832 };
833
834 let request = RequestBody {
835 model: DEEPSEEK_MODEL.to_string(),
836 input: Input::Text("用一句话介绍你自己。".to_string()),
837 instructions: Some("You are a helpful assistant.".to_string()),
838 ..Default::default()
839 };
840
841 let response = request
842 .get_response(
843 &default_client(),
844 DEEPSEEK_URL,
845 &RequestOptions::bearer(&api_key),
846 )
847 .await?;
848
849 println!("deepseek responses no-stream: {response:#?}");
850 assert_eq!(response.object, ResponseObject::Response);
851 assert!(response.is_completed());
852 assert_eq!(response.store, Some(false));
854 assert!(!response.output_text().is_empty());
855 Ok(())
856 }
857
858 #[tokio::test]
861 async fn test_deepseek_response_stream() -> Result<(), anyhow::Error> {
862 let Some(api_key) = deepseek_api_key() else {
863 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
864 return Ok(());
865 };
866
867 let request = RequestBody {
868 model: DEEPSEEK_MODEL.to_string(),
869 input: Input::Text("用一句话介绍你自己。".to_string()),
870 stream: Some(true),
871 ..Default::default()
872 };
873
874 let response = run_stream(
875 &request,
876 &default_client(),
877 DEEPSEEK_URL,
878 &RequestOptions::bearer(&api_key),
879 )
880 .await?;
881
882 println!("deepseek responses stream final: {response:#?}");
883 assert!(response.is_completed());
884 assert!(!response.output_text().is_empty());
885 Ok(())
886 }
887
888 #[tokio::test]
891 async fn test_qwen_response_no_stream() -> Result<(), anyhow::Error> {
892 let Some(api_key) = qwen_api_key() else {
893 println!("Skipping: set QWEN_API_KEY to run this test");
894 return Ok(());
895 };
896
897 let request = RequestBody {
898 model: QWEN_MODEL.to_string(),
899 input: Input::Text("用一句话介绍你自己。".to_string()),
900 instructions: Some("You are a helpful assistant.".to_string()),
901 store: Some(false),
904 ..Default::default()
905 };
906
907 let response = request
908 .get_response(
909 &default_client(),
910 QWEN_URL,
911 &RequestOptions::bearer(&api_key),
912 )
913 .await?;
914
915 println!("qwen responses no-stream: {response:#?}");
916 assert_eq!(response.object, ResponseObject::Response);
917 assert!(response.is_completed());
918 assert!(!response.output_text().is_empty());
919 Ok(())
920 }
921
922 #[tokio::test]
924 async fn test_qwen_response_stream() -> Result<(), anyhow::Error> {
925 let Some(api_key) = qwen_api_key() else {
926 println!("Skipping: set QWEN_API_KEY to run this test");
927 return Ok(());
928 };
929
930 let request = RequestBody {
931 model: QWEN_MODEL.to_string(),
932 input: Input::Text("用一句话介绍你自己。".to_string()),
933 stream: Some(true),
934 store: Some(false),
935 ..Default::default()
936 };
937
938 let response = run_stream(
939 &request,
940 &default_client(),
941 QWEN_URL,
942 &RequestOptions::bearer(&api_key),
943 )
944 .await?;
945
946 println!("qwen responses stream final: {response:#?}");
947 assert!(response.is_completed());
948 assert!(!response.output_text().is_empty());
949 Ok(())
950 }
951
952 #[tokio::test]
954 async fn test_qwen_response_previous_response_id() -> Result<(), anyhow::Error> {
955 let Some(api_key) = qwen_api_key() else {
956 println!("Skipping: set QWEN_API_KEY to run this test");
957 return Ok(());
958 };
959
960 let client = default_client();
961
962 let first = RequestBody {
965 model: QWEN_MODEL.to_string(),
966 input: Input::Text("法国的首都是哪里?".to_string()),
967 ..Default::default()
968 }
969 .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
970 .await?;
971 assert!(first.is_completed());
972
973 let second = RequestBody {
974 model: QWEN_MODEL.to_string(),
975 input: Input::Text("它的人口大约是多少?".to_string()),
976 previous_response_id: Some(first.id.clone()),
977 store: Some(false),
978 ..Default::default()
979 }
980 .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
981 .await?;
982
983 println!("qwen responses second turn: {second:#?}");
984 assert!(second.is_completed());
985 assert_eq!(
986 second.previous_response_id.as_deref(),
987 Some(first.id.as_str())
988 );
989 assert!(!second.output_text().is_empty());
990 Ok(())
991 }
992
993 #[tokio::test]
995 async fn test_qwen_response_store_retrieve_delete() -> Result<(), anyhow::Error> {
996 let Some(api_key) = qwen_api_key() else {
997 println!("Skipping: set QWEN_API_KEY to run this test");
998 return Ok(());
999 };
1000
1001 let client = default_client();
1002
1003 let created = RequestBody {
1004 model: QWEN_MODEL.to_string(),
1005 input: Input::Text("说一个法语单词。".to_string()),
1006 store: Some(true),
1007 ..Default::default()
1008 }
1009 .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
1010 .await?;
1011 assert!(created.is_completed());
1012
1013 let retrieved = crate::responses::retrieve::RetrieveRequest {
1015 response_id: &created.id,
1016 }
1017 .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
1018 .await?;
1019 assert_eq!(retrieved.id, created.id);
1020 assert_eq!(retrieved.output_text(), created.output_text());
1021
1022 let deleted = crate::responses::delete::DeleteRequest {
1024 response_id: &created.id,
1025 }
1026 .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
1027 .await?;
1028 assert_eq!(deleted.id, created.id);
1029 assert!(deleted.deleted);
1030
1031 Ok(())
1032 }
1033}