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 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 api_key: &str,
803 ) -> anyhow::Result<Response> {
804 let mut stream = request
805 .get_stream_response(client, base_url, api_key)
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(&default_client(), DEEPSEEK_URL, &api_key)
843 .await?;
844
845 println!("deepseek responses no-stream: {response:#?}");
846 assert_eq!(response.object, ResponseObject::Response);
847 assert!(response.is_completed());
848 assert_eq!(response.store, Some(false));
850 assert!(!response.output_text().is_empty());
851 Ok(())
852 }
853
854 #[tokio::test]
857 async fn test_deepseek_response_stream() -> Result<(), anyhow::Error> {
858 let Some(api_key) = deepseek_api_key() else {
859 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
860 return Ok(());
861 };
862
863 let request = RequestBody {
864 model: DEEPSEEK_MODEL.to_string(),
865 input: Input::Text("用一句话介绍你自己。".to_string()),
866 stream: Some(true),
867 ..Default::default()
868 };
869
870 let response = run_stream(&request, &default_client(), DEEPSEEK_URL, &api_key).await?;
871
872 println!("deepseek responses stream final: {response:#?}");
873 assert!(response.is_completed());
874 assert!(!response.output_text().is_empty());
875 Ok(())
876 }
877
878 #[tokio::test]
881 async fn test_qwen_response_no_stream() -> Result<(), anyhow::Error> {
882 let Some(api_key) = qwen_api_key() else {
883 println!("Skipping: set QWEN_API_KEY to run this test");
884 return Ok(());
885 };
886
887 let request = RequestBody {
888 model: QWEN_MODEL.to_string(),
889 input: Input::Text("用一句话介绍你自己。".to_string()),
890 instructions: Some("You are a helpful assistant.".to_string()),
891 store: Some(false),
894 ..Default::default()
895 };
896
897 let response = request
898 .get_response(&default_client(), QWEN_URL, &api_key)
899 .await?;
900
901 println!("qwen responses no-stream: {response:#?}");
902 assert_eq!(response.object, ResponseObject::Response);
903 assert!(response.is_completed());
904 assert!(!response.output_text().is_empty());
905 Ok(())
906 }
907
908 #[tokio::test]
910 async fn test_qwen_response_stream() -> Result<(), anyhow::Error> {
911 let Some(api_key) = qwen_api_key() else {
912 println!("Skipping: set QWEN_API_KEY to run this test");
913 return Ok(());
914 };
915
916 let request = RequestBody {
917 model: QWEN_MODEL.to_string(),
918 input: Input::Text("用一句话介绍你自己。".to_string()),
919 stream: Some(true),
920 store: Some(false),
921 ..Default::default()
922 };
923
924 let response = run_stream(&request, &default_client(), QWEN_URL, &api_key).await?;
925
926 println!("qwen responses stream final: {response:#?}");
927 assert!(response.is_completed());
928 assert!(!response.output_text().is_empty());
929 Ok(())
930 }
931
932 #[tokio::test]
934 async fn test_qwen_response_previous_response_id() -> Result<(), anyhow::Error> {
935 let Some(api_key) = qwen_api_key() else {
936 println!("Skipping: set QWEN_API_KEY to run this test");
937 return Ok(());
938 };
939
940 let client = default_client();
941
942 let first = RequestBody {
945 model: QWEN_MODEL.to_string(),
946 input: Input::Text("法国的首都是哪里?".to_string()),
947 ..Default::default()
948 }
949 .get_response(&client, QWEN_URL, &api_key)
950 .await?;
951 assert!(first.is_completed());
952
953 let second = RequestBody {
954 model: QWEN_MODEL.to_string(),
955 input: Input::Text("它的人口大约是多少?".to_string()),
956 previous_response_id: Some(first.id.clone()),
957 store: Some(false),
958 ..Default::default()
959 }
960 .get_response(&client, QWEN_URL, &api_key)
961 .await?;
962
963 println!("qwen responses second turn: {second:#?}");
964 assert!(second.is_completed());
965 assert_eq!(
966 second.previous_response_id.as_deref(),
967 Some(first.id.as_str())
968 );
969 assert!(!second.output_text().is_empty());
970 Ok(())
971 }
972
973 #[tokio::test]
975 async fn test_qwen_response_store_retrieve_delete() -> Result<(), anyhow::Error> {
976 let Some(api_key) = qwen_api_key() else {
977 println!("Skipping: set QWEN_API_KEY to run this test");
978 return Ok(());
979 };
980
981 let client = default_client();
982
983 let created = RequestBody {
984 model: QWEN_MODEL.to_string(),
985 input: Input::Text("说一个法语单词。".to_string()),
986 store: Some(true),
987 ..Default::default()
988 }
989 .get_response(&client, QWEN_URL, &api_key)
990 .await?;
991 assert!(created.is_completed());
992
993 let retrieved = crate::responses::retrieve::RetrieveRequest {
995 response_id: &created.id,
996 }
997 .get_response(&client, QWEN_URL, &api_key)
998 .await?;
999 assert_eq!(retrieved.id, created.id);
1000 assert_eq!(retrieved.output_text(), created.output_text());
1001
1002 let deleted = crate::responses::delete::DeleteRequest {
1004 response_id: &created.id,
1005 }
1006 .get_response(&client, QWEN_URL, &api_key)
1007 .await?;
1008 assert_eq!(deleted.id, created.id);
1009 assert!(deleted.deleted);
1010
1011 Ok(())
1012 }
1013}