1use super::{
2 client::{ApiErrorResponse, ApiResponse, Client, Usage},
3 streaming::StreamingCompletionResponse,
4};
5use crate::message::{
6 self, AudioMediaType, DocumentMediaType, DocumentSourceKind, ImageDetail, MimeType,
7 VideoMediaType,
8};
9use crate::telemetry::SpanCombinator;
10use crate::{
11 OneOrMany,
12 completion::{self, CompletionError, CompletionRequest},
13 http_client::HttpClientExt,
14 json_utils,
15 one_or_many::string_or_one_or_many,
16 providers::openai,
17};
18use bytes::Bytes;
19use serde::{Deserialize, Serialize, Serializer};
20use std::collections::HashMap;
21use tracing::{Instrument, Level, enabled, info_span};
22
23pub const QWEN_QWQ_32B: &str = "qwen/qwq-32b";
29pub const CLAUDE_3_7_SONNET: &str = "anthropic/claude-3.7-sonnet";
31pub const PERPLEXITY_SONAR_PRO: &str = "perplexity/sonar-pro";
33pub const GEMINI_FLASH_2_0: &str = "google/gemini-2.0-flash-001";
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
45#[serde(rename_all = "lowercase")]
46pub enum DataCollection {
47 #[default]
49 Allow,
50 Deny,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum Quantization {
60 #[serde(rename = "int4")]
62 Int4,
63 #[serde(rename = "int8")]
65 Int8,
66 #[serde(rename = "fp16")]
68 Fp16,
69 #[serde(rename = "bf16")]
71 Bf16,
72 #[serde(rename = "fp32")]
74 Fp32,
75 #[serde(rename = "fp8")]
77 Fp8,
78 #[serde(rename = "unknown")]
80 Unknown,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum ProviderSortStrategy {
91 Price,
93 Throughput,
95 Latency,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "lowercase")]
102pub enum SortPartition {
103 Model,
105 None,
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct ProviderSortConfig {
114 pub by: ProviderSortStrategy,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub partition: Option<SortPartition>,
120}
121
122impl ProviderSortConfig {
123 pub fn new(by: ProviderSortStrategy) -> Self {
125 Self {
126 by,
127 partition: None,
128 }
129 }
130
131 pub fn partition(mut self, partition: SortPartition) -> Self {
133 self.partition = Some(partition);
134 self
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[serde(untagged)]
144pub enum ProviderSort {
145 Simple(ProviderSortStrategy),
147 Complex(ProviderSortConfig),
149}
150
151impl From<ProviderSortStrategy> for ProviderSort {
152 fn from(strategy: ProviderSortStrategy) -> Self {
153 ProviderSort::Simple(strategy)
154 }
155}
156
157impl From<ProviderSortConfig> for ProviderSort {
158 fn from(config: ProviderSortConfig) -> Self {
159 ProviderSort::Complex(config)
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167#[serde(untagged)]
168pub enum ThroughputThreshold {
169 Simple(f64),
171 Percentile(PercentileThresholds),
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[serde(untagged)]
180pub enum LatencyThreshold {
181 Simple(f64),
183 Percentile(PercentileThresholds),
185}
186
187#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
189pub struct PercentileThresholds {
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub p50: Option<f64>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub p75: Option<f64>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub p90: Option<f64>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub p99: Option<f64>,
202}
203
204impl PercentileThresholds {
205 pub fn new() -> Self {
207 Self::default()
208 }
209
210 pub fn p50(mut self, value: f64) -> Self {
212 self.p50 = Some(value);
213 self
214 }
215
216 pub fn p75(mut self, value: f64) -> Self {
218 self.p75 = Some(value);
219 self
220 }
221
222 pub fn p90(mut self, value: f64) -> Self {
224 self.p90 = Some(value);
225 self
226 }
227
228 pub fn p99(mut self, value: f64) -> Self {
230 self.p99 = Some(value);
231 self
232 }
233}
234
235#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
240pub struct MaxPrice {
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub prompt: Option<f64>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub completion: Option<f64>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub request: Option<f64>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub image: Option<f64>,
253}
254
255impl MaxPrice {
256 pub fn new() -> Self {
258 Self::default()
259 }
260
261 pub fn prompt(mut self, price: f64) -> Self {
263 self.prompt = Some(price);
264 self
265 }
266
267 pub fn completion(mut self, price: f64) -> Self {
269 self.completion = Some(price);
270 self
271 }
272
273 pub fn request(mut self, price: f64) -> Self {
275 self.request = Some(price);
276 self
277 }
278
279 pub fn image(mut self, price: f64) -> Self {
281 self.image = Some(price);
282 self
283 }
284}
285
286#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
306pub struct ProviderPreferences {
307 #[serde(skip_serializing_if = "Option::is_none")]
311 pub order: Option<Vec<String>>,
312
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub only: Option<Vec<String>>,
316
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub ignore: Option<Vec<String>>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
324 pub allow_fallbacks: Option<bool>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
333 pub require_parameters: Option<bool>,
334
335 #[serde(skip_serializing_if = "Option::is_none")]
338 pub data_collection: Option<DataCollection>,
339
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub zdr: Option<bool>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
348 pub sort: Option<ProviderSort>,
349
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub preferred_min_throughput: Option<ThroughputThreshold>,
353
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub preferred_max_latency: Option<LatencyThreshold>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub max_price: Option<MaxPrice>,
361
362 #[serde(skip_serializing_if = "Option::is_none")]
365 pub quantizations: Option<Vec<Quantization>>,
366}
367
368impl ProviderPreferences {
369 pub fn new() -> Self {
371 Self::default()
372 }
373
374 pub fn order(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
390 self.order = Some(providers.into_iter().map(|p| p.into()).collect());
391 self
392 }
393
394 pub fn only(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
406 self.only = Some(providers.into_iter().map(|p| p.into()).collect());
407 self
408 }
409
410 pub fn ignore(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
421 self.ignore = Some(providers.into_iter().map(|p| p.into()).collect());
422 self
423 }
424
425 pub fn allow_fallbacks(mut self, allow: bool) -> Self {
430 self.allow_fallbacks = Some(allow);
431 self
432 }
433
434 pub fn require_parameters(mut self, require: bool) -> Self {
440 self.require_parameters = Some(require);
441 self
442 }
443
444 pub fn data_collection(mut self, policy: DataCollection) -> Self {
448 self.data_collection = Some(policy);
449 self
450 }
451
452 pub fn zdr(mut self, enable: bool) -> Self {
463 self.zdr = Some(enable);
464 self
465 }
466
467 pub fn sort(mut self, sort: impl Into<ProviderSort>) -> Self {
483 self.sort = Some(sort.into());
484 self
485 }
486
487 pub fn preferred_min_throughput(mut self, threshold: ThroughputThreshold) -> Self {
507 self.preferred_min_throughput = Some(threshold);
508 self
509 }
510
511 pub fn preferred_max_latency(mut self, threshold: LatencyThreshold) -> Self {
515 self.preferred_max_latency = Some(threshold);
516 self
517 }
518
519 pub fn max_price(mut self, price: MaxPrice) -> Self {
523 self.max_price = Some(price);
524 self
525 }
526
527 pub fn quantizations(mut self, quantizations: impl IntoIterator<Item = Quantization>) -> Self {
540 self.quantizations = Some(quantizations.into_iter().collect());
541 self
542 }
543
544 pub fn zero_data_retention(self) -> Self {
548 self.zdr(true)
549 }
550
551 pub fn fastest(self) -> Self {
553 self.sort(ProviderSortStrategy::Throughput)
554 }
555
556 pub fn cheapest(self) -> Self {
558 self.sort(ProviderSortStrategy::Price)
559 }
560
561 pub fn lowest_latency(self) -> Self {
563 self.sort(ProviderSortStrategy::Latency)
564 }
565
566 pub fn to_json(&self) -> serde_json::Value {
568 serde_json::json!({
569 "provider": self
570 })
571 }
572}
573
574#[derive(Debug, Serialize, Deserialize)]
578pub struct CompletionResponse {
579 pub id: String,
580 pub object: String,
581 pub created: u64,
582 pub model: String,
583 pub choices: Vec<Choice>,
584 pub system_fingerprint: Option<String>,
585 pub usage: Option<Usage>,
586}
587
588impl From<ApiErrorResponse> for CompletionError {
589 fn from(err: ApiErrorResponse) -> Self {
590 CompletionError::ProviderError(err.message)
591 }
592}
593
594impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
595 type Error = CompletionError;
596
597 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
598 let choice = response.choices.first().ok_or_else(|| {
599 CompletionError::ResponseError("Response contained no choices".to_owned())
600 })?;
601
602 let content = match &choice.message {
603 Message::Assistant {
604 content,
605 tool_calls,
606 reasoning,
607 reasoning_details,
608 ..
609 } => {
610 let mut content = content
611 .iter()
612 .map(|c| match c {
613 openai::AssistantContent::Text { text } => {
614 completion::AssistantContent::text(text)
615 }
616 openai::AssistantContent::Refusal { refusal } => {
617 completion::AssistantContent::text(refusal)
618 }
619 })
620 .collect::<Vec<_>>();
621
622 content.extend(tool_calls.iter().map(|call| {
623 completion::AssistantContent::tool_call(
624 &call.id,
625 &call.function.name,
626 call.function.arguments.clone(),
627 )
628 }));
629
630 let mut grouped_reasoning: HashMap<
631 Option<String>,
632 Vec<(usize, usize, message::ReasoningContent)>,
633 > = HashMap::new();
634 let mut reasoning_order: Vec<Option<String>> = Vec::new();
635 for (position, detail) in reasoning_details.iter().enumerate() {
636 let (reasoning_id, sort_index, parsed_content) = match detail {
637 ReasoningDetails::Summary {
638 id, index, summary, ..
639 } => (
640 id.clone(),
641 *index,
642 Some(message::ReasoningContent::Summary(summary.clone())),
643 ),
644 ReasoningDetails::Encrypted {
645 id, index, data, ..
646 } => (
647 id.clone(),
648 *index,
649 Some(message::ReasoningContent::Encrypted(data.clone())),
650 ),
651 ReasoningDetails::Text {
652 id,
653 index,
654 text,
655 signature,
656 ..
657 } => (
658 id.clone(),
659 *index,
660 text.as_ref().map(|text| message::ReasoningContent::Text {
661 text: text.clone(),
662 signature: signature.clone(),
663 }),
664 ),
665 };
666
667 let Some(parsed_content) = parsed_content else {
668 continue;
669 };
670 let sort_index = sort_index.unwrap_or(position);
671
672 let entry = grouped_reasoning.entry(reasoning_id.clone());
673 if matches!(entry, std::collections::hash_map::Entry::Vacant(_)) {
674 reasoning_order.push(reasoning_id);
675 }
676 entry
677 .or_default()
678 .push((sort_index, position, parsed_content));
679 }
680
681 if grouped_reasoning.is_empty() {
682 if let Some(reasoning) = reasoning {
683 content.push(completion::AssistantContent::reasoning(reasoning));
684 }
685 } else {
686 for reasoning_id in reasoning_order {
687 let Some(mut blocks) = grouped_reasoning.remove(&reasoning_id) else {
688 continue;
689 };
690 blocks.sort_by_key(|(index, position, _)| (*index, *position));
691 content.push(completion::AssistantContent::Reasoning(
692 message::Reasoning {
693 id: reasoning_id,
694 content: blocks
695 .into_iter()
696 .map(|(_, _, content)| content)
697 .collect::<Vec<_>>(),
698 },
699 ));
700 }
701 }
702
703 Ok(content)
704 }
705 _ => Err(CompletionError::ResponseError(
706 "Response did not contain a valid message or tool call".into(),
707 )),
708 }?;
709
710 let choice = OneOrMany::many(content).map_err(|_| {
711 CompletionError::ResponseError(
712 "Response contained no message or tool call (empty)".to_owned(),
713 )
714 })?;
715
716 let usage = response
717 .usage
718 .as_ref()
719 .map(|usage| completion::Usage {
720 input_tokens: usage.prompt_tokens as u64,
721 output_tokens: (usage.total_tokens - usage.prompt_tokens) as u64,
722 total_tokens: usage.total_tokens as u64,
723 cached_input_tokens: 0,
724 cache_creation_input_tokens: 0,
725 reasoning_tokens: 0,
726 })
727 .unwrap_or_default();
728
729 Ok(completion::CompletionResponse {
730 choice,
731 usage,
732 raw_response: response,
733 message_id: None,
734 })
735 }
736}
737
738#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
778#[serde(tag = "type", rename_all = "snake_case")]
779pub enum UserContent {
780 Text { text: String },
782
783 #[serde(rename = "image_url")]
787 ImageUrl { image_url: ImageUrl },
788
789 File { file: FileContent },
794
795 InputAudio { input_audio: openai::InputAudio },
799
800 #[serde(rename = "video_url")]
805 VideoUrl { video_url: VideoUrlContent },
806}
807
808impl UserContent {
809 pub fn text(text: impl Into<String>) -> Self {
811 UserContent::Text { text: text.into() }
812 }
813
814 pub fn image_url(url: impl Into<String>) -> Self {
816 UserContent::ImageUrl {
817 image_url: ImageUrl {
818 url: url.into(),
819 detail: None,
820 },
821 }
822 }
823
824 pub fn image_url_with_detail(url: impl Into<String>, detail: ImageDetail) -> Self {
826 UserContent::ImageUrl {
827 image_url: ImageUrl {
828 url: url.into(),
829 detail: Some(detail),
830 },
831 }
832 }
833
834 pub fn image_base64(
841 data: impl Into<String>,
842 mime_type: &str,
843 detail: Option<ImageDetail>,
844 ) -> Self {
845 let data_uri = format!("data:{};base64,{}", mime_type, data.into());
846 UserContent::ImageUrl {
847 image_url: ImageUrl {
848 url: data_uri,
849 detail,
850 },
851 }
852 }
853
854 pub fn file_url(url: impl Into<String>, filename: Option<String>) -> Self {
860 UserContent::File {
861 file: FileContent {
862 filename,
863 file_data: Some(url.into()),
864 },
865 }
866 }
867
868 pub fn file_base64(data: impl Into<String>, mime_type: &str, filename: Option<String>) -> Self {
875 let data_uri = format!("data:{};base64,{}", mime_type, data.into());
876 UserContent::File {
877 file: FileContent {
878 filename,
879 file_data: Some(data_uri),
880 },
881 }
882 }
883
884 pub fn audio_base64(data: impl Into<String>, format: AudioMediaType) -> Self {
892 UserContent::InputAudio {
893 input_audio: openai::InputAudio {
894 data: data.into(),
895 format,
896 },
897 }
898 }
899
900 pub fn video_url(url: impl Into<String>) -> Self {
907 UserContent::VideoUrl {
908 video_url: VideoUrlContent { url: url.into() },
909 }
910 }
911
912 pub fn video_base64(data: impl Into<String>, media_type: VideoMediaType) -> Self {
918 let mime = media_type.to_mime_type();
919 let data_uri = format!("data:{mime};base64,{}", data.into());
920 UserContent::VideoUrl {
921 video_url: VideoUrlContent { url: data_uri },
922 }
923 }
924}
925
926impl From<String> for UserContent {
927 fn from(text: String) -> Self {
928 UserContent::Text { text }
929 }
930}
931
932impl From<&str> for UserContent {
933 fn from(text: &str) -> Self {
934 UserContent::Text {
935 text: text.to_string(),
936 }
937 }
938}
939
940impl std::str::FromStr for UserContent {
941 type Err = std::convert::Infallible;
942
943 fn from_str(s: &str) -> Result<Self, Self::Err> {
944 Ok(UserContent::Text {
945 text: s.to_string(),
946 })
947 }
948}
949
950#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
952pub struct ImageUrl {
953 pub url: String,
955 #[serde(skip_serializing_if = "Option::is_none")]
957 pub detail: Option<ImageDetail>,
958}
959
960#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
966pub struct VideoUrlContent {
967 pub url: String,
969}
970
971#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
978pub struct FileContent {
979 #[serde(skip_serializing_if = "Option::is_none")]
981 pub filename: Option<String>,
982 #[serde(skip_serializing_if = "Option::is_none")]
984 pub file_data: Option<String>,
985}
986
987fn serialize_user_content<S>(
990 content: &OneOrMany<UserContent>,
991 serializer: S,
992) -> Result<S::Ok, S::Error>
993where
994 S: Serializer,
995{
996 if content.len() == 1
997 && let UserContent::Text { text } = content.first_ref()
998 {
999 return serializer.serialize_str(text);
1000 }
1001 content.serialize(serializer)
1002}
1003
1004impl TryFrom<message::UserContent> for UserContent {
1005 type Error = message::MessageError;
1006
1007 fn try_from(value: message::UserContent) -> Result<Self, Self::Error> {
1008 match value {
1009 message::UserContent::Text(message::Text { text }) => Ok(UserContent::Text { text }),
1010
1011 message::UserContent::Image(message::Image {
1012 data,
1013 detail,
1014 media_type,
1015 ..
1016 }) => {
1017 let url = match data {
1018 DocumentSourceKind::Url(url) => url,
1019 DocumentSourceKind::Base64(data) => {
1020 let mime = media_type
1021 .ok_or_else(|| {
1022 message::MessageError::ConversionError(
1023 "Image media type required for base64 encoding".into(),
1024 )
1025 })?
1026 .to_mime_type();
1027 format!("data:{mime};base64,{data}")
1028 }
1029 DocumentSourceKind::Raw(_) => {
1030 return Err(message::MessageError::ConversionError(
1031 "Raw bytes not supported, encode as base64 first".into(),
1032 ));
1033 }
1034 DocumentSourceKind::FileId(_) => {
1035 return Err(message::MessageError::ConversionError(
1036 "File IDs are not supported for images".into(),
1037 ));
1038 }
1039 DocumentSourceKind::String(_) => {
1040 return Err(message::MessageError::ConversionError(
1041 "String source not supported for images".into(),
1042 ));
1043 }
1044 DocumentSourceKind::Unknown => {
1045 return Err(message::MessageError::ConversionError(
1046 "Image has no data".into(),
1047 ));
1048 }
1049 };
1050 Ok(UserContent::ImageUrl {
1051 image_url: ImageUrl { url, detail },
1052 })
1053 }
1054
1055 message::UserContent::Document(message::Document {
1056 data, media_type, ..
1057 }) => match data {
1058 DocumentSourceKind::FileId(_) => Err(message::MessageError::ConversionError(
1059 "Provider file IDs are not supported for OpenRouter document inputs".into(),
1060 )),
1061 DocumentSourceKind::Url(url) => {
1062 let filename = media_type.as_ref().map(|mt| match mt {
1063 DocumentMediaType::PDF => "document.pdf",
1064 DocumentMediaType::TXT => "document.txt",
1065 DocumentMediaType::HTML => "document.html",
1066 DocumentMediaType::MARKDOWN => "document.md",
1067 DocumentMediaType::CSV => "document.csv",
1068 DocumentMediaType::XML => "document.xml",
1069 _ => "document",
1070 });
1071 Ok(UserContent::File {
1072 file: FileContent {
1073 filename: filename.map(String::from),
1074 file_data: Some(url),
1075 },
1076 })
1077 }
1078 DocumentSourceKind::Base64(data) => {
1079 let mime = media_type
1080 .as_ref()
1081 .map(|m| m.to_mime_type())
1082 .unwrap_or("application/pdf");
1083 let data_uri = format!("data:{mime};base64,{data}");
1084
1085 let filename = media_type.as_ref().map(|mt| match mt {
1086 DocumentMediaType::PDF => "document.pdf",
1087 DocumentMediaType::TXT => "document.txt",
1088 DocumentMediaType::HTML => "document.html",
1089 DocumentMediaType::MARKDOWN => "document.md",
1090 DocumentMediaType::CSV => "document.csv",
1091 DocumentMediaType::XML => "document.xml",
1092 _ => "document",
1093 });
1094
1095 Ok(UserContent::File {
1096 file: FileContent {
1097 filename: filename.map(String::from),
1098 file_data: Some(data_uri),
1099 },
1100 })
1101 }
1102 DocumentSourceKind::String(text) => Ok(UserContent::Text { text }),
1103 DocumentSourceKind::Raw(_) => Err(message::MessageError::ConversionError(
1104 "Raw bytes not supported for documents, encode as base64 first".into(),
1105 )),
1106 DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
1107 "Document has no data".into(),
1108 )),
1109 },
1110
1111 message::UserContent::Audio(message::Audio {
1112 data, media_type, ..
1113 }) => match data {
1114 DocumentSourceKind::Base64(data) => {
1115 let format = media_type.ok_or_else(|| {
1116 message::MessageError::ConversionError(
1117 "Audio media type required for base64 encoding".into(),
1118 )
1119 })?;
1120 Ok(UserContent::InputAudio {
1121 input_audio: openai::InputAudio { data, format },
1122 })
1123 }
1124 DocumentSourceKind::Url(_) => Err(message::MessageError::ConversionError(
1125 "OpenRouter does not support audio URLs, encode as base64 first".into(),
1126 )),
1127 DocumentSourceKind::Raw(_) => Err(message::MessageError::ConversionError(
1128 "Raw bytes not supported for audio, encode as base64 first".into(),
1129 )),
1130 DocumentSourceKind::FileId(_) => Err(message::MessageError::ConversionError(
1131 "File IDs are not supported for audio".into(),
1132 )),
1133 DocumentSourceKind::String(_) => Err(message::MessageError::ConversionError(
1134 "String source not supported for audio".into(),
1135 )),
1136 DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
1137 "Audio has no data".into(),
1138 )),
1139 },
1140
1141 message::UserContent::Video(message::Video {
1142 data, media_type, ..
1143 }) => {
1144 let url = match data {
1145 DocumentSourceKind::Url(url) => url,
1146 DocumentSourceKind::Base64(data) => {
1147 let mime = media_type
1148 .ok_or_else(|| {
1149 message::MessageError::ConversionError(
1150 "Video media type required for base64 encoding".into(),
1151 )
1152 })?
1153 .to_mime_type();
1154 format!("data:{mime};base64,{data}")
1155 }
1156 DocumentSourceKind::Raw(_) => {
1157 return Err(message::MessageError::ConversionError(
1158 "Raw bytes not supported for video, encode as base64 first".into(),
1159 ));
1160 }
1161 DocumentSourceKind::FileId(_) => {
1162 return Err(message::MessageError::ConversionError(
1163 "File IDs are not supported for video".into(),
1164 ));
1165 }
1166 DocumentSourceKind::String(_) => {
1167 return Err(message::MessageError::ConversionError(
1168 "String source not supported for video".into(),
1169 ));
1170 }
1171 DocumentSourceKind::Unknown => {
1172 return Err(message::MessageError::ConversionError(
1173 "Video has no data".into(),
1174 ));
1175 }
1176 };
1177 Ok(UserContent::VideoUrl {
1178 video_url: VideoUrlContent { url },
1179 })
1180 }
1181
1182 message::UserContent::ToolResult(_) => Err(message::MessageError::ConversionError(
1183 "Tool results should be handled as separate messages".into(),
1184 )),
1185 }
1186 }
1187}
1188
1189impl TryFrom<OneOrMany<message::UserContent>> for Vec<Message> {
1190 type Error = message::MessageError;
1191
1192 fn try_from(value: OneOrMany<message::UserContent>) -> Result<Self, Self::Error> {
1193 let (tool_results, other_content): (Vec<_>, Vec<_>) = value
1194 .into_iter()
1195 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1196
1197 if !tool_results.is_empty() {
1200 tool_results
1201 .into_iter()
1202 .map(|content| match content {
1203 message::UserContent::ToolResult(tool_result) => Ok(Message::ToolResult {
1204 tool_call_id: tool_result.id,
1205 content: tool_result
1206 .content
1207 .into_iter()
1208 .map(|c| match c {
1209 message::ToolResultContent::Text(message::Text { text }) => text,
1210 message::ToolResultContent::Image(_) => {
1211 "[Image content not supported in tool results]".to_string()
1212 }
1213 })
1214 .collect::<Vec<_>>()
1215 .join("\n"),
1216 }),
1217 _ => Err(message::MessageError::ConversionError(
1218 "expected tool result content while converting OpenRouter input".into(),
1219 )),
1220 })
1221 .collect::<Result<Vec<_>, _>>()
1222 } else {
1223 let user_content: Vec<UserContent> = other_content
1224 .into_iter()
1225 .map(|content| content.try_into())
1226 .collect::<Result<Vec<_>, _>>()?;
1227
1228 let content = OneOrMany::many(user_content).map_err(|_| {
1229 message::MessageError::ConversionError(
1230 "OpenRouter user message did not contain any non-tool content".into(),
1231 )
1232 })?;
1233
1234 Ok(vec![Message::User {
1235 content,
1236 name: None,
1237 }])
1238 }
1239 }
1240}
1241
1242#[derive(Debug, Deserialize, Serialize)]
1247pub struct Choice {
1248 pub index: usize,
1249 pub native_finish_reason: Option<String>,
1250 pub message: Message,
1251 pub finish_reason: Option<String>,
1252}
1253
1254#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1260#[serde(tag = "role", rename_all = "lowercase")]
1261pub enum Message {
1262 #[serde(alias = "developer")]
1263 System {
1264 #[serde(deserialize_with = "string_or_one_or_many")]
1265 content: OneOrMany<openai::SystemContent>,
1266 #[serde(skip_serializing_if = "Option::is_none")]
1267 name: Option<String>,
1268 },
1269 User {
1270 #[serde(
1271 deserialize_with = "string_or_one_or_many",
1272 serialize_with = "serialize_user_content"
1273 )]
1274 content: OneOrMany<UserContent>,
1275 #[serde(skip_serializing_if = "Option::is_none")]
1276 name: Option<String>,
1277 },
1278 Assistant {
1279 #[serde(
1280 default,
1281 deserialize_with = "json_utils::string_or_vec",
1282 skip_serializing_if = "Vec::is_empty"
1283 )]
1284 content: Vec<openai::AssistantContent>,
1285 #[serde(skip_serializing_if = "Option::is_none")]
1286 refusal: Option<String>,
1287 #[serde(skip_serializing_if = "Option::is_none")]
1288 audio: Option<openai::AudioAssistant>,
1289 #[serde(skip_serializing_if = "Option::is_none")]
1290 name: Option<String>,
1291 #[serde(
1292 default,
1293 deserialize_with = "json_utils::null_or_vec",
1294 skip_serializing_if = "Vec::is_empty"
1295 )]
1296 tool_calls: Vec<openai::ToolCall>,
1297 #[serde(skip_serializing_if = "Option::is_none")]
1298 reasoning: Option<String>,
1299 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1300 reasoning_details: Vec<ReasoningDetails>,
1301 },
1302 #[serde(rename = "tool")]
1303 ToolResult {
1304 tool_call_id: String,
1305 content: String,
1306 },
1307}
1308
1309impl Message {
1310 pub fn system(content: &str) -> Self {
1311 Message::System {
1312 content: OneOrMany::one(content.to_owned().into()),
1313 name: None,
1314 }
1315 }
1316}
1317
1318#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1319#[serde(tag = "type", rename_all = "snake_case")]
1320pub enum ReasoningDetails {
1321 #[serde(rename = "reasoning.summary")]
1322 Summary {
1323 id: Option<String>,
1324 format: Option<String>,
1325 index: Option<usize>,
1326 summary: String,
1327 },
1328 #[serde(rename = "reasoning.encrypted")]
1329 Encrypted {
1330 id: Option<String>,
1331 format: Option<String>,
1332 index: Option<usize>,
1333 data: String,
1334 },
1335 #[serde(rename = "reasoning.text")]
1336 Text {
1337 id: Option<String>,
1338 format: Option<String>,
1339 index: Option<usize>,
1340 text: Option<String>,
1341 signature: Option<String>,
1342 },
1343}
1344
1345#[derive(Debug, Deserialize, PartialEq, Clone)]
1346#[serde(untagged)]
1347enum ToolCallAdditionalParams {
1348 ReasoningDetails(ReasoningDetails),
1349 Minimal {
1350 id: Option<String>,
1351 format: Option<String>,
1352 },
1353}
1354
1355impl TryFrom<openai::UserContent> for UserContent {
1357 type Error = message::MessageError;
1358
1359 fn try_from(value: openai::UserContent) -> Result<Self, Self::Error> {
1360 Ok(match value {
1361 openai::UserContent::Text { text } => UserContent::Text { text },
1362 openai::UserContent::Image { image_url } => UserContent::ImageUrl {
1363 image_url: ImageUrl {
1364 url: image_url.url,
1365 detail: Some(image_url.detail),
1366 },
1367 },
1368 openai::UserContent::Audio { input_audio } => UserContent::InputAudio { input_audio },
1369 openai::UserContent::File { file } => match file.file_data {
1370 Some(file_data) => UserContent::File {
1371 file: FileContent {
1372 filename: file.filename,
1373 file_data: Some(file_data),
1374 },
1375 },
1376 None => {
1377 return Err(message::MessageError::ConversionError(
1378 "OpenRouter file inputs require URL or base64 file_data; provider file IDs are not supported".into(),
1379 ));
1380 }
1381 },
1382 })
1383 }
1384}
1385
1386impl TryFrom<openai::Message> for Message {
1387 type Error = message::MessageError;
1388
1389 fn try_from(value: openai::Message) -> Result<Self, Self::Error> {
1390 Ok(match value {
1391 openai::Message::System { content, name } => Self::System { content, name },
1392 openai::Message::User { content, name } => {
1393 let converted_content = content.try_map(UserContent::try_from)?;
1394 Self::User {
1395 content: converted_content,
1396 name,
1397 }
1398 }
1399 openai::Message::Assistant {
1400 content,
1401 reasoning,
1402 refusal,
1403 audio,
1404 name,
1405 tool_calls,
1406 } => Self::Assistant {
1407 content,
1408 refusal,
1409 audio,
1410 name,
1411 tool_calls,
1412 reasoning,
1413 reasoning_details: Vec::new(),
1414 },
1415 openai::Message::ToolResult {
1416 tool_call_id,
1417 content,
1418 } => Self::ToolResult {
1419 tool_call_id,
1420 content: content.as_text(),
1421 },
1422 })
1423 }
1424}
1425
1426impl TryFrom<OneOrMany<message::AssistantContent>> for Vec<Message> {
1427 type Error = message::MessageError;
1428
1429 fn try_from(value: OneOrMany<message::AssistantContent>) -> Result<Self, Self::Error> {
1430 let mut text_content = Vec::new();
1431 let mut tool_calls = Vec::new();
1432 let mut reasoning = None;
1433 let mut reasoning_details = Vec::new();
1434
1435 for content in value.into_iter() {
1436 match content {
1437 message::AssistantContent::Text(text) => text_content.push(text),
1438 message::AssistantContent::ToolCall(tool_call) => {
1439 if let Some(additional_params) = &tool_call.additional_params
1445 && let Ok(additional_params) =
1446 serde_json::from_value::<ToolCallAdditionalParams>(
1447 additional_params.clone(),
1448 )
1449 {
1450 match additional_params {
1451 ToolCallAdditionalParams::ReasoningDetails(full) => {
1452 reasoning_details.push(full);
1453 }
1454 ToolCallAdditionalParams::Minimal { id, format } => {
1455 let id = id.or_else(|| tool_call.call_id.clone());
1456 if let Some(signature) = &tool_call.signature
1457 && let Some(id) = id
1458 {
1459 reasoning_details.push(ReasoningDetails::Encrypted {
1460 id: Some(id),
1461 format,
1462 index: None,
1463 data: signature.clone(),
1464 })
1465 }
1466 }
1467 }
1468 } else if let Some(signature) = &tool_call.signature {
1469 reasoning_details.push(ReasoningDetails::Encrypted {
1470 id: tool_call.call_id.clone(),
1471 format: None,
1472 index: None,
1473 data: signature.clone(),
1474 });
1475 }
1476 tool_calls.push(tool_call.into())
1477 }
1478 message::AssistantContent::Reasoning(r) => {
1479 if r.content.is_empty() {
1480 let display = r.display_text();
1481 if !display.is_empty() {
1482 reasoning = Some(display);
1483 }
1484 } else {
1485 for reasoning_block in &r.content {
1486 let index = Some(reasoning_details.len());
1487 match reasoning_block {
1488 message::ReasoningContent::Text { text, signature } => {
1489 reasoning_details.push(ReasoningDetails::Text {
1490 id: r.id.clone(),
1491 format: None,
1492 index,
1493 text: Some(text.clone()),
1494 signature: signature.clone(),
1495 });
1496 }
1497 message::ReasoningContent::Summary(summary) => {
1498 reasoning_details.push(ReasoningDetails::Summary {
1499 id: r.id.clone(),
1500 format: None,
1501 index,
1502 summary: summary.clone(),
1503 });
1504 }
1505 message::ReasoningContent::Encrypted(data)
1506 | message::ReasoningContent::Redacted { data } => {
1507 reasoning_details.push(ReasoningDetails::Encrypted {
1508 id: r.id.clone(),
1509 format: None,
1510 index,
1511 data: data.clone(),
1512 });
1513 }
1514 }
1515 }
1516 }
1517 }
1518 message::AssistantContent::Image(_) => {
1519 return Err(Self::Error::ConversionError(
1520 "OpenRouter currently doesn't support images.".into(),
1521 ));
1522 }
1523 }
1524 }
1525
1526 Ok(vec![Message::Assistant {
1529 content: text_content
1530 .into_iter()
1531 .map(|content| content.text.into())
1532 .collect::<Vec<_>>(),
1533 refusal: None,
1534 audio: None,
1535 name: None,
1536 tool_calls,
1537 reasoning,
1538 reasoning_details,
1539 }])
1540 }
1541}
1542
1543impl TryFrom<message::Message> for Vec<Message> {
1546 type Error = message::MessageError;
1547
1548 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1549 match message {
1550 message::Message::System { content } => Ok(vec![Message::System {
1551 content: OneOrMany::one(content.into()),
1552 name: None,
1553 }]),
1554 message::Message::User { content } => {
1555 content.try_into()
1558 }
1559 message::Message::Assistant { content, .. } => content.try_into(),
1560 }
1561 }
1562}
1563
1564#[derive(Debug, Serialize, Deserialize)]
1565#[serde(untagged, rename_all = "snake_case")]
1566pub enum ToolChoice {
1567 None,
1568 Auto,
1569 Required,
1570 Function(Vec<ToolChoiceFunctionKind>),
1571}
1572
1573impl TryFrom<crate::message::ToolChoice> for ToolChoice {
1574 type Error = CompletionError;
1575
1576 fn try_from(value: crate::message::ToolChoice) -> Result<Self, Self::Error> {
1577 let res = match value {
1578 crate::message::ToolChoice::None => Self::None,
1579 crate::message::ToolChoice::Auto => Self::Auto,
1580 crate::message::ToolChoice::Required => Self::Required,
1581 crate::message::ToolChoice::Specific { function_names } => {
1582 let vec: Vec<ToolChoiceFunctionKind> = function_names
1583 .into_iter()
1584 .map(|name| ToolChoiceFunctionKind::Function { name })
1585 .collect();
1586
1587 Self::Function(vec)
1588 }
1589 };
1590
1591 Ok(res)
1592 }
1593}
1594
1595#[derive(Debug, Serialize, Deserialize)]
1596#[serde(tag = "type", content = "function")]
1597pub enum ToolChoiceFunctionKind {
1598 Function { name: String },
1599}
1600
1601#[derive(Debug, Serialize, Deserialize)]
1602pub(super) struct OpenrouterCompletionRequest {
1603 model: String,
1604 pub messages: Vec<Message>,
1605 #[serde(skip_serializing_if = "Option::is_none")]
1606 temperature: Option<f64>,
1607 #[serde(skip_serializing_if = "Vec::is_empty")]
1608 tools: Vec<crate::providers::openai::completion::ToolDefinition>,
1609 #[serde(skip_serializing_if = "Option::is_none")]
1610 tool_choice: Option<crate::providers::openai::completion::ToolChoice>,
1611 #[serde(flatten, skip_serializing_if = "Option::is_none")]
1612 pub additional_params: Option<serde_json::Value>,
1613}
1614
1615pub struct OpenRouterRequestParams<'a> {
1617 pub model: &'a str,
1618 pub request: CompletionRequest,
1619 pub strict_tools: bool,
1620}
1621
1622impl TryFrom<OpenRouterRequestParams<'_>> for OpenrouterCompletionRequest {
1623 type Error = CompletionError;
1624
1625 fn try_from(params: OpenRouterRequestParams) -> Result<Self, Self::Error> {
1626 let OpenRouterRequestParams {
1627 model,
1628 request: req,
1629 strict_tools,
1630 } = params;
1631 let model = req.model.clone().unwrap_or_else(|| model.to_string());
1632
1633 let mut full_history: Vec<Message> = match &req.preamble {
1634 Some(preamble) => vec![Message::system(preamble)],
1635 None => vec![],
1636 };
1637 if let Some(docs) = req.normalized_documents() {
1638 let docs: Vec<Message> = docs.try_into()?;
1639 full_history.extend(docs);
1640 }
1641
1642 let chat_history: Vec<Message> = req
1643 .chat_history
1644 .clone()
1645 .into_iter()
1646 .map(|message| message.try_into())
1647 .collect::<Result<Vec<Vec<Message>>, _>>()?
1648 .into_iter()
1649 .flatten()
1650 .collect();
1651
1652 full_history.extend(chat_history);
1653
1654 let tool_choice = req
1655 .tool_choice
1656 .clone()
1657 .map(crate::providers::openai::completion::ToolChoice::try_from)
1658 .transpose()?;
1659
1660 let tools: Vec<crate::providers::openai::completion::ToolDefinition> = req
1661 .tools
1662 .clone()
1663 .into_iter()
1664 .map(|tool| {
1665 let def = crate::providers::openai::completion::ToolDefinition::from(tool);
1666 if strict_tools { def.with_strict() } else { def }
1667 })
1668 .collect();
1669
1670 let additional_params = if let Some(schema) = req.output_schema {
1671 let name = schema
1672 .as_object()
1673 .and_then(|o| o.get("title"))
1674 .and_then(|v| v.as_str())
1675 .unwrap_or("response_schema")
1676 .to_string();
1677 let mut schema_value = schema.to_value();
1678 openai::sanitize_schema(&mut schema_value);
1679 let response_format = serde_json::json!({
1680 "response_format": {
1681 "type": "json_schema",
1682 "json_schema": {
1683 "name": name,
1684 "strict": true,
1685 "schema": schema_value
1686 }
1687 }
1688 });
1689 Some(match req.additional_params {
1690 Some(existing) => json_utils::merge(existing, response_format),
1691 None => response_format,
1692 })
1693 } else {
1694 req.additional_params
1695 };
1696
1697 Ok(Self {
1698 model,
1699 messages: full_history,
1700 temperature: req.temperature,
1701 tools,
1702 tool_choice,
1703 additional_params,
1704 })
1705 }
1706}
1707
1708impl TryFrom<(&str, CompletionRequest)> for OpenrouterCompletionRequest {
1709 type Error = CompletionError;
1710
1711 fn try_from((model, req): (&str, CompletionRequest)) -> Result<Self, Self::Error> {
1712 let model = req.model.clone().unwrap_or_else(|| model.to_string());
1713 OpenrouterCompletionRequest::try_from(OpenRouterRequestParams {
1714 model: &model,
1715 request: req,
1716 strict_tools: false,
1717 })
1718 }
1719}
1720
1721#[derive(Clone)]
1722pub struct CompletionModel<T = reqwest::Client> {
1723 pub(crate) client: Client<T>,
1724 pub model: String,
1725 pub strict_tools: bool,
1728}
1729
1730impl<T> CompletionModel<T> {
1731 pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
1732 Self {
1733 client,
1734 model: model.into(),
1735 strict_tools: false,
1736 }
1737 }
1738
1739 pub fn with_strict_tools(mut self) -> Self {
1748 self.strict_tools = true;
1749 self
1750 }
1751}
1752
1753impl<T> completion::CompletionModel for CompletionModel<T>
1754where
1755 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
1756{
1757 type Response = CompletionResponse;
1758 type StreamingResponse = StreamingCompletionResponse;
1759
1760 type Client = Client<T>;
1761
1762 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1763 Self::new(client.clone(), model)
1764 }
1765
1766 async fn completion(
1767 &self,
1768 completion_request: CompletionRequest,
1769 ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
1770 let request_model = completion_request
1771 .model
1772 .clone()
1773 .unwrap_or_else(|| self.model.clone());
1774 let preamble = completion_request.preamble.clone();
1775 let request = OpenrouterCompletionRequest::try_from(OpenRouterRequestParams {
1776 model: request_model.as_ref(),
1777 request: completion_request,
1778 strict_tools: self.strict_tools,
1779 })?;
1780
1781 if enabled!(Level::TRACE) {
1782 tracing::trace!(
1783 target: "rig::completions",
1784 "OpenRouter completion request: {}",
1785 serde_json::to_string_pretty(&request)?
1786 );
1787 }
1788
1789 let span = if tracing::Span::current().is_disabled() {
1790 info_span!(
1791 target: "rig::completions",
1792 "chat",
1793 gen_ai.operation.name = "chat",
1794 gen_ai.provider.name = "openrouter",
1795 gen_ai.request.model = &request_model,
1796 gen_ai.system_instructions = preamble,
1797 gen_ai.response.id = tracing::field::Empty,
1798 gen_ai.response.model = tracing::field::Empty,
1799 gen_ai.usage.output_tokens = tracing::field::Empty,
1800 gen_ai.usage.input_tokens = tracing::field::Empty,
1801 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
1802 )
1803 } else {
1804 tracing::Span::current()
1805 };
1806
1807 let body = serde_json::to_vec(&request)?;
1808
1809 let req = self
1810 .client
1811 .post("/chat/completions")?
1812 .body(body)
1813 .map_err(|x| CompletionError::HttpError(x.into()))?;
1814
1815 async move {
1816 let response = self.client.send::<_, Bytes>(req).await?;
1817 let status = response.status();
1818 let response_body = response.into_body().into_future().await?.to_vec();
1819
1820 if status.is_success() {
1821 let parsed: ApiResponse<CompletionResponse> =
1822 serde_json::from_slice(&response_body).map_err(|e| {
1823 CompletionError::ResponseError(format!(
1824 "Failed to parse OpenRouter completion response: {}, response body: {}",
1825 e,
1826 String::from_utf8_lossy(&response_body)
1827 ))
1828 })?;
1829 match parsed {
1830 ApiResponse::Ok(response) => {
1831 let span = tracing::Span::current();
1832 span.record_token_usage(&response.usage);
1833 span.record("gen_ai.response.id", &response.id);
1834 span.record("gen_ai.response.model", &response.model);
1835
1836 tracing::debug!(target: "rig::completions",
1837 "OpenRouter response: {response:?}");
1838 response.try_into()
1839 }
1840 ApiResponse::Err(err) => Err(CompletionError::ProviderError(err.message)),
1841 }
1842 } else {
1843 Err(CompletionError::ProviderError(
1844 String::from_utf8_lossy(&response_body).to_string(),
1845 ))
1846 }
1847 }
1848 .instrument(span)
1849 .await
1850 }
1851
1852 async fn stream(
1853 &self,
1854 completion_request: CompletionRequest,
1855 ) -> Result<
1856 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1857 CompletionError,
1858 > {
1859 CompletionModel::stream(self, completion_request).await
1860 }
1861}
1862
1863#[cfg(test)]
1864mod tests {
1865 use super::*;
1866 use serde_json::json;
1867
1868 #[test]
1869 fn test_openrouter_request_uses_request_model_override() {
1870 let request = CompletionRequest {
1871 model: Some("google/gemini-2.5-flash".to_string()),
1872 preamble: None,
1873 chat_history: crate::OneOrMany::one("Hello".into()),
1874 documents: vec![],
1875 tools: vec![],
1876 temperature: None,
1877 max_tokens: None,
1878 tool_choice: None,
1879 additional_params: None,
1880 output_schema: None,
1881 };
1882
1883 let openrouter_request =
1884 OpenrouterCompletionRequest::try_from(("openai/gpt-4o-mini", request))
1885 .expect("request conversion should succeed");
1886 let serialized =
1887 serde_json::to_value(openrouter_request).expect("serialization should succeed");
1888
1889 assert_eq!(serialized["model"], "google/gemini-2.5-flash");
1890 }
1891
1892 #[test]
1893 fn test_openrouter_request_uses_default_model_when_override_unset() {
1894 let request = CompletionRequest {
1895 model: None,
1896 preamble: None,
1897 chat_history: crate::OneOrMany::one("Hello".into()),
1898 documents: vec![],
1899 tools: vec![],
1900 temperature: None,
1901 max_tokens: None,
1902 tool_choice: None,
1903 additional_params: None,
1904 output_schema: None,
1905 };
1906
1907 let openrouter_request =
1908 OpenrouterCompletionRequest::try_from(("openai/gpt-4o-mini", request))
1909 .expect("request conversion should succeed");
1910 let serialized =
1911 serde_json::to_value(openrouter_request).expect("serialization should succeed");
1912
1913 assert_eq!(serialized["model"], "openai/gpt-4o-mini");
1914 }
1915
1916 #[test]
1917 fn test_openrouter_request_maps_output_schema_to_response_format() {
1918 let schema: schemars::Schema = serde_json::from_value(json!({
1919 "title": "WeatherResponse",
1920 "type": "object",
1921 "properties": {
1922 "city": { "type": "string" },
1923 "weather": { "type": "string" }
1924 }
1925 }))
1926 .expect("schema should deserialize");
1927
1928 let request = CompletionRequest {
1929 model: None,
1930 preamble: None,
1931 chat_history: crate::OneOrMany::one("Hello".into()),
1932 documents: vec![],
1933 tools: vec![],
1934 temperature: None,
1935 max_tokens: None,
1936 tool_choice: None,
1937 additional_params: None,
1938 output_schema: Some(schema),
1939 };
1940
1941 let openrouter_request =
1942 OpenrouterCompletionRequest::try_from(("openai/gpt-4o-mini", request))
1943 .expect("request conversion should succeed");
1944 let serialized =
1945 serde_json::to_value(openrouter_request).expect("serialization should succeed");
1946
1947 assert_eq!(
1948 serialized["response_format"],
1949 json!({
1950 "type": "json_schema",
1951 "json_schema": {
1952 "name": "WeatherResponse",
1953 "strict": true,
1954 "schema": {
1955 "title": "WeatherResponse",
1956 "type": "object",
1957 "properties": {
1958 "city": { "type": "string" },
1959 "weather": { "type": "string" }
1960 },
1961 "additionalProperties": false,
1962 "required": ["city", "weather"]
1963 }
1964 }
1965 })
1966 );
1967 }
1968
1969 #[test]
1970 fn test_openrouter_request_merges_output_schema_with_provider_preferences() {
1971 let schema: schemars::Schema = serde_json::from_value(json!({
1972 "type": "object",
1973 "properties": {
1974 "answer": { "type": "string" }
1975 }
1976 }))
1977 .expect("schema should deserialize");
1978
1979 let request = CompletionRequest {
1980 model: None,
1981 preamble: None,
1982 chat_history: crate::OneOrMany::one("Hello".into()),
1983 documents: vec![],
1984 tools: vec![],
1985 temperature: None,
1986 max_tokens: None,
1987 tool_choice: None,
1988 additional_params: Some(
1989 ProviderPreferences::new()
1990 .require_parameters(true)
1991 .to_json(),
1992 ),
1993 output_schema: Some(schema),
1994 };
1995
1996 let openrouter_request =
1997 OpenrouterCompletionRequest::try_from(("openai/gpt-4o-mini", request))
1998 .expect("request conversion should succeed");
1999 let serialized =
2000 serde_json::to_value(openrouter_request).expect("serialization should succeed");
2001
2002 assert_eq!(serialized["provider"]["require_parameters"], true);
2003 assert_eq!(serialized["response_format"]["type"], "json_schema");
2004 assert_eq!(
2005 serialized["response_format"]["json_schema"]["name"],
2006 "response_schema"
2007 );
2008 assert_eq!(
2009 serialized["response_format"]["json_schema"]["schema"]["additionalProperties"],
2010 false
2011 );
2012 }
2013
2014 #[test]
2015 fn test_completion_response_deserialization_gemini_flash() {
2016 let json = json!({
2018 "id": "gen-AAAAAAAAAA-AAAAAAAAAAAAAAAAAAAA",
2019 "provider": "Google",
2020 "model": "google/gemini-2.5-flash",
2021 "object": "chat.completion",
2022 "created": 1765971703u64,
2023 "choices": [{
2024 "logprobs": null,
2025 "finish_reason": "stop",
2026 "native_finish_reason": "STOP",
2027 "index": 0,
2028 "message": {
2029 "role": "assistant",
2030 "content": "CONTENT",
2031 "refusal": null,
2032 "reasoning": null
2033 }
2034 }],
2035 "usage": {
2036 "prompt_tokens": 669,
2037 "completion_tokens": 5,
2038 "total_tokens": 674
2039 }
2040 });
2041
2042 let response: CompletionResponse = serde_json::from_value(json).unwrap();
2043 assert_eq!(response.id, "gen-AAAAAAAAAA-AAAAAAAAAAAAAAAAAAAA");
2044 assert_eq!(response.model, "google/gemini-2.5-flash");
2045 assert_eq!(response.choices.len(), 1);
2046 assert_eq!(response.choices[0].finish_reason, Some("stop".to_string()));
2047 }
2048
2049 #[test]
2050 fn test_message_assistant_without_reasoning_details() {
2051 let json = json!({
2053 "role": "assistant",
2054 "content": "Hello world",
2055 "refusal": null,
2056 "reasoning": null
2057 });
2058
2059 let message: Message = serde_json::from_value(json).unwrap();
2060 match message {
2061 Message::Assistant {
2062 content,
2063 reasoning_details,
2064 ..
2065 } => {
2066 assert_eq!(content.len(), 1);
2067 assert!(reasoning_details.is_empty());
2068 }
2069 _ => panic!("Expected Assistant message"),
2070 }
2071 }
2072
2073 #[test]
2074 fn test_data_collection_serialization() {
2075 assert_eq!(
2076 serde_json::to_string(&DataCollection::Allow).unwrap(),
2077 r#""allow""#
2078 );
2079 assert_eq!(
2080 serde_json::to_string(&DataCollection::Deny).unwrap(),
2081 r#""deny""#
2082 );
2083 }
2084
2085 #[test]
2086 fn test_data_collection_default() {
2087 assert_eq!(DataCollection::default(), DataCollection::Allow);
2088 }
2089
2090 #[test]
2091 fn test_quantization_serialization() {
2092 assert_eq!(
2093 serde_json::to_string(&Quantization::Int4).unwrap(),
2094 r#""int4""#
2095 );
2096 assert_eq!(
2097 serde_json::to_string(&Quantization::Int8).unwrap(),
2098 r#""int8""#
2099 );
2100 assert_eq!(
2101 serde_json::to_string(&Quantization::Fp16).unwrap(),
2102 r#""fp16""#
2103 );
2104 assert_eq!(
2105 serde_json::to_string(&Quantization::Bf16).unwrap(),
2106 r#""bf16""#
2107 );
2108 assert_eq!(
2109 serde_json::to_string(&Quantization::Fp32).unwrap(),
2110 r#""fp32""#
2111 );
2112 assert_eq!(
2113 serde_json::to_string(&Quantization::Fp8).unwrap(),
2114 r#""fp8""#
2115 );
2116 assert_eq!(
2117 serde_json::to_string(&Quantization::Unknown).unwrap(),
2118 r#""unknown""#
2119 );
2120 }
2121
2122 #[test]
2123 fn test_provider_sort_strategy_serialization() {
2124 assert_eq!(
2125 serde_json::to_string(&ProviderSortStrategy::Price).unwrap(),
2126 r#""price""#
2127 );
2128 assert_eq!(
2129 serde_json::to_string(&ProviderSortStrategy::Throughput).unwrap(),
2130 r#""throughput""#
2131 );
2132 assert_eq!(
2133 serde_json::to_string(&ProviderSortStrategy::Latency).unwrap(),
2134 r#""latency""#
2135 );
2136 }
2137
2138 #[test]
2139 fn test_sort_partition_serialization() {
2140 assert_eq!(
2141 serde_json::to_string(&SortPartition::Model).unwrap(),
2142 r#""model""#
2143 );
2144 assert_eq!(
2145 serde_json::to_string(&SortPartition::None).unwrap(),
2146 r#""none""#
2147 );
2148 }
2149
2150 #[test]
2151 fn test_provider_sort_simple() {
2152 let sort = ProviderSort::Simple(ProviderSortStrategy::Latency);
2153 let json = serde_json::to_value(&sort).unwrap();
2154 assert_eq!(json, "latency");
2155 }
2156
2157 #[test]
2158 fn test_provider_sort_complex() {
2159 let sort = ProviderSort::Complex(
2160 ProviderSortConfig::new(ProviderSortStrategy::Price).partition(SortPartition::None),
2161 );
2162 let json = serde_json::to_value(&sort).unwrap();
2163 assert_eq!(json["by"], "price");
2164 assert_eq!(json["partition"], "none");
2165 }
2166
2167 #[test]
2168 fn test_provider_sort_complex_without_partition() {
2169 let sort = ProviderSort::Complex(ProviderSortConfig::new(ProviderSortStrategy::Throughput));
2170 let json = serde_json::to_value(&sort).unwrap();
2171 assert_eq!(json["by"], "throughput");
2172 assert!(json.get("partition").is_none());
2173 }
2174
2175 #[test]
2176 fn test_provider_sort_from_strategy() {
2177 let sort: ProviderSort = ProviderSortStrategy::Price.into();
2178 assert_eq!(sort, ProviderSort::Simple(ProviderSortStrategy::Price));
2179 }
2180
2181 #[test]
2182 fn test_provider_sort_from_config() {
2183 let config = ProviderSortConfig::new(ProviderSortStrategy::Latency);
2184 let sort: ProviderSort = config.into();
2185 match sort {
2186 ProviderSort::Complex(c) => assert_eq!(c.by, ProviderSortStrategy::Latency),
2187 _ => panic!("Expected Complex variant"),
2188 }
2189 }
2190
2191 #[test]
2192 fn test_percentile_thresholds_builder() {
2193 let thresholds = PercentileThresholds::new()
2194 .p50(10.0)
2195 .p75(25.0)
2196 .p90(50.0)
2197 .p99(100.0);
2198
2199 assert_eq!(thresholds.p50, Some(10.0));
2200 assert_eq!(thresholds.p75, Some(25.0));
2201 assert_eq!(thresholds.p90, Some(50.0));
2202 assert_eq!(thresholds.p99, Some(100.0));
2203 }
2204
2205 #[test]
2206 fn test_percentile_thresholds_default() {
2207 let thresholds = PercentileThresholds::default();
2208 assert_eq!(thresholds.p50, None);
2209 assert_eq!(thresholds.p75, None);
2210 assert_eq!(thresholds.p90, None);
2211 assert_eq!(thresholds.p99, None);
2212 }
2213
2214 #[test]
2215 fn test_throughput_threshold_simple() {
2216 let threshold = ThroughputThreshold::Simple(50.0);
2217 let json = serde_json::to_value(&threshold).unwrap();
2218 assert_eq!(json, 50.0);
2219 }
2220
2221 #[test]
2222 fn test_throughput_threshold_percentile() {
2223 let threshold = ThroughputThreshold::Percentile(PercentileThresholds::new().p90(50.0));
2224 let json = serde_json::to_value(&threshold).unwrap();
2225 assert_eq!(json["p90"], 50.0);
2226 }
2227
2228 #[test]
2229 fn test_latency_threshold_simple() {
2230 let threshold = LatencyThreshold::Simple(0.5);
2231 let json = serde_json::to_value(&threshold).unwrap();
2232 assert_eq!(json, 0.5);
2233 }
2234
2235 #[test]
2236 fn test_latency_threshold_percentile() {
2237 let threshold = LatencyThreshold::Percentile(PercentileThresholds::new().p50(0.1).p99(1.0));
2238 let json = serde_json::to_value(&threshold).unwrap();
2239 assert_eq!(json["p50"], 0.1);
2240 assert_eq!(json["p99"], 1.0);
2241 }
2242
2243 #[test]
2244 fn test_max_price_builder() {
2245 let price = MaxPrice::new().prompt(0.001).completion(0.002);
2246
2247 assert_eq!(price.prompt, Some(0.001));
2248 assert_eq!(price.completion, Some(0.002));
2249 assert_eq!(price.request, None);
2250 assert_eq!(price.image, None);
2251 }
2252
2253 #[test]
2254 fn test_max_price_all_fields() {
2255 let price = MaxPrice::new()
2256 .prompt(0.001)
2257 .completion(0.002)
2258 .request(0.01)
2259 .image(0.05);
2260
2261 let json = serde_json::to_value(&price).unwrap();
2262 assert_eq!(json["prompt"], 0.001);
2263 assert_eq!(json["completion"], 0.002);
2264 assert_eq!(json["request"], 0.01);
2265 assert_eq!(json["image"], 0.05);
2266 }
2267
2268 #[test]
2269 fn test_max_price_default() {
2270 let price = MaxPrice::default();
2271 assert_eq!(price.prompt, None);
2272 assert_eq!(price.completion, None);
2273 assert_eq!(price.request, None);
2274 assert_eq!(price.image, None);
2275 }
2276
2277 #[test]
2278 fn test_provider_preferences_default() {
2279 let prefs = ProviderPreferences::default();
2280 assert!(prefs.order.is_none());
2281 assert!(prefs.only.is_none());
2282 assert!(prefs.ignore.is_none());
2283 assert!(prefs.allow_fallbacks.is_none());
2284 assert!(prefs.require_parameters.is_none());
2285 assert!(prefs.data_collection.is_none());
2286 assert!(prefs.zdr.is_none());
2287 assert!(prefs.sort.is_none());
2288 assert!(prefs.preferred_min_throughput.is_none());
2289 assert!(prefs.preferred_max_latency.is_none());
2290 assert!(prefs.max_price.is_none());
2291 assert!(prefs.quantizations.is_none());
2292 }
2293
2294 #[test]
2295 fn test_provider_preferences_order_with_fallbacks() {
2296 let prefs = ProviderPreferences::new()
2297 .order(["anthropic", "openai"])
2298 .allow_fallbacks(true);
2299
2300 let json = prefs.to_json();
2301 let provider = &json["provider"];
2302
2303 assert_eq!(provider["order"], json!(["anthropic", "openai"]));
2304 assert_eq!(provider["allow_fallbacks"], true);
2305 }
2306
2307 #[test]
2308 fn test_provider_preferences_only_allowlist() {
2309 let prefs = ProviderPreferences::new()
2310 .only(["azure", "together"])
2311 .allow_fallbacks(false);
2312
2313 let json = prefs.to_json();
2314 let provider = &json["provider"];
2315
2316 assert_eq!(provider["only"], json!(["azure", "together"]));
2317 assert_eq!(provider["allow_fallbacks"], false);
2318 }
2319
2320 #[test]
2321 fn test_provider_preferences_ignore() {
2322 let prefs = ProviderPreferences::new().ignore(["deepinfra"]);
2323
2324 let json = prefs.to_json();
2325 let provider = &json["provider"];
2326
2327 assert_eq!(provider["ignore"], json!(["deepinfra"]));
2328 }
2329
2330 #[test]
2331 fn test_provider_preferences_sort_latency() {
2332 let prefs = ProviderPreferences::new().sort(ProviderSortStrategy::Latency);
2333
2334 let json = prefs.to_json();
2335 let provider = &json["provider"];
2336
2337 assert_eq!(provider["sort"], "latency");
2338 }
2339
2340 #[test]
2341 fn test_provider_preferences_price_with_throughput() {
2342 let prefs = ProviderPreferences::new()
2343 .sort(ProviderSortStrategy::Price)
2344 .preferred_min_throughput(ThroughputThreshold::Percentile(
2345 PercentileThresholds::new().p90(50.0),
2346 ));
2347
2348 let json = prefs.to_json();
2349 let provider = &json["provider"];
2350
2351 assert_eq!(provider["sort"], "price");
2352 assert_eq!(provider["preferred_min_throughput"]["p90"], 50.0);
2353 }
2354
2355 #[test]
2356 fn test_provider_preferences_require_parameters() {
2357 let prefs = ProviderPreferences::new().require_parameters(true);
2358
2359 let json = prefs.to_json();
2360 let provider = &json["provider"];
2361
2362 assert_eq!(provider["require_parameters"], true);
2363 }
2364
2365 #[test]
2366 fn test_provider_preferences_data_policy_and_zdr() {
2367 let prefs = ProviderPreferences::new()
2368 .data_collection(DataCollection::Deny)
2369 .zdr(true);
2370
2371 let json = prefs.to_json();
2372 let provider = &json["provider"];
2373
2374 assert_eq!(provider["data_collection"], "deny");
2375 assert_eq!(provider["zdr"], true);
2376 }
2377
2378 #[test]
2379 fn test_provider_preferences_quantizations() {
2380 let prefs =
2381 ProviderPreferences::new().quantizations([Quantization::Int8, Quantization::Fp16]);
2382
2383 let json = prefs.to_json();
2384 let provider = &json["provider"];
2385
2386 assert_eq!(provider["quantizations"], json!(["int8", "fp16"]));
2387 }
2388
2389 #[test]
2390 fn test_provider_preferences_convenience_methods() {
2391 let prefs = ProviderPreferences::new().zero_data_retention().fastest();
2392
2393 assert_eq!(prefs.zdr, Some(true));
2394 assert_eq!(
2395 prefs.sort,
2396 Some(ProviderSort::Simple(ProviderSortStrategy::Throughput))
2397 );
2398
2399 let prefs2 = ProviderPreferences::new().cheapest();
2400 assert_eq!(
2401 prefs2.sort,
2402 Some(ProviderSort::Simple(ProviderSortStrategy::Price))
2403 );
2404
2405 let prefs3 = ProviderPreferences::new().lowest_latency();
2406 assert_eq!(
2407 prefs3.sort,
2408 Some(ProviderSort::Simple(ProviderSortStrategy::Latency))
2409 );
2410 }
2411
2412 #[test]
2413 fn test_provider_preferences_serialization_skips_none() {
2414 let prefs = ProviderPreferences::new().sort(ProviderSortStrategy::Price);
2415
2416 let json = serde_json::to_value(&prefs).unwrap();
2417
2418 assert_eq!(json["sort"], "price");
2419 assert!(json.get("order").is_none());
2420 assert!(json.get("only").is_none());
2421 assert!(json.get("ignore").is_none());
2422 assert!(json.get("zdr").is_none());
2423 }
2424
2425 #[test]
2426 fn test_provider_preferences_deserialization() {
2427 let json = json!({
2428 "order": ["anthropic", "openai"],
2429 "sort": "throughput",
2430 "data_collection": "deny",
2431 "zdr": true,
2432 "quantizations": ["int8", "fp16"]
2433 });
2434
2435 let prefs: ProviderPreferences = serde_json::from_value(json).unwrap();
2436
2437 assert_eq!(
2438 prefs.order,
2439 Some(vec!["anthropic".to_string(), "openai".to_string()])
2440 );
2441 assert_eq!(
2442 prefs.sort,
2443 Some(ProviderSort::Simple(ProviderSortStrategy::Throughput))
2444 );
2445 assert_eq!(prefs.data_collection, Some(DataCollection::Deny));
2446 assert_eq!(prefs.zdr, Some(true));
2447 assert_eq!(
2448 prefs.quantizations,
2449 Some(vec![Quantization::Int8, Quantization::Fp16])
2450 );
2451 }
2452
2453 #[test]
2454 fn test_provider_preferences_deserialization_complex_sort() {
2455 let json = json!({
2456 "sort": {
2457 "by": "latency",
2458 "partition": "model"
2459 }
2460 });
2461
2462 let prefs: ProviderPreferences = serde_json::from_value(json).unwrap();
2463
2464 match prefs.sort {
2465 Some(ProviderSort::Complex(config)) => {
2466 assert_eq!(config.by, ProviderSortStrategy::Latency);
2467 assert_eq!(config.partition, Some(SortPartition::Model));
2468 }
2469 _ => panic!("Expected Complex sort variant"),
2470 }
2471 }
2472
2473 #[test]
2474 fn test_provider_preferences_full_integration() {
2475 let prefs = ProviderPreferences::new()
2476 .order(["anthropic", "openai"])
2477 .only(["anthropic", "openai", "google"])
2478 .sort(ProviderSortStrategy::Throughput)
2479 .data_collection(DataCollection::Deny)
2480 .zdr(true)
2481 .quantizations([Quantization::Int8])
2482 .allow_fallbacks(false);
2483
2484 let json = prefs.to_json();
2485
2486 assert!(json.get("provider").is_some());
2487 let provider = &json["provider"];
2488 assert_eq!(provider["order"], json!(["anthropic", "openai"]));
2489 assert_eq!(provider["only"], json!(["anthropic", "openai", "google"]));
2490 assert_eq!(provider["sort"], "throughput");
2491 assert_eq!(provider["data_collection"], "deny");
2492 assert_eq!(provider["zdr"], true);
2493 assert_eq!(provider["quantizations"], json!(["int8"]));
2494 assert_eq!(provider["allow_fallbacks"], false);
2495 }
2496
2497 #[test]
2498 fn test_provider_preferences_max_price() {
2499 let prefs =
2500 ProviderPreferences::new().max_price(MaxPrice::new().prompt(0.001).completion(0.002));
2501
2502 let json = prefs.to_json();
2503 let provider = &json["provider"];
2504
2505 assert_eq!(provider["max_price"]["prompt"], 0.001);
2506 assert_eq!(provider["max_price"]["completion"], 0.002);
2507 }
2508
2509 #[test]
2510 fn test_provider_preferences_preferred_max_latency() {
2511 let prefs = ProviderPreferences::new().preferred_max_latency(LatencyThreshold::Simple(0.5));
2512
2513 let json = prefs.to_json();
2514 let provider = &json["provider"];
2515
2516 assert_eq!(provider["preferred_max_latency"], 0.5);
2517 }
2518
2519 #[test]
2520 fn test_provider_preferences_empty_arrays() {
2521 let prefs = ProviderPreferences::new()
2522 .order(Vec::<String>::new())
2523 .quantizations(Vec::<Quantization>::new());
2524
2525 let json = prefs.to_json();
2526 let provider = &json["provider"];
2527
2528 assert_eq!(provider["order"], json!([]));
2529 assert_eq!(provider["quantizations"], json!([]));
2530 }
2531
2532 #[test]
2537 fn test_user_content_text_serialization() {
2538 let content = UserContent::text("Hello, world!");
2539 let json = serde_json::to_value(&content).unwrap();
2540
2541 assert_eq!(json["type"], "text");
2542 assert_eq!(json["text"], "Hello, world!");
2543 }
2544
2545 #[test]
2546 fn test_user_content_image_url_serialization() {
2547 let content = UserContent::image_url("https://example.com/image.png");
2548 let json = serde_json::to_value(&content).unwrap();
2549
2550 assert_eq!(json["type"], "image_url");
2551 assert_eq!(json["image_url"]["url"], "https://example.com/image.png");
2552 assert!(json["image_url"].get("detail").is_none());
2553 }
2554
2555 #[test]
2556 fn test_user_content_image_url_with_detail_serialization() {
2557 let content =
2558 UserContent::image_url_with_detail("https://example.com/image.png", ImageDetail::High);
2559 let json = serde_json::to_value(&content).unwrap();
2560
2561 assert_eq!(json["type"], "image_url");
2562 assert_eq!(json["image_url"]["url"], "https://example.com/image.png");
2563 assert_eq!(json["image_url"]["detail"], "high");
2564 }
2565
2566 #[test]
2567 fn test_user_content_image_base64_serialization() {
2568 let content = UserContent::image_base64("SGVsbG8=", "image/png", Some(ImageDetail::Low));
2569 let json = serde_json::to_value(&content).unwrap();
2570
2571 assert_eq!(json["type"], "image_url");
2572 assert_eq!(json["image_url"]["url"], "data:image/png;base64,SGVsbG8=");
2573 assert_eq!(json["image_url"]["detail"], "low");
2574 }
2575
2576 #[test]
2577 fn test_user_content_file_url_serialization() {
2578 let content = UserContent::file_url(
2579 "https://example.com/doc.pdf",
2580 Some("document.pdf".to_string()),
2581 );
2582 let json = serde_json::to_value(&content).unwrap();
2583
2584 assert_eq!(json["type"], "file");
2585 assert_eq!(json["file"]["file_data"], "https://example.com/doc.pdf");
2586 assert_eq!(json["file"]["filename"], "document.pdf");
2587 }
2588
2589 #[test]
2590 fn test_user_content_file_base64_serialization() {
2591 let content = UserContent::file_base64(
2592 "JVBERi0xLjQ=",
2593 "application/pdf",
2594 Some("report.pdf".to_string()),
2595 );
2596 let json = serde_json::to_value(&content).unwrap();
2597
2598 assert_eq!(json["type"], "file");
2599 assert_eq!(
2600 json["file"]["file_data"],
2601 "data:application/pdf;base64,JVBERi0xLjQ="
2602 );
2603 assert_eq!(json["file"]["filename"], "report.pdf");
2604 }
2605
2606 #[test]
2607 fn test_user_content_text_deserialization() {
2608 let json = json!({
2609 "type": "text",
2610 "text": "Hello!"
2611 });
2612
2613 let content: UserContent = serde_json::from_value(json).unwrap();
2614 assert_eq!(
2615 content,
2616 UserContent::Text {
2617 text: "Hello!".to_string()
2618 }
2619 );
2620 }
2621
2622 #[test]
2623 fn test_user_content_image_url_deserialization() {
2624 let json = json!({
2625 "type": "image_url",
2626 "image_url": {
2627 "url": "https://example.com/img.jpg",
2628 "detail": "high"
2629 }
2630 });
2631
2632 let content: UserContent = serde_json::from_value(json).unwrap();
2633 match content {
2634 UserContent::ImageUrl { image_url } => {
2635 assert_eq!(image_url.url, "https://example.com/img.jpg");
2636 assert_eq!(image_url.detail, Some(ImageDetail::High));
2637 }
2638 _ => panic!("Expected ImageUrl variant"),
2639 }
2640 }
2641
2642 #[test]
2643 fn test_user_content_file_deserialization() {
2644 let json = json!({
2645 "type": "file",
2646 "file": {
2647 "filename": "doc.pdf",
2648 "file_data": "https://example.com/doc.pdf"
2649 }
2650 });
2651
2652 let content: UserContent = serde_json::from_value(json).unwrap();
2653 match content {
2654 UserContent::File { file } => {
2655 assert_eq!(file.filename, Some("doc.pdf".to_string()));
2656 assert_eq!(
2657 file.file_data,
2658 Some("https://example.com/doc.pdf".to_string())
2659 );
2660 }
2661 _ => panic!("Expected File variant"),
2662 }
2663 }
2664
2665 #[test]
2666 fn test_message_user_with_text_serialization() {
2667 let message = Message::User {
2668 content: OneOrMany::one(UserContent::text("Hello")),
2669 name: None,
2670 };
2671 let json = serde_json::to_value(&message).unwrap();
2672
2673 assert_eq!(json["role"], "user");
2675 assert_eq!(json["content"], "Hello");
2676 }
2677
2678 #[test]
2679 fn test_message_user_with_mixed_content_serialization() {
2680 let message = Message::User {
2681 content: OneOrMany::many(vec![
2682 UserContent::text("Check this image:"),
2683 UserContent::image_url("https://example.com/img.png"),
2684 ])
2685 .unwrap(),
2686 name: None,
2687 };
2688 let json = serde_json::to_value(&message).unwrap();
2689
2690 assert_eq!(json["role"], "user");
2691 let content = json["content"].as_array().unwrap();
2692 assert_eq!(content.len(), 2);
2693 assert_eq!(content[0]["type"], "text");
2694 assert_eq!(content[1]["type"], "image_url");
2695 }
2696
2697 #[test]
2698 fn test_message_user_with_file_serialization() {
2699 let message = Message::User {
2700 content: OneOrMany::many(vec![
2701 UserContent::text("Analyze this PDF:"),
2702 UserContent::file_url(
2703 "https://example.com/doc.pdf",
2704 Some("document.pdf".to_string()),
2705 ),
2706 ])
2707 .unwrap(),
2708 name: None,
2709 };
2710 let json = serde_json::to_value(&message).unwrap();
2711
2712 assert_eq!(json["role"], "user");
2713 let content = json["content"].as_array().unwrap();
2714 assert_eq!(content.len(), 2);
2715 assert_eq!(content[0]["type"], "text");
2716 assert_eq!(content[1]["type"], "file");
2717 assert_eq!(
2718 content[1]["file"]["file_data"],
2719 "https://example.com/doc.pdf"
2720 );
2721 }
2722
2723 #[test]
2724 fn test_user_content_from_rig_text() {
2725 let rig_content = message::UserContent::Text(message::Text {
2726 text: "Hello".to_string(),
2727 });
2728 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2729
2730 assert_eq!(
2731 openrouter_content,
2732 UserContent::Text {
2733 text: "Hello".to_string()
2734 }
2735 );
2736 }
2737
2738 #[test]
2739 fn test_user_content_from_rig_image_url() {
2740 let rig_content = message::UserContent::Image(message::Image {
2741 data: DocumentSourceKind::Url("https://example.com/img.png".to_string()),
2742 media_type: Some(message::ImageMediaType::PNG),
2743 detail: Some(ImageDetail::High),
2744 additional_params: None,
2745 });
2746 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2747
2748 match openrouter_content {
2749 UserContent::ImageUrl { image_url } => {
2750 assert_eq!(image_url.url, "https://example.com/img.png");
2751 assert_eq!(image_url.detail, Some(ImageDetail::High));
2752 }
2753 _ => panic!("Expected ImageUrl variant"),
2754 }
2755 }
2756
2757 #[test]
2758 fn test_user_content_from_rig_image_base64() {
2759 let rig_content = message::UserContent::Image(message::Image {
2760 data: DocumentSourceKind::Base64("SGVsbG8=".to_string()),
2761 media_type: Some(message::ImageMediaType::JPEG),
2762 detail: Some(ImageDetail::Low),
2763 additional_params: None,
2764 });
2765 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2766
2767 match openrouter_content {
2768 UserContent::ImageUrl { image_url } => {
2769 assert_eq!(image_url.url, "data:image/jpeg;base64,SGVsbG8=");
2770 assert_eq!(image_url.detail, Some(ImageDetail::Low));
2771 }
2772 _ => panic!("Expected ImageUrl variant"),
2773 }
2774 }
2775
2776 #[test]
2777 fn test_user_content_from_rig_document_url() {
2778 let rig_content = message::UserContent::Document(message::Document {
2779 data: DocumentSourceKind::Url("https://example.com/doc.pdf".to_string()),
2780 media_type: Some(DocumentMediaType::PDF),
2781 additional_params: None,
2782 });
2783 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2784
2785 match openrouter_content {
2786 UserContent::File { file } => {
2787 assert_eq!(
2788 file.file_data,
2789 Some("https://example.com/doc.pdf".to_string())
2790 );
2791 assert_eq!(file.filename, Some("document.pdf".to_string()));
2792 }
2793 _ => panic!("Expected File variant"),
2794 }
2795 }
2796
2797 #[test]
2798 fn test_user_content_from_rig_document_base64() {
2799 let rig_content = message::UserContent::Document(message::Document {
2800 data: DocumentSourceKind::Base64("JVBERi0xLjQ=".to_string()),
2801 media_type: Some(DocumentMediaType::PDF),
2802 additional_params: None,
2803 });
2804 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2805
2806 match openrouter_content {
2807 UserContent::File { file } => {
2808 assert_eq!(
2809 file.file_data,
2810 Some("data:application/pdf;base64,JVBERi0xLjQ=".to_string())
2811 );
2812 assert_eq!(file.filename, Some("document.pdf".to_string()));
2813 }
2814 _ => panic!("Expected File variant"),
2815 }
2816 }
2817
2818 #[test]
2819 fn test_user_content_from_rig_document_file_id() {
2820 let rig_content = message::UserContent::Document(message::Document {
2821 data: DocumentSourceKind::FileId("file_abc".to_string()),
2822 media_type: None,
2823 additional_params: None,
2824 });
2825
2826 let result: Result<UserContent, _> = rig_content.try_into();
2827 assert!(matches!(
2828 result,
2829 Err(message::MessageError::ConversionError(message))
2830 if message.contains("Provider file IDs are not supported")
2831 ));
2832 }
2833
2834 #[test]
2835 fn test_openai_file_id_content_round_trips_through_rig_to_openrouter_error() {
2836 let openai_content = openai::UserContent::File {
2837 file: openai::FileData {
2838 file_data: None,
2839 file_id: Some("file_abc".to_string()),
2840 filename: None,
2841 },
2842 };
2843 let rig_content: message::UserContent = openai_content.into();
2844
2845 let result: Result<UserContent, _> = rig_content.try_into();
2846 assert!(matches!(
2847 result,
2848 Err(message::MessageError::ConversionError(message))
2849 if message.contains("Provider file IDs are not supported")
2850 ));
2851 }
2852
2853 #[test]
2854 fn test_user_content_from_rig_document_string_becomes_text() {
2855 let rig_content = message::UserContent::Document(message::Document {
2856 data: DocumentSourceKind::String("Plain text document content".to_string()),
2857 media_type: Some(DocumentMediaType::TXT),
2858 additional_params: None,
2859 });
2860 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2861
2862 assert_eq!(
2863 openrouter_content,
2864 UserContent::Text {
2865 text: "Plain text document content".to_string()
2866 }
2867 );
2868 }
2869
2870 #[test]
2871 fn test_completion_response_with_reasoning_details_maps_to_typed_reasoning() {
2872 let json = json!({
2873 "id": "resp_123",
2874 "object": "chat.completion",
2875 "created": 1,
2876 "model": "openrouter/test-model",
2877 "choices": [{
2878 "index": 0,
2879 "finish_reason": "stop",
2880 "message": {
2881 "role": "assistant",
2882 "content": "hello",
2883 "reasoning": null,
2884 "reasoning_details": [
2885 {"type":"reasoning.summary","id":"rs_1","summary":"s1"},
2886 {"type":"reasoning.text","id":"rs_1","text":"t1","signature":"sig_1"},
2887 {"type":"reasoning.encrypted","id":"rs_1","data":"enc_1"}
2888 ]
2889 }
2890 }]
2891 });
2892
2893 let response: CompletionResponse = serde_json::from_value(json).unwrap();
2894 let converted: completion::CompletionResponse<CompletionResponse> =
2895 response.try_into().unwrap();
2896 let items: Vec<completion::AssistantContent> = converted.choice.into_iter().collect();
2897
2898 assert!(items.iter().any(|item| matches!(
2899 item,
2900 completion::AssistantContent::Reasoning(message::Reasoning { id: Some(id), content })
2901 if id == "rs_1" && content.len() == 3
2902 )));
2903 }
2904
2905 #[test]
2906 fn test_assistant_reasoning_emits_openrouter_reasoning_details() {
2907 let reasoning = message::Reasoning {
2908 id: Some("rs_2".to_string()),
2909 content: vec![
2910 message::ReasoningContent::Text {
2911 text: "step".to_string(),
2912 signature: Some("sig_step".to_string()),
2913 },
2914 message::ReasoningContent::Summary("summary".to_string()),
2915 message::ReasoningContent::Encrypted("enc_blob".to_string()),
2916 ],
2917 };
2918
2919 let messages = Vec::<Message>::try_from(OneOrMany::one(
2920 message::AssistantContent::Reasoning(reasoning),
2921 ))
2922 .unwrap();
2923 let Message::Assistant {
2924 reasoning,
2925 reasoning_details,
2926 ..
2927 } = messages.first().expect("assistant message")
2928 else {
2929 panic!("Expected assistant message");
2930 };
2931
2932 assert!(reasoning.is_none());
2933 assert_eq!(reasoning_details.len(), 3);
2934 assert!(matches!(
2935 reasoning_details.first(),
2936 Some(ReasoningDetails::Text {
2937 id: Some(id),
2938 text: Some(text),
2939 signature: Some(signature),
2940 ..
2941 }) if id == "rs_2" && text == "step" && signature == "sig_step"
2942 ));
2943 }
2944
2945 #[test]
2946 fn test_assistant_redacted_reasoning_emits_encrypted_detail_not_text() {
2947 let reasoning = message::Reasoning {
2948 id: Some("rs_redacted".to_string()),
2949 content: vec![message::ReasoningContent::Redacted {
2950 data: "opaque-redacted-data".to_string(),
2951 }],
2952 };
2953
2954 let messages = Vec::<Message>::try_from(OneOrMany::one(
2955 message::AssistantContent::Reasoning(reasoning),
2956 ))
2957 .unwrap();
2958
2959 let Message::Assistant {
2960 reasoning_details,
2961 reasoning,
2962 ..
2963 } = messages.first().expect("assistant message")
2964 else {
2965 panic!("Expected assistant message");
2966 };
2967
2968 assert!(reasoning.is_none());
2969 assert_eq!(reasoning_details.len(), 1);
2970 assert!(matches!(
2971 reasoning_details.first(),
2972 Some(ReasoningDetails::Encrypted {
2973 id: Some(id),
2974 data,
2975 ..
2976 }) if id == "rs_redacted" && data == "opaque-redacted-data"
2977 ));
2978 }
2979
2980 #[test]
2981 fn test_completion_response_reasoning_details_respects_index_ordering() {
2982 let json = json!({
2983 "id": "resp_ordering",
2984 "object": "chat.completion",
2985 "created": 1,
2986 "model": "openrouter/test-model",
2987 "choices": [{
2988 "index": 0,
2989 "finish_reason": "stop",
2990 "message": {
2991 "role": "assistant",
2992 "content": "hello",
2993 "reasoning": null,
2994 "reasoning_details": [
2995 {"type":"reasoning.summary","id":"rs_order","index":1,"summary":"second"},
2996 {"type":"reasoning.summary","id":"rs_order","index":0,"summary":"first"}
2997 ]
2998 }
2999 }]
3000 });
3001
3002 let response: CompletionResponse = serde_json::from_value(json).unwrap();
3003 let converted: completion::CompletionResponse<CompletionResponse> =
3004 response.try_into().unwrap();
3005 let items: Vec<completion::AssistantContent> = converted.choice.into_iter().collect();
3006 let reasoning_blocks: Vec<_> = items
3007 .into_iter()
3008 .filter_map(|item| match item {
3009 completion::AssistantContent::Reasoning(reasoning) => Some(reasoning),
3010 _ => None,
3011 })
3012 .collect();
3013
3014 assert_eq!(reasoning_blocks.len(), 1);
3015 assert_eq!(reasoning_blocks[0].id.as_deref(), Some("rs_order"));
3016 assert_eq!(
3017 reasoning_blocks[0].content,
3018 vec![
3019 message::ReasoningContent::Summary("first".to_string()),
3020 message::ReasoningContent::Summary("second".to_string()),
3021 ]
3022 );
3023 }
3024
3025 #[test]
3026 fn test_user_content_from_rig_image_missing_media_type_error() {
3027 let rig_content = message::UserContent::Image(message::Image {
3028 data: DocumentSourceKind::Base64("SGVsbG8=".to_string()),
3029 media_type: None, detail: None,
3031 additional_params: None,
3032 });
3033 let result: Result<UserContent, _> = rig_content.try_into();
3034
3035 assert!(result.is_err());
3036 let err = result.unwrap_err();
3037 assert!(err.to_string().contains("media type required"));
3038 }
3039
3040 #[test]
3041 fn test_user_content_from_rig_image_raw_bytes_error() {
3042 let rig_content = message::UserContent::Image(message::Image {
3043 data: DocumentSourceKind::Raw(vec![1, 2, 3]),
3044 media_type: Some(message::ImageMediaType::PNG),
3045 detail: None,
3046 additional_params: None,
3047 });
3048 let result: Result<UserContent, _> = rig_content.try_into();
3049
3050 assert!(result.is_err());
3051 let err = result.unwrap_err();
3052 assert!(err.to_string().contains("base64"));
3053 }
3054
3055 #[test]
3056 fn test_user_content_from_rig_video_url() {
3057 let rig_content = message::UserContent::Video(message::Video {
3058 data: DocumentSourceKind::Url("https://example.com/video.mp4".to_string()),
3059 media_type: Some(message::VideoMediaType::MP4),
3060 additional_params: None,
3061 });
3062 let openrouter_content: UserContent = rig_content.try_into().unwrap();
3063
3064 match openrouter_content {
3065 UserContent::VideoUrl { video_url } => {
3066 assert_eq!(video_url.url, "https://example.com/video.mp4");
3067 }
3068 _ => panic!("Expected VideoUrl variant"),
3069 }
3070 }
3071
3072 #[test]
3073 fn test_user_content_from_rig_video_base64() {
3074 let rig_content = message::UserContent::Video(message::Video {
3075 data: DocumentSourceKind::Base64("SGVsbG8=".to_string()),
3076 media_type: Some(message::VideoMediaType::MP4),
3077 additional_params: None,
3078 });
3079 let openrouter_content: UserContent = rig_content.try_into().unwrap();
3080
3081 match openrouter_content {
3082 UserContent::VideoUrl { video_url } => {
3083 assert_eq!(video_url.url, "data:video/mp4;base64,SGVsbG8=");
3084 }
3085 _ => panic!("Expected VideoUrl variant"),
3086 }
3087 }
3088
3089 #[test]
3090 fn test_user_content_from_rig_video_base64_missing_media_type_error() {
3091 let rig_content = message::UserContent::Video(message::Video {
3092 data: DocumentSourceKind::Base64("SGVsbG8=".to_string()),
3093 media_type: None,
3094 additional_params: None,
3095 });
3096 let result: Result<UserContent, _> = rig_content.try_into();
3097
3098 assert!(result.is_err());
3099 let err = result.unwrap_err();
3100 assert!(err.to_string().contains("media type"));
3101 }
3102
3103 #[test]
3104 fn test_user_content_from_rig_video_raw_bytes_error() {
3105 let rig_content = message::UserContent::Video(message::Video {
3106 data: DocumentSourceKind::Raw(vec![1, 2, 3]),
3107 media_type: Some(message::VideoMediaType::MP4),
3108 additional_params: None,
3109 });
3110 let result: Result<UserContent, _> = rig_content.try_into();
3111
3112 assert!(result.is_err());
3113 let err = result.unwrap_err();
3114 assert!(err.to_string().contains("base64"));
3115 }
3116
3117 #[test]
3118 fn test_user_content_from_rig_audio_base64() {
3119 let rig_content = message::UserContent::Audio(message::Audio {
3120 data: DocumentSourceKind::Base64("audiodata".to_string()),
3121 media_type: Some(message::AudioMediaType::MP3),
3122 additional_params: None,
3123 });
3124 let openrouter_content: UserContent = rig_content.try_into().unwrap();
3125
3126 match openrouter_content {
3127 UserContent::InputAudio { input_audio } => {
3128 assert_eq!(input_audio.data, "audiodata");
3129 assert_eq!(input_audio.format, message::AudioMediaType::MP3);
3130 }
3131 _ => panic!("Expected InputAudio variant"),
3132 }
3133 }
3134
3135 #[test]
3136 fn test_user_content_from_rig_audio_missing_media_type_error() {
3137 let rig_content = message::UserContent::Audio(message::Audio {
3138 data: DocumentSourceKind::Base64("audiodata".to_string()),
3139 media_type: None, additional_params: None,
3141 });
3142 let result: Result<UserContent, _> = rig_content.try_into();
3143
3144 assert!(result.is_err());
3145 let err = result.unwrap_err();
3146 assert!(err.to_string().contains("media type required"));
3147 }
3148
3149 #[test]
3150 fn test_user_content_from_rig_audio_url_error() {
3151 let rig_content = message::UserContent::Audio(message::Audio {
3152 data: DocumentSourceKind::Url("https://example.com/audio.wav".to_string()),
3153 media_type: Some(message::AudioMediaType::WAV),
3154 additional_params: None,
3155 });
3156 let result: Result<UserContent, _> = rig_content.try_into();
3157
3158 assert!(result.is_err());
3159 let err = result.unwrap_err();
3160 assert!(err.to_string().contains("base64"));
3161 }
3162
3163 #[test]
3164 fn test_user_content_from_rig_audio_raw_bytes_error() {
3165 let rig_content = message::UserContent::Audio(message::Audio {
3166 data: DocumentSourceKind::Raw(vec![1, 2, 3]),
3167 media_type: Some(message::AudioMediaType::WAV),
3168 additional_params: None,
3169 });
3170 let result: Result<UserContent, _> = rig_content.try_into();
3171
3172 assert!(result.is_err());
3173 let err = result.unwrap_err();
3174 assert!(err.to_string().contains("base64"));
3175 }
3176
3177 #[test]
3178 fn test_message_conversion_with_pdf() {
3179 let rig_message = message::Message::User {
3180 content: OneOrMany::many(vec![
3181 message::UserContent::Text(message::Text {
3182 text: "Summarize this document".to_string(),
3183 }),
3184 message::UserContent::Document(message::Document {
3185 data: DocumentSourceKind::Url("https://example.com/paper.pdf".to_string()),
3186 media_type: Some(DocumentMediaType::PDF),
3187 additional_params: None,
3188 }),
3189 ])
3190 .unwrap(),
3191 };
3192
3193 let openrouter_messages: Vec<Message> = rig_message.try_into().unwrap();
3194 assert_eq!(openrouter_messages.len(), 1);
3195
3196 match &openrouter_messages[0] {
3197 Message::User { content, .. } => {
3198 assert_eq!(content.len(), 2);
3199
3200 match content.first_ref() {
3202 UserContent::Text { text } => assert_eq!(text, "Summarize this document"),
3203 _ => panic!("Expected Text"),
3204 }
3205 }
3206 _ => panic!("Expected User message"),
3207 }
3208 }
3209
3210 #[test]
3211 fn test_user_content_from_string() {
3212 let content: UserContent = "Hello".into();
3213 assert_eq!(
3214 content,
3215 UserContent::Text {
3216 text: "Hello".to_string()
3217 }
3218 );
3219
3220 let content: UserContent = String::from("World").into();
3221 assert_eq!(
3222 content,
3223 UserContent::Text {
3224 text: "World".to_string()
3225 }
3226 );
3227 }
3228
3229 #[test]
3230 fn test_openai_user_content_conversion() {
3231 let openai_text = openai::UserContent::Text {
3233 text: "Hello".to_string(),
3234 };
3235 let converted: UserContent = openai_text.try_into().unwrap();
3236 assert_eq!(
3237 converted,
3238 UserContent::Text {
3239 text: "Hello".to_string()
3240 }
3241 );
3242
3243 let openai_image = openai::UserContent::Image {
3244 image_url: openai::ImageUrl {
3245 url: "https://example.com/img.png".to_string(),
3246 detail: ImageDetail::Auto,
3247 },
3248 };
3249 let converted: UserContent = openai_image.try_into().unwrap();
3250 match converted {
3251 UserContent::ImageUrl { image_url } => {
3252 assert_eq!(image_url.url, "https://example.com/img.png");
3253 assert_eq!(image_url.detail, Some(ImageDetail::Auto));
3254 }
3255 _ => panic!("Expected ImageUrl"),
3256 }
3257
3258 let openai_audio = openai::UserContent::Audio {
3259 input_audio: openai::InputAudio {
3260 data: "audiodata".to_string(),
3261 format: AudioMediaType::FLAC,
3262 },
3263 };
3264 let converted: UserContent = openai_audio.try_into().unwrap();
3265 match converted {
3266 UserContent::InputAudio { input_audio } => {
3267 assert_eq!(input_audio.data, "audiodata");
3268 assert_eq!(input_audio.format, AudioMediaType::FLAC);
3269 }
3270 _ => panic!("Expected InputAudio"),
3271 }
3272
3273 let openai_file = openai::UserContent::File {
3274 file: openai::FileData {
3275 file_data: Some("data:application/pdf;base64,AAAA".to_string()),
3276 file_id: None,
3277 filename: Some("uploaded.pdf".to_string()),
3278 },
3279 };
3280 let converted: UserContent = openai_file.try_into().unwrap();
3281 match converted {
3282 UserContent::File { file } => {
3283 assert_eq!(file.filename, Some("uploaded.pdf".to_string()));
3284 assert_eq!(
3285 file.file_data,
3286 Some("data:application/pdf;base64,AAAA".to_string())
3287 );
3288 }
3289 _ => panic!("Expected File"),
3290 }
3291
3292 let openai_file_id = openai::UserContent::File {
3293 file: openai::FileData {
3294 file_data: None,
3295 file_id: Some("file_abc".to_string()),
3296 filename: Some("uploaded.pdf".to_string()),
3297 },
3298 };
3299 let result: Result<UserContent, _> = openai_file_id.try_into();
3300 assert!(matches!(
3301 result,
3302 Err(message::MessageError::ConversionError(message))
3303 if message.contains("provider file IDs are not supported")
3304 ));
3305 }
3306
3307 #[test]
3308 fn test_completion_response_reasoning_details_with_multiple_ids_stay_separate() {
3309 let json = json!({
3310 "id": "resp_multi_id",
3311 "object": "chat.completion",
3312 "created": 1,
3313 "model": "openrouter/test-model",
3314 "choices": [{
3315 "index": 0,
3316 "finish_reason": "stop",
3317 "message": {
3318 "role": "assistant",
3319 "content": "hello",
3320 "reasoning": null,
3321 "reasoning_details": [
3322 {"type":"reasoning.summary","id":"rs_a","summary":"a1"},
3323 {"type":"reasoning.summary","id":"rs_b","summary":"b1"},
3324 {"type":"reasoning.summary","id":"rs_a","summary":"a2"}
3325 ]
3326 }
3327 }]
3328 });
3329
3330 let response: CompletionResponse = serde_json::from_value(json).unwrap();
3331 let converted: completion::CompletionResponse<CompletionResponse> =
3332 response.try_into().unwrap();
3333 let items: Vec<completion::AssistantContent> = converted.choice.into_iter().collect();
3334 let reasoning_blocks: Vec<_> = items
3335 .into_iter()
3336 .filter_map(|item| match item {
3337 completion::AssistantContent::Reasoning(reasoning) => Some(reasoning),
3338 _ => None,
3339 })
3340 .collect();
3341
3342 assert_eq!(reasoning_blocks.len(), 2);
3343 assert_eq!(reasoning_blocks[0].id.as_deref(), Some("rs_a"));
3344 assert_eq!(
3345 reasoning_blocks[0].content,
3346 vec![
3347 message::ReasoningContent::Summary("a1".to_string()),
3348 message::ReasoningContent::Summary("a2".to_string()),
3349 ]
3350 );
3351 assert_eq!(reasoning_blocks[1].id.as_deref(), Some("rs_b"));
3352 assert_eq!(
3353 reasoning_blocks[1].content,
3354 vec![message::ReasoningContent::Summary("b1".to_string())]
3355 );
3356 }
3357
3358 #[test]
3359 fn test_user_content_audio_serialization() {
3360 let content = UserContent::audio_base64("SGVsbG8=", AudioMediaType::WAV);
3361 let json = serde_json::to_value(&content).unwrap();
3362
3363 assert_eq!(json["type"], "input_audio");
3364 assert_eq!(json["input_audio"]["data"], "SGVsbG8=");
3365 assert_eq!(json["input_audio"]["format"], "wav");
3366 }
3367
3368 #[test]
3369 fn test_user_content_audio_deserialization() {
3370 let json = json!({
3371 "type": "input_audio",
3372 "input_audio": {
3373 "data": "SGVsbG8=",
3374 "format": "wav"
3375 }
3376 });
3377
3378 let content: UserContent = serde_json::from_value(json).unwrap();
3379 match content {
3380 UserContent::InputAudio { input_audio } => {
3381 assert_eq!(input_audio.data, "SGVsbG8=");
3382 assert_eq!(input_audio.format, AudioMediaType::WAV);
3383 }
3384 _ => panic!("Expected InputAudio variant"),
3385 }
3386 }
3387
3388 #[test]
3389 fn test_message_user_with_audio_serialization() {
3390 let msg = Message::User {
3391 content: OneOrMany::many(vec![
3392 UserContent::text("Transcribe this audio:"),
3393 UserContent::audio_base64("SGVsbG8=", AudioMediaType::MP3),
3394 ])
3395 .unwrap(),
3396 name: None,
3397 };
3398 let json = serde_json::to_value(&msg).unwrap();
3399
3400 assert_eq!(json["role"], "user");
3401 let content = json["content"].as_array().unwrap();
3402 assert_eq!(content.len(), 2);
3403 assert_eq!(content[0]["type"], "text");
3404 assert_eq!(content[1]["type"], "input_audio");
3405 assert_eq!(content[1]["input_audio"]["data"], "SGVsbG8=");
3406 assert_eq!(content[1]["input_audio"]["format"], "mp3");
3407 }
3408
3409 #[test]
3410 fn test_user_content_video_url_serialization() {
3411 let content = UserContent::video_url("https://example.com/video.mp4");
3412 let json = serde_json::to_value(&content).unwrap();
3413
3414 assert_eq!(json["type"], "video_url");
3415 assert_eq!(json["video_url"]["url"], "https://example.com/video.mp4");
3416 }
3417
3418 #[test]
3419 fn test_user_content_video_base64_serialization() {
3420 let content = UserContent::video_base64("SGVsbG8=", VideoMediaType::MP4);
3421 let json = serde_json::to_value(&content).unwrap();
3422
3423 assert_eq!(json["type"], "video_url");
3424 assert_eq!(json["video_url"]["url"], "data:video/mp4;base64,SGVsbG8=");
3425 }
3426
3427 #[test]
3428 fn test_user_content_video_url_deserialization() {
3429 let json = json!({
3430 "type": "video_url",
3431 "video_url": {
3432 "url": "https://example.com/video.mp4"
3433 }
3434 });
3435
3436 let content: UserContent = serde_json::from_value(json).unwrap();
3437 match content {
3438 UserContent::VideoUrl { video_url } => {
3439 assert_eq!(video_url.url, "https://example.com/video.mp4");
3440 }
3441 _ => panic!("Expected VideoUrl variant"),
3442 }
3443 }
3444
3445 #[test]
3446 fn test_message_user_with_video_serialization() {
3447 let msg = Message::User {
3448 content: OneOrMany::many(vec![
3449 UserContent::text("Describe this video:"),
3450 UserContent::video_url("https://example.com/video.mp4"),
3451 ])
3452 .unwrap(),
3453 name: None,
3454 };
3455 let json = serde_json::to_value(&msg).unwrap();
3456
3457 assert_eq!(json["role"], "user");
3458 let content = json["content"].as_array().unwrap();
3459 assert_eq!(content.len(), 2);
3460 assert_eq!(content[0]["type"], "text");
3461 assert_eq!(content[1]["type"], "video_url");
3462 assert_eq!(
3463 content[1]["video_url"]["url"],
3464 "https://example.com/video.mp4"
3465 );
3466 }
3467
3468 #[test]
3469 fn test_user_content_video_url_no_media_type_needed() {
3470 let rig_content = message::UserContent::Video(message::Video {
3471 data: DocumentSourceKind::Url("https://example.com/video.mp4".to_string()),
3472 media_type: None,
3473 additional_params: None,
3474 });
3475 let openrouter_content: UserContent = rig_content.try_into().unwrap();
3476
3477 match openrouter_content {
3478 UserContent::VideoUrl { video_url } => {
3479 assert_eq!(video_url.url, "https://example.com/video.mp4");
3480 }
3481 _ => panic!("Expected VideoUrl variant"),
3482 }
3483 }
3484}