1use super::{
2 client::{ApiErrorResponse, ApiResponse, Client, Usage},
3 streaming::StreamingCompletionResponse,
4};
5use crate::message::{self, DocumentMediaType, DocumentSourceKind, ImageDetail, MimeType};
6use crate::telemetry::SpanCombinator;
7use crate::{
8 OneOrMany,
9 completion::{self, CompletionError, CompletionRequest},
10 http_client::HttpClientExt,
11 json_utils,
12 one_or_many::string_or_one_or_many,
13 providers::openai,
14};
15use bytes::Bytes;
16use serde::{Deserialize, Serialize, Serializer};
17use std::collections::HashMap;
18use tracing::{Instrument, Level, enabled, info_span};
19
20pub const QWEN_QWQ_32B: &str = "qwen/qwq-32b";
26pub const CLAUDE_3_7_SONNET: &str = "anthropic/claude-3.7-sonnet";
28pub const PERPLEXITY_SONAR_PRO: &str = "perplexity/sonar-pro";
30pub const GEMINI_FLASH_2_0: &str = "google/gemini-2.0-flash-001";
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
42#[serde(rename_all = "lowercase")]
43pub enum DataCollection {
44 #[default]
46 Allow,
47 Deny,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum Quantization {
57 #[serde(rename = "int4")]
59 Int4,
60 #[serde(rename = "int8")]
62 Int8,
63 #[serde(rename = "fp16")]
65 Fp16,
66 #[serde(rename = "bf16")]
68 Bf16,
69 #[serde(rename = "fp32")]
71 Fp32,
72 #[serde(rename = "fp8")]
74 Fp8,
75 #[serde(rename = "unknown")]
77 Unknown,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "lowercase")]
87pub enum ProviderSortStrategy {
88 Price,
90 Throughput,
92 Latency,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "lowercase")]
99pub enum SortPartition {
100 Model,
102 None,
104}
105
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct ProviderSortConfig {
111 pub by: ProviderSortStrategy,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub partition: Option<SortPartition>,
117}
118
119impl ProviderSortConfig {
120 pub fn new(by: ProviderSortStrategy) -> Self {
122 Self {
123 by,
124 partition: None,
125 }
126 }
127
128 pub fn partition(mut self, partition: SortPartition) -> Self {
130 self.partition = Some(partition);
131 self
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140#[serde(untagged)]
141pub enum ProviderSort {
142 Simple(ProviderSortStrategy),
144 Complex(ProviderSortConfig),
146}
147
148impl From<ProviderSortStrategy> for ProviderSort {
149 fn from(strategy: ProviderSortStrategy) -> Self {
150 ProviderSort::Simple(strategy)
151 }
152}
153
154impl From<ProviderSortConfig> for ProviderSort {
155 fn from(config: ProviderSortConfig) -> Self {
156 ProviderSort::Complex(config)
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(untagged)]
165pub enum ThroughputThreshold {
166 Simple(f64),
168 Percentile(PercentileThresholds),
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176#[serde(untagged)]
177pub enum LatencyThreshold {
178 Simple(f64),
180 Percentile(PercentileThresholds),
182}
183
184#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
186pub struct PercentileThresholds {
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub p50: Option<f64>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub p75: Option<f64>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub p90: Option<f64>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub p99: Option<f64>,
199}
200
201impl PercentileThresholds {
202 pub fn new() -> Self {
204 Self::default()
205 }
206
207 pub fn p50(mut self, value: f64) -> Self {
209 self.p50 = Some(value);
210 self
211 }
212
213 pub fn p75(mut self, value: f64) -> Self {
215 self.p75 = Some(value);
216 self
217 }
218
219 pub fn p90(mut self, value: f64) -> Self {
221 self.p90 = Some(value);
222 self
223 }
224
225 pub fn p99(mut self, value: f64) -> Self {
227 self.p99 = Some(value);
228 self
229 }
230}
231
232#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
237pub struct MaxPrice {
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub prompt: Option<f64>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub completion: Option<f64>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub request: Option<f64>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub image: Option<f64>,
250}
251
252impl MaxPrice {
253 pub fn new() -> Self {
255 Self::default()
256 }
257
258 pub fn prompt(mut self, price: f64) -> Self {
260 self.prompt = Some(price);
261 self
262 }
263
264 pub fn completion(mut self, price: f64) -> Self {
266 self.completion = Some(price);
267 self
268 }
269
270 pub fn request(mut self, price: f64) -> Self {
272 self.request = Some(price);
273 self
274 }
275
276 pub fn image(mut self, price: f64) -> Self {
278 self.image = Some(price);
279 self
280 }
281}
282
283#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
303pub struct ProviderPreferences {
304 #[serde(skip_serializing_if = "Option::is_none")]
308 pub order: Option<Vec<String>>,
309
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub only: Option<Vec<String>>,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub ignore: Option<Vec<String>>,
317
318 #[serde(skip_serializing_if = "Option::is_none")]
321 pub allow_fallbacks: Option<bool>,
322
323 #[serde(skip_serializing_if = "Option::is_none")]
327 pub require_parameters: Option<bool>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
332 pub data_collection: Option<DataCollection>,
333
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub zdr: Option<bool>,
337
338 #[serde(skip_serializing_if = "Option::is_none")]
342 pub sort: Option<ProviderSort>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub preferred_min_throughput: Option<ThroughputThreshold>,
347
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub preferred_max_latency: Option<LatencyThreshold>,
351
352 #[serde(skip_serializing_if = "Option::is_none")]
354 pub max_price: Option<MaxPrice>,
355
356 #[serde(skip_serializing_if = "Option::is_none")]
359 pub quantizations: Option<Vec<Quantization>>,
360}
361
362impl ProviderPreferences {
363 pub fn new() -> Self {
365 Self::default()
366 }
367
368 pub fn order(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
384 self.order = Some(providers.into_iter().map(|p| p.into()).collect());
385 self
386 }
387
388 pub fn only(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
400 self.only = Some(providers.into_iter().map(|p| p.into()).collect());
401 self
402 }
403
404 pub fn ignore(mut self, providers: impl IntoIterator<Item = impl Into<String>>) -> Self {
415 self.ignore = Some(providers.into_iter().map(|p| p.into()).collect());
416 self
417 }
418
419 pub fn allow_fallbacks(mut self, allow: bool) -> Self {
424 self.allow_fallbacks = Some(allow);
425 self
426 }
427
428 pub fn require_parameters(mut self, require: bool) -> Self {
434 self.require_parameters = Some(require);
435 self
436 }
437
438 pub fn data_collection(mut self, policy: DataCollection) -> Self {
442 self.data_collection = Some(policy);
443 self
444 }
445
446 pub fn zdr(mut self, enable: bool) -> Self {
457 self.zdr = Some(enable);
458 self
459 }
460
461 pub fn sort(mut self, sort: impl Into<ProviderSort>) -> Self {
477 self.sort = Some(sort.into());
478 self
479 }
480
481 pub fn preferred_min_throughput(mut self, threshold: ThroughputThreshold) -> Self {
501 self.preferred_min_throughput = Some(threshold);
502 self
503 }
504
505 pub fn preferred_max_latency(mut self, threshold: LatencyThreshold) -> Self {
509 self.preferred_max_latency = Some(threshold);
510 self
511 }
512
513 pub fn max_price(mut self, price: MaxPrice) -> Self {
517 self.max_price = Some(price);
518 self
519 }
520
521 pub fn quantizations(mut self, quantizations: impl IntoIterator<Item = Quantization>) -> Self {
534 self.quantizations = Some(quantizations.into_iter().collect());
535 self
536 }
537
538 pub fn zero_data_retention(self) -> Self {
542 self.zdr(true)
543 }
544
545 pub fn fastest(self) -> Self {
547 self.sort(ProviderSortStrategy::Throughput)
548 }
549
550 pub fn cheapest(self) -> Self {
552 self.sort(ProviderSortStrategy::Price)
553 }
554
555 pub fn lowest_latency(self) -> Self {
557 self.sort(ProviderSortStrategy::Latency)
558 }
559
560 pub fn to_json(&self) -> serde_json::Value {
562 serde_json::json!({
563 "provider": self
564 })
565 }
566}
567
568#[derive(Debug, Serialize, Deserialize)]
572pub struct CompletionResponse {
573 pub id: String,
574 pub object: String,
575 pub created: u64,
576 pub model: String,
577 pub choices: Vec<Choice>,
578 pub system_fingerprint: Option<String>,
579 pub usage: Option<Usage>,
580}
581
582impl From<ApiErrorResponse> for CompletionError {
583 fn from(err: ApiErrorResponse) -> Self {
584 CompletionError::ProviderError(err.message)
585 }
586}
587
588impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
589 type Error = CompletionError;
590
591 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
592 let choice = response.choices.first().ok_or_else(|| {
593 CompletionError::ResponseError("Response contained no choices".to_owned())
594 })?;
595
596 let content = match &choice.message {
597 Message::Assistant {
598 content,
599 tool_calls,
600 reasoning,
601 reasoning_details,
602 ..
603 } => {
604 let mut content = content
605 .iter()
606 .map(|c| match c {
607 openai::AssistantContent::Text { text } => {
608 completion::AssistantContent::text(text)
609 }
610 openai::AssistantContent::Refusal { refusal } => {
611 completion::AssistantContent::text(refusal)
612 }
613 })
614 .collect::<Vec<_>>();
615
616 content.extend(tool_calls.iter().map(|call| {
617 completion::AssistantContent::tool_call(
618 &call.id,
619 &call.function.name,
620 call.function.arguments.clone(),
621 )
622 }));
623
624 let mut grouped_reasoning: HashMap<
625 Option<String>,
626 Vec<(usize, usize, message::ReasoningContent)>,
627 > = HashMap::new();
628 let mut reasoning_order: Vec<Option<String>> = Vec::new();
629 for (position, detail) in reasoning_details.iter().enumerate() {
630 let (reasoning_id, sort_index, parsed_content) = match detail {
631 ReasoningDetails::Summary {
632 id, index, summary, ..
633 } => (
634 id.clone(),
635 *index,
636 Some(message::ReasoningContent::Summary(summary.clone())),
637 ),
638 ReasoningDetails::Encrypted {
639 id, index, data, ..
640 } => (
641 id.clone(),
642 *index,
643 Some(message::ReasoningContent::Encrypted(data.clone())),
644 ),
645 ReasoningDetails::Text {
646 id,
647 index,
648 text,
649 signature,
650 ..
651 } => (
652 id.clone(),
653 *index,
654 text.as_ref().map(|text| message::ReasoningContent::Text {
655 text: text.clone(),
656 signature: signature.clone(),
657 }),
658 ),
659 };
660
661 let Some(parsed_content) = parsed_content else {
662 continue;
663 };
664 let sort_index = sort_index.unwrap_or(position);
665
666 let entry = grouped_reasoning.entry(reasoning_id.clone());
667 if matches!(entry, std::collections::hash_map::Entry::Vacant(_)) {
668 reasoning_order.push(reasoning_id);
669 }
670 entry
671 .or_default()
672 .push((sort_index, position, parsed_content));
673 }
674
675 if grouped_reasoning.is_empty() {
676 if let Some(reasoning) = reasoning {
677 content.push(completion::AssistantContent::reasoning(reasoning));
678 }
679 } else {
680 for reasoning_id in reasoning_order {
681 let Some(mut blocks) = grouped_reasoning.remove(&reasoning_id) else {
682 continue;
683 };
684 blocks.sort_by_key(|(index, position, _)| (*index, *position));
685 content.push(completion::AssistantContent::Reasoning(
686 message::Reasoning {
687 id: reasoning_id,
688 content: blocks
689 .into_iter()
690 .map(|(_, _, content)| content)
691 .collect::<Vec<_>>(),
692 },
693 ));
694 }
695 }
696
697 Ok(content)
698 }
699 _ => Err(CompletionError::ResponseError(
700 "Response did not contain a valid message or tool call".into(),
701 )),
702 }?;
703
704 let choice = OneOrMany::many(content).map_err(|_| {
705 CompletionError::ResponseError(
706 "Response contained no message or tool call (empty)".to_owned(),
707 )
708 })?;
709
710 let usage = response
711 .usage
712 .as_ref()
713 .map(|usage| completion::Usage {
714 input_tokens: usage.prompt_tokens as u64,
715 output_tokens: (usage.total_tokens - usage.prompt_tokens) as u64,
716 total_tokens: usage.total_tokens as u64,
717 cached_input_tokens: 0,
718 })
719 .unwrap_or_default();
720
721 Ok(completion::CompletionResponse {
722 choice,
723 usage,
724 raw_response: response,
725 message_id: None,
726 })
727 }
728}
729
730#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
756#[serde(tag = "type", rename_all = "snake_case")]
757pub enum UserContent {
758 Text { text: String },
760
761 #[serde(rename = "image_url")]
765 ImageUrl { image_url: ImageUrl },
766
767 File { file: FileContent },
772}
773
774impl UserContent {
775 pub fn text(text: impl Into<String>) -> Self {
777 UserContent::Text { text: text.into() }
778 }
779
780 pub fn image_url(url: impl Into<String>) -> Self {
782 UserContent::ImageUrl {
783 image_url: ImageUrl {
784 url: url.into(),
785 detail: None,
786 },
787 }
788 }
789
790 pub fn image_url_with_detail(url: impl Into<String>, detail: ImageDetail) -> Self {
792 UserContent::ImageUrl {
793 image_url: ImageUrl {
794 url: url.into(),
795 detail: Some(detail),
796 },
797 }
798 }
799
800 pub fn image_base64(
807 data: impl Into<String>,
808 mime_type: &str,
809 detail: Option<ImageDetail>,
810 ) -> Self {
811 let data_uri = format!("data:{};base64,{}", mime_type, data.into());
812 UserContent::ImageUrl {
813 image_url: ImageUrl {
814 url: data_uri,
815 detail,
816 },
817 }
818 }
819
820 pub fn file_url(url: impl Into<String>, filename: Option<String>) -> Self {
826 UserContent::File {
827 file: FileContent {
828 filename,
829 file_data: Some(url.into()),
830 },
831 }
832 }
833
834 pub fn file_base64(data: impl Into<String>, mime_type: &str, filename: Option<String>) -> Self {
841 let data_uri = format!("data:{};base64,{}", mime_type, data.into());
842 UserContent::File {
843 file: FileContent {
844 filename,
845 file_data: Some(data_uri),
846 },
847 }
848 }
849}
850
851impl From<String> for UserContent {
852 fn from(text: String) -> Self {
853 UserContent::Text { text }
854 }
855}
856
857impl From<&str> for UserContent {
858 fn from(text: &str) -> Self {
859 UserContent::Text {
860 text: text.to_string(),
861 }
862 }
863}
864
865impl std::str::FromStr for UserContent {
866 type Err = std::convert::Infallible;
867
868 fn from_str(s: &str) -> Result<Self, Self::Err> {
869 Ok(UserContent::Text {
870 text: s.to_string(),
871 })
872 }
873}
874
875#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
877pub struct ImageUrl {
878 pub url: String,
880 #[serde(skip_serializing_if = "Option::is_none")]
882 pub detail: Option<ImageDetail>,
883}
884
885#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
892pub struct FileContent {
893 #[serde(skip_serializing_if = "Option::is_none")]
895 pub filename: Option<String>,
896 #[serde(skip_serializing_if = "Option::is_none")]
898 pub file_data: Option<String>,
899}
900
901fn serialize_user_content<S>(
904 content: &OneOrMany<UserContent>,
905 serializer: S,
906) -> Result<S::Ok, S::Error>
907where
908 S: Serializer,
909{
910 if content.len() == 1
911 && let UserContent::Text { text } = content.first_ref()
912 {
913 return serializer.serialize_str(text);
914 }
915 content.serialize(serializer)
916}
917
918impl TryFrom<message::UserContent> for UserContent {
919 type Error = message::MessageError;
920
921 fn try_from(value: message::UserContent) -> Result<Self, Self::Error> {
922 match value {
923 message::UserContent::Text(message::Text { text }) => Ok(UserContent::Text { text }),
924
925 message::UserContent::Image(message::Image {
926 data,
927 detail,
928 media_type,
929 ..
930 }) => {
931 let url = match data {
932 DocumentSourceKind::Url(url) => url,
933 DocumentSourceKind::Base64(data) => {
934 let mime = media_type
935 .ok_or_else(|| {
936 message::MessageError::ConversionError(
937 "Image media type required for base64 encoding".into(),
938 )
939 })?
940 .to_mime_type();
941 format!("data:{mime};base64,{data}")
942 }
943 DocumentSourceKind::Raw(_) => {
944 return Err(message::MessageError::ConversionError(
945 "Raw bytes not supported, encode as base64 first".into(),
946 ));
947 }
948 DocumentSourceKind::String(_) => {
949 return Err(message::MessageError::ConversionError(
950 "String source not supported for images".into(),
951 ));
952 }
953 DocumentSourceKind::Unknown => {
954 return Err(message::MessageError::ConversionError(
955 "Image has no data".into(),
956 ));
957 }
958 };
959 Ok(UserContent::ImageUrl {
960 image_url: ImageUrl { url, detail },
961 })
962 }
963
964 message::UserContent::Document(message::Document {
965 data, media_type, ..
966 }) => match data {
967 DocumentSourceKind::Url(url) => {
968 let filename = media_type.as_ref().map(|mt| match mt {
969 DocumentMediaType::PDF => "document.pdf",
970 DocumentMediaType::TXT => "document.txt",
971 DocumentMediaType::HTML => "document.html",
972 DocumentMediaType::MARKDOWN => "document.md",
973 DocumentMediaType::CSV => "document.csv",
974 DocumentMediaType::XML => "document.xml",
975 _ => "document",
976 });
977 Ok(UserContent::File {
978 file: FileContent {
979 filename: filename.map(String::from),
980 file_data: Some(url),
981 },
982 })
983 }
984 DocumentSourceKind::Base64(data) => {
985 let mime = media_type
986 .as_ref()
987 .map(|m| m.to_mime_type())
988 .unwrap_or("application/pdf");
989 let data_uri = format!("data:{mime};base64,{data}");
990
991 let filename = media_type.as_ref().map(|mt| match mt {
992 DocumentMediaType::PDF => "document.pdf",
993 DocumentMediaType::TXT => "document.txt",
994 DocumentMediaType::HTML => "document.html",
995 DocumentMediaType::MARKDOWN => "document.md",
996 DocumentMediaType::CSV => "document.csv",
997 DocumentMediaType::XML => "document.xml",
998 _ => "document",
999 });
1000
1001 Ok(UserContent::File {
1002 file: FileContent {
1003 filename: filename.map(String::from),
1004 file_data: Some(data_uri),
1005 },
1006 })
1007 }
1008 DocumentSourceKind::String(text) => Ok(UserContent::Text { text }),
1009 DocumentSourceKind::Raw(_) => Err(message::MessageError::ConversionError(
1010 "Raw bytes not supported for documents, encode as base64 first".into(),
1011 )),
1012 DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
1013 "Document has no data".into(),
1014 )),
1015 },
1016
1017 message::UserContent::Audio(_) => Err(message::MessageError::ConversionError(
1018 "Audio content not supported by OpenRouter file implementation. \
1019 Use the OpenAI-compatible audio types for audio support."
1020 .into(),
1021 )),
1022
1023 message::UserContent::Video(_) => Err(message::MessageError::ConversionError(
1024 "Video content not supported by OpenRouter file implementation".into(),
1025 )),
1026
1027 message::UserContent::ToolResult(_) => Err(message::MessageError::ConversionError(
1028 "Tool results should be handled as separate messages".into(),
1029 )),
1030 }
1031 }
1032}
1033
1034impl TryFrom<OneOrMany<message::UserContent>> for Vec<Message> {
1035 type Error = message::MessageError;
1036
1037 fn try_from(value: OneOrMany<message::UserContent>) -> Result<Self, Self::Error> {
1038 let (tool_results, other_content): (Vec<_>, Vec<_>) = value
1039 .into_iter()
1040 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1041
1042 if !tool_results.is_empty() {
1045 tool_results
1046 .into_iter()
1047 .map(|content| match content {
1048 message::UserContent::ToolResult(tool_result) => Ok(Message::ToolResult {
1049 tool_call_id: tool_result.id,
1050 content: tool_result
1051 .content
1052 .into_iter()
1053 .map(|c| match c {
1054 message::ToolResultContent::Text(message::Text { text }) => text,
1055 message::ToolResultContent::Image(_) => {
1056 "[Image content not supported in tool results]".to_string()
1057 }
1058 })
1059 .collect::<Vec<_>>()
1060 .join("\n"),
1061 }),
1062 _ => unreachable!(),
1063 })
1064 .collect::<Result<Vec<_>, _>>()
1065 } else {
1066 let user_content: Vec<UserContent> = other_content
1067 .into_iter()
1068 .map(|content| content.try_into())
1069 .collect::<Result<Vec<_>, _>>()?;
1070
1071 let content = OneOrMany::many(user_content)
1072 .expect("There must be content here if there were no tool result content");
1073
1074 Ok(vec![Message::User {
1075 content,
1076 name: None,
1077 }])
1078 }
1079 }
1080}
1081
1082#[derive(Debug, Deserialize, Serialize)]
1087pub struct Choice {
1088 pub index: usize,
1089 pub native_finish_reason: Option<String>,
1090 pub message: Message,
1091 pub finish_reason: Option<String>,
1092}
1093
1094#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1100#[serde(tag = "role", rename_all = "lowercase")]
1101pub enum Message {
1102 #[serde(alias = "developer")]
1103 System {
1104 #[serde(deserialize_with = "string_or_one_or_many")]
1105 content: OneOrMany<openai::SystemContent>,
1106 #[serde(skip_serializing_if = "Option::is_none")]
1107 name: Option<String>,
1108 },
1109 User {
1110 #[serde(
1111 deserialize_with = "string_or_one_or_many",
1112 serialize_with = "serialize_user_content"
1113 )]
1114 content: OneOrMany<UserContent>,
1115 #[serde(skip_serializing_if = "Option::is_none")]
1116 name: Option<String>,
1117 },
1118 Assistant {
1119 #[serde(default, deserialize_with = "json_utils::string_or_vec")]
1120 content: Vec<openai::AssistantContent>,
1121 #[serde(skip_serializing_if = "Option::is_none")]
1122 refusal: Option<String>,
1123 #[serde(skip_serializing_if = "Option::is_none")]
1124 audio: Option<openai::AudioAssistant>,
1125 #[serde(skip_serializing_if = "Option::is_none")]
1126 name: Option<String>,
1127 #[serde(
1128 default,
1129 deserialize_with = "json_utils::null_or_vec",
1130 skip_serializing_if = "Vec::is_empty"
1131 )]
1132 tool_calls: Vec<openai::ToolCall>,
1133 #[serde(skip_serializing_if = "Option::is_none")]
1134 reasoning: Option<String>,
1135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1136 reasoning_details: Vec<ReasoningDetails>,
1137 },
1138 #[serde(rename = "tool")]
1139 ToolResult {
1140 tool_call_id: String,
1141 content: String,
1142 },
1143}
1144
1145impl Message {
1146 pub fn system(content: &str) -> Self {
1147 Message::System {
1148 content: OneOrMany::one(content.to_owned().into()),
1149 name: None,
1150 }
1151 }
1152}
1153
1154#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1155#[serde(tag = "type", rename_all = "snake_case")]
1156pub enum ReasoningDetails {
1157 #[serde(rename = "reasoning.summary")]
1158 Summary {
1159 id: Option<String>,
1160 format: Option<String>,
1161 index: Option<usize>,
1162 summary: String,
1163 },
1164 #[serde(rename = "reasoning.encrypted")]
1165 Encrypted {
1166 id: Option<String>,
1167 format: Option<String>,
1168 index: Option<usize>,
1169 data: String,
1170 },
1171 #[serde(rename = "reasoning.text")]
1172 Text {
1173 id: Option<String>,
1174 format: Option<String>,
1175 index: Option<usize>,
1176 text: Option<String>,
1177 signature: Option<String>,
1178 },
1179}
1180
1181#[derive(Debug, Deserialize, PartialEq, Clone)]
1182#[serde(untagged)]
1183enum ToolCallAdditionalParams {
1184 ReasoningDetails(ReasoningDetails),
1185 Minimal {
1186 id: Option<String>,
1187 format: Option<String>,
1188 },
1189}
1190
1191impl From<openai::UserContent> for UserContent {
1193 fn from(value: openai::UserContent) -> Self {
1194 match value {
1195 openai::UserContent::Text { text } => UserContent::Text { text },
1196 openai::UserContent::Image { image_url } => UserContent::ImageUrl {
1197 image_url: ImageUrl {
1198 url: image_url.url,
1199 detail: Some(image_url.detail),
1200 },
1201 },
1202 openai::UserContent::Audio { input_audio } => {
1203 UserContent::Text {
1206 text: format!("[Audio content: format={:?}]", input_audio.format),
1207 }
1208 }
1209 }
1210 }
1211}
1212
1213impl From<openai::Message> for Message {
1214 fn from(value: openai::Message) -> Self {
1215 match value {
1216 openai::Message::System { content, name } => Self::System { content, name },
1217 openai::Message::User { content, name } => {
1218 let converted_content = content.map(UserContent::from);
1220 Self::User {
1221 content: converted_content,
1222 name,
1223 }
1224 }
1225 openai::Message::Assistant {
1226 content,
1227 refusal,
1228 audio,
1229 name,
1230 tool_calls,
1231 } => Self::Assistant {
1232 content,
1233 refusal,
1234 audio,
1235 name,
1236 tool_calls,
1237 reasoning: None,
1238 reasoning_details: Vec::new(),
1239 },
1240 openai::Message::ToolResult {
1241 tool_call_id,
1242 content,
1243 } => Self::ToolResult {
1244 tool_call_id,
1245 content: content.as_text(),
1246 },
1247 }
1248 }
1249}
1250
1251impl TryFrom<OneOrMany<message::AssistantContent>> for Vec<Message> {
1252 type Error = message::MessageError;
1253
1254 fn try_from(value: OneOrMany<message::AssistantContent>) -> Result<Self, Self::Error> {
1255 let mut text_content = Vec::new();
1256 let mut tool_calls = Vec::new();
1257 let mut reasoning = None;
1258 let mut reasoning_details = Vec::new();
1259
1260 for content in value.into_iter() {
1261 match content {
1262 message::AssistantContent::Text(text) => text_content.push(text),
1263 message::AssistantContent::ToolCall(tool_call) => {
1264 if let Some(additional_params) = &tool_call.additional_params
1270 && let Ok(additional_params) =
1271 serde_json::from_value::<ToolCallAdditionalParams>(
1272 additional_params.clone(),
1273 )
1274 {
1275 match additional_params {
1276 ToolCallAdditionalParams::ReasoningDetails(full) => {
1277 reasoning_details.push(full);
1278 }
1279 ToolCallAdditionalParams::Minimal { id, format } => {
1280 let id = id.or_else(|| tool_call.call_id.clone());
1281 if let Some(signature) = &tool_call.signature
1282 && let Some(id) = id
1283 {
1284 reasoning_details.push(ReasoningDetails::Encrypted {
1285 id: Some(id),
1286 format,
1287 index: None,
1288 data: signature.clone(),
1289 })
1290 }
1291 }
1292 }
1293 } else if let Some(signature) = &tool_call.signature {
1294 reasoning_details.push(ReasoningDetails::Encrypted {
1295 id: tool_call.call_id.clone(),
1296 format: None,
1297 index: None,
1298 data: signature.clone(),
1299 });
1300 }
1301 tool_calls.push(tool_call.into())
1302 }
1303 message::AssistantContent::Reasoning(r) => {
1304 if r.content.is_empty() {
1305 let display = r.display_text();
1306 if !display.is_empty() {
1307 reasoning = Some(display);
1308 }
1309 } else {
1310 for reasoning_block in &r.content {
1311 let index = Some(reasoning_details.len());
1312 match reasoning_block {
1313 message::ReasoningContent::Text { text, signature } => {
1314 reasoning_details.push(ReasoningDetails::Text {
1315 id: r.id.clone(),
1316 format: None,
1317 index,
1318 text: Some(text.clone()),
1319 signature: signature.clone(),
1320 });
1321 }
1322 message::ReasoningContent::Summary(summary) => {
1323 reasoning_details.push(ReasoningDetails::Summary {
1324 id: r.id.clone(),
1325 format: None,
1326 index,
1327 summary: summary.clone(),
1328 });
1329 }
1330 message::ReasoningContent::Encrypted(data)
1331 | message::ReasoningContent::Redacted { data } => {
1332 reasoning_details.push(ReasoningDetails::Encrypted {
1333 id: r.id.clone(),
1334 format: None,
1335 index,
1336 data: data.clone(),
1337 });
1338 }
1339 }
1340 }
1341 }
1342 }
1343 message::AssistantContent::Image(_) => {
1344 return Err(Self::Error::ConversionError(
1345 "OpenRouter currently doesn't support images.".into(),
1346 ));
1347 }
1348 }
1349 }
1350
1351 Ok(vec![Message::Assistant {
1354 content: text_content
1355 .into_iter()
1356 .map(|content| content.text.into())
1357 .collect::<Vec<_>>(),
1358 refusal: None,
1359 audio: None,
1360 name: None,
1361 tool_calls,
1362 reasoning,
1363 reasoning_details,
1364 }])
1365 }
1366}
1367
1368impl TryFrom<message::Message> for Vec<Message> {
1371 type Error = message::MessageError;
1372
1373 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1374 match message {
1375 message::Message::User { content } => {
1376 content.try_into()
1379 }
1380 message::Message::Assistant { content, .. } => content.try_into(),
1381 }
1382 }
1383}
1384
1385#[derive(Debug, Serialize, Deserialize)]
1386#[serde(untagged, rename_all = "snake_case")]
1387pub enum ToolChoice {
1388 None,
1389 Auto,
1390 Required,
1391 Function(Vec<ToolChoiceFunctionKind>),
1392}
1393
1394impl TryFrom<crate::message::ToolChoice> for ToolChoice {
1395 type Error = CompletionError;
1396
1397 fn try_from(value: crate::message::ToolChoice) -> Result<Self, Self::Error> {
1398 let res = match value {
1399 crate::message::ToolChoice::None => Self::None,
1400 crate::message::ToolChoice::Auto => Self::Auto,
1401 crate::message::ToolChoice::Required => Self::Required,
1402 crate::message::ToolChoice::Specific { function_names } => {
1403 let vec: Vec<ToolChoiceFunctionKind> = function_names
1404 .into_iter()
1405 .map(|name| ToolChoiceFunctionKind::Function { name })
1406 .collect();
1407
1408 Self::Function(vec)
1409 }
1410 };
1411
1412 Ok(res)
1413 }
1414}
1415
1416#[derive(Debug, Serialize, Deserialize)]
1417#[serde(tag = "type", content = "function")]
1418pub enum ToolChoiceFunctionKind {
1419 Function { name: String },
1420}
1421
1422#[derive(Debug, Serialize, Deserialize)]
1423pub(super) struct OpenrouterCompletionRequest {
1424 model: String,
1425 pub messages: Vec<Message>,
1426 #[serde(skip_serializing_if = "Option::is_none")]
1427 temperature: Option<f64>,
1428 #[serde(skip_serializing_if = "Vec::is_empty")]
1429 tools: Vec<crate::providers::openai::completion::ToolDefinition>,
1430 #[serde(skip_serializing_if = "Option::is_none")]
1431 tool_choice: Option<crate::providers::openai::completion::ToolChoice>,
1432 #[serde(flatten, skip_serializing_if = "Option::is_none")]
1433 pub additional_params: Option<serde_json::Value>,
1434}
1435
1436pub struct OpenRouterRequestParams<'a> {
1438 pub model: &'a str,
1439 pub request: CompletionRequest,
1440 pub strict_tools: bool,
1441}
1442
1443impl TryFrom<OpenRouterRequestParams<'_>> for OpenrouterCompletionRequest {
1444 type Error = CompletionError;
1445
1446 fn try_from(params: OpenRouterRequestParams) -> Result<Self, Self::Error> {
1447 let OpenRouterRequestParams {
1448 model,
1449 request: req,
1450 strict_tools,
1451 } = params;
1452 let model = req.model.clone().unwrap_or_else(|| model.to_string());
1453
1454 if req.output_schema.is_some() {
1455 tracing::warn!("Structured outputs currently not supported for OpenRouter");
1456 }
1457
1458 let mut full_history: Vec<Message> = match &req.preamble {
1459 Some(preamble) => vec![Message::system(preamble)],
1460 None => vec![],
1461 };
1462 if let Some(docs) = req.normalized_documents() {
1463 let docs: Vec<Message> = docs.try_into()?;
1464 full_history.extend(docs);
1465 }
1466
1467 let chat_history: Vec<Message> = req
1468 .chat_history
1469 .clone()
1470 .into_iter()
1471 .map(|message| message.try_into())
1472 .collect::<Result<Vec<Vec<Message>>, _>>()?
1473 .into_iter()
1474 .flatten()
1475 .collect();
1476
1477 full_history.extend(chat_history);
1478
1479 let tool_choice = req
1480 .tool_choice
1481 .clone()
1482 .map(crate::providers::openai::completion::ToolChoice::try_from)
1483 .transpose()?;
1484
1485 let tools: Vec<crate::providers::openai::completion::ToolDefinition> = req
1486 .tools
1487 .clone()
1488 .into_iter()
1489 .map(|tool| {
1490 let def = crate::providers::openai::completion::ToolDefinition::from(tool);
1491 if strict_tools { def.with_strict() } else { def }
1492 })
1493 .collect();
1494
1495 Ok(Self {
1496 model,
1497 messages: full_history,
1498 temperature: req.temperature,
1499 tools,
1500 tool_choice,
1501 additional_params: req.additional_params,
1502 })
1503 }
1504}
1505
1506impl TryFrom<(&str, CompletionRequest)> for OpenrouterCompletionRequest {
1507 type Error = CompletionError;
1508
1509 fn try_from((model, req): (&str, CompletionRequest)) -> Result<Self, Self::Error> {
1510 let model = req.model.clone().unwrap_or_else(|| model.to_string());
1511 OpenrouterCompletionRequest::try_from(OpenRouterRequestParams {
1512 model: &model,
1513 request: req,
1514 strict_tools: false,
1515 })
1516 }
1517}
1518
1519#[derive(Clone)]
1520pub struct CompletionModel<T = reqwest::Client> {
1521 pub(crate) client: Client<T>,
1522 pub model: String,
1523 pub strict_tools: bool,
1526}
1527
1528impl<T> CompletionModel<T> {
1529 pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
1530 Self {
1531 client,
1532 model: model.into(),
1533 strict_tools: false,
1534 }
1535 }
1536
1537 pub fn with_strict_tools(mut self) -> Self {
1546 self.strict_tools = true;
1547 self
1548 }
1549}
1550
1551impl<T> completion::CompletionModel for CompletionModel<T>
1552where
1553 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
1554{
1555 type Response = CompletionResponse;
1556 type StreamingResponse = StreamingCompletionResponse;
1557
1558 type Client = Client<T>;
1559
1560 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1561 Self::new(client.clone(), model)
1562 }
1563
1564 async fn completion(
1565 &self,
1566 completion_request: CompletionRequest,
1567 ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
1568 let request_model = completion_request
1569 .model
1570 .clone()
1571 .unwrap_or_else(|| self.model.clone());
1572 let preamble = completion_request.preamble.clone();
1573 let request = OpenrouterCompletionRequest::try_from(OpenRouterRequestParams {
1574 model: request_model.as_ref(),
1575 request: completion_request,
1576 strict_tools: self.strict_tools,
1577 })?;
1578
1579 if enabled!(Level::TRACE) {
1580 tracing::trace!(
1581 target: "rig::completions",
1582 "OpenRouter completion request: {}",
1583 serde_json::to_string_pretty(&request)?
1584 );
1585 }
1586
1587 let span = if tracing::Span::current().is_disabled() {
1588 info_span!(
1589 target: "rig::completions",
1590 "chat",
1591 gen_ai.operation.name = "chat",
1592 gen_ai.provider.name = "openrouter",
1593 gen_ai.request.model = &request_model,
1594 gen_ai.system_instructions = preamble,
1595 gen_ai.response.id = tracing::field::Empty,
1596 gen_ai.response.model = tracing::field::Empty,
1597 gen_ai.usage.output_tokens = tracing::field::Empty,
1598 gen_ai.usage.input_tokens = tracing::field::Empty,
1599 )
1600 } else {
1601 tracing::Span::current()
1602 };
1603
1604 let body = serde_json::to_vec(&request)?;
1605
1606 let req = self
1607 .client
1608 .post("/chat/completions")?
1609 .body(body)
1610 .map_err(|x| CompletionError::HttpError(x.into()))?;
1611
1612 async move {
1613 let response = self.client.send::<_, Bytes>(req).await?;
1614 let status = response.status();
1615 let response_body = response.into_body().into_future().await?.to_vec();
1616
1617 if status.is_success() {
1618 match serde_json::from_slice::<ApiResponse<CompletionResponse>>(&response_body)? {
1619 ApiResponse::Ok(response) => {
1620 let span = tracing::Span::current();
1621 span.record_token_usage(&response.usage);
1622 span.record("gen_ai.response.id", &response.id);
1623 span.record("gen_ai.response.model_name", &response.model);
1624
1625 tracing::debug!(target: "rig::completions",
1626 "OpenRouter response: {response:?}");
1627 response.try_into()
1628 }
1629 ApiResponse::Err(err) => Err(CompletionError::ProviderError(err.message)),
1630 }
1631 } else {
1632 Err(CompletionError::ProviderError(
1633 String::from_utf8_lossy(&response_body).to_string(),
1634 ))
1635 }
1636 }
1637 .instrument(span)
1638 .await
1639 }
1640
1641 async fn stream(
1642 &self,
1643 completion_request: CompletionRequest,
1644 ) -> Result<
1645 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1646 CompletionError,
1647 > {
1648 CompletionModel::stream(self, completion_request).await
1649 }
1650}
1651
1652#[cfg(test)]
1653mod tests {
1654 use super::*;
1655 use serde_json::json;
1656
1657 #[test]
1658 fn test_openrouter_request_uses_request_model_override() {
1659 let request = CompletionRequest {
1660 model: Some("google/gemini-2.5-flash".to_string()),
1661 preamble: None,
1662 chat_history: crate::OneOrMany::one("Hello".into()),
1663 documents: vec![],
1664 tools: vec![],
1665 temperature: None,
1666 max_tokens: None,
1667 tool_choice: None,
1668 additional_params: None,
1669 output_schema: None,
1670 };
1671
1672 let openrouter_request =
1673 OpenrouterCompletionRequest::try_from(("openai/gpt-4o-mini", request))
1674 .expect("request conversion should succeed");
1675 let serialized =
1676 serde_json::to_value(openrouter_request).expect("serialization should succeed");
1677
1678 assert_eq!(serialized["model"], "google/gemini-2.5-flash");
1679 }
1680
1681 #[test]
1682 fn test_openrouter_request_uses_default_model_when_override_unset() {
1683 let request = CompletionRequest {
1684 model: None,
1685 preamble: None,
1686 chat_history: crate::OneOrMany::one("Hello".into()),
1687 documents: vec![],
1688 tools: vec![],
1689 temperature: None,
1690 max_tokens: None,
1691 tool_choice: None,
1692 additional_params: None,
1693 output_schema: None,
1694 };
1695
1696 let openrouter_request =
1697 OpenrouterCompletionRequest::try_from(("openai/gpt-4o-mini", request))
1698 .expect("request conversion should succeed");
1699 let serialized =
1700 serde_json::to_value(openrouter_request).expect("serialization should succeed");
1701
1702 assert_eq!(serialized["model"], "openai/gpt-4o-mini");
1703 }
1704
1705 #[test]
1706 fn test_completion_response_deserialization_gemini_flash() {
1707 let json = json!({
1709 "id": "gen-AAAAAAAAAA-AAAAAAAAAAAAAAAAAAAA",
1710 "provider": "Google",
1711 "model": "google/gemini-2.5-flash",
1712 "object": "chat.completion",
1713 "created": 1765971703u64,
1714 "choices": [{
1715 "logprobs": null,
1716 "finish_reason": "stop",
1717 "native_finish_reason": "STOP",
1718 "index": 0,
1719 "message": {
1720 "role": "assistant",
1721 "content": "CONTENT",
1722 "refusal": null,
1723 "reasoning": null
1724 }
1725 }],
1726 "usage": {
1727 "prompt_tokens": 669,
1728 "completion_tokens": 5,
1729 "total_tokens": 674
1730 }
1731 });
1732
1733 let response: CompletionResponse = serde_json::from_value(json).unwrap();
1734 assert_eq!(response.id, "gen-AAAAAAAAAA-AAAAAAAAAAAAAAAAAAAA");
1735 assert_eq!(response.model, "google/gemini-2.5-flash");
1736 assert_eq!(response.choices.len(), 1);
1737 assert_eq!(response.choices[0].finish_reason, Some("stop".to_string()));
1738 }
1739
1740 #[test]
1741 fn test_message_assistant_without_reasoning_details() {
1742 let json = json!({
1744 "role": "assistant",
1745 "content": "Hello world",
1746 "refusal": null,
1747 "reasoning": null
1748 });
1749
1750 let message: Message = serde_json::from_value(json).unwrap();
1751 match message {
1752 Message::Assistant {
1753 content,
1754 reasoning_details,
1755 ..
1756 } => {
1757 assert_eq!(content.len(), 1);
1758 assert!(reasoning_details.is_empty());
1759 }
1760 _ => panic!("Expected Assistant message"),
1761 }
1762 }
1763
1764 #[test]
1765 fn test_data_collection_serialization() {
1766 assert_eq!(
1767 serde_json::to_string(&DataCollection::Allow).unwrap(),
1768 r#""allow""#
1769 );
1770 assert_eq!(
1771 serde_json::to_string(&DataCollection::Deny).unwrap(),
1772 r#""deny""#
1773 );
1774 }
1775
1776 #[test]
1777 fn test_data_collection_default() {
1778 assert_eq!(DataCollection::default(), DataCollection::Allow);
1779 }
1780
1781 #[test]
1782 fn test_quantization_serialization() {
1783 assert_eq!(
1784 serde_json::to_string(&Quantization::Int4).unwrap(),
1785 r#""int4""#
1786 );
1787 assert_eq!(
1788 serde_json::to_string(&Quantization::Int8).unwrap(),
1789 r#""int8""#
1790 );
1791 assert_eq!(
1792 serde_json::to_string(&Quantization::Fp16).unwrap(),
1793 r#""fp16""#
1794 );
1795 assert_eq!(
1796 serde_json::to_string(&Quantization::Bf16).unwrap(),
1797 r#""bf16""#
1798 );
1799 assert_eq!(
1800 serde_json::to_string(&Quantization::Fp32).unwrap(),
1801 r#""fp32""#
1802 );
1803 assert_eq!(
1804 serde_json::to_string(&Quantization::Fp8).unwrap(),
1805 r#""fp8""#
1806 );
1807 assert_eq!(
1808 serde_json::to_string(&Quantization::Unknown).unwrap(),
1809 r#""unknown""#
1810 );
1811 }
1812
1813 #[test]
1814 fn test_provider_sort_strategy_serialization() {
1815 assert_eq!(
1816 serde_json::to_string(&ProviderSortStrategy::Price).unwrap(),
1817 r#""price""#
1818 );
1819 assert_eq!(
1820 serde_json::to_string(&ProviderSortStrategy::Throughput).unwrap(),
1821 r#""throughput""#
1822 );
1823 assert_eq!(
1824 serde_json::to_string(&ProviderSortStrategy::Latency).unwrap(),
1825 r#""latency""#
1826 );
1827 }
1828
1829 #[test]
1830 fn test_sort_partition_serialization() {
1831 assert_eq!(
1832 serde_json::to_string(&SortPartition::Model).unwrap(),
1833 r#""model""#
1834 );
1835 assert_eq!(
1836 serde_json::to_string(&SortPartition::None).unwrap(),
1837 r#""none""#
1838 );
1839 }
1840
1841 #[test]
1842 fn test_provider_sort_simple() {
1843 let sort = ProviderSort::Simple(ProviderSortStrategy::Latency);
1844 let json = serde_json::to_value(&sort).unwrap();
1845 assert_eq!(json, "latency");
1846 }
1847
1848 #[test]
1849 fn test_provider_sort_complex() {
1850 let sort = ProviderSort::Complex(
1851 ProviderSortConfig::new(ProviderSortStrategy::Price).partition(SortPartition::None),
1852 );
1853 let json = serde_json::to_value(&sort).unwrap();
1854 assert_eq!(json["by"], "price");
1855 assert_eq!(json["partition"], "none");
1856 }
1857
1858 #[test]
1859 fn test_provider_sort_complex_without_partition() {
1860 let sort = ProviderSort::Complex(ProviderSortConfig::new(ProviderSortStrategy::Throughput));
1861 let json = serde_json::to_value(&sort).unwrap();
1862 assert_eq!(json["by"], "throughput");
1863 assert!(json.get("partition").is_none());
1864 }
1865
1866 #[test]
1867 fn test_provider_sort_from_strategy() {
1868 let sort: ProviderSort = ProviderSortStrategy::Price.into();
1869 assert_eq!(sort, ProviderSort::Simple(ProviderSortStrategy::Price));
1870 }
1871
1872 #[test]
1873 fn test_provider_sort_from_config() {
1874 let config = ProviderSortConfig::new(ProviderSortStrategy::Latency);
1875 let sort: ProviderSort = config.into();
1876 match sort {
1877 ProviderSort::Complex(c) => assert_eq!(c.by, ProviderSortStrategy::Latency),
1878 _ => panic!("Expected Complex variant"),
1879 }
1880 }
1881
1882 #[test]
1883 fn test_percentile_thresholds_builder() {
1884 let thresholds = PercentileThresholds::new()
1885 .p50(10.0)
1886 .p75(25.0)
1887 .p90(50.0)
1888 .p99(100.0);
1889
1890 assert_eq!(thresholds.p50, Some(10.0));
1891 assert_eq!(thresholds.p75, Some(25.0));
1892 assert_eq!(thresholds.p90, Some(50.0));
1893 assert_eq!(thresholds.p99, Some(100.0));
1894 }
1895
1896 #[test]
1897 fn test_percentile_thresholds_default() {
1898 let thresholds = PercentileThresholds::default();
1899 assert_eq!(thresholds.p50, None);
1900 assert_eq!(thresholds.p75, None);
1901 assert_eq!(thresholds.p90, None);
1902 assert_eq!(thresholds.p99, None);
1903 }
1904
1905 #[test]
1906 fn test_throughput_threshold_simple() {
1907 let threshold = ThroughputThreshold::Simple(50.0);
1908 let json = serde_json::to_value(&threshold).unwrap();
1909 assert_eq!(json, 50.0);
1910 }
1911
1912 #[test]
1913 fn test_throughput_threshold_percentile() {
1914 let threshold = ThroughputThreshold::Percentile(PercentileThresholds::new().p90(50.0));
1915 let json = serde_json::to_value(&threshold).unwrap();
1916 assert_eq!(json["p90"], 50.0);
1917 }
1918
1919 #[test]
1920 fn test_latency_threshold_simple() {
1921 let threshold = LatencyThreshold::Simple(0.5);
1922 let json = serde_json::to_value(&threshold).unwrap();
1923 assert_eq!(json, 0.5);
1924 }
1925
1926 #[test]
1927 fn test_latency_threshold_percentile() {
1928 let threshold = LatencyThreshold::Percentile(PercentileThresholds::new().p50(0.1).p99(1.0));
1929 let json = serde_json::to_value(&threshold).unwrap();
1930 assert_eq!(json["p50"], 0.1);
1931 assert_eq!(json["p99"], 1.0);
1932 }
1933
1934 #[test]
1935 fn test_max_price_builder() {
1936 let price = MaxPrice::new().prompt(0.001).completion(0.002);
1937
1938 assert_eq!(price.prompt, Some(0.001));
1939 assert_eq!(price.completion, Some(0.002));
1940 assert_eq!(price.request, None);
1941 assert_eq!(price.image, None);
1942 }
1943
1944 #[test]
1945 fn test_max_price_all_fields() {
1946 let price = MaxPrice::new()
1947 .prompt(0.001)
1948 .completion(0.002)
1949 .request(0.01)
1950 .image(0.05);
1951
1952 let json = serde_json::to_value(&price).unwrap();
1953 assert_eq!(json["prompt"], 0.001);
1954 assert_eq!(json["completion"], 0.002);
1955 assert_eq!(json["request"], 0.01);
1956 assert_eq!(json["image"], 0.05);
1957 }
1958
1959 #[test]
1960 fn test_max_price_default() {
1961 let price = MaxPrice::default();
1962 assert_eq!(price.prompt, None);
1963 assert_eq!(price.completion, None);
1964 assert_eq!(price.request, None);
1965 assert_eq!(price.image, None);
1966 }
1967
1968 #[test]
1969 fn test_provider_preferences_default() {
1970 let prefs = ProviderPreferences::default();
1971 assert!(prefs.order.is_none());
1972 assert!(prefs.only.is_none());
1973 assert!(prefs.ignore.is_none());
1974 assert!(prefs.allow_fallbacks.is_none());
1975 assert!(prefs.require_parameters.is_none());
1976 assert!(prefs.data_collection.is_none());
1977 assert!(prefs.zdr.is_none());
1978 assert!(prefs.sort.is_none());
1979 assert!(prefs.preferred_min_throughput.is_none());
1980 assert!(prefs.preferred_max_latency.is_none());
1981 assert!(prefs.max_price.is_none());
1982 assert!(prefs.quantizations.is_none());
1983 }
1984
1985 #[test]
1986 fn test_provider_preferences_order_with_fallbacks() {
1987 let prefs = ProviderPreferences::new()
1988 .order(["anthropic", "openai"])
1989 .allow_fallbacks(true);
1990
1991 let json = prefs.to_json();
1992 let provider = &json["provider"];
1993
1994 assert_eq!(provider["order"], json!(["anthropic", "openai"]));
1995 assert_eq!(provider["allow_fallbacks"], true);
1996 }
1997
1998 #[test]
1999 fn test_provider_preferences_only_allowlist() {
2000 let prefs = ProviderPreferences::new()
2001 .only(["azure", "together"])
2002 .allow_fallbacks(false);
2003
2004 let json = prefs.to_json();
2005 let provider = &json["provider"];
2006
2007 assert_eq!(provider["only"], json!(["azure", "together"]));
2008 assert_eq!(provider["allow_fallbacks"], false);
2009 }
2010
2011 #[test]
2012 fn test_provider_preferences_ignore() {
2013 let prefs = ProviderPreferences::new().ignore(["deepinfra"]);
2014
2015 let json = prefs.to_json();
2016 let provider = &json["provider"];
2017
2018 assert_eq!(provider["ignore"], json!(["deepinfra"]));
2019 }
2020
2021 #[test]
2022 fn test_provider_preferences_sort_latency() {
2023 let prefs = ProviderPreferences::new().sort(ProviderSortStrategy::Latency);
2024
2025 let json = prefs.to_json();
2026 let provider = &json["provider"];
2027
2028 assert_eq!(provider["sort"], "latency");
2029 }
2030
2031 #[test]
2032 fn test_provider_preferences_price_with_throughput() {
2033 let prefs = ProviderPreferences::new()
2034 .sort(ProviderSortStrategy::Price)
2035 .preferred_min_throughput(ThroughputThreshold::Percentile(
2036 PercentileThresholds::new().p90(50.0),
2037 ));
2038
2039 let json = prefs.to_json();
2040 let provider = &json["provider"];
2041
2042 assert_eq!(provider["sort"], "price");
2043 assert_eq!(provider["preferred_min_throughput"]["p90"], 50.0);
2044 }
2045
2046 #[test]
2047 fn test_provider_preferences_require_parameters() {
2048 let prefs = ProviderPreferences::new().require_parameters(true);
2049
2050 let json = prefs.to_json();
2051 let provider = &json["provider"];
2052
2053 assert_eq!(provider["require_parameters"], true);
2054 }
2055
2056 #[test]
2057 fn test_provider_preferences_data_policy_and_zdr() {
2058 let prefs = ProviderPreferences::new()
2059 .data_collection(DataCollection::Deny)
2060 .zdr(true);
2061
2062 let json = prefs.to_json();
2063 let provider = &json["provider"];
2064
2065 assert_eq!(provider["data_collection"], "deny");
2066 assert_eq!(provider["zdr"], true);
2067 }
2068
2069 #[test]
2070 fn test_provider_preferences_quantizations() {
2071 let prefs =
2072 ProviderPreferences::new().quantizations([Quantization::Int8, Quantization::Fp16]);
2073
2074 let json = prefs.to_json();
2075 let provider = &json["provider"];
2076
2077 assert_eq!(provider["quantizations"], json!(["int8", "fp16"]));
2078 }
2079
2080 #[test]
2081 fn test_provider_preferences_convenience_methods() {
2082 let prefs = ProviderPreferences::new().zero_data_retention().fastest();
2083
2084 assert_eq!(prefs.zdr, Some(true));
2085 assert_eq!(
2086 prefs.sort,
2087 Some(ProviderSort::Simple(ProviderSortStrategy::Throughput))
2088 );
2089
2090 let prefs2 = ProviderPreferences::new().cheapest();
2091 assert_eq!(
2092 prefs2.sort,
2093 Some(ProviderSort::Simple(ProviderSortStrategy::Price))
2094 );
2095
2096 let prefs3 = ProviderPreferences::new().lowest_latency();
2097 assert_eq!(
2098 prefs3.sort,
2099 Some(ProviderSort::Simple(ProviderSortStrategy::Latency))
2100 );
2101 }
2102
2103 #[test]
2104 fn test_provider_preferences_serialization_skips_none() {
2105 let prefs = ProviderPreferences::new().sort(ProviderSortStrategy::Price);
2106
2107 let json = serde_json::to_value(&prefs).unwrap();
2108
2109 assert_eq!(json["sort"], "price");
2110 assert!(json.get("order").is_none());
2111 assert!(json.get("only").is_none());
2112 assert!(json.get("ignore").is_none());
2113 assert!(json.get("zdr").is_none());
2114 }
2115
2116 #[test]
2117 fn test_provider_preferences_deserialization() {
2118 let json = json!({
2119 "order": ["anthropic", "openai"],
2120 "sort": "throughput",
2121 "data_collection": "deny",
2122 "zdr": true,
2123 "quantizations": ["int8", "fp16"]
2124 });
2125
2126 let prefs: ProviderPreferences = serde_json::from_value(json).unwrap();
2127
2128 assert_eq!(
2129 prefs.order,
2130 Some(vec!["anthropic".to_string(), "openai".to_string()])
2131 );
2132 assert_eq!(
2133 prefs.sort,
2134 Some(ProviderSort::Simple(ProviderSortStrategy::Throughput))
2135 );
2136 assert_eq!(prefs.data_collection, Some(DataCollection::Deny));
2137 assert_eq!(prefs.zdr, Some(true));
2138 assert_eq!(
2139 prefs.quantizations,
2140 Some(vec![Quantization::Int8, Quantization::Fp16])
2141 );
2142 }
2143
2144 #[test]
2145 fn test_provider_preferences_deserialization_complex_sort() {
2146 let json = json!({
2147 "sort": {
2148 "by": "latency",
2149 "partition": "model"
2150 }
2151 });
2152
2153 let prefs: ProviderPreferences = serde_json::from_value(json).unwrap();
2154
2155 match prefs.sort {
2156 Some(ProviderSort::Complex(config)) => {
2157 assert_eq!(config.by, ProviderSortStrategy::Latency);
2158 assert_eq!(config.partition, Some(SortPartition::Model));
2159 }
2160 _ => panic!("Expected Complex sort variant"),
2161 }
2162 }
2163
2164 #[test]
2165 fn test_provider_preferences_full_integration() {
2166 let prefs = ProviderPreferences::new()
2167 .order(["anthropic", "openai"])
2168 .only(["anthropic", "openai", "google"])
2169 .sort(ProviderSortStrategy::Throughput)
2170 .data_collection(DataCollection::Deny)
2171 .zdr(true)
2172 .quantizations([Quantization::Int8])
2173 .allow_fallbacks(false);
2174
2175 let json = prefs.to_json();
2176
2177 assert!(json.get("provider").is_some());
2178 let provider = &json["provider"];
2179 assert_eq!(provider["order"], json!(["anthropic", "openai"]));
2180 assert_eq!(provider["only"], json!(["anthropic", "openai", "google"]));
2181 assert_eq!(provider["sort"], "throughput");
2182 assert_eq!(provider["data_collection"], "deny");
2183 assert_eq!(provider["zdr"], true);
2184 assert_eq!(provider["quantizations"], json!(["int8"]));
2185 assert_eq!(provider["allow_fallbacks"], false);
2186 }
2187
2188 #[test]
2189 fn test_provider_preferences_max_price() {
2190 let prefs =
2191 ProviderPreferences::new().max_price(MaxPrice::new().prompt(0.001).completion(0.002));
2192
2193 let json = prefs.to_json();
2194 let provider = &json["provider"];
2195
2196 assert_eq!(provider["max_price"]["prompt"], 0.001);
2197 assert_eq!(provider["max_price"]["completion"], 0.002);
2198 }
2199
2200 #[test]
2201 fn test_provider_preferences_preferred_max_latency() {
2202 let prefs = ProviderPreferences::new().preferred_max_latency(LatencyThreshold::Simple(0.5));
2203
2204 let json = prefs.to_json();
2205 let provider = &json["provider"];
2206
2207 assert_eq!(provider["preferred_max_latency"], 0.5);
2208 }
2209
2210 #[test]
2211 fn test_provider_preferences_empty_arrays() {
2212 let prefs = ProviderPreferences::new()
2213 .order(Vec::<String>::new())
2214 .quantizations(Vec::<Quantization>::new());
2215
2216 let json = prefs.to_json();
2217 let provider = &json["provider"];
2218
2219 assert_eq!(provider["order"], json!([]));
2220 assert_eq!(provider["quantizations"], json!([]));
2221 }
2222
2223 #[test]
2228 fn test_user_content_text_serialization() {
2229 let content = UserContent::text("Hello, world!");
2230 let json = serde_json::to_value(&content).unwrap();
2231
2232 assert_eq!(json["type"], "text");
2233 assert_eq!(json["text"], "Hello, world!");
2234 }
2235
2236 #[test]
2237 fn test_user_content_image_url_serialization() {
2238 let content = UserContent::image_url("https://example.com/image.png");
2239 let json = serde_json::to_value(&content).unwrap();
2240
2241 assert_eq!(json["type"], "image_url");
2242 assert_eq!(json["image_url"]["url"], "https://example.com/image.png");
2243 assert!(json["image_url"].get("detail").is_none());
2244 }
2245
2246 #[test]
2247 fn test_user_content_image_url_with_detail_serialization() {
2248 let content =
2249 UserContent::image_url_with_detail("https://example.com/image.png", ImageDetail::High);
2250 let json = serde_json::to_value(&content).unwrap();
2251
2252 assert_eq!(json["type"], "image_url");
2253 assert_eq!(json["image_url"]["url"], "https://example.com/image.png");
2254 assert_eq!(json["image_url"]["detail"], "high");
2255 }
2256
2257 #[test]
2258 fn test_user_content_image_base64_serialization() {
2259 let content = UserContent::image_base64("SGVsbG8=", "image/png", Some(ImageDetail::Low));
2260 let json = serde_json::to_value(&content).unwrap();
2261
2262 assert_eq!(json["type"], "image_url");
2263 assert_eq!(json["image_url"]["url"], "data:image/png;base64,SGVsbG8=");
2264 assert_eq!(json["image_url"]["detail"], "low");
2265 }
2266
2267 #[test]
2268 fn test_user_content_file_url_serialization() {
2269 let content = UserContent::file_url(
2270 "https://example.com/doc.pdf",
2271 Some("document.pdf".to_string()),
2272 );
2273 let json = serde_json::to_value(&content).unwrap();
2274
2275 assert_eq!(json["type"], "file");
2276 assert_eq!(json["file"]["file_data"], "https://example.com/doc.pdf");
2277 assert_eq!(json["file"]["filename"], "document.pdf");
2278 }
2279
2280 #[test]
2281 fn test_user_content_file_base64_serialization() {
2282 let content = UserContent::file_base64(
2283 "JVBERi0xLjQ=",
2284 "application/pdf",
2285 Some("report.pdf".to_string()),
2286 );
2287 let json = serde_json::to_value(&content).unwrap();
2288
2289 assert_eq!(json["type"], "file");
2290 assert_eq!(
2291 json["file"]["file_data"],
2292 "data:application/pdf;base64,JVBERi0xLjQ="
2293 );
2294 assert_eq!(json["file"]["filename"], "report.pdf");
2295 }
2296
2297 #[test]
2298 fn test_user_content_text_deserialization() {
2299 let json = json!({
2300 "type": "text",
2301 "text": "Hello!"
2302 });
2303
2304 let content: UserContent = serde_json::from_value(json).unwrap();
2305 assert_eq!(
2306 content,
2307 UserContent::Text {
2308 text: "Hello!".to_string()
2309 }
2310 );
2311 }
2312
2313 #[test]
2314 fn test_user_content_image_url_deserialization() {
2315 let json = json!({
2316 "type": "image_url",
2317 "image_url": {
2318 "url": "https://example.com/img.jpg",
2319 "detail": "high"
2320 }
2321 });
2322
2323 let content: UserContent = serde_json::from_value(json).unwrap();
2324 match content {
2325 UserContent::ImageUrl { image_url } => {
2326 assert_eq!(image_url.url, "https://example.com/img.jpg");
2327 assert_eq!(image_url.detail, Some(ImageDetail::High));
2328 }
2329 _ => panic!("Expected ImageUrl variant"),
2330 }
2331 }
2332
2333 #[test]
2334 fn test_user_content_file_deserialization() {
2335 let json = json!({
2336 "type": "file",
2337 "file": {
2338 "filename": "doc.pdf",
2339 "file_data": "https://example.com/doc.pdf"
2340 }
2341 });
2342
2343 let content: UserContent = serde_json::from_value(json).unwrap();
2344 match content {
2345 UserContent::File { file } => {
2346 assert_eq!(file.filename, Some("doc.pdf".to_string()));
2347 assert_eq!(
2348 file.file_data,
2349 Some("https://example.com/doc.pdf".to_string())
2350 );
2351 }
2352 _ => panic!("Expected File variant"),
2353 }
2354 }
2355
2356 #[test]
2357 fn test_message_user_with_text_serialization() {
2358 let message = Message::User {
2359 content: OneOrMany::one(UserContent::text("Hello")),
2360 name: None,
2361 };
2362 let json = serde_json::to_value(&message).unwrap();
2363
2364 assert_eq!(json["role"], "user");
2366 assert_eq!(json["content"], "Hello");
2367 }
2368
2369 #[test]
2370 fn test_message_user_with_mixed_content_serialization() {
2371 let message = Message::User {
2372 content: OneOrMany::many(vec![
2373 UserContent::text("Check this image:"),
2374 UserContent::image_url("https://example.com/img.png"),
2375 ])
2376 .unwrap(),
2377 name: None,
2378 };
2379 let json = serde_json::to_value(&message).unwrap();
2380
2381 assert_eq!(json["role"], "user");
2382 let content = json["content"].as_array().unwrap();
2383 assert_eq!(content.len(), 2);
2384 assert_eq!(content[0]["type"], "text");
2385 assert_eq!(content[1]["type"], "image_url");
2386 }
2387
2388 #[test]
2389 fn test_message_user_with_file_serialization() {
2390 let message = Message::User {
2391 content: OneOrMany::many(vec![
2392 UserContent::text("Analyze this PDF:"),
2393 UserContent::file_url(
2394 "https://example.com/doc.pdf",
2395 Some("document.pdf".to_string()),
2396 ),
2397 ])
2398 .unwrap(),
2399 name: None,
2400 };
2401 let json = serde_json::to_value(&message).unwrap();
2402
2403 assert_eq!(json["role"], "user");
2404 let content = json["content"].as_array().unwrap();
2405 assert_eq!(content.len(), 2);
2406 assert_eq!(content[0]["type"], "text");
2407 assert_eq!(content[1]["type"], "file");
2408 assert_eq!(
2409 content[1]["file"]["file_data"],
2410 "https://example.com/doc.pdf"
2411 );
2412 }
2413
2414 #[test]
2415 fn test_user_content_from_rig_text() {
2416 let rig_content = message::UserContent::Text(message::Text {
2417 text: "Hello".to_string(),
2418 });
2419 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2420
2421 assert_eq!(
2422 openrouter_content,
2423 UserContent::Text {
2424 text: "Hello".to_string()
2425 }
2426 );
2427 }
2428
2429 #[test]
2430 fn test_user_content_from_rig_image_url() {
2431 let rig_content = message::UserContent::Image(message::Image {
2432 data: DocumentSourceKind::Url("https://example.com/img.png".to_string()),
2433 media_type: Some(message::ImageMediaType::PNG),
2434 detail: Some(ImageDetail::High),
2435 additional_params: None,
2436 });
2437 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2438
2439 match openrouter_content {
2440 UserContent::ImageUrl { image_url } => {
2441 assert_eq!(image_url.url, "https://example.com/img.png");
2442 assert_eq!(image_url.detail, Some(ImageDetail::High));
2443 }
2444 _ => panic!("Expected ImageUrl variant"),
2445 }
2446 }
2447
2448 #[test]
2449 fn test_user_content_from_rig_image_base64() {
2450 let rig_content = message::UserContent::Image(message::Image {
2451 data: DocumentSourceKind::Base64("SGVsbG8=".to_string()),
2452 media_type: Some(message::ImageMediaType::JPEG),
2453 detail: Some(ImageDetail::Low),
2454 additional_params: None,
2455 });
2456 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2457
2458 match openrouter_content {
2459 UserContent::ImageUrl { image_url } => {
2460 assert_eq!(image_url.url, "data:image/jpeg;base64,SGVsbG8=");
2461 assert_eq!(image_url.detail, Some(ImageDetail::Low));
2462 }
2463 _ => panic!("Expected ImageUrl variant"),
2464 }
2465 }
2466
2467 #[test]
2468 fn test_user_content_from_rig_document_url() {
2469 let rig_content = message::UserContent::Document(message::Document {
2470 data: DocumentSourceKind::Url("https://example.com/doc.pdf".to_string()),
2471 media_type: Some(DocumentMediaType::PDF),
2472 additional_params: None,
2473 });
2474 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2475
2476 match openrouter_content {
2477 UserContent::File { file } => {
2478 assert_eq!(
2479 file.file_data,
2480 Some("https://example.com/doc.pdf".to_string())
2481 );
2482 assert_eq!(file.filename, Some("document.pdf".to_string()));
2483 }
2484 _ => panic!("Expected File variant"),
2485 }
2486 }
2487
2488 #[test]
2489 fn test_user_content_from_rig_document_base64() {
2490 let rig_content = message::UserContent::Document(message::Document {
2491 data: DocumentSourceKind::Base64("JVBERi0xLjQ=".to_string()),
2492 media_type: Some(DocumentMediaType::PDF),
2493 additional_params: None,
2494 });
2495 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2496
2497 match openrouter_content {
2498 UserContent::File { file } => {
2499 assert_eq!(
2500 file.file_data,
2501 Some("data:application/pdf;base64,JVBERi0xLjQ=".to_string())
2502 );
2503 assert_eq!(file.filename, Some("document.pdf".to_string()));
2504 }
2505 _ => panic!("Expected File variant"),
2506 }
2507 }
2508
2509 #[test]
2510 fn test_user_content_from_rig_document_string_becomes_text() {
2511 let rig_content = message::UserContent::Document(message::Document {
2512 data: DocumentSourceKind::String("Plain text document content".to_string()),
2513 media_type: Some(DocumentMediaType::TXT),
2514 additional_params: None,
2515 });
2516 let openrouter_content: UserContent = rig_content.try_into().unwrap();
2517
2518 assert_eq!(
2519 openrouter_content,
2520 UserContent::Text {
2521 text: "Plain text document content".to_string()
2522 }
2523 );
2524 }
2525
2526 #[test]
2527 fn test_completion_response_with_reasoning_details_maps_to_typed_reasoning() {
2528 let json = json!({
2529 "id": "resp_123",
2530 "object": "chat.completion",
2531 "created": 1,
2532 "model": "openrouter/test-model",
2533 "choices": [{
2534 "index": 0,
2535 "finish_reason": "stop",
2536 "message": {
2537 "role": "assistant",
2538 "content": "hello",
2539 "reasoning": null,
2540 "reasoning_details": [
2541 {"type":"reasoning.summary","id":"rs_1","summary":"s1"},
2542 {"type":"reasoning.text","id":"rs_1","text":"t1","signature":"sig_1"},
2543 {"type":"reasoning.encrypted","id":"rs_1","data":"enc_1"}
2544 ]
2545 }
2546 }]
2547 });
2548
2549 let response: CompletionResponse = serde_json::from_value(json).unwrap();
2550 let converted: completion::CompletionResponse<CompletionResponse> =
2551 response.try_into().unwrap();
2552 let items: Vec<completion::AssistantContent> = converted.choice.into_iter().collect();
2553
2554 assert!(items.iter().any(|item| matches!(
2555 item,
2556 completion::AssistantContent::Reasoning(message::Reasoning { id: Some(id), content })
2557 if id == "rs_1" && content.len() == 3
2558 )));
2559 }
2560
2561 #[test]
2562 fn test_assistant_reasoning_emits_openrouter_reasoning_details() {
2563 let reasoning = message::Reasoning {
2564 id: Some("rs_2".to_string()),
2565 content: vec![
2566 message::ReasoningContent::Text {
2567 text: "step".to_string(),
2568 signature: Some("sig_step".to_string()),
2569 },
2570 message::ReasoningContent::Summary("summary".to_string()),
2571 message::ReasoningContent::Encrypted("enc_blob".to_string()),
2572 ],
2573 };
2574
2575 let messages = Vec::<Message>::try_from(OneOrMany::one(
2576 message::AssistantContent::Reasoning(reasoning),
2577 ))
2578 .unwrap();
2579 let Message::Assistant {
2580 reasoning,
2581 reasoning_details,
2582 ..
2583 } = messages.first().expect("assistant message")
2584 else {
2585 panic!("Expected assistant message");
2586 };
2587
2588 assert!(reasoning.is_none());
2589 assert_eq!(reasoning_details.len(), 3);
2590 assert!(matches!(
2591 reasoning_details.first(),
2592 Some(ReasoningDetails::Text {
2593 id: Some(id),
2594 text: Some(text),
2595 signature: Some(signature),
2596 ..
2597 }) if id == "rs_2" && text == "step" && signature == "sig_step"
2598 ));
2599 }
2600
2601 #[test]
2602 fn test_assistant_redacted_reasoning_emits_encrypted_detail_not_text() {
2603 let reasoning = message::Reasoning {
2604 id: Some("rs_redacted".to_string()),
2605 content: vec![message::ReasoningContent::Redacted {
2606 data: "opaque-redacted-data".to_string(),
2607 }],
2608 };
2609
2610 let messages = Vec::<Message>::try_from(OneOrMany::one(
2611 message::AssistantContent::Reasoning(reasoning),
2612 ))
2613 .unwrap();
2614
2615 let Message::Assistant {
2616 reasoning_details,
2617 reasoning,
2618 ..
2619 } = messages.first().expect("assistant message")
2620 else {
2621 panic!("Expected assistant message");
2622 };
2623
2624 assert!(reasoning.is_none());
2625 assert_eq!(reasoning_details.len(), 1);
2626 assert!(matches!(
2627 reasoning_details.first(),
2628 Some(ReasoningDetails::Encrypted {
2629 id: Some(id),
2630 data,
2631 ..
2632 }) if id == "rs_redacted" && data == "opaque-redacted-data"
2633 ));
2634 }
2635
2636 #[test]
2637 fn test_completion_response_reasoning_details_respects_index_ordering() {
2638 let json = json!({
2639 "id": "resp_ordering",
2640 "object": "chat.completion",
2641 "created": 1,
2642 "model": "openrouter/test-model",
2643 "choices": [{
2644 "index": 0,
2645 "finish_reason": "stop",
2646 "message": {
2647 "role": "assistant",
2648 "content": "hello",
2649 "reasoning": null,
2650 "reasoning_details": [
2651 {"type":"reasoning.summary","id":"rs_order","index":1,"summary":"second"},
2652 {"type":"reasoning.summary","id":"rs_order","index":0,"summary":"first"}
2653 ]
2654 }
2655 }]
2656 });
2657
2658 let response: CompletionResponse = serde_json::from_value(json).unwrap();
2659 let converted: completion::CompletionResponse<CompletionResponse> =
2660 response.try_into().unwrap();
2661 let items: Vec<completion::AssistantContent> = converted.choice.into_iter().collect();
2662 let reasoning_blocks: Vec<_> = items
2663 .into_iter()
2664 .filter_map(|item| match item {
2665 completion::AssistantContent::Reasoning(reasoning) => Some(reasoning),
2666 _ => None,
2667 })
2668 .collect();
2669
2670 assert_eq!(reasoning_blocks.len(), 1);
2671 assert_eq!(reasoning_blocks[0].id.as_deref(), Some("rs_order"));
2672 assert_eq!(
2673 reasoning_blocks[0].content,
2674 vec![
2675 message::ReasoningContent::Summary("first".to_string()),
2676 message::ReasoningContent::Summary("second".to_string()),
2677 ]
2678 );
2679 }
2680
2681 #[test]
2682 fn test_user_content_from_rig_image_missing_media_type_error() {
2683 let rig_content = message::UserContent::Image(message::Image {
2684 data: DocumentSourceKind::Base64("SGVsbG8=".to_string()),
2685 media_type: None, detail: None,
2687 additional_params: None,
2688 });
2689 let result: Result<UserContent, _> = rig_content.try_into();
2690
2691 assert!(result.is_err());
2692 let err = result.unwrap_err();
2693 assert!(err.to_string().contains("media type required"));
2694 }
2695
2696 #[test]
2697 fn test_user_content_from_rig_image_raw_bytes_error() {
2698 let rig_content = message::UserContent::Image(message::Image {
2699 data: DocumentSourceKind::Raw(vec![1, 2, 3]),
2700 media_type: Some(message::ImageMediaType::PNG),
2701 detail: None,
2702 additional_params: None,
2703 });
2704 let result: Result<UserContent, _> = rig_content.try_into();
2705
2706 assert!(result.is_err());
2707 let err = result.unwrap_err();
2708 assert!(err.to_string().contains("base64"));
2709 }
2710
2711 #[test]
2712 fn test_user_content_from_rig_video_not_supported() {
2713 let rig_content = message::UserContent::Video(message::Video {
2714 data: DocumentSourceKind::Url("https://example.com/video.mp4".to_string()),
2715 media_type: Some(message::VideoMediaType::MP4),
2716 additional_params: None,
2717 });
2718 let result: Result<UserContent, _> = rig_content.try_into();
2719
2720 assert!(result.is_err());
2721 let err = result.unwrap_err();
2722 assert!(err.to_string().contains("Video"));
2723 }
2724
2725 #[test]
2726 fn test_user_content_from_rig_audio_not_supported() {
2727 let rig_content = message::UserContent::Audio(message::Audio {
2728 data: DocumentSourceKind::Base64("audiodata".to_string()),
2729 media_type: Some(message::AudioMediaType::MP3),
2730 additional_params: None,
2731 });
2732 let result: Result<UserContent, _> = rig_content.try_into();
2733
2734 assert!(result.is_err());
2735 let err = result.unwrap_err();
2736 assert!(err.to_string().contains("Audio"));
2737 }
2738
2739 #[test]
2740 fn test_message_conversion_with_pdf() {
2741 let rig_message = message::Message::User {
2742 content: OneOrMany::many(vec![
2743 message::UserContent::Text(message::Text {
2744 text: "Summarize this document".to_string(),
2745 }),
2746 message::UserContent::Document(message::Document {
2747 data: DocumentSourceKind::Url("https://example.com/paper.pdf".to_string()),
2748 media_type: Some(DocumentMediaType::PDF),
2749 additional_params: None,
2750 }),
2751 ])
2752 .unwrap(),
2753 };
2754
2755 let openrouter_messages: Vec<Message> = rig_message.try_into().unwrap();
2756 assert_eq!(openrouter_messages.len(), 1);
2757
2758 match &openrouter_messages[0] {
2759 Message::User { content, .. } => {
2760 assert_eq!(content.len(), 2);
2761
2762 match content.first_ref() {
2764 UserContent::Text { text } => assert_eq!(text, "Summarize this document"),
2765 _ => panic!("Expected Text"),
2766 }
2767 }
2768 _ => panic!("Expected User message"),
2769 }
2770 }
2771
2772 #[test]
2773 fn test_user_content_from_string() {
2774 let content: UserContent = "Hello".into();
2775 assert_eq!(
2776 content,
2777 UserContent::Text {
2778 text: "Hello".to_string()
2779 }
2780 );
2781
2782 let content: UserContent = String::from("World").into();
2783 assert_eq!(
2784 content,
2785 UserContent::Text {
2786 text: "World".to_string()
2787 }
2788 );
2789 }
2790
2791 #[test]
2792 fn test_openai_user_content_conversion() {
2793 let openai_text = openai::UserContent::Text {
2795 text: "Hello".to_string(),
2796 };
2797 let converted: UserContent = openai_text.into();
2798 assert_eq!(
2799 converted,
2800 UserContent::Text {
2801 text: "Hello".to_string()
2802 }
2803 );
2804
2805 let openai_image = openai::UserContent::Image {
2806 image_url: openai::ImageUrl {
2807 url: "https://example.com/img.png".to_string(),
2808 detail: ImageDetail::Auto,
2809 },
2810 };
2811 let converted: UserContent = openai_image.into();
2812 match converted {
2813 UserContent::ImageUrl { image_url } => {
2814 assert_eq!(image_url.url, "https://example.com/img.png");
2815 assert_eq!(image_url.detail, Some(ImageDetail::Auto));
2816 }
2817 _ => panic!("Expected ImageUrl"),
2818 }
2819 }
2820
2821 #[test]
2822 fn test_completion_response_reasoning_details_with_multiple_ids_stay_separate() {
2823 let json = json!({
2824 "id": "resp_multi_id",
2825 "object": "chat.completion",
2826 "created": 1,
2827 "model": "openrouter/test-model",
2828 "choices": [{
2829 "index": 0,
2830 "finish_reason": "stop",
2831 "message": {
2832 "role": "assistant",
2833 "content": "hello",
2834 "reasoning": null,
2835 "reasoning_details": [
2836 {"type":"reasoning.summary","id":"rs_a","summary":"a1"},
2837 {"type":"reasoning.summary","id":"rs_b","summary":"b1"},
2838 {"type":"reasoning.summary","id":"rs_a","summary":"a2"}
2839 ]
2840 }
2841 }]
2842 });
2843
2844 let response: CompletionResponse = serde_json::from_value(json).unwrap();
2845 let converted: completion::CompletionResponse<CompletionResponse> =
2846 response.try_into().unwrap();
2847 let items: Vec<completion::AssistantContent> = converted.choice.into_iter().collect();
2848 let reasoning_blocks: Vec<_> = items
2849 .into_iter()
2850 .filter_map(|item| match item {
2851 completion::AssistantContent::Reasoning(reasoning) => Some(reasoning),
2852 _ => None,
2853 })
2854 .collect();
2855
2856 assert_eq!(reasoning_blocks.len(), 2);
2857 assert_eq!(reasoning_blocks[0].id.as_deref(), Some("rs_a"));
2858 assert_eq!(
2859 reasoning_blocks[0].content,
2860 vec![
2861 message::ReasoningContent::Summary("a1".to_string()),
2862 message::ReasoningContent::Summary("a2".to_string()),
2863 ]
2864 );
2865 assert_eq!(reasoning_blocks[1].id.as_deref(), Some("rs_b"));
2866 assert_eq!(
2867 reasoning_blocks[1].content,
2868 vec![message::ReasoningContent::Summary("b1".to_string())]
2869 );
2870 }
2871}