1pub mod create;
47pub mod delete;
48pub mod retrieve;
49
50use std::collections::HashMap;
51
52use serde::Deserialize;
53
54use crate::chat::ServiceTier;
55use crate::responses::create::request::Truncation;
56
57#[derive(Debug, Deserialize, Clone)]
60pub struct Response {
61 pub id: String,
63
64 pub created_at: u64,
66
67 #[serde(default)]
69 pub error: Option<ResponseError>,
70
71 #[serde(default)]
73 pub incomplete_details: Option<IncompleteDetails>,
74
75 #[serde(default)]
78 pub instructions: Option<String>,
79
80 #[serde(default)]
83 pub metadata: Option<HashMap<String, String>>,
84
85 pub model: String,
87
88 pub object: ResponseObject,
90
91 #[serde(default)]
94 pub output: Vec<ResponseOutputItem>,
95
96 #[serde(default)]
98 pub parallel_tool_calls: Option<bool>,
99
100 #[serde(default)]
103 pub previous_response_id: Option<String>,
104
105 #[serde(default)]
107 pub temperature: Option<f32>,
108
109 #[serde(default)]
111 pub top_p: Option<f32>,
112
113 #[serde(default)]
115 pub store: Option<bool>,
116
117 #[serde(default)]
119 pub status: Option<ResponseStatus>,
120
121 #[serde(default)]
123 pub usage: Option<ResponseUsage>,
124
125 #[serde(default)]
128 pub background: Option<bool>,
129
130 #[serde(default)]
133 pub completed_at: Option<u64>,
134
135 #[serde(default)]
138 pub max_output_tokens: Option<u32>,
139
140 #[serde(default)]
143 pub max_tool_calls: Option<u32>,
144
145 #[serde(default)]
148 pub top_logprobs: Option<u32>,
149
150 #[serde(default)]
153 pub truncation: Option<Truncation>,
154
155 #[serde(default)]
158 pub service_tier: Option<ServiceTier>,
159
160 #[serde(default)]
162 pub user: Option<String>,
163
164 #[serde(default)]
167 pub safety_identifier: Option<String>,
168
169 #[serde(default)]
171 pub prompt_cache_key: Option<String>,
172}
173
174impl Response {
175 pub fn is_completed(&self) -> bool {
177 matches!(self.status, Some(ResponseStatus::Completed))
178 }
179
180 pub fn output_text(&self) -> String {
184 let mut text = String::new();
185 for item in &self.output {
186 if let ResponseOutputItem::Message(message) = item {
187 for part in &message.content {
188 if let OutputContent::OutputText { text: t, .. } = part {
189 text.push_str(t);
190 }
191 }
192 }
193 }
194 text
195 }
196
197 pub fn reasoning_text(&self) -> String {
202 let mut text = String::new();
203 for item in &self.output {
204 if let ResponseOutputItem::Reasoning(reasoning) = item {
205 for part in reasoning.content.as_deref().unwrap_or_default() {
206 let ReasoningContent::ReasoningText { text: t } = part;
207 text.push_str(t);
208 }
209 }
210 }
211 text
212 }
213}
214
215#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
217pub enum ResponseObject {
218 #[serde(rename = "response")]
219 Response,
220}
221
222#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
224#[serde(rename_all = "snake_case")]
225pub enum ResponseStatus {
226 InProgress,
227 Completed,
228 Incomplete,
229 Failed,
230 Cancelled,
231 Queued,
232}
233
234#[derive(Debug, Deserialize, Clone)]
236pub struct ResponseError {
237 #[serde(default)]
239 pub code: Option<String>,
240 #[serde(default)]
242 pub message: Option<String>,
243}
244
245#[derive(Debug, Deserialize, Clone)]
247pub struct IncompleteDetails {
248 #[serde(default)]
250 pub reason: Option<IncompleteReason>,
251}
252
253#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
255#[serde(rename_all = "snake_case")]
256pub enum IncompleteReason {
257 MaxOutputTokens,
258 MaxMessages,
259 ContentFilter,
260}
261
262#[derive(Debug, Deserialize, Clone)]
268#[serde(tag = "type", rename_all = "snake_case")]
269pub enum ResponseOutputItem {
270 Message(ResponseOutputMessage),
272 Reasoning(ResponseReasoningItem),
274 FunctionCall(ResponseFunctionCall),
276 WebSearchCall(ResponseWebSearchCall),
278 #[cfg(feature = "qwen")]
280 McpCall(McpCall),
281 #[cfg(feature = "qwen")]
283 FileSearchCall(FileSearchCall),
284 #[cfg(feature = "qwen")]
286 CodeInterpreterCall(CodeInterpreterCall),
287 #[cfg(feature = "qwen")]
289 WebExtractorCall(WebExtractorCall),
290 #[cfg(feature = "qwen")]
292 WebSearchImageCall(WebSearchImageCall),
293 #[cfg(feature = "qwen")]
295 ImageSearchCall(ImageSearchCall),
296 #[serde(other)]
298 Other,
299}
300
301#[derive(Debug, Deserialize, Clone)]
303pub struct ResponseOutputMessage {
304 #[serde(default)]
306 pub id: Option<String>,
307 #[serde(default)]
309 pub role: Option<String>,
310 #[serde(default)]
312 pub content: Vec<OutputContent>,
313 #[serde(default)]
315 pub status: Option<ItemStatus>,
316}
317
318#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
320#[serde(rename_all = "snake_case")]
321pub enum ItemStatus {
322 InProgress,
323 Completed,
324 Incomplete,
325}
326
327#[derive(Debug, Deserialize, Clone)]
329#[serde(tag = "type", rename_all = "snake_case")]
330pub enum OutputContent {
331 OutputText {
333 text: String,
335 #[serde(default)]
337 annotations: Vec<Annotation>,
338 },
339 Refusal {
341 refusal: String,
343 },
344}
345
346#[derive(Debug, Deserialize, Clone)]
348#[serde(tag = "type", rename_all = "snake_case")]
349pub enum Annotation {
350 UrlCitation {
352 url: String,
354 #[serde(default)]
356 title: Option<String>,
357 #[serde(default)]
359 url_citation_index: Option<u32>,
360 #[serde(default)]
362 start_index: Option<u32>,
363 #[serde(default)]
365 end_index: Option<u32>,
366 },
367}
368
369#[derive(Debug, Deserialize, Clone)]
371pub struct ResponseFunctionCall {
372 #[serde(default)]
374 pub id: Option<String>,
375 #[serde(default)]
378 pub call_id: Option<String>,
379 #[serde(default)]
381 pub name: Option<String>,
382 #[serde(default)]
384 pub arguments: Option<String>,
385 #[serde(default)]
387 pub status: Option<ItemStatus>,
388}
389
390#[derive(Debug, Deserialize, Clone)]
392pub struct ResponseReasoningItem {
393 #[serde(default)]
395 pub id: Option<String>,
396 #[serde(default)]
400 pub content: Option<Vec<ReasoningContent>>,
401 #[serde(default)]
403 pub summary: Vec<ReasoningSummary>,
404 #[serde(default)]
406 pub status: Option<ItemStatus>,
407}
408
409#[derive(Debug, Deserialize, Clone)]
415#[serde(tag = "type", rename_all = "snake_case")]
416pub enum ReasoningContent {
417 ReasoningText {
419 text: String,
421 },
422}
423
424#[derive(Debug, Deserialize, Clone)]
426pub struct ReasoningSummary {
427 #[serde(rename = "type")]
429 pub summary_type: Option<String>,
430 #[serde(default)]
432 pub text: Option<String>,
433}
434
435#[derive(Debug, Deserialize, Clone)]
437pub struct ResponseWebSearchCall {
438 #[serde(default)]
440 pub id: Option<String>,
441 #[serde(default)]
443 pub status: Option<ItemStatus>,
444 #[serde(default)]
446 pub action: Option<WebSearchAction>,
447}
448
449#[derive(Debug, Deserialize, Clone)]
451pub struct WebSearchAction {
452 #[serde(rename = "type")]
454 pub action_type: Option<String>,
455 #[serde(default)]
457 pub query: Option<String>,
458 #[serde(default)]
460 pub sources: Vec<WebSearchSource>,
461}
462
463#[derive(Debug, Deserialize, Clone)]
465pub struct WebSearchSource {
466 #[serde(rename = "type")]
468 pub source_type: Option<String>,
469 #[serde(default)]
471 pub url: Option<String>,
472}
473
474#[cfg(feature = "qwen")]
476#[derive(Debug, Deserialize, Clone)]
477pub struct McpCall {
478 #[serde(default)]
480 pub id: Option<String>,
481 #[serde(default)]
483 pub name: Option<String>,
484 #[serde(default)]
486 pub server_label: Option<String>,
487 #[serde(default)]
489 pub arguments: Option<String>,
490 #[serde(default)]
492 pub output: Option<String>,
493 #[serde(default)]
495 pub status: Option<ItemStatus>,
496}
497
498#[cfg(feature = "qwen")]
500#[derive(Debug, Deserialize, Clone)]
501pub struct FileSearchCall {
502 #[serde(default)]
504 pub id: Option<String>,
505 #[serde(default)]
507 pub queries: Vec<String>,
508 #[serde(default)]
510 pub results: Vec<serde_json::Value>,
511 #[serde(default)]
513 pub status: Option<ItemStatus>,
514}
515
516#[cfg(feature = "qwen")]
518#[derive(Debug, Deserialize, Clone)]
519pub struct CodeInterpreterCall {
520 #[serde(default)]
522 pub id: Option<String>,
523 #[serde(default)]
525 pub code: Option<String>,
526 #[serde(default)]
528 pub outputs: Vec<serde_json::Value>,
529 #[serde(default)]
531 pub container_id: Option<String>,
532 #[serde(default)]
534 pub status: Option<ItemStatus>,
535}
536
537#[cfg(feature = "qwen")]
539#[derive(Debug, Deserialize, Clone)]
540pub struct WebExtractorCall {
541 #[serde(default)]
543 pub id: Option<String>,
544 #[serde(default)]
546 pub goal: Option<String>,
547 #[serde(default)]
549 pub output: Option<String>,
550 #[serde(default)]
552 pub urls: Vec<String>,
553 #[serde(default)]
555 pub status: Option<ItemStatus>,
556}
557
558#[cfg(feature = "qwen")]
560#[derive(Debug, Deserialize, Clone)]
561pub struct WebSearchImageCall {
562 #[serde(default)]
564 pub id: Option<String>,
565 #[serde(default)]
567 pub name: Option<String>,
568 #[serde(default)]
570 pub arguments: Option<String>,
571 #[serde(default)]
573 pub output: Option<String>,
574 #[serde(default)]
576 pub status: Option<ItemStatus>,
577}
578
579#[cfg(feature = "qwen")]
581#[derive(Debug, Deserialize, Clone)]
582pub struct ImageSearchCall {
583 #[serde(default)]
585 pub id: Option<String>,
586 #[serde(default)]
588 pub name: Option<String>,
589 #[serde(default)]
592 pub arguments: Option<String>,
593 #[serde(default)]
595 pub output: Option<String>,
596 #[serde(default)]
598 pub status: Option<ItemStatus>,
599}
600
601#[derive(Debug, Deserialize, Clone)]
603pub struct ResponseUsage {
604 pub input_tokens: usize,
606 #[serde(default)]
608 pub input_tokens_details: Option<InputTokensDetails>,
609 pub output_tokens: usize,
611 #[serde(default)]
613 pub output_tokens_details: Option<OutputTokensDetails>,
614 pub total_tokens: usize,
616 #[cfg(feature = "qwen")]
619 #[serde(default)]
620 pub x_details: Option<Vec<serde_json::Value>>,
621 #[cfg(feature = "qwen")]
624 #[serde(default)]
625 pub x_tools: Option<serde_json::Value>,
626}
627
628#[derive(Debug, Deserialize, Clone)]
630pub struct InputTokensDetails {
631 #[serde(default)]
633 pub cached_tokens: Option<usize>,
634 #[serde(default)]
636 pub cache_write_tokens: Option<usize>,
637}
638
639#[derive(Debug, Deserialize, Clone)]
641pub struct OutputTokensDetails {
642 #[serde(default)]
644 pub reasoning_tokens: Option<usize>,
645}
646
647crate::impl_from_str!(Response);
648
649#[cfg(test)]
650mod tests {
651 use std::str::FromStr;
655
656 use super::*;
657
658 const DEEPSEEK_RESPONSE_JSON: &str = r#"
661 {
662 "id": "24778070-1c36-4ae0-a4bd-870afc7fc13e",
663 "object": "response",
664 "created_at": 1753000000,
665 "status": "completed",
666 "model": "deepseek-v4-flash",
667 "output": [
668 {
669 "type": "reasoning",
670 "id": "rs_1",
671 "status": "completed",
672 "content": [
673 {
674 "type": "reasoning_text",
675 "text": "The user greets me. I should reply politely."
676 }
677 ],
678 "summary": []
679 },
680 {
681 "type": "message",
682 "id": "msg_1",
683 "status": "completed",
684 "role": "assistant",
685 "content": [
686 {
687 "type": "output_text",
688 "text": "Hello! How can I help you today?",
689 "annotations": []
690 }
691 ]
692 }
693 ],
694 "usage": {
695 "input_tokens": 22,
696 "input_tokens_details": { "cached_tokens": 0 },
697 "output_tokens": 29,
698 "output_tokens_details": { "reasoning_tokens": 27 },
699 "total_tokens": 51
700 },
701 "store": false,
702 "parallel_tool_calls": true,
703 "previous_response_id": null,
704 "error": null,
705 "incomplete_details": null
706 }"#;
707
708 #[test]
709 fn parses_documented_deepseek_response() {
710 let response = Response::from_str(DEEPSEEK_RESPONSE_JSON).unwrap();
711 assert_eq!(response.object, ResponseObject::Response);
712 assert_eq!(response.model, "deepseek-v4-flash");
713 assert!(response.is_completed());
714 assert_eq!(response.store, Some(false));
715 assert_eq!(response.parallel_tool_calls, Some(true));
716 assert_eq!(response.output.len(), 2);
717 assert_eq!(response.output_text(), "Hello! How can I help you today?");
718 assert_eq!(
719 response.reasoning_text(),
720 "The user greets me. I should reply politely."
721 );
722 let usage = response.usage.unwrap();
723 assert_eq!(usage.total_tokens, 51);
724 assert_eq!(usage.input_tokens_details.unwrap().cached_tokens, Some(0));
725 assert_eq!(
726 usage.output_tokens_details.unwrap().reasoning_tokens,
727 Some(27)
728 );
729 }
730
731 #[test]
732 fn unknown_output_item_type_is_preserved() {
733 let json = r#"{
734 "id": "resp_1",
735 "object": "response",
736 "created_at": 0,
737 "status": "completed",
738 "model": "m",
739 "output": [{"type": "something_future"}]
740 }"#;
741 let response = Response::from_str(json).unwrap();
742 assert!(matches!(
743 response.output.first(),
744 Some(ResponseOutputItem::Other)
745 ));
746 }
747}
748
749#[cfg(test)]
750mod integration {
751 use futures_util::StreamExt;
759
760 use crate::responses::create::request::{Input, RequestBody};
761 use crate::responses::create::response::ResponseStreamEvent;
762 use crate::rest::{
763 default_client,
764 delete::DeleteNoStream,
765 get::GetNoStream,
766 post::{PostNoStream, PostStream},
767 };
768
769 use super::{Response, ResponseObject};
770
771 const DEEPSEEK_URL: &str = "https://api.deepseek.com";
772 const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
773
774 const QWEN_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
775 const QWEN_MODEL: &str = "qwen3-max";
776
777 fn deepseek_api_key() -> Option<String> {
778 std::env::var("DEEPSEEK_API_KEY")
779 .ok()
780 .map(|key| key.trim().to_string())
781 .filter(|key| !key.is_empty())
782 }
783
784 fn qwen_api_key() -> Option<String> {
785 std::env::var("QWEN_API_KEY")
786 .ok()
787 .map(|key| key.trim().to_string())
788 .filter(|key| !key.is_empty())
789 }
790
791 async fn run_stream(
792 request: &RequestBody,
793 client: &reqwest::Client,
794 base_url: &str,
795 api_key: &str,
796 ) -> anyhow::Result<Response> {
797 let mut stream = request
798 .get_stream_response(client, base_url, api_key)
799 .await?;
800
801 let mut final_response: Option<Response> = None;
802 while let Some(event) = stream.next().await {
803 let event: ResponseStreamEvent = event?;
804 println!(
805 "responses stream event: {:?}",
806 std::mem::discriminant(&event)
807 );
808 if let Some(response) = event.final_response() {
809 final_response = Some(response.clone());
810 }
811 }
812
813 let response =
814 final_response.expect("the stream should end with a terminal response event");
815 Ok(response)
816 }
817
818 #[tokio::test]
821 async fn test_deepseek_response_no_stream() -> Result<(), anyhow::Error> {
822 let Some(api_key) = deepseek_api_key() else {
823 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
824 return Ok(());
825 };
826
827 let request = RequestBody {
828 model: DEEPSEEK_MODEL.to_string(),
829 input: Input::Text("用一句话介绍你自己。".to_string()),
830 instructions: Some("You are a helpful assistant.".to_string()),
831 ..Default::default()
832 };
833
834 let response = request
835 .get_response(&default_client(), DEEPSEEK_URL, &api_key)
836 .await?;
837
838 println!("deepseek responses no-stream: {response:#?}");
839 assert_eq!(response.object, ResponseObject::Response);
840 assert!(response.is_completed());
841 assert_eq!(response.store, Some(false));
843 assert!(!response.output_text().is_empty());
844 Ok(())
845 }
846
847 #[tokio::test]
850 async fn test_deepseek_response_stream() -> Result<(), anyhow::Error> {
851 let Some(api_key) = deepseek_api_key() else {
852 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
853 return Ok(());
854 };
855
856 let request = RequestBody {
857 model: DEEPSEEK_MODEL.to_string(),
858 input: Input::Text("用一句话介绍你自己。".to_string()),
859 stream: Some(true),
860 ..Default::default()
861 };
862
863 let response = run_stream(&request, &default_client(), DEEPSEEK_URL, &api_key).await?;
864
865 println!("deepseek responses stream final: {response:#?}");
866 assert!(response.is_completed());
867 assert!(!response.output_text().is_empty());
868 Ok(())
869 }
870
871 #[tokio::test]
874 async fn test_qwen_response_no_stream() -> Result<(), anyhow::Error> {
875 let Some(api_key) = qwen_api_key() else {
876 println!("Skipping: set QWEN_API_KEY to run this test");
877 return Ok(());
878 };
879
880 let request = RequestBody {
881 model: QWEN_MODEL.to_string(),
882 input: Input::Text("用一句话介绍你自己。".to_string()),
883 instructions: Some("You are a helpful assistant.".to_string()),
884 store: Some(false),
887 ..Default::default()
888 };
889
890 let response = request
891 .get_response(&default_client(), QWEN_URL, &api_key)
892 .await?;
893
894 println!("qwen responses no-stream: {response:#?}");
895 assert_eq!(response.object, ResponseObject::Response);
896 assert!(response.is_completed());
897 assert!(!response.output_text().is_empty());
898 Ok(())
899 }
900
901 #[tokio::test]
903 async fn test_qwen_response_stream() -> Result<(), anyhow::Error> {
904 let Some(api_key) = qwen_api_key() else {
905 println!("Skipping: set QWEN_API_KEY to run this test");
906 return Ok(());
907 };
908
909 let request = RequestBody {
910 model: QWEN_MODEL.to_string(),
911 input: Input::Text("用一句话介绍你自己。".to_string()),
912 stream: Some(true),
913 store: Some(false),
914 ..Default::default()
915 };
916
917 let response = run_stream(&request, &default_client(), QWEN_URL, &api_key).await?;
918
919 println!("qwen responses stream final: {response:#?}");
920 assert!(response.is_completed());
921 assert!(!response.output_text().is_empty());
922 Ok(())
923 }
924
925 #[tokio::test]
927 async fn test_qwen_response_previous_response_id() -> Result<(), anyhow::Error> {
928 let Some(api_key) = qwen_api_key() else {
929 println!("Skipping: set QWEN_API_KEY to run this test");
930 return Ok(());
931 };
932
933 let client = default_client();
934
935 let first = RequestBody {
938 model: QWEN_MODEL.to_string(),
939 input: Input::Text("法国的首都是哪里?".to_string()),
940 ..Default::default()
941 }
942 .get_response(&client, QWEN_URL, &api_key)
943 .await?;
944 assert!(first.is_completed());
945
946 let second = RequestBody {
947 model: QWEN_MODEL.to_string(),
948 input: Input::Text("它的人口大约是多少?".to_string()),
949 previous_response_id: Some(first.id.clone()),
950 store: Some(false),
951 ..Default::default()
952 }
953 .get_response(&client, QWEN_URL, &api_key)
954 .await?;
955
956 println!("qwen responses second turn: {second:#?}");
957 assert!(second.is_completed());
958 assert_eq!(
959 second.previous_response_id.as_deref(),
960 Some(first.id.as_str())
961 );
962 assert!(!second.output_text().is_empty());
963 Ok(())
964 }
965
966 #[tokio::test]
968 async fn test_qwen_response_store_retrieve_delete() -> Result<(), anyhow::Error> {
969 let Some(api_key) = qwen_api_key() else {
970 println!("Skipping: set QWEN_API_KEY to run this test");
971 return Ok(());
972 };
973
974 let client = default_client();
975
976 let created = RequestBody {
977 model: QWEN_MODEL.to_string(),
978 input: Input::Text("说一个法语单词。".to_string()),
979 store: Some(true),
980 ..Default::default()
981 }
982 .get_response(&client, QWEN_URL, &api_key)
983 .await?;
984 assert!(created.is_completed());
985
986 let retrieved = crate::responses::retrieve::RetrieveRequest {
988 response_id: &created.id,
989 }
990 .get_response(&client, QWEN_URL, &api_key)
991 .await?;
992 assert_eq!(retrieved.id, created.id);
993 assert_eq!(retrieved.output_text(), created.output_text());
994
995 let deleted = crate::responses::delete::DeleteRequest {
997 response_id: &created.id,
998 }
999 .get_response(&client, QWEN_URL, &api_key)
1000 .await?;
1001 assert_eq!(deleted.id, created.id);
1002 assert!(deleted.deleted);
1003
1004 Ok(())
1005 }
1006}