1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use url::Url;
7
8use crate::{
9 chat::ServiceTier,
10 errors::OapiError,
11 rest::post::{Post, PostNoStream, PostStream},
12};
13
14#[derive(Serialize, Deserialize, Debug, Default, Clone)]
54pub struct RequestBody {
55 #[serde(skip_serializing_if = "Option::is_none")]
59 pub audio: Option<ChatCompletionAudioParam>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
65 pub frequency_penalty: Option<f32>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
71 pub logprobs: Option<bool>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
76 pub max_completion_tokens: Option<u32>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
82 pub max_tokens: Option<u32>,
83
84 pub messages: Vec<Message>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
92 pub logit_bias: Option<HashMap<u32, i32>>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
97 pub moderation: Option<ChatModerationParam>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
106 pub metadata: Option<HashMap<String, String>>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
119 pub modalities: Option<Vec<Modality>>,
120
121 pub model: String, #[serde(skip_serializing_if = "Option::is_none")]
128 pub n: Option<u32>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
134 pub parallel_tool_calls: Option<bool>,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
139 pub prediction: Option<ChatCompletionPredictionContentParam>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
145 pub presence_penalty: Option<f32>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
151 pub prompt_cache_key: Option<String>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
158 pub prompt_cache_options: Option<PromptCacheOptions>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
169 pub reasoning_effort: Option<ReasoningEffort>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
181 pub response_format: Option<ResponseFormat>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
188 pub safety_identifier: Option<String>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
194 pub seed: Option<i64>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
213 pub service_tier: Option<ServiceTier>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
218 pub stop: Option<StopKeywords>,
219
220 #[serde(skip_serializing_if = "Option::is_none")]
226 pub store: Option<bool>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
237 pub stream: Option<bool>,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub stream_options: Option<StreamOptions>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
248 pub temperature: Option<f32>,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
256 pub top_p: Option<f32>,
257
258 #[serde(skip_serializing_if = "Option::is_none")]
265 pub tool_choice: Option<ToolChoice>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub tools: Option<Vec<RequestTool>>,
270
271 #[serde(skip_serializing_if = "Option::is_none")]
275 pub top_logprobs: Option<u32>,
276
277 #[cfg(feature = "deepseek")]
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub thinking: Option<DeepSeekThinking>,
283
284 #[cfg(feature = "deepseek")]
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub user_id: Option<String>,
291
292 #[cfg(feature = "qwen")]
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub enable_thinking: Option<bool>,
298 #[cfg(feature = "qwen")]
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub thinking_budget: Option<u32>,
303 #[cfg(any(feature = "qwen", feature = "vllm"))]
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub top_k: Option<u32>,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
320 pub user: Option<String>,
321
322 #[serde(skip_serializing_if = "Option::is_none")]
326 pub verbosity: Option<LowMediumHighEnum>,
327
328 #[serde(rename = "web_search_options", skip_serializing_if = "Option::is_none")]
332 pub web_search_options: Option<WebSearchOptions>,
333
334 #[cfg(feature = "vllm")]
338 #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
339 pub vllm_sampling: Option<crate::vllm::SamplingParams>,
340
341 #[cfg(feature = "vllm")]
346 #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
347 pub vllm_chat: Option<crate::vllm::ChatParams>,
348
349 #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
352 pub extra_body_map: Option<serde_json::Map<String, serde_json::Value>>,
353}
354
355#[derive(Serialize, Deserialize, Debug, Clone)]
368#[serde(tag = "role", rename_all = "lowercase")]
369pub enum Message {
370 System(SystemMessage),
373 User(UserMessage),
376 Assistant(AssistantMessage),
379 Tool(ToolMessage),
382 Function(FunctionMessage),
385 Developer(DeveloperMessage),
388}
389
390impl Message {
391 #[must_use]
394 pub fn system(content: impl Into<MessageContent>) -> Self {
395 Self::System(SystemMessage {
396 content: content.into(),
397 name: None,
398 })
399 }
400
401 #[must_use]
405 pub fn user(content: impl Into<MessageContent>) -> Self {
406 Self::User(UserMessage {
407 content: content.into(),
408 name: None,
409 })
410 }
411
412 #[must_use]
416 pub fn assistant(content: impl Into<String>) -> Self {
417 Self::Assistant(AssistantMessage {
418 content: Some(content.into()),
419 ..Default::default()
420 })
421 }
422
423 #[must_use]
425 pub fn tool(content: impl Into<MessageContent>, tool_call_id: impl Into<String>) -> Self {
426 Self::Tool(ToolMessage {
427 content: content.into(),
428 tool_call_id: tool_call_id.into(),
429 })
430 }
431
432 #[must_use]
435 pub fn function(name: impl Into<String>, content: impl Into<String>) -> Self {
436 Self::Function(FunctionMessage {
437 content: content.into(),
438 name: name.into(),
439 })
440 }
441
442 #[must_use]
445 pub fn developer(content: impl Into<MessageContent>) -> Self {
446 Self::Developer(DeveloperMessage {
447 content: content.into(),
448 name: None,
449 })
450 }
451}
452
453#[derive(Serialize, Deserialize, Debug, Clone, Default)]
455pub struct SystemMessage {
456 pub content: MessageContent,
459 #[serde(skip_serializing_if = "Option::is_none")]
464 pub name: Option<String>,
465}
466
467#[derive(Serialize, Deserialize, Debug, Clone, Default)]
469pub struct UserMessage {
470 pub content: MessageContent,
474 #[serde(skip_serializing_if = "Option::is_none")]
479 pub name: Option<String>,
480}
481
482#[derive(Serialize, Deserialize, Debug, Clone, Default)]
484pub struct AssistantMessage {
485 pub content: Option<String>,
489 #[serde(skip_serializing_if = "Option::is_none")]
492 pub audio: Option<AssistantAudio>,
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub refusal: Option<String>,
496 #[serde(skip_serializing_if = "Option::is_none")]
497 pub name: Option<String>,
498 #[cfg(feature = "deepseek")]
502 #[serde(default, skip_serializing_if = "is_false")]
503 pub prefix: bool,
504 #[cfg(feature = "reasoning")]
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub reasoning_content: Option<String>,
514
515 #[serde(skip_serializing_if = "Option::is_none")]
517 pub tool_calls: Option<Vec<AssistantToolCall>>,
518}
519
520#[derive(Serialize, Deserialize, Debug, Clone, Default)]
522pub struct ToolMessage {
523 pub content: MessageContent,
526 pub tool_call_id: String,
528}
529
530#[derive(Serialize, Deserialize, Debug, Clone, Default)]
532pub struct FunctionMessage {
533 pub content: String,
535 pub name: String,
537}
538
539#[derive(Serialize, Deserialize, Debug, Clone, Default)]
541pub struct DeveloperMessage {
542 pub content: MessageContent,
545 #[serde(skip_serializing_if = "Option::is_none")]
550 pub name: Option<String>,
551}
552
553#[derive(Debug, Serialize, Deserialize, Clone)]
556#[serde(untagged)]
557pub enum MessageContent {
558 Text(String),
560 Parts(Vec<ContentPart>),
563}
564
565impl From<&str> for MessageContent {
566 fn from(value: &str) -> Self {
567 Self::Text(value.to_string())
568 }
569}
570
571impl From<String> for MessageContent {
572 fn from(value: String) -> Self {
573 Self::Text(value)
574 }
575}
576
577impl From<Vec<ContentPart>> for MessageContent {
578 fn from(value: Vec<ContentPart>) -> Self {
579 Self::Parts(value)
580 }
581}
582
583impl Default for MessageContent {
584 fn default() -> Self {
585 Self::Text(String::new())
586 }
587}
588
589#[derive(Debug, Serialize, Deserialize, Clone)]
591#[serde(tag = "type", rename_all = "snake_case")]
592pub enum ContentPart {
593 Text {
595 text: String,
597 #[serde(skip_serializing_if = "Option::is_none")]
600 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
601 },
602 ImageUrl {
604 image_url: ContentPartImageUrl,
606 #[serde(skip_serializing_if = "Option::is_none")]
609 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
610 },
611 InputAudio {
613 input_audio: ContentPartInputAudio,
615 #[serde(skip_serializing_if = "Option::is_none")]
618 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
619 },
620 File {
622 file: ContentPartFile,
625 #[serde(skip_serializing_if = "Option::is_none")]
628 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
629 },
630}
631
632#[derive(Debug, Serialize, Deserialize, Clone)]
634pub struct PromptCacheBreakpoint {
635 pub mode: PromptCacheBreakpointMode,
637}
638
639#[derive(Debug, Serialize, Deserialize, Clone)]
641#[serde(rename_all = "lowercase")]
642pub enum PromptCacheBreakpointMode {
643 Explicit,
644}
645
646#[derive(Debug, Serialize, Deserialize, Clone)]
648pub struct ContentPartImageUrl {
649 pub url: String,
651 #[serde(skip_serializing_if = "Option::is_none")]
656 pub detail: Option<ImageDetail>,
657}
658
659#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
661#[serde(rename_all = "lowercase")]
662pub enum ImageDetail {
663 Auto,
664 Low,
665 High,
666}
667
668#[derive(Debug, Serialize, Deserialize, Clone)]
670pub struct ContentPartInputAudio {
671 pub data: String,
673 pub format: InputAudioFormat,
676}
677
678#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
680#[serde(rename_all = "lowercase")]
681pub enum InputAudioFormat {
682 Wav,
683 Mp3,
684}
685
686#[derive(Debug, Serialize, Deserialize, Clone, Default)]
689pub struct ContentPartFile {
690 #[serde(skip_serializing_if = "Option::is_none")]
693 pub file_data: Option<String>,
694 #[serde(skip_serializing_if = "Option::is_none")]
696 pub file_id: Option<String>,
697 #[serde(skip_serializing_if = "Option::is_none")]
700 pub filename: Option<String>,
701}
702
703#[derive(Debug, Serialize, Deserialize, Clone)]
706pub struct ChatModerationParam {
707 pub model: String,
710 #[serde(skip_serializing_if = "Option::is_none")]
712 pub policy: Option<ModerationPolicyParam>,
713}
714
715#[derive(Debug, Serialize, Deserialize, Clone, Default)]
717pub struct ModerationPolicyParam {
718 #[serde(skip_serializing_if = "Option::is_none")]
720 pub input: Option<ModerationPolicySideParam>,
721 #[serde(skip_serializing_if = "Option::is_none")]
723 pub output: Option<ModerationPolicySideParam>,
724}
725
726#[derive(Debug, Serialize, Deserialize, Clone)]
728pub struct ModerationPolicySideParam {
729 pub mode: ModerationPolicyMode,
732}
733
734#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
736#[serde(rename_all = "lowercase")]
737pub enum ModerationPolicyMode {
738 Score,
739 Block,
740}
741
742#[derive(Debug, Serialize, Deserialize, Clone, Default)]
744pub struct PromptCacheOptions {
745 #[serde(skip_serializing_if = "Option::is_none")]
748 pub mode: Option<PromptCacheMode>,
749 #[serde(skip_serializing_if = "Option::is_none")]
753 pub ttl: Option<PromptCacheTtl>,
754}
755
756#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
758#[serde(rename_all = "lowercase")]
759pub enum PromptCacheMode {
760 Implicit,
761 Explicit,
762}
763
764#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
766pub enum PromptCacheTtl {
767 #[serde(rename = "30m")]
768 ThirtyMinutes,
769}
770
771#[derive(Debug, Serialize, Deserialize, Clone)]
772#[serde(tag = "type", rename_all = "lowercase")]
773pub enum AssistantToolCall {
774 Function {
775 id: String,
777 function: ToolCallFunction,
779 },
780 Custom {
781 id: String,
783 custom: ToolCallCustom,
785 },
786}
787
788#[derive(Debug, Serialize, Deserialize, Clone)]
789pub struct ToolCallFunction {
790 pub arguments: String,
795 pub name: String,
797}
798
799#[derive(Debug, Serialize, Deserialize, Clone)]
800pub struct ToolCallCustom {
801 pub input: String,
803 pub name: String,
805}
806
807#[derive(Debug, Serialize, Deserialize, Clone)]
810pub struct AssistantAudio {
811 pub id: String,
814 #[serde(skip_serializing_if = "Option::is_none")]
816 pub data: Option<String>,
817}
818
819#[derive(Debug, Serialize, Deserialize, Clone)]
820#[serde(tag = "type", rename_all = "snake_case")]
821pub enum ResponseFormat {
822 JsonSchema {
824 json_schema: JSONSchema,
826 },
827 JsonObject,
829 Text,
831}
832
833#[derive(Debug, Serialize, Deserialize, Clone)]
834pub struct JSONSchema {
835 pub name: String,
838 #[serde(skip_serializing_if = "Option::is_none")]
841 pub description: Option<String>,
842 #[serde(skip_serializing_if = "Option::is_none")]
845 pub schema: Option<serde_json::Map<String, serde_json::Value>>,
846 #[serde(skip_serializing_if = "Option::is_none")]
852 pub strict: Option<bool>,
853}
854
855#[derive(Serialize, Deserialize, Debug, Clone)]
856#[serde(rename_all = "snake_case")]
857pub enum Modality {
858 Text,
859 Audio,
860}
861
862#[derive(Serialize, Deserialize, Debug, Clone)]
864pub struct ChatCompletionAudioParam {
865 pub format: AudioFormat,
868 pub voice: Voice,
870}
871
872#[derive(Serialize, Deserialize, Debug, Clone)]
874#[serde(rename_all = "snake_case")]
875pub enum AudioFormat {
876 Wav,
877 Aac,
878 Mp3,
879 Flac,
880 Opus,
881 Pcm16,
882}
883
884#[derive(Serialize, Deserialize, Debug, Clone)]
886#[serde(untagged)]
887pub enum Voice {
888 BuiltIn(String),
891 Custom {
893 id: String,
895 },
896}
897
898#[derive(Serialize, Deserialize, Debug, Clone)]
899pub struct ChatCompletionPredictionContentParam {
900 pub content: ChatCompletionPredictionContentParamContent,
904
905 #[serde(rename = "type")]
908 pub type_: ChatCompletionPredictionContentParamType,
909}
910
911#[derive(Serialize, Deserialize, Debug, Clone)]
912#[serde(untagged)]
913pub enum ChatCompletionPredictionContentParamContent {
914 Text(String),
915 ChatCompletionContentPartTextParam {
916 text: String,
918 #[serde(rename = "type")]
920 type_: ChatCompletionContentPartTextParamType,
921 },
922}
923
924#[derive(Serialize, Deserialize, Debug, Clone)]
925#[serde(rename_all = "snake_case")]
926pub enum ChatCompletionContentPartTextParamType {
927 Text,
928}
929
930#[derive(Serialize, Deserialize, Debug, Clone)]
931#[serde(rename_all = "snake_case")]
932pub enum ChatCompletionPredictionContentParamType {
933 Content,
934}
935
936#[cfg(feature = "deepseek")]
938#[inline]
939fn is_false(value: &bool) -> bool {
940 !value
941}
942
943#[derive(Serialize, Deserialize, Debug, Clone)]
944#[serde(untagged)]
945pub enum StopKeywords {
946 Word(String),
947 Words(Vec<String>),
948}
949
950#[derive(Serialize, Deserialize, Debug, Clone)]
951#[serde(rename_all = "snake_case")]
952pub enum LowMediumHighEnum {
953 Low,
954 Medium,
955 High,
956}
957
958#[derive(Serialize, Deserialize, Debug, Clone, Default)]
959pub struct WebSearchOptions {
960 #[serde(skip_serializing_if = "Option::is_none")]
963 pub search_context_size: Option<LowMediumHighEnum>,
964
965 #[serde(skip_serializing_if = "Option::is_none")]
966 pub user_location: Option<WebSearchOptionsUserLocation>,
967}
968
969#[derive(Serialize, Deserialize, Debug, Clone)]
970#[serde(tag = "type", rename_all = "snake_case")]
971pub enum WebSearchOptionsUserLocation {
972 Approximate {
974 approximate: WebSearchOptionsUserLocationApproximate,
976 },
977}
978
979#[derive(Serialize, Deserialize, Debug, Clone, Default)]
980pub struct WebSearchOptionsUserLocationApproximate {
981 #[serde(skip_serializing_if = "Option::is_none")]
983 pub city: Option<String>,
984
985 #[serde(skip_serializing_if = "Option::is_none")]
988 pub country: Option<String>,
989
990 #[serde(skip_serializing_if = "Option::is_none")]
992 pub region: Option<String>,
993
994 #[serde(skip_serializing_if = "Option::is_none")]
997 pub timezone: Option<String>,
998}
999
1000#[derive(Serialize, Deserialize, Debug, Clone)]
1001pub struct StreamOptions {
1002 pub include_usage: bool,
1011}
1012
1013#[derive(Serialize, Deserialize, Debug, Clone)]
1014#[serde(tag = "type", rename_all = "snake_case")]
1015pub enum RequestTool {
1016 Function { function: ToolFunction },
1018 Custom {
1020 custom: ToolCustom,
1022 },
1023}
1024
1025#[derive(Serialize, Deserialize, Debug, Clone)]
1026pub struct ToolFunction {
1027 pub name: String,
1031 #[serde(skip_serializing_if = "Option::is_none")]
1034 pub description: Option<String>,
1035 #[serde(skip_serializing_if = "Option::is_none")]
1045 pub parameters: Option<serde_json::Map<String, serde_json::Value>>,
1046 #[serde(skip_serializing_if = "Option::is_none")]
1053 pub strict: Option<bool>,
1054}
1055
1056#[derive(Serialize, Deserialize, Debug, Clone)]
1057pub struct ToolCustom {
1058 pub name: String,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1062 pub description: Option<String>,
1063 #[serde(skip_serializing_if = "Option::is_none")]
1065 pub format: Option<ToolCustomFormat>,
1066}
1067
1068#[derive(Serialize, Deserialize, Debug, Clone)]
1069#[serde(rename_all = "snake_case", tag = "type")]
1070pub enum ToolCustomFormat {
1071 Text,
1073 Grammar {
1075 grammar: ToolCustomFormatGrammarGrammar,
1077 },
1078}
1079
1080#[derive(Debug, Serialize, Deserialize, Clone)]
1081pub struct ToolCustomFormatGrammarGrammar {
1082 pub definition: String,
1084 pub syntax: ToolCustomFormatGrammarGrammarSyntax,
1086}
1087
1088#[derive(Debug, Serialize, Deserialize, Clone)]
1089#[serde(rename_all = "snake_case")]
1090pub enum ToolCustomFormatGrammarGrammarSyntax {
1091 Lark,
1092 Regex,
1093}
1094
1095#[derive(Debug, Serialize, Deserialize, Clone)]
1096#[serde(rename_all = "snake_case")]
1097pub enum ToolChoice {
1098 None,
1099 Auto,
1100 Required,
1101 #[serde(untagged)]
1102 Specific(ToolChoiceSpecific),
1103}
1104
1105#[derive(Debug, Serialize, Deserialize, Clone)]
1106#[serde(rename_all = "snake_case", tag = "type")]
1107pub enum ToolChoiceSpecific {
1108 AllowedTools {
1110 allowed_tools: ToolChoiceAllowedTools,
1112 },
1113 Function { function: ToolChoiceFunction },
1115 Custom { custom: ToolChoiceCustom },
1117}
1118
1119#[derive(Debug, Serialize, Deserialize, Clone)]
1120pub struct ToolChoiceAllowedTools {
1121 pub mode: ToolChoiceAllowedToolsMode,
1127 pub tools: Vec<serde_json::Map<String, serde_json::Value>>,
1138}
1139
1140#[derive(Debug, Serialize, Deserialize, Clone)]
1148#[serde(rename_all = "lowercase")]
1149pub enum ToolChoiceAllowedToolsMode {
1150 Auto,
1152 Required,
1154}
1155
1156#[derive(Debug, Serialize, Deserialize, Clone)]
1157pub struct ToolChoiceFunction {
1158 pub name: String,
1160}
1161
1162#[derive(Debug, Serialize, Deserialize, Clone)]
1163pub struct ToolChoiceCustom {
1164 pub name: String,
1166}
1167
1168#[cfg(feature = "deepseek")]
1170#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1171pub struct DeepSeekThinking {
1172 #[serde(rename = "type")]
1175 pub type_: DeepSeekThinkingType,
1176}
1177
1178#[cfg(feature = "deepseek")]
1180#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1181#[serde(rename_all = "lowercase")]
1182pub enum DeepSeekThinkingType {
1183 Enabled,
1184 Disabled,
1185}
1186
1187#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1192#[serde(rename_all = "lowercase")]
1193pub enum ReasoningEffort {
1194 None,
1195 Minimal,
1196 Low,
1197 Medium,
1198 High,
1199 Xhigh,
1200 Max,
1201}
1202
1203impl RequestBody {
1204 pub fn is_streaming(&self) -> bool {
1207 self.stream.unwrap_or(false)
1208 }
1209}
1210
1211impl Post for RequestBody {
1212 fn is_streaming(&self) -> bool {
1213 RequestBody::is_streaming(self)
1214 }
1215
1216 fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
1220 let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
1221 url.path_segments_mut()
1222 .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
1223 .push("chat")
1224 .push("completions");
1225
1226 Ok(url.to_string())
1227 }
1228}
1229
1230impl PostNoStream for RequestBody {
1231 type Response = super::response::no_streaming::ChatCompletion;
1232}
1233
1234impl PostStream for RequestBody {
1235 type Response = super::response::streaming::ChatCompletionChunk;
1236}
1237
1238#[cfg(test)]
1239mod request_test {
1240 use futures_util::StreamExt;
1241
1242 use super::*;
1243
1244 const DEEPSEEK_CHAT_URL: &str = "https://api.deepseek.com";
1245 const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
1246
1247 fn deepseek_api_key() -> Option<String> {
1248 std::env::var("DEEPSEEK_API_KEY")
1249 .ok()
1250 .map(|key| key.trim().to_string())
1251 .filter(|key| !key.is_empty())
1252 }
1253
1254 #[tokio::test]
1255 async fn test_deepseek_no_stream() {
1256 let Some(api_key) = deepseek_api_key() else {
1257 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
1258 return;
1259 };
1260
1261 let request = RequestBody {
1262 messages: vec![
1263 Message::system("This is a request of test purpose. Reply briefly"),
1264 Message::user("What's your name?"),
1265 ],
1266 model: DEEPSEEK_MODEL.to_string(),
1267 stream: Some(false),
1268 ..Default::default()
1269 };
1270
1271 let response = request
1272 .get_response_string(
1273 &crate::rest::default_client(),
1274 DEEPSEEK_CHAT_URL,
1275 &crate::rest::RequestOptions::bearer(&api_key),
1276 )
1277 .await
1278 .unwrap();
1279
1280 println!("{}", response);
1281
1282 assert!(response.to_ascii_lowercase().contains("deepseek"));
1283 }
1284
1285 #[tokio::test]
1286 async fn test_deepseek_stream() {
1287 let Some(api_key) = deepseek_api_key() else {
1288 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
1289 return;
1290 };
1291
1292 let request = RequestBody {
1293 messages: vec![
1294 Message::system("This is a request of test purpose. Reply briefly"),
1295 Message::user("Who are you?"),
1296 ],
1297 model: DEEPSEEK_MODEL.to_string(),
1298 stream: Some(true),
1299 ..Default::default()
1300 };
1301
1302 let mut response = request
1303 .get_stream_response_string(
1304 &crate::rest::default_client(),
1305 DEEPSEEK_CHAT_URL,
1306 &crate::rest::RequestOptions::bearer(&api_key),
1307 )
1308 .await
1309 .unwrap();
1310
1311 while let Some(chunk) = response.next().await {
1312 println!("{}", chunk.unwrap());
1313 }
1314 }
1315
1316 #[test]
1319 fn assistant_tool_call_serialization() {
1320 let function_call = AssistantToolCall::Function {
1321 id: "call_abc".to_string(),
1322 function: ToolCallFunction {
1323 arguments: "{\"city\":\"paris\"}".to_string(),
1324 name: "get_weather".to_string(),
1325 },
1326 };
1327 let json = serde_json::to_string(&function_call).unwrap();
1328 assert!(json.contains(r#""type":"function""#), "json: {json}");
1329 assert!(!json.contains(r#""role""#), "json: {json}");
1330
1331 let custom_call = AssistantToolCall::Custom {
1332 id: "call_def".to_string(),
1333 custom: ToolCallCustom {
1334 input: "2+2".to_string(),
1335 name: "calculator".to_string(),
1336 },
1337 };
1338 let json = serde_json::to_string(&custom_call).unwrap();
1339 assert!(json.contains(r#""type":"custom""#), "json: {json}");
1340 assert!(!json.contains(r#""role""#), "json: {json}");
1341 }
1342
1343 #[test]
1346 fn prediction_type_serialization() {
1347 let prediction = ChatCompletionPredictionContentParam {
1348 content: ChatCompletionPredictionContentParamContent::Text(
1349 "The capital of France is Paris.".to_string(),
1350 ),
1351 type_: ChatCompletionPredictionContentParamType::Content,
1352 };
1353 let json = serde_json::to_string(&prediction).unwrap();
1354 assert!(json.contains(r#""type":"content""#), "json: {json}");
1355 assert!(!json.contains("type_"), "json: {json}");
1356 }
1357
1358 #[test]
1361 fn allowed_tools_choice_serialization() {
1362 let mut weather = serde_json::Map::new();
1363 weather.insert("type".to_string(), serde_json::json!("function"));
1364 weather.insert(
1365 "function".to_string(),
1366 serde_json::json!({ "name": "get_weather" }),
1367 );
1368
1369 let choice = ToolChoiceSpecific::AllowedTools {
1370 allowed_tools: ToolChoiceAllowedTools {
1371 mode: ToolChoiceAllowedToolsMode::Required,
1372 tools: vec![weather],
1373 },
1374 };
1375 let json = serde_json::to_string(&choice).unwrap();
1376 assert!(json.contains(r#""type":"allowed_tools""#), "json: {json}");
1377 assert!(json.contains(r#""mode":"required""#), "json: {json}");
1378 assert!(json.contains(r#""tools":[{"#), "json: {json}");
1380 }
1381
1382 #[test]
1385 fn web_search_options_serialization() {
1386 let options = WebSearchOptions {
1387 search_context_size: None,
1388 user_location: Some(WebSearchOptionsUserLocation::Approximate {
1389 approximate: WebSearchOptionsUserLocationApproximate {
1390 city: Some("San Francisco".to_string()),
1391 country: None,
1392 region: None,
1393 timezone: None,
1394 },
1395 }),
1396 };
1397 let json = serde_json::to_string(&options).unwrap();
1398 assert!(!json.contains("search_context_size"), "json: {json}");
1399 assert!(json.contains(r#""type":"approximate""#), "json: {json}");
1400 assert!(
1401 json.contains(r#""approximate":{"city":"San Francisco"}"#),
1402 "json: {json}"
1403 );
1404 }
1405
1406 #[test]
1408 fn json_schema_optional_fields_serialization() {
1409 let schema = JSONSchema {
1410 name: "Answer".to_string(),
1411 description: None,
1412 schema: None,
1413 strict: None,
1414 };
1415 let json = serde_json::to_string(&schema).unwrap();
1416 assert_eq!(json, r#"{"name":"Answer"}"#);
1417
1418 let function = ToolFunction {
1419 name: "get_weather".to_string(),
1420 description: None,
1421 parameters: None,
1422 strict: None,
1423 };
1424 let json = serde_json::to_string(&function).unwrap();
1425 assert_eq!(json, r#"{"name":"get_weather"}"#);
1426 }
1427
1428 #[test]
1431 fn user_text_content_serialization() {
1432 let request = RequestBody {
1433 messages: vec![Message::user("Hi")],
1434 model: "gpt-4o".to_string(),
1435 ..Default::default()
1436 };
1437
1438 let json = serde_json::to_string(&request).unwrap();
1439 assert!(json.contains(r#""content":"Hi""#), "json: {json}");
1440 }
1441
1442 #[test]
1445 fn message_deserialization() {
1446 let system: Message =
1447 serde_json::from_str(r#"{"role":"system","content":"Be terse"}"#).unwrap();
1448 assert!(matches!(
1449 system,
1450 Message::System(SystemMessage {
1451 content: MessageContent::Text(_),
1452 name: None
1453 })
1454 ));
1455
1456 let user: Message =
1457 serde_json::from_str(r#"{"role":"user","content":"Hi","name":"jimmy"}"#).unwrap();
1458 let Message::User(user) = user else {
1459 panic!("must be a user message");
1460 };
1461 assert_eq!(user.name.as_deref(), Some("jimmy"));
1462
1463 let assistant: Message = serde_json::from_str(
1464 r#"{"role":"assistant","content":null,"tool_calls":[{"type":"function","id":"call_1","function":{"name":"f","arguments":"{}"}}]}"#,
1465 )
1466 .unwrap();
1467 let Message::Assistant(assistant) = assistant else {
1468 panic!("must be an assistant message");
1469 };
1470 assert_eq!(assistant.content, None);
1471 assert_eq!(assistant.tool_calls.expect("tool calls").len(), 1);
1472
1473 let tool: Message =
1474 serde_json::from_str(r#"{"role":"tool","content":"42","tool_call_id":"call_1"}"#)
1475 .unwrap();
1476 let Message::Tool(tool) = tool else {
1477 panic!("must be a tool message");
1478 };
1479 assert_eq!(tool.tool_call_id, "call_1");
1480
1481 let developer: Message =
1482 serde_json::from_str(r#"{"role":"developer","content":"New rules"}"#).unwrap();
1483 assert!(matches!(developer, Message::Developer(_)));
1484
1485 let function: Message =
1486 serde_json::from_str(r#"{"role":"function","name":"f","content":"ok"}"#).unwrap();
1487 assert!(matches!(function, Message::Function(_)));
1488 }
1489
1490 #[test]
1493 fn unknown_role_fails_deserialization() {
1494 let result = serde_json::from_str::<Message>(r#"{"role":"weird","content":"x"}"#);
1495 assert!(result.is_err(), "unknown roles must be rejected");
1496 }
1497
1498 #[test]
1502 fn request_body_deserializes_with_extra_fields() {
1503 let json = r#"{
1504 "model": "qwen-plus",
1505 "messages": [{"role": "user", "content": "Hi"}],
1506 "stream": true,
1507 "vendor_extension": {"depth": 3}
1508 }"#;
1509 let request = serde_json::from_str::<RequestBody>(json).unwrap();
1510 assert_eq!(request.model, "qwen-plus");
1511 assert_eq!(request.stream, Some(true));
1512 assert_eq!(request.messages.len(), 1);
1513
1514 let extra = request
1515 .extra_body_map
1516 .as_ref()
1517 .expect("extra fields captured");
1518 assert_eq!(
1519 extra.get("vendor_extension"),
1520 Some(&serde_json::json!({"depth": 3}))
1521 );
1522
1523 let serialized = serde_json::to_value(&request).unwrap();
1524 assert_eq!(serialized["vendor_extension"]["depth"], 3);
1525 }
1526
1527 #[test]
1529 fn message_constructors() {
1530 let request = RequestBody {
1531 messages: vec![
1532 Message::system("Be terse"),
1533 Message::user("Hi"),
1534 Message::assistant("Hello!"),
1535 Message::tool(r#"{"temp":21}"#, "call_1"),
1536 ],
1537 model: "gpt-4o".to_string(),
1538 ..Default::default()
1539 };
1540
1541 let json = serde_json::to_string(&request).unwrap();
1542 assert!(json.contains(r#""role":"system","content":"Be terse""#),);
1543 assert!(json.contains(r#""role":"user","content":"Hi""#));
1544 assert!(json.contains(r#""role":"assistant","content":"Hello!""#));
1545 assert!(
1546 json.contains(r#""role":"tool","content":"{\"temp\":21}","tool_call_id":"call_1""#)
1547 );
1548 }
1549
1550 #[test]
1554 fn system_developer_tool_content_serialization() {
1555 let request = RequestBody {
1556 messages: vec![
1557 Message::system("Be terse"),
1558 Message::developer(MessageContent::Parts(vec![ContentPart::Text {
1559 text: "Prefer Rust".to_string(),
1560 prompt_cache_breakpoint: None,
1561 }])),
1562 Message::tool(
1563 MessageContent::Parts(vec![ContentPart::Text {
1564 text: r#"{"temp": 21}"#.to_string(),
1565 prompt_cache_breakpoint: None,
1566 }]),
1567 "call_1",
1568 ),
1569 ],
1570 model: "gpt-4o".to_string(),
1571 ..Default::default()
1572 };
1573
1574 let json = serde_json::to_string(&request).unwrap();
1575 assert!(
1576 json.contains(r#""role":"system","content":"Be terse""#),
1577 "json: {json}"
1578 );
1579 assert!(
1580 json.contains(r#""role":"developer","content":[{"type":"text","text":"Prefer Rust"}]"#),
1581 "json: {json}"
1582 );
1583 assert!(
1584 json.contains(
1585 r#""role":"tool","content":[{"type":"text","text":"{\"temp\": 21}"}],"tool_call_id":"call_1""#
1586 ),
1587 "json: {json}"
1588 );
1589 }
1590
1591 #[test]
1594 fn multimodal_content_serialization() {
1595 let request = RequestBody {
1596 messages: vec![Message::user(MessageContent::Parts(vec![
1597 ContentPart::ImageUrl {
1598 image_url: ContentPartImageUrl {
1599 url: "https://example.com/cat.png".to_string(),
1600 detail: Some(ImageDetail::High),
1601 },
1602 prompt_cache_breakpoint: None,
1603 },
1604 ContentPart::Text {
1605 text: "What's in this image?".to_string(),
1606 prompt_cache_breakpoint: Some(PromptCacheBreakpoint {
1607 mode: PromptCacheBreakpointMode::Explicit,
1608 }),
1609 },
1610 ]))],
1611 model: "gpt-4o".to_string(),
1612 ..Default::default()
1613 };
1614
1615 let json = serde_json::to_string(&request).unwrap();
1616 assert!(json.contains(r#""type":"image_url""#), "json: {json}");
1617 assert!(
1618 json.contains(r#""url":"https://example.com/cat.png""#),
1619 "json: {json}"
1620 );
1621 assert!(json.contains(r#""detail":"high""#), "json: {json}");
1622 assert!(json.contains(r#""type":"text""#), "json: {json}");
1623 assert!(
1624 json.contains(r#""prompt_cache_breakpoint":{"mode":"explicit"}"#),
1625 "json: {json}"
1626 );
1627 }
1628
1629 #[test]
1632 fn audio_and_file_content_serialization() {
1633 let content = MessageContent::Parts(vec![
1634 ContentPart::InputAudio {
1635 input_audio: ContentPartInputAudio {
1636 data: "aGVsbG8=".to_string(),
1637 format: InputAudioFormat::Wav,
1638 },
1639 prompt_cache_breakpoint: None,
1640 },
1641 ContentPart::File {
1642 file: ContentPartFile {
1643 file_id: Some("file-abc".to_string()),
1644 ..Default::default()
1645 },
1646 prompt_cache_breakpoint: None,
1647 },
1648 ]);
1649
1650 let json = serde_json::to_string(&content).unwrap();
1651 assert!(json.contains(r#""type":"input_audio""#), "json: {json}");
1652 assert!(json.contains(r#""data":"aGVsbG8=""#), "json: {json}");
1653 assert!(json.contains(r#""format":"wav""#), "json: {json}");
1654 assert!(json.contains(r#""type":"file""#), "json: {json}");
1655 assert!(
1656 json.contains(r#""file":{"file_id":"file-abc"}"#),
1657 "json: {json}"
1658 );
1659 assert!(!json.contains("file_data"), "json: {json}");
1661 }
1662
1663 #[test]
1666 fn new_params_serialization() {
1667 let mut logit_bias = HashMap::new();
1668 logit_bias.insert(40u32, -100i32);
1669
1670 let request = RequestBody {
1671 messages: vec![Message::user("Hi")],
1672 model: "gpt-5".to_string(),
1673 logit_bias: Some(logit_bias),
1674 moderation: Some(ChatModerationParam {
1675 model: "omni-moderation-latest".to_string(),
1676 policy: Some(ModerationPolicyParam {
1677 input: Some(ModerationPolicySideParam {
1678 mode: ModerationPolicyMode::Block,
1679 }),
1680 output: None,
1681 }),
1682 }),
1683 prompt_cache_options: Some(PromptCacheOptions {
1684 mode: Some(PromptCacheMode::Explicit),
1685 ttl: Some(PromptCacheTtl::ThirtyMinutes),
1686 }),
1687 ..Default::default()
1688 };
1689
1690 let json = serde_json::to_string(&request).unwrap();
1691 assert!(json.contains(r#""logit_bias":{"40":-100}"#), "json: {json}");
1692 assert!(
1693 json.contains(
1694 r#""moderation":{"model":"omni-moderation-latest","policy":{"input":{"mode":"block"}}}"#
1695 ),
1696 "json: {json}"
1697 );
1698 assert!(
1699 json.contains(r#""prompt_cache_options":{"mode":"explicit","ttl":"30m"}"#),
1700 "json: {json}"
1701 );
1702 }
1703
1704 #[test]
1706 fn reasoning_effort_serialization() {
1707 let request = RequestBody {
1708 messages: vec![Message::user("What's your name?")],
1709 model: "gpt-5".to_string(),
1710 reasoning_effort: Some(ReasoningEffort::Xhigh),
1711 ..Default::default()
1712 };
1713
1714 let json = serde_json::to_string(&request).unwrap();
1715 assert!(
1716 json.contains(r#""reasoning_effort":"xhigh""#),
1717 "json: {json}"
1718 );
1719 }
1720
1721 #[cfg(feature = "deepseek")]
1723 #[test]
1724 fn deepseek_assistant_prefix_serialization() {
1725 let request = RequestBody {
1726 messages: vec![
1727 Message::user("Please write quick sort code"),
1728 Message::Assistant(AssistantMessage {
1729 content: Some("```python\n".to_string()),
1730 prefix: true,
1731 ..Default::default()
1732 }),
1733 ],
1734 model: DEEPSEEK_MODEL.to_string(),
1735 ..Default::default()
1736 };
1737
1738 let json = serde_json::to_string(&request).unwrap();
1739 assert!(json.contains(r#""prefix":true"#), "json: {json}");
1740 }
1741
1742 #[cfg(feature = "deepseek")]
1745 #[test]
1746 fn deepseek_thinking_params_serialization() {
1747 let request = RequestBody {
1748 messages: vec![Message::user("What's your name?")],
1749 model: DEEPSEEK_MODEL.to_string(),
1750 thinking: Some(DeepSeekThinking {
1751 type_: DeepSeekThinkingType::Disabled,
1752 }),
1753 user_id: Some("user-123".to_string()),
1754 ..Default::default()
1755 };
1756
1757 let json = serde_json::to_string(&request).unwrap();
1758 assert!(
1759 json.contains(r#""thinking":{"type":"disabled"}"#),
1760 "json: {json}"
1761 );
1762 assert!(json.contains(r#""user_id":"user-123""#), "json: {json}");
1763 }
1764
1765 #[cfg(feature = "qwen")]
1768 #[test]
1769 fn qwen_params_serialization() {
1770 let request = RequestBody {
1771 messages: vec![Message::user("What's your name?")],
1772 model: "qwen-plus".to_string(),
1773 enable_thinking: Some(false),
1774 thinking_budget: Some(1024),
1775 top_k: Some(20),
1776 ..Default::default()
1777 };
1778
1779 let json = serde_json::to_string(&request).unwrap();
1780 assert!(json.contains(r#""enable_thinking":false"#), "json: {json}");
1781 assert!(json.contains(r#""thinking_budget":1024"#), "json: {json}");
1782 assert!(json.contains(r#""top_k":20"#), "json: {json}");
1783 }
1784
1785 const QWEN_CHAT_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
1786 const QWEN_MULTIMODAL_MODEL: &str = "qwen3.8-flash";
1789
1790 fn qwen_api_key() -> Option<String> {
1791 std::env::var("QWEN_API_KEY")
1792 .ok()
1793 .map(|key| key.trim().to_string())
1794 .filter(|key| !key.is_empty())
1795 }
1796
1797 #[tokio::test]
1801 async fn test_qwen_image_input() -> Result<(), anyhow::Error> {
1802 let Some(api_key) = qwen_api_key() else {
1803 println!("Skipping: set QWEN_API_KEY to run this test");
1804 return Ok(());
1805 };
1806
1807 let request = RequestBody {
1808 messages: vec![
1809 Message::system("This is a request of test purpose. Reply briefly"),
1810 Message::user(MessageContent::Parts(vec![
1811 ContentPart::ImageUrl {
1812 image_url: ContentPartImageUrl {
1813 url: "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/xzsgiz/football1.jpg"
1814 .to_string(),
1815 detail: None,
1816 },
1817 prompt_cache_breakpoint: None,
1818 },
1819 ContentPart::Text {
1820 text: "What is shown in this image? Answer with one short sentence."
1821 .to_string(),
1822 prompt_cache_breakpoint: None,
1823 },
1824 ])),
1825 ],
1826 model: QWEN_MULTIMODAL_MODEL.to_string(),
1827 ..Default::default()
1828 };
1829
1830 let response = request
1831 .get_response(
1832 &crate::rest::default_client(),
1833 QWEN_CHAT_URL,
1834 &crate::rest::RequestOptions::bearer(&api_key),
1835 )
1836 .await?;
1837
1838 let content = response.choices[0]
1839 .message
1840 .content
1841 .clone()
1842 .unwrap_or_default();
1843 println!("image response: {content}");
1844 assert!(
1845 !content.trim().is_empty(),
1846 "empty content for a valid image request"
1847 );
1848 Ok(())
1849 }
1850
1851 #[tokio::test]
1862 async fn test_qwen_audio_input() -> Result<(), anyhow::Error> {
1863 let Some(api_key) = qwen_api_key() else {
1864 println!("Skipping: set QWEN_API_KEY to run this test");
1865 return Ok(());
1866 };
1867
1868 let request = RequestBody {
1869 messages: vec![Message::user(MessageContent::Parts(vec![
1870 ContentPart::InputAudio {
1871 input_audio: ContentPartInputAudio {
1872 data: "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250211/tixcef/cherry.wav"
1873 .to_string(),
1874 format: InputAudioFormat::Wav,
1875 },
1876 prompt_cache_breakpoint: None,
1877 },
1878 ContentPart::Text {
1879 text: "What does the speaker say in this audio? Reply briefly."
1880 .to_string(),
1881 prompt_cache_breakpoint: None,
1882 },
1883 ]))],
1884 model: "qwen-omni-turbo".to_string(),
1885 stream: Some(true),
1886 modalities: Some(vec![Modality::Text]),
1887 ..Default::default()
1888 };
1889
1890 let mut stream = request
1891 .get_stream_response(
1892 &crate::rest::default_client(),
1893 QWEN_CHAT_URL,
1894 &crate::rest::RequestOptions::bearer(&api_key),
1895 )
1896 .await?;
1897
1898 let mut message = String::new();
1899 while let Some(chunk) = stream.next().await {
1900 let chunk = chunk?;
1901 if let Some(choice) = chunk.choices.first()
1902 && let Some(content) = choice.delta.content.as_deref()
1903 {
1904 message.push_str(content);
1905 }
1906 }
1907
1908 println!("audio response: {message}");
1909 assert!(
1910 !message.trim().is_empty(),
1911 "empty content for a valid audio request"
1912 );
1913 Ok(())
1914 }
1915
1916 #[tokio::test]
1919 async fn test_qwen_text_input() -> Result<(), anyhow::Error> {
1920 let Some(api_key) = qwen_api_key() else {
1921 println!("Skipping: set QWEN_API_KEY to run this test");
1922 return Ok(());
1923 };
1924
1925 let request = RequestBody {
1926 messages: vec![Message::user("Reply with exactly one word.")],
1927 model: QWEN_MULTIMODAL_MODEL.to_string(),
1928 ..Default::default()
1929 };
1930
1931 let response = request
1932 .get_response(
1933 &crate::rest::default_client(),
1934 QWEN_CHAT_URL,
1935 &crate::rest::RequestOptions::bearer(&api_key),
1936 )
1937 .await?;
1938
1939 let content = response.choices[0]
1940 .message
1941 .content
1942 .clone()
1943 .unwrap_or_default();
1944 println!("text response: {content}");
1945 assert!(!content.trim().is_empty(), "empty content for text input");
1946 Ok(())
1947 }
1948
1949 #[tokio::test]
1952 async fn test_qwen_multimodal_stream() -> Result<(), anyhow::Error> {
1953 let Some(api_key) = qwen_api_key() else {
1954 println!("Skipping: set QWEN_API_KEY to run this test");
1955 return Ok(());
1956 };
1957
1958 let request = RequestBody {
1959 messages: vec![Message::user(MessageContent::Parts(vec![
1960 ContentPart::ImageUrl {
1961 image_url: ContentPartImageUrl {
1962 url: "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/xzsgiz/football1.jpg"
1963 .to_string(),
1964 detail: None,
1965 },
1966 prompt_cache_breakpoint: None,
1967 },
1968 ContentPart::Text {
1969 text: "What is shown in this image? Answer with one short sentence."
1970 .to_string(),
1971 prompt_cache_breakpoint: None,
1972 },
1973 ]))],
1974 model: QWEN_MULTIMODAL_MODEL.to_string(),
1975 stream: Some(true),
1976 ..Default::default()
1977 };
1978
1979 let mut stream = request
1980 .get_stream_response(
1981 &crate::rest::default_client(),
1982 QWEN_CHAT_URL,
1983 &crate::rest::RequestOptions::bearer(&api_key),
1984 )
1985 .await?;
1986
1987 let mut message = String::new();
1988 while let Some(chunk) = stream.next().await {
1989 let chunk = chunk?;
1990 if let Some(choice) = chunk.choices.first()
1991 && let Some(content) = choice.delta.content.as_deref()
1992 {
1993 message.push_str(content);
1994 }
1995 }
1996
1997 println!("streamed message: {message}");
1998 assert!(
1999 !message.trim().is_empty(),
2000 "empty streamed content for a valid image request"
2001 );
2002 Ok(())
2003 }
2004}