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(feature = "qwen")]
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub top_k: Option<u32>,
308
309 #[serde(skip_serializing_if = "Option::is_none")]
315 pub user: Option<String>,
316
317 #[serde(skip_serializing_if = "Option::is_none")]
321 pub verbosity: Option<LowMediumHighEnum>,
322
323 #[serde(rename = "web_search_options", skip_serializing_if = "Option::is_none")]
327 pub web_search_options: Option<WebSearchOptions>,
328
329 #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
332 pub extra_body_map: Option<serde_json::Map<String, serde_json::Value>>,
333}
334
335#[derive(Serialize, Deserialize, Debug, Clone)]
348#[serde(tag = "role", rename_all = "lowercase")]
349pub enum Message {
350 System(SystemMessage),
353 User(UserMessage),
356 Assistant(AssistantMessage),
359 Tool(ToolMessage),
362 Function(FunctionMessage),
365 Developer(DeveloperMessage),
368}
369
370impl Message {
371 #[must_use]
374 pub fn system(content: impl Into<MessageContent>) -> Self {
375 Self::System(SystemMessage {
376 content: content.into(),
377 name: None,
378 })
379 }
380
381 #[must_use]
385 pub fn user(content: impl Into<MessageContent>) -> Self {
386 Self::User(UserMessage {
387 content: content.into(),
388 name: None,
389 })
390 }
391
392 #[must_use]
396 pub fn assistant(content: impl Into<String>) -> Self {
397 Self::Assistant(AssistantMessage {
398 content: Some(content.into()),
399 ..Default::default()
400 })
401 }
402
403 #[must_use]
405 pub fn tool(content: impl Into<MessageContent>, tool_call_id: impl Into<String>) -> Self {
406 Self::Tool(ToolMessage {
407 content: content.into(),
408 tool_call_id: tool_call_id.into(),
409 })
410 }
411
412 #[must_use]
415 pub fn function(name: impl Into<String>, content: impl Into<String>) -> Self {
416 Self::Function(FunctionMessage {
417 content: content.into(),
418 name: name.into(),
419 })
420 }
421
422 #[must_use]
425 pub fn developer(content: impl Into<MessageContent>) -> Self {
426 Self::Developer(DeveloperMessage {
427 content: content.into(),
428 name: None,
429 })
430 }
431}
432
433#[derive(Serialize, Deserialize, Debug, Clone, Default)]
435pub struct SystemMessage {
436 pub content: MessageContent,
439 #[serde(skip_serializing_if = "Option::is_none")]
444 pub name: Option<String>,
445}
446
447#[derive(Serialize, Deserialize, Debug, Clone, Default)]
449pub struct UserMessage {
450 pub content: MessageContent,
454 #[serde(skip_serializing_if = "Option::is_none")]
459 pub name: Option<String>,
460}
461
462#[derive(Serialize, Deserialize, Debug, Clone, Default)]
464pub struct AssistantMessage {
465 pub content: Option<String>,
469 #[serde(skip_serializing_if = "Option::is_none")]
472 pub audio: Option<AssistantAudio>,
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub refusal: Option<String>,
476 #[serde(skip_serializing_if = "Option::is_none")]
477 pub name: Option<String>,
478 #[cfg(feature = "deepseek")]
482 #[serde(default, skip_serializing_if = "is_false")]
483 pub prefix: bool,
484 #[cfg(feature = "reasoning")]
492 #[serde(skip_serializing_if = "Option::is_none")]
493 pub reasoning_content: Option<String>,
494
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub tool_calls: Option<Vec<AssistantToolCall>>,
498}
499
500#[derive(Serialize, Deserialize, Debug, Clone, Default)]
502pub struct ToolMessage {
503 pub content: MessageContent,
506 pub tool_call_id: String,
508}
509
510#[derive(Serialize, Deserialize, Debug, Clone, Default)]
512pub struct FunctionMessage {
513 pub content: String,
515 pub name: String,
517}
518
519#[derive(Serialize, Deserialize, Debug, Clone, Default)]
521pub struct DeveloperMessage {
522 pub content: MessageContent,
525 #[serde(skip_serializing_if = "Option::is_none")]
530 pub name: Option<String>,
531}
532
533#[derive(Debug, Serialize, Deserialize, Clone)]
536#[serde(untagged)]
537pub enum MessageContent {
538 Text(String),
540 Parts(Vec<ContentPart>),
543}
544
545impl From<&str> for MessageContent {
546 fn from(value: &str) -> Self {
547 Self::Text(value.to_string())
548 }
549}
550
551impl From<String> for MessageContent {
552 fn from(value: String) -> Self {
553 Self::Text(value)
554 }
555}
556
557impl From<Vec<ContentPart>> for MessageContent {
558 fn from(value: Vec<ContentPart>) -> Self {
559 Self::Parts(value)
560 }
561}
562
563impl Default for MessageContent {
564 fn default() -> Self {
565 Self::Text(String::new())
566 }
567}
568
569#[derive(Debug, Serialize, Deserialize, Clone)]
571#[serde(tag = "type", rename_all = "snake_case")]
572pub enum ContentPart {
573 Text {
575 text: String,
577 #[serde(skip_serializing_if = "Option::is_none")]
580 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
581 },
582 ImageUrl {
584 image_url: ContentPartImageUrl,
586 #[serde(skip_serializing_if = "Option::is_none")]
589 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
590 },
591 InputAudio {
593 input_audio: ContentPartInputAudio,
595 #[serde(skip_serializing_if = "Option::is_none")]
598 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
599 },
600 File {
602 file: ContentPartFile,
605 #[serde(skip_serializing_if = "Option::is_none")]
608 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
609 },
610}
611
612#[derive(Debug, Serialize, Deserialize, Clone)]
614pub struct PromptCacheBreakpoint {
615 pub mode: PromptCacheBreakpointMode,
617}
618
619#[derive(Debug, Serialize, Deserialize, Clone)]
621#[serde(rename_all = "lowercase")]
622pub enum PromptCacheBreakpointMode {
623 Explicit,
624}
625
626#[derive(Debug, Serialize, Deserialize, Clone)]
628pub struct ContentPartImageUrl {
629 pub url: String,
631 #[serde(skip_serializing_if = "Option::is_none")]
634 pub detail: Option<ImageDetail>,
635}
636
637#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
639#[serde(rename_all = "lowercase")]
640pub enum ImageDetail {
641 Auto,
642 Low,
643 High,
644}
645
646#[derive(Debug, Serialize, Deserialize, Clone)]
648pub struct ContentPartInputAudio {
649 pub data: String,
651 pub format: InputAudioFormat,
654}
655
656#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
658#[serde(rename_all = "lowercase")]
659pub enum InputAudioFormat {
660 Wav,
661 Mp3,
662}
663
664#[derive(Debug, Serialize, Deserialize, Clone, Default)]
667pub struct ContentPartFile {
668 #[serde(skip_serializing_if = "Option::is_none")]
671 pub file_data: Option<String>,
672 #[serde(skip_serializing_if = "Option::is_none")]
674 pub file_id: Option<String>,
675 #[serde(skip_serializing_if = "Option::is_none")]
678 pub filename: Option<String>,
679}
680
681#[derive(Debug, Serialize, Deserialize, Clone)]
684pub struct ChatModerationParam {
685 pub model: String,
688 #[serde(skip_serializing_if = "Option::is_none")]
690 pub policy: Option<ModerationPolicyParam>,
691}
692
693#[derive(Debug, Serialize, Deserialize, Clone, Default)]
695pub struct ModerationPolicyParam {
696 #[serde(skip_serializing_if = "Option::is_none")]
698 pub input: Option<ModerationPolicySideParam>,
699 #[serde(skip_serializing_if = "Option::is_none")]
701 pub output: Option<ModerationPolicySideParam>,
702}
703
704#[derive(Debug, Serialize, Deserialize, Clone)]
706pub struct ModerationPolicySideParam {
707 pub mode: ModerationPolicyMode,
710}
711
712#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
714#[serde(rename_all = "lowercase")]
715pub enum ModerationPolicyMode {
716 Score,
717 Block,
718}
719
720#[derive(Debug, Serialize, Deserialize, Clone, Default)]
722pub struct PromptCacheOptions {
723 #[serde(skip_serializing_if = "Option::is_none")]
726 pub mode: Option<PromptCacheMode>,
727 #[serde(skip_serializing_if = "Option::is_none")]
731 pub ttl: Option<PromptCacheTtl>,
732}
733
734#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
736#[serde(rename_all = "lowercase")]
737pub enum PromptCacheMode {
738 Implicit,
739 Explicit,
740}
741
742#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
744pub enum PromptCacheTtl {
745 #[serde(rename = "30m")]
746 ThirtyMinutes,
747}
748
749#[derive(Debug, Serialize, Deserialize, Clone)]
750#[serde(tag = "type", rename_all = "lowercase")]
751pub enum AssistantToolCall {
752 Function {
753 id: String,
755 function: ToolCallFunction,
757 },
758 Custom {
759 id: String,
761 custom: ToolCallCustom,
763 },
764}
765
766#[derive(Debug, Serialize, Deserialize, Clone)]
767pub struct ToolCallFunction {
768 pub arguments: String,
773 pub name: String,
775}
776
777#[derive(Debug, Serialize, Deserialize, Clone)]
778pub struct ToolCallCustom {
779 pub input: String,
781 pub name: String,
783}
784
785#[derive(Debug, Serialize, Deserialize, Clone)]
788pub struct AssistantAudio {
789 pub id: String,
792 #[serde(skip_serializing_if = "Option::is_none")]
794 pub data: Option<String>,
795}
796
797#[derive(Debug, Serialize, Deserialize, Clone)]
798#[serde(tag = "type", rename_all = "snake_case")]
799pub enum ResponseFormat {
800 JsonSchema {
802 json_schema: JSONSchema,
804 },
805 JsonObject,
807 Text,
809}
810
811#[derive(Debug, Serialize, Deserialize, Clone)]
812pub struct JSONSchema {
813 pub name: String,
816 #[serde(skip_serializing_if = "Option::is_none")]
819 pub description: Option<String>,
820 #[serde(skip_serializing_if = "Option::is_none")]
823 pub schema: Option<serde_json::Map<String, serde_json::Value>>,
824 #[serde(skip_serializing_if = "Option::is_none")]
830 pub strict: Option<bool>,
831}
832
833#[derive(Serialize, Deserialize, Debug, Clone)]
834#[serde(rename_all = "snake_case")]
835pub enum Modality {
836 Text,
837 Audio,
838}
839
840#[derive(Serialize, Deserialize, Debug, Clone)]
842pub struct ChatCompletionAudioParam {
843 pub format: AudioFormat,
846 pub voice: Voice,
848}
849
850#[derive(Serialize, Deserialize, Debug, Clone)]
852#[serde(rename_all = "snake_case")]
853pub enum AudioFormat {
854 Wav,
855 Aac,
856 Mp3,
857 Flac,
858 Opus,
859 Pcm16,
860}
861
862#[derive(Serialize, Deserialize, Debug, Clone)]
864#[serde(untagged)]
865pub enum Voice {
866 BuiltIn(String),
869 Custom {
871 id: String,
873 },
874}
875
876#[derive(Serialize, Deserialize, Debug, Clone)]
877pub struct ChatCompletionPredictionContentParam {
878 pub content: ChatCompletionPredictionContentParamContent,
882
883 #[serde(rename = "type")]
886 pub type_: ChatCompletionPredictionContentParamType,
887}
888
889#[derive(Serialize, Deserialize, Debug, Clone)]
890#[serde(untagged)]
891pub enum ChatCompletionPredictionContentParamContent {
892 Text(String),
893 ChatCompletionContentPartTextParam {
894 text: String,
896 #[serde(rename = "type")]
898 type_: ChatCompletionContentPartTextParamType,
899 },
900}
901
902#[derive(Serialize, Deserialize, Debug, Clone)]
903#[serde(rename_all = "snake_case")]
904pub enum ChatCompletionContentPartTextParamType {
905 Text,
906}
907
908#[derive(Serialize, Deserialize, Debug, Clone)]
909#[serde(rename_all = "snake_case")]
910pub enum ChatCompletionPredictionContentParamType {
911 Content,
912}
913
914#[cfg(feature = "deepseek")]
916#[inline]
917fn is_false(value: &bool) -> bool {
918 !value
919}
920
921#[derive(Serialize, Deserialize, Debug, Clone)]
922#[serde(untagged)]
923pub enum StopKeywords {
924 Word(String),
925 Words(Vec<String>),
926}
927
928#[derive(Serialize, Deserialize, Debug, Clone)]
929#[serde(rename_all = "snake_case")]
930pub enum LowMediumHighEnum {
931 Low,
932 Medium,
933 High,
934}
935
936#[derive(Serialize, Deserialize, Debug, Clone, Default)]
937pub struct WebSearchOptions {
938 #[serde(skip_serializing_if = "Option::is_none")]
941 pub search_context_size: Option<LowMediumHighEnum>,
942
943 #[serde(skip_serializing_if = "Option::is_none")]
944 pub user_location: Option<WebSearchOptionsUserLocation>,
945}
946
947#[derive(Serialize, Deserialize, Debug, Clone)]
948#[serde(tag = "type", rename_all = "snake_case")]
949pub enum WebSearchOptionsUserLocation {
950 Approximate {
952 approximate: WebSearchOptionsUserLocationApproximate,
954 },
955}
956
957#[derive(Serialize, Deserialize, Debug, Clone, Default)]
958pub struct WebSearchOptionsUserLocationApproximate {
959 #[serde(skip_serializing_if = "Option::is_none")]
961 pub city: Option<String>,
962
963 #[serde(skip_serializing_if = "Option::is_none")]
966 pub country: Option<String>,
967
968 #[serde(skip_serializing_if = "Option::is_none")]
970 pub region: Option<String>,
971
972 #[serde(skip_serializing_if = "Option::is_none")]
975 pub timezone: Option<String>,
976}
977
978#[derive(Serialize, Deserialize, Debug, Clone)]
979pub struct StreamOptions {
980 pub include_usage: bool,
989}
990
991#[derive(Serialize, Deserialize, Debug, Clone)]
992#[serde(tag = "type", rename_all = "snake_case")]
993pub enum RequestTool {
994 Function { function: ToolFunction },
996 Custom {
998 custom: ToolCustom,
1000 },
1001}
1002
1003#[derive(Serialize, Deserialize, Debug, Clone)]
1004pub struct ToolFunction {
1005 pub name: String,
1009 #[serde(skip_serializing_if = "Option::is_none")]
1012 pub description: Option<String>,
1013 #[serde(skip_serializing_if = "Option::is_none")]
1023 pub parameters: Option<serde_json::Map<String, serde_json::Value>>,
1024 #[serde(skip_serializing_if = "Option::is_none")]
1031 pub strict: Option<bool>,
1032}
1033
1034#[derive(Serialize, Deserialize, Debug, Clone)]
1035pub struct ToolCustom {
1036 pub name: String,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub description: Option<String>,
1041 #[serde(skip_serializing_if = "Option::is_none")]
1043 pub format: Option<ToolCustomFormat>,
1044}
1045
1046#[derive(Serialize, Deserialize, Debug, Clone)]
1047#[serde(rename_all = "snake_case", tag = "type")]
1048pub enum ToolCustomFormat {
1049 Text,
1051 Grammar {
1053 grammar: ToolCustomFormatGrammarGrammar,
1055 },
1056}
1057
1058#[derive(Debug, Serialize, Deserialize, Clone)]
1059pub struct ToolCustomFormatGrammarGrammar {
1060 pub definition: String,
1062 pub syntax: ToolCustomFormatGrammarGrammarSyntax,
1064}
1065
1066#[derive(Debug, Serialize, Deserialize, Clone)]
1067#[serde(rename_all = "snake_case")]
1068pub enum ToolCustomFormatGrammarGrammarSyntax {
1069 Lark,
1070 Regex,
1071}
1072
1073#[derive(Debug, Serialize, Deserialize, Clone)]
1074#[serde(rename_all = "snake_case")]
1075pub enum ToolChoice {
1076 None,
1077 Auto,
1078 Required,
1079 #[serde(untagged)]
1080 Specific(ToolChoiceSpecific),
1081}
1082
1083#[derive(Debug, Serialize, Deserialize, Clone)]
1084#[serde(rename_all = "snake_case", tag = "type")]
1085pub enum ToolChoiceSpecific {
1086 AllowedTools {
1088 allowed_tools: ToolChoiceAllowedTools,
1090 },
1091 Function { function: ToolChoiceFunction },
1093 Custom { custom: ToolChoiceCustom },
1095}
1096
1097#[derive(Debug, Serialize, Deserialize, Clone)]
1098pub struct ToolChoiceAllowedTools {
1099 pub mode: ToolChoiceAllowedToolsMode,
1105 pub tools: Vec<serde_json::Map<String, serde_json::Value>>,
1116}
1117
1118#[derive(Debug, Serialize, Deserialize, Clone)]
1126#[serde(rename_all = "lowercase")]
1127pub enum ToolChoiceAllowedToolsMode {
1128 Auto,
1130 Required,
1132}
1133
1134#[derive(Debug, Serialize, Deserialize, Clone)]
1135pub struct ToolChoiceFunction {
1136 pub name: String,
1138}
1139
1140#[derive(Debug, Serialize, Deserialize, Clone)]
1141pub struct ToolChoiceCustom {
1142 pub name: String,
1144}
1145
1146#[cfg(feature = "deepseek")]
1148#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1149pub struct DeepSeekThinking {
1150 #[serde(rename = "type")]
1153 pub type_: DeepSeekThinkingType,
1154}
1155
1156#[cfg(feature = "deepseek")]
1158#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1159#[serde(rename_all = "lowercase")]
1160pub enum DeepSeekThinkingType {
1161 Enabled,
1162 Disabled,
1163}
1164
1165#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1170#[serde(rename_all = "lowercase")]
1171pub enum ReasoningEffort {
1172 None,
1173 Minimal,
1174 Low,
1175 Medium,
1176 High,
1177 Xhigh,
1178 Max,
1179}
1180
1181impl RequestBody {
1182 pub fn is_streaming(&self) -> bool {
1185 self.stream.unwrap_or(false)
1186 }
1187}
1188
1189impl Post for RequestBody {
1190 fn is_streaming(&self) -> bool {
1191 RequestBody::is_streaming(self)
1192 }
1193
1194 fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
1198 let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
1199 url.path_segments_mut()
1200 .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
1201 .push("chat")
1202 .push("completions");
1203
1204 Ok(url.to_string())
1205 }
1206}
1207
1208impl PostNoStream for RequestBody {
1209 type Response = super::response::no_streaming::ChatCompletion;
1210}
1211
1212impl PostStream for RequestBody {
1213 type Response = super::response::streaming::ChatCompletionChunk;
1214}
1215
1216#[cfg(test)]
1217mod request_test {
1218 use futures_util::StreamExt;
1219
1220 use super::*;
1221
1222 const DEEPSEEK_CHAT_URL: &str = "https://api.deepseek.com";
1223 const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
1224
1225 fn deepseek_api_key() -> Option<String> {
1226 std::env::var("DEEPSEEK_API_KEY")
1227 .ok()
1228 .map(|key| key.trim().to_string())
1229 .filter(|key| !key.is_empty())
1230 }
1231
1232 #[tokio::test]
1233 async fn test_deepseek_no_stream() {
1234 let Some(api_key) = deepseek_api_key() else {
1235 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
1236 return;
1237 };
1238
1239 let request = RequestBody {
1240 messages: vec![
1241 Message::system("This is a request of test purpose. Reply briefly"),
1242 Message::user("What's your name?"),
1243 ],
1244 model: DEEPSEEK_MODEL.to_string(),
1245 stream: Some(false),
1246 ..Default::default()
1247 };
1248
1249 let response = request
1250 .get_response_string(
1251 &crate::rest::default_client(),
1252 DEEPSEEK_CHAT_URL,
1253 &crate::rest::RequestOptions::bearer(&api_key),
1254 )
1255 .await
1256 .unwrap();
1257
1258 println!("{}", response);
1259
1260 assert!(response.to_ascii_lowercase().contains("deepseek"));
1261 }
1262
1263 #[tokio::test]
1264 async fn test_deepseek_stream() {
1265 let Some(api_key) = deepseek_api_key() else {
1266 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
1267 return;
1268 };
1269
1270 let request = RequestBody {
1271 messages: vec![
1272 Message::system("This is a request of test purpose. Reply briefly"),
1273 Message::user("Who are you?"),
1274 ],
1275 model: DEEPSEEK_MODEL.to_string(),
1276 stream: Some(true),
1277 ..Default::default()
1278 };
1279
1280 let mut response = request
1281 .get_stream_response_string(
1282 &crate::rest::default_client(),
1283 DEEPSEEK_CHAT_URL,
1284 &crate::rest::RequestOptions::bearer(&api_key),
1285 )
1286 .await
1287 .unwrap();
1288
1289 while let Some(chunk) = response.next().await {
1290 println!("{}", chunk.unwrap());
1291 }
1292 }
1293
1294 #[test]
1297 fn assistant_tool_call_serialization() {
1298 let function_call = AssistantToolCall::Function {
1299 id: "call_abc".to_string(),
1300 function: ToolCallFunction {
1301 arguments: "{\"city\":\"paris\"}".to_string(),
1302 name: "get_weather".to_string(),
1303 },
1304 };
1305 let json = serde_json::to_string(&function_call).unwrap();
1306 assert!(json.contains(r#""type":"function""#), "json: {json}");
1307 assert!(!json.contains(r#""role""#), "json: {json}");
1308
1309 let custom_call = AssistantToolCall::Custom {
1310 id: "call_def".to_string(),
1311 custom: ToolCallCustom {
1312 input: "2+2".to_string(),
1313 name: "calculator".to_string(),
1314 },
1315 };
1316 let json = serde_json::to_string(&custom_call).unwrap();
1317 assert!(json.contains(r#""type":"custom""#), "json: {json}");
1318 assert!(!json.contains(r#""role""#), "json: {json}");
1319 }
1320
1321 #[test]
1324 fn prediction_type_serialization() {
1325 let prediction = ChatCompletionPredictionContentParam {
1326 content: ChatCompletionPredictionContentParamContent::Text(
1327 "The capital of France is Paris.".to_string(),
1328 ),
1329 type_: ChatCompletionPredictionContentParamType::Content,
1330 };
1331 let json = serde_json::to_string(&prediction).unwrap();
1332 assert!(json.contains(r#""type":"content""#), "json: {json}");
1333 assert!(!json.contains("type_"), "json: {json}");
1334 }
1335
1336 #[test]
1339 fn allowed_tools_choice_serialization() {
1340 let mut weather = serde_json::Map::new();
1341 weather.insert("type".to_string(), serde_json::json!("function"));
1342 weather.insert(
1343 "function".to_string(),
1344 serde_json::json!({ "name": "get_weather" }),
1345 );
1346
1347 let choice = ToolChoiceSpecific::AllowedTools {
1348 allowed_tools: ToolChoiceAllowedTools {
1349 mode: ToolChoiceAllowedToolsMode::Required,
1350 tools: vec![weather],
1351 },
1352 };
1353 let json = serde_json::to_string(&choice).unwrap();
1354 assert!(json.contains(r#""type":"allowed_tools""#), "json: {json}");
1355 assert!(json.contains(r#""mode":"required""#), "json: {json}");
1356 assert!(json.contains(r#""tools":[{"#), "json: {json}");
1358 }
1359
1360 #[test]
1363 fn web_search_options_serialization() {
1364 let options = WebSearchOptions {
1365 search_context_size: None,
1366 user_location: Some(WebSearchOptionsUserLocation::Approximate {
1367 approximate: WebSearchOptionsUserLocationApproximate {
1368 city: Some("San Francisco".to_string()),
1369 country: None,
1370 region: None,
1371 timezone: None,
1372 },
1373 }),
1374 };
1375 let json = serde_json::to_string(&options).unwrap();
1376 assert!(!json.contains("search_context_size"), "json: {json}");
1377 assert!(json.contains(r#""type":"approximate""#), "json: {json}");
1378 assert!(
1379 json.contains(r#""approximate":{"city":"San Francisco"}"#),
1380 "json: {json}"
1381 );
1382 }
1383
1384 #[test]
1386 fn json_schema_optional_fields_serialization() {
1387 let schema = JSONSchema {
1388 name: "Answer".to_string(),
1389 description: None,
1390 schema: None,
1391 strict: None,
1392 };
1393 let json = serde_json::to_string(&schema).unwrap();
1394 assert_eq!(json, r#"{"name":"Answer"}"#);
1395
1396 let function = ToolFunction {
1397 name: "get_weather".to_string(),
1398 description: None,
1399 parameters: None,
1400 strict: None,
1401 };
1402 let json = serde_json::to_string(&function).unwrap();
1403 assert_eq!(json, r#"{"name":"get_weather"}"#);
1404 }
1405
1406 #[test]
1409 fn user_text_content_serialization() {
1410 let request = RequestBody {
1411 messages: vec![Message::user("Hi")],
1412 model: "gpt-4o".to_string(),
1413 ..Default::default()
1414 };
1415
1416 let json = serde_json::to_string(&request).unwrap();
1417 assert!(json.contains(r#""content":"Hi""#), "json: {json}");
1418 }
1419
1420 #[test]
1423 fn message_deserialization() {
1424 let system: Message =
1425 serde_json::from_str(r#"{"role":"system","content":"Be terse"}"#).unwrap();
1426 assert!(matches!(
1427 system,
1428 Message::System(SystemMessage {
1429 content: MessageContent::Text(_),
1430 name: None
1431 })
1432 ));
1433
1434 let user: Message =
1435 serde_json::from_str(r#"{"role":"user","content":"Hi","name":"jimmy"}"#).unwrap();
1436 let Message::User(user) = user else {
1437 panic!("must be a user message");
1438 };
1439 assert_eq!(user.name.as_deref(), Some("jimmy"));
1440
1441 let assistant: Message = serde_json::from_str(
1442 r#"{"role":"assistant","content":null,"tool_calls":[{"type":"function","id":"call_1","function":{"name":"f","arguments":"{}"}}]}"#,
1443 )
1444 .unwrap();
1445 let Message::Assistant(assistant) = assistant else {
1446 panic!("must be an assistant message");
1447 };
1448 assert_eq!(assistant.content, None);
1449 assert_eq!(assistant.tool_calls.expect("tool calls").len(), 1);
1450
1451 let tool: Message =
1452 serde_json::from_str(r#"{"role":"tool","content":"42","tool_call_id":"call_1"}"#)
1453 .unwrap();
1454 let Message::Tool(tool) = tool else {
1455 panic!("must be a tool message");
1456 };
1457 assert_eq!(tool.tool_call_id, "call_1");
1458
1459 let developer: Message =
1460 serde_json::from_str(r#"{"role":"developer","content":"New rules"}"#).unwrap();
1461 assert!(matches!(developer, Message::Developer(_)));
1462
1463 let function: Message =
1464 serde_json::from_str(r#"{"role":"function","name":"f","content":"ok"}"#).unwrap();
1465 assert!(matches!(function, Message::Function(_)));
1466 }
1467
1468 #[test]
1471 fn unknown_role_fails_deserialization() {
1472 let result = serde_json::from_str::<Message>(r#"{"role":"weird","content":"x"}"#);
1473 assert!(result.is_err(), "unknown roles must be rejected");
1474 }
1475
1476 #[test]
1480 fn request_body_deserializes_with_extra_fields() {
1481 let json = r#"{
1482 "model": "qwen-plus",
1483 "messages": [{"role": "user", "content": "Hi"}],
1484 "stream": true,
1485 "vendor_extension": {"depth": 3}
1486 }"#;
1487 let request = serde_json::from_str::<RequestBody>(json).unwrap();
1488 assert_eq!(request.model, "qwen-plus");
1489 assert_eq!(request.stream, Some(true));
1490 assert_eq!(request.messages.len(), 1);
1491
1492 let extra = request
1493 .extra_body_map
1494 .as_ref()
1495 .expect("extra fields captured");
1496 assert_eq!(
1497 extra.get("vendor_extension"),
1498 Some(&serde_json::json!({"depth": 3}))
1499 );
1500
1501 let serialized = serde_json::to_value(&request).unwrap();
1502 assert_eq!(serialized["vendor_extension"]["depth"], 3);
1503 }
1504
1505 #[test]
1507 fn message_constructors() {
1508 let request = RequestBody {
1509 messages: vec![
1510 Message::system("Be terse"),
1511 Message::user("Hi"),
1512 Message::assistant("Hello!"),
1513 Message::tool(r#"{"temp":21}"#, "call_1"),
1514 ],
1515 model: "gpt-4o".to_string(),
1516 ..Default::default()
1517 };
1518
1519 let json = serde_json::to_string(&request).unwrap();
1520 assert!(json.contains(r#""role":"system","content":"Be terse""#),);
1521 assert!(json.contains(r#""role":"user","content":"Hi""#));
1522 assert!(json.contains(r#""role":"assistant","content":"Hello!""#));
1523 assert!(
1524 json.contains(r#""role":"tool","content":"{\"temp\":21}","tool_call_id":"call_1""#)
1525 );
1526 }
1527
1528 #[test]
1532 fn system_developer_tool_content_serialization() {
1533 let request = RequestBody {
1534 messages: vec![
1535 Message::system("Be terse"),
1536 Message::developer(MessageContent::Parts(vec![ContentPart::Text {
1537 text: "Prefer Rust".to_string(),
1538 prompt_cache_breakpoint: None,
1539 }])),
1540 Message::tool(
1541 MessageContent::Parts(vec![ContentPart::Text {
1542 text: r#"{"temp": 21}"#.to_string(),
1543 prompt_cache_breakpoint: None,
1544 }]),
1545 "call_1",
1546 ),
1547 ],
1548 model: "gpt-4o".to_string(),
1549 ..Default::default()
1550 };
1551
1552 let json = serde_json::to_string(&request).unwrap();
1553 assert!(
1554 json.contains(r#""role":"system","content":"Be terse""#),
1555 "json: {json}"
1556 );
1557 assert!(
1558 json.contains(r#""role":"developer","content":[{"type":"text","text":"Prefer Rust"}]"#),
1559 "json: {json}"
1560 );
1561 assert!(
1562 json.contains(
1563 r#""role":"tool","content":[{"type":"text","text":"{\"temp\": 21}"}],"tool_call_id":"call_1""#
1564 ),
1565 "json: {json}"
1566 );
1567 }
1568
1569 #[test]
1572 fn multimodal_content_serialization() {
1573 let request = RequestBody {
1574 messages: vec![Message::user(MessageContent::Parts(vec![
1575 ContentPart::ImageUrl {
1576 image_url: ContentPartImageUrl {
1577 url: "https://example.com/cat.png".to_string(),
1578 detail: Some(ImageDetail::High),
1579 },
1580 prompt_cache_breakpoint: None,
1581 },
1582 ContentPart::Text {
1583 text: "What's in this image?".to_string(),
1584 prompt_cache_breakpoint: Some(PromptCacheBreakpoint {
1585 mode: PromptCacheBreakpointMode::Explicit,
1586 }),
1587 },
1588 ]))],
1589 model: "gpt-4o".to_string(),
1590 ..Default::default()
1591 };
1592
1593 let json = serde_json::to_string(&request).unwrap();
1594 assert!(json.contains(r#""type":"image_url""#), "json: {json}");
1595 assert!(
1596 json.contains(r#""url":"https://example.com/cat.png""#),
1597 "json: {json}"
1598 );
1599 assert!(json.contains(r#""detail":"high""#), "json: {json}");
1600 assert!(json.contains(r#""type":"text""#), "json: {json}");
1601 assert!(
1602 json.contains(r#""prompt_cache_breakpoint":{"mode":"explicit"}"#),
1603 "json: {json}"
1604 );
1605 }
1606
1607 #[test]
1610 fn audio_and_file_content_serialization() {
1611 let content = MessageContent::Parts(vec![
1612 ContentPart::InputAudio {
1613 input_audio: ContentPartInputAudio {
1614 data: "aGVsbG8=".to_string(),
1615 format: InputAudioFormat::Wav,
1616 },
1617 prompt_cache_breakpoint: None,
1618 },
1619 ContentPart::File {
1620 file: ContentPartFile {
1621 file_id: Some("file-abc".to_string()),
1622 ..Default::default()
1623 },
1624 prompt_cache_breakpoint: None,
1625 },
1626 ]);
1627
1628 let json = serde_json::to_string(&content).unwrap();
1629 assert!(json.contains(r#""type":"input_audio""#), "json: {json}");
1630 assert!(json.contains(r#""data":"aGVsbG8=""#), "json: {json}");
1631 assert!(json.contains(r#""format":"wav""#), "json: {json}");
1632 assert!(json.contains(r#""type":"file""#), "json: {json}");
1633 assert!(
1634 json.contains(r#""file":{"file_id":"file-abc"}"#),
1635 "json: {json}"
1636 );
1637 assert!(!json.contains("file_data"), "json: {json}");
1639 }
1640
1641 #[test]
1644 fn new_params_serialization() {
1645 let mut logit_bias = HashMap::new();
1646 logit_bias.insert(40u32, -100i32);
1647
1648 let request = RequestBody {
1649 messages: vec![Message::user("Hi")],
1650 model: "gpt-5".to_string(),
1651 logit_bias: Some(logit_bias),
1652 moderation: Some(ChatModerationParam {
1653 model: "omni-moderation-latest".to_string(),
1654 policy: Some(ModerationPolicyParam {
1655 input: Some(ModerationPolicySideParam {
1656 mode: ModerationPolicyMode::Block,
1657 }),
1658 output: None,
1659 }),
1660 }),
1661 prompt_cache_options: Some(PromptCacheOptions {
1662 mode: Some(PromptCacheMode::Explicit),
1663 ttl: Some(PromptCacheTtl::ThirtyMinutes),
1664 }),
1665 ..Default::default()
1666 };
1667
1668 let json = serde_json::to_string(&request).unwrap();
1669 assert!(json.contains(r#""logit_bias":{"40":-100}"#), "json: {json}");
1670 assert!(
1671 json.contains(
1672 r#""moderation":{"model":"omni-moderation-latest","policy":{"input":{"mode":"block"}}}"#
1673 ),
1674 "json: {json}"
1675 );
1676 assert!(
1677 json.contains(r#""prompt_cache_options":{"mode":"explicit","ttl":"30m"}"#),
1678 "json: {json}"
1679 );
1680 }
1681
1682 #[test]
1684 fn reasoning_effort_serialization() {
1685 let request = RequestBody {
1686 messages: vec![Message::user("What's your name?")],
1687 model: "gpt-5".to_string(),
1688 reasoning_effort: Some(ReasoningEffort::Xhigh),
1689 ..Default::default()
1690 };
1691
1692 let json = serde_json::to_string(&request).unwrap();
1693 assert!(
1694 json.contains(r#""reasoning_effort":"xhigh""#),
1695 "json: {json}"
1696 );
1697 }
1698
1699 #[cfg(feature = "deepseek")]
1701 #[test]
1702 fn deepseek_assistant_prefix_serialization() {
1703 let request = RequestBody {
1704 messages: vec![
1705 Message::user("Please write quick sort code"),
1706 Message::Assistant(AssistantMessage {
1707 content: Some("```python\n".to_string()),
1708 prefix: true,
1709 ..Default::default()
1710 }),
1711 ],
1712 model: DEEPSEEK_MODEL.to_string(),
1713 ..Default::default()
1714 };
1715
1716 let json = serde_json::to_string(&request).unwrap();
1717 assert!(json.contains(r#""prefix":true"#), "json: {json}");
1718 }
1719
1720 #[cfg(feature = "deepseek")]
1723 #[test]
1724 fn deepseek_thinking_params_serialization() {
1725 let request = RequestBody {
1726 messages: vec![Message::user("What's your name?")],
1727 model: DEEPSEEK_MODEL.to_string(),
1728 thinking: Some(DeepSeekThinking {
1729 type_: DeepSeekThinkingType::Disabled,
1730 }),
1731 user_id: Some("user-123".to_string()),
1732 ..Default::default()
1733 };
1734
1735 let json = serde_json::to_string(&request).unwrap();
1736 assert!(
1737 json.contains(r#""thinking":{"type":"disabled"}"#),
1738 "json: {json}"
1739 );
1740 assert!(json.contains(r#""user_id":"user-123""#), "json: {json}");
1741 }
1742
1743 #[cfg(feature = "qwen")]
1746 #[test]
1747 fn qwen_params_serialization() {
1748 let request = RequestBody {
1749 messages: vec![Message::user("What's your name?")],
1750 model: "qwen-plus".to_string(),
1751 enable_thinking: Some(false),
1752 thinking_budget: Some(1024),
1753 top_k: Some(20),
1754 ..Default::default()
1755 };
1756
1757 let json = serde_json::to_string(&request).unwrap();
1758 assert!(json.contains(r#""enable_thinking":false"#), "json: {json}");
1759 assert!(json.contains(r#""thinking_budget":1024"#), "json: {json}");
1760 assert!(json.contains(r#""top_k":20"#), "json: {json}");
1761 }
1762
1763 const QWEN_CHAT_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
1764 const QWEN_MULTIMODAL_MODEL: &str = "qwen3.8-flash";
1767
1768 fn qwen_api_key() -> Option<String> {
1769 std::env::var("QWEN_API_KEY")
1770 .ok()
1771 .map(|key| key.trim().to_string())
1772 .filter(|key| !key.is_empty())
1773 }
1774
1775 #[tokio::test]
1779 async fn test_qwen_image_input() -> Result<(), anyhow::Error> {
1780 let Some(api_key) = qwen_api_key() else {
1781 println!("Skipping: set QWEN_API_KEY to run this test");
1782 return Ok(());
1783 };
1784
1785 let request = RequestBody {
1786 messages: vec![
1787 Message::system("This is a request of test purpose. Reply briefly"),
1788 Message::user(MessageContent::Parts(vec![
1789 ContentPart::ImageUrl {
1790 image_url: ContentPartImageUrl {
1791 url: "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/xzsgiz/football1.jpg"
1792 .to_string(),
1793 detail: None,
1794 },
1795 prompt_cache_breakpoint: None,
1796 },
1797 ContentPart::Text {
1798 text: "What is shown in this image? Answer with one short sentence."
1799 .to_string(),
1800 prompt_cache_breakpoint: None,
1801 },
1802 ])),
1803 ],
1804 model: QWEN_MULTIMODAL_MODEL.to_string(),
1805 ..Default::default()
1806 };
1807
1808 let response = request
1809 .get_response(
1810 &crate::rest::default_client(),
1811 QWEN_CHAT_URL,
1812 &crate::rest::RequestOptions::bearer(&api_key),
1813 )
1814 .await?;
1815
1816 let content = response.choices[0]
1817 .message
1818 .content
1819 .clone()
1820 .unwrap_or_default();
1821 println!("image response: {content}");
1822 assert!(
1823 !content.trim().is_empty(),
1824 "empty content for a valid image request"
1825 );
1826 Ok(())
1827 }
1828
1829 #[tokio::test]
1840 async fn test_qwen_audio_input() -> Result<(), anyhow::Error> {
1841 let Some(api_key) = qwen_api_key() else {
1842 println!("Skipping: set QWEN_API_KEY to run this test");
1843 return Ok(());
1844 };
1845
1846 let request = RequestBody {
1847 messages: vec![Message::user(MessageContent::Parts(vec![
1848 ContentPart::InputAudio {
1849 input_audio: ContentPartInputAudio {
1850 data: "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250211/tixcef/cherry.wav"
1851 .to_string(),
1852 format: InputAudioFormat::Wav,
1853 },
1854 prompt_cache_breakpoint: None,
1855 },
1856 ContentPart::Text {
1857 text: "What does the speaker say in this audio? Reply briefly."
1858 .to_string(),
1859 prompt_cache_breakpoint: None,
1860 },
1861 ]))],
1862 model: "qwen-omni-turbo".to_string(),
1863 stream: Some(true),
1864 modalities: Some(vec![Modality::Text]),
1865 ..Default::default()
1866 };
1867
1868 let mut stream = request
1869 .get_stream_response(
1870 &crate::rest::default_client(),
1871 QWEN_CHAT_URL,
1872 &crate::rest::RequestOptions::bearer(&api_key),
1873 )
1874 .await?;
1875
1876 let mut message = String::new();
1877 while let Some(chunk) = stream.next().await {
1878 let chunk = chunk?;
1879 if let Some(choice) = chunk.choices.first()
1880 && let Some(content) = choice.delta.content.as_deref()
1881 {
1882 message.push_str(content);
1883 }
1884 }
1885
1886 println!("audio response: {message}");
1887 assert!(
1888 !message.trim().is_empty(),
1889 "empty content for a valid audio request"
1890 );
1891 Ok(())
1892 }
1893
1894 #[tokio::test]
1897 async fn test_qwen_text_input() -> Result<(), anyhow::Error> {
1898 let Some(api_key) = qwen_api_key() else {
1899 println!("Skipping: set QWEN_API_KEY to run this test");
1900 return Ok(());
1901 };
1902
1903 let request = RequestBody {
1904 messages: vec![Message::user("Reply with exactly one word.")],
1905 model: QWEN_MULTIMODAL_MODEL.to_string(),
1906 ..Default::default()
1907 };
1908
1909 let response = request
1910 .get_response(
1911 &crate::rest::default_client(),
1912 QWEN_CHAT_URL,
1913 &crate::rest::RequestOptions::bearer(&api_key),
1914 )
1915 .await?;
1916
1917 let content = response.choices[0]
1918 .message
1919 .content
1920 .clone()
1921 .unwrap_or_default();
1922 println!("text response: {content}");
1923 assert!(!content.trim().is_empty(), "empty content for text input");
1924 Ok(())
1925 }
1926
1927 #[tokio::test]
1930 async fn test_qwen_multimodal_stream() -> Result<(), anyhow::Error> {
1931 let Some(api_key) = qwen_api_key() else {
1932 println!("Skipping: set QWEN_API_KEY to run this test");
1933 return Ok(());
1934 };
1935
1936 let request = RequestBody {
1937 messages: vec![Message::user(MessageContent::Parts(vec![
1938 ContentPart::ImageUrl {
1939 image_url: ContentPartImageUrl {
1940 url: "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/xzsgiz/football1.jpg"
1941 .to_string(),
1942 detail: None,
1943 },
1944 prompt_cache_breakpoint: None,
1945 },
1946 ContentPart::Text {
1947 text: "What is shown in this image? Answer with one short sentence."
1948 .to_string(),
1949 prompt_cache_breakpoint: None,
1950 },
1951 ]))],
1952 model: QWEN_MULTIMODAL_MODEL.to_string(),
1953 stream: Some(true),
1954 ..Default::default()
1955 };
1956
1957 let mut stream = request
1958 .get_stream_response(
1959 &crate::rest::default_client(),
1960 QWEN_CHAT_URL,
1961 &crate::rest::RequestOptions::bearer(&api_key),
1962 )
1963 .await?;
1964
1965 let mut message = String::new();
1966 while let Some(chunk) = stream.next().await {
1967 let chunk = chunk?;
1968 if let Some(choice) = chunk.choices.first()
1969 && let Some(content) = choice.delta.content.as_deref()
1970 {
1971 message.push_str(content);
1972 }
1973 }
1974
1975 println!("streamed message: {message}");
1976 assert!(
1977 !message.trim().is_empty(),
1978 "empty streamed content for a valid image request"
1979 );
1980 Ok(())
1981 }
1982}