1use crate::credential_schema::CredentialFormSchema;
18use crate::error::{AgentLoopError, LlmErrorKind, Result};
19use crate::openresponses_protocol::{CompactOutputItem, CompactRequest, CompactResponse};
20use crate::tool_types::{ToolCall, ToolDefinition};
21use async_trait::async_trait;
22use chrono::{DateTime, Utc};
23use futures::Stream;
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26use std::pin::Pin;
27use std::sync::Arc;
28
29pub type LlmResponseStream = Pin<Box<dyn Stream<Item = Result<LlmStreamEvent>> + Send>>;
35
36#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43pub enum ProviderOpaqueContext {
44 OpenResponsesCompact { output: Vec<CompactOutputItem> },
46}
47
48impl std::fmt::Debug for ProviderOpaqueContext {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::OpenResponsesCompact { output } => f
52 .debug_struct("OpenResponsesCompact")
53 .field("item_count", &output.len())
54 .finish_non_exhaustive(),
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct LlmStreamError {
66 pub code: Option<String>,
68 pub status: Option<u16>,
70 pub message: String,
72}
73
74impl LlmStreamError {
75 pub fn new(message: impl Into<String>) -> Self {
76 Self {
77 code: None,
78 status: None,
79 message: message.into(),
80 }
81 }
82
83 pub fn provider(
85 code: Option<impl Into<String>>,
86 status: Option<u16>,
87 message: impl Into<String>,
88 ) -> Self {
89 Self {
90 code: code.map(Into::into),
91 status,
92 message: message.into(),
93 }
94 }
95
96 pub fn kind(&self) -> LlmErrorKind {
98 if let Some(code) = self.code.as_deref()
99 && let Some(kind) = LlmErrorKind::from_provider_code(code)
100 {
101 return kind;
102 }
103 if let Some(status) = self.status {
104 return LlmErrorKind::from_provider_status(status, &self.message);
105 }
106 LlmErrorKind::from_error_text(&self.message)
107 }
108}
109
110impl std::error::Error for LlmStreamError {}
111
112impl std::fmt::Display for LlmStreamError {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match (&self.code, self.status) {
115 (Some(code), Some(status)) => write!(f, "{code} ({status}): {}", self.message),
116 (Some(code), None) => write!(f, "{code}: {}", self.message),
117 (None, Some(status)) => write!(f, "({status}): {}", self.message),
118 (None, None) => f.write_str(&self.message),
119 }
120 }
121}
122
123impl From<String> for LlmStreamError {
124 fn from(message: String) -> Self {
125 Self::new(message)
126 }
127}
128
129impl From<&str> for LlmStreamError {
130 fn from(message: &str) -> Self {
131 Self::new(message)
132 }
133}
134
135#[derive(Debug, Clone)]
137pub enum LlmStreamEvent {
138 TextDelta(String),
140 ThinkingDelta(String),
142 ThinkingSignature(String),
145 ReasonItem {
151 provider: String,
153 model: Option<String>,
155 item_id: String,
157 encrypted_content: Option<String>,
159 summary: Vec<String>,
161 token_count: Option<u32>,
163 },
164 ToolCalls(Vec<ToolCall>),
166 MessagePhase(crate::execution_phase::ExecutionPhase),
176 Done(Box<LlmCompletionMetadata>),
178 Error(LlmStreamError),
180}
181
182#[derive(Debug, Clone)]
193pub struct DiscoveredModel {
194 pub model_id: String,
196 pub display_name: Option<String>,
198 pub created_at: Option<DateTime<Utc>>,
200 pub owned_by: Option<String>,
202 pub capabilities: Vec<String>,
206 pub discovered_profile: Option<crate::model::ModelProfile>,
209}
210
211#[derive(Debug, Clone, Default)]
224pub struct LlmCompletionMetadata {
225 pub total_tokens: Option<u32>,
227 pub prompt_tokens: Option<u32>,
229 pub completion_tokens: Option<u32>,
231 pub cache_read_tokens: Option<u32>,
233 pub cache_creation_tokens: Option<u32>,
235 pub provider_cost_usd: Option<f64>,
239 pub model: Option<String>,
241 pub finish_reason: Option<String>,
243 pub retry_metadata: Option<crate::llm_retry::RetryMetadata>,
245 pub response_id: Option<String>,
248 pub phase: Option<String>,
252}
253
254pub fn disjoint_prompt_tokens(reported_input: u32, cache_read: Option<u32>) -> u32 {
264 reported_input.saturating_sub(cache_read.unwrap_or(0))
265}
266
267#[async_trait]
289pub trait ChatDriver: Send + Sync {
290 async fn chat_completion_stream(
292 &self,
293 endpoint: &crate::runtime_provider::ProviderEndpoint,
294 messages: Vec<LlmMessage>,
295 config: &LlmCallConfig,
296 ) -> Result<LlmResponseStream>;
297
298 async fn chat_completion(
300 &self,
301 endpoint: &crate::runtime_provider::ProviderEndpoint,
302 messages: Vec<LlmMessage>,
303 config: &LlmCallConfig,
304 ) -> Result<LlmResponse> {
305 use futures::StreamExt;
306
307 let mut stream = self
308 .chat_completion_stream(endpoint, messages, config)
309 .await?;
310 let mut text = String::new();
311 let mut thinking = String::new();
312 let mut thinking_signature: Option<String> = None;
313 let mut tool_calls = Vec::new();
314 let mut metadata = LlmCompletionMetadata::default();
315
316 while let Some(event) = stream.next().await {
317 match event? {
318 LlmStreamEvent::TextDelta(delta) => text.push_str(&delta),
319 LlmStreamEvent::ThinkingDelta(delta) => thinking.push_str(&delta),
320 LlmStreamEvent::ThinkingSignature(sig) => thinking_signature = Some(sig),
321 LlmStreamEvent::ReasonItem {
322 encrypted_content, ..
323 } => {
324 if let Some(sig) = encrypted_content {
325 thinking_signature = Some(sig);
326 }
327 }
328 LlmStreamEvent::ToolCalls(calls) => tool_calls = calls,
329 LlmStreamEvent::MessagePhase(_) => {}
332 LlmStreamEvent::Done(meta) => metadata = *meta,
333 LlmStreamEvent::Error(err) => {
334 return Err(crate::error::AgentLoopError::llm_kind(
335 err.kind(),
336 err.to_string(),
337 ));
338 }
339 }
340 }
341
342 Ok(LlmResponse {
343 text,
344 thinking: if thinking.is_empty() {
345 None
346 } else {
347 Some(thinking)
348 },
349 thinking_signature,
350 tool_calls: if tool_calls.is_empty() {
351 None
352 } else {
353 Some(tool_calls)
354 },
355 metadata,
356 })
357 }
358
359 async fn list_models(
367 &self,
368 _endpoint: &crate::runtime_provider::ProviderEndpoint,
369 ) -> Result<Option<Vec<DiscoveredModel>>> {
370 Ok(None)
372 }
373
374 fn supports_compact(&self) -> bool {
383 false
385 }
386
387 fn supports_stateful_responses(&self) -> bool {
393 false
394 }
395
396 fn effective_context_window(&self, _model: &str) -> Option<usize> {
402 None
403 }
404
405 fn supports_parallel_tool_calls(&self, _model: &str) -> bool {
416 false
417 }
418
419 async fn compact(
439 &self,
440 _endpoint: &crate::runtime_provider::ProviderEndpoint,
441 _request: CompactRequest,
442 ) -> Result<Option<CompactResponse>> {
443 Ok(None)
445 }
446}
447
448#[async_trait]
450impl ChatDriver for Box<dyn ChatDriver> {
451 async fn chat_completion_stream(
452 &self,
453 endpoint: &crate::runtime_provider::ProviderEndpoint,
454 messages: Vec<LlmMessage>,
455 config: &LlmCallConfig,
456 ) -> Result<LlmResponseStream> {
457 (**self)
458 .chat_completion_stream(endpoint, messages, config)
459 .await
460 }
461
462 async fn chat_completion(
463 &self,
464 endpoint: &crate::runtime_provider::ProviderEndpoint,
465 messages: Vec<LlmMessage>,
466 config: &LlmCallConfig,
467 ) -> Result<LlmResponse> {
468 (**self).chat_completion(endpoint, messages, config).await
469 }
470
471 async fn list_models(
472 &self,
473 endpoint: &crate::runtime_provider::ProviderEndpoint,
474 ) -> Result<Option<Vec<DiscoveredModel>>> {
475 (**self).list_models(endpoint).await
476 }
477
478 fn supports_compact(&self) -> bool {
479 (**self).supports_compact()
480 }
481
482 fn supports_stateful_responses(&self) -> bool {
483 (**self).supports_stateful_responses()
484 }
485
486 fn effective_context_window(&self, model: &str) -> Option<usize> {
487 (**self).effective_context_window(model)
488 }
489
490 fn supports_parallel_tool_calls(&self, model: &str) -> bool {
491 (**self).supports_parallel_tool_calls(model)
492 }
493
494 async fn compact(
495 &self,
496 endpoint: &crate::runtime_provider::ProviderEndpoint,
497 request: CompactRequest,
498 ) -> Result<Option<CompactResponse>> {
499 (**self).compact(endpoint, request).await
500 }
501}
502
503#[derive(Debug, Clone)]
509pub struct LlmMessage {
510 pub role: LlmMessageRole,
511 pub content: LlmMessageContent,
512 pub tool_calls: Option<Vec<ToolCall>>,
513 pub tool_call_id: Option<String>,
514 pub phase: Option<crate::execution_phase::ExecutionPhase>,
519 pub thinking: Option<String>,
522 pub thinking_signature: Option<String>,
525}
526
527impl LlmMessage {
528 pub fn text(role: LlmMessageRole, content: impl Into<String>) -> Self {
530 Self {
531 role,
532 content: LlmMessageContent::Text(content.into()),
533 tool_calls: None,
534 tool_call_id: None,
535 phase: None,
536 thinking: None,
537 thinking_signature: None,
538 }
539 }
540
541 pub fn parts(role: LlmMessageRole, parts: Vec<LlmContentPart>) -> Self {
543 Self {
544 role,
545 content: LlmMessageContent::Parts(parts),
546 tool_calls: None,
547 tool_call_id: None,
548 phase: None,
549 thinking: None,
550 thinking_signature: None,
551 }
552 }
553
554 pub fn content_as_text(&self) -> String {
556 self.content.to_text()
557 }
558
559 pub fn prepend_text_prefix(&mut self, prefix: &str) {
564 match &mut self.content {
565 LlmMessageContent::Text(text) => {
566 *text = format!("{}{}", prefix, text);
567 }
568 LlmMessageContent::Parts(parts) => {
569 for part in parts.iter_mut() {
570 if let LlmContentPart::Text { text } = part {
571 *text = format!("{}{}", prefix, text);
572 return;
573 }
574 }
575 parts.insert(
577 0,
578 LlmContentPart::Text {
579 text: prefix.to_string(),
580 },
581 );
582 }
583 }
584 }
585}
586
587pub fn fold_system_messages(messages: &[LlmMessage]) -> Option<String> {
598 let mut system: Option<String> = None;
599 for msg in messages {
600 if msg.role == LlmMessageRole::System {
601 let text = msg.content.to_text();
602 system = Some(match system.take() {
603 Some(existing) if !existing.is_empty() => format!("{existing}\n\n{text}"),
604 _ => text,
605 });
606 }
607 }
608 system
609}
610
611#[derive(Debug, Clone)]
613pub enum LlmMessageContent {
614 Text(String),
616 Parts(Vec<LlmContentPart>),
618}
619
620impl LlmMessageContent {
621 pub fn to_text(&self) -> String {
623 match self {
624 LlmMessageContent::Text(s) => s.clone(),
625 LlmMessageContent::Parts(parts) => parts
626 .iter()
627 .filter_map(|p| match p {
628 LlmContentPart::Text { text } => Some(text.clone()),
629 _ => None,
630 })
631 .collect::<Vec<_>>()
632 .join(""),
633 }
634 }
635
636 pub fn is_text(&self) -> bool {
638 matches!(self, LlmMessageContent::Text(_))
639 }
640
641 pub fn is_parts(&self) -> bool {
643 matches!(self, LlmMessageContent::Parts(_))
644 }
645}
646
647impl From<String> for LlmMessageContent {
648 fn from(s: String) -> Self {
649 LlmMessageContent::Text(s)
650 }
651}
652
653impl From<&str> for LlmMessageContent {
654 fn from(s: &str) -> Self {
655 LlmMessageContent::Text(s.to_string())
656 }
657}
658
659#[derive(Debug, Clone)]
661pub enum LlmContentPart {
662 Text { text: String },
664 Image { url: String },
666 Audio { url: String },
668}
669
670impl LlmContentPart {
671 pub fn text(text: impl Into<String>) -> Self {
673 LlmContentPart::Text { text: text.into() }
674 }
675
676 pub fn image(url: impl Into<String>) -> Self {
678 LlmContentPart::Image { url: url.into() }
679 }
680
681 pub fn audio(url: impl Into<String>) -> Self {
683 LlmContentPart::Audio { url: url.into() }
684 }
685}
686
687#[derive(Debug, Clone, PartialEq, Eq)]
689pub enum LlmMessageRole {
690 System,
691 User,
692 Assistant,
693 Tool,
694}
695
696#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
706pub struct ToolSearchConfig {
707 pub enabled: bool,
709 pub threshold: usize,
712}
713
714#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
716#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
717#[serde(rename_all = "snake_case")]
718pub enum PromptCacheStrategy {
719 #[default]
721 Auto,
722}
723
724#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
729#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
730pub struct PromptCacheConfig {
731 pub enabled: bool,
733 #[serde(default)]
735 pub strategy: PromptCacheStrategy,
736 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub gemini_cached_content: Option<String>,
744}
745
746#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
756#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
757#[serde(tag = "kind", rename_all = "snake_case")]
758pub enum OpenRouterRoutingPreset {
759 CheapestWithTools,
761 LowestLatencyReview,
763 ZdrOnly,
765 ByokFirst,
767 NoDataCollection,
769 StrictJson,
771 ReasoningRequired,
773 MaxPrice {
776 #[serde(default, skip_serializing_if = "Option::is_none")]
778 prompt_usd_per_million: Option<f64>,
779 #[serde(default, skip_serializing_if = "Option::is_none")]
781 completion_usd_per_million: Option<f64>,
782 },
783}
784
785#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
793#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
794#[serde(rename_all = "snake_case")]
795pub enum OpenRouterCapacityStrategy {
796 #[default]
798 SharedCapacity,
799 ByokFirst,
803 ByokOnly,
808}
809
810#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
818#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
819#[serde(rename_all = "snake_case")]
820pub enum OpenRouterServerToolKind {
821 WebSearch,
822 WebFetch,
823 Datetime,
824 ImageGeneration,
825 ApplyPatch,
826 Fusion,
827 Advisor,
828 Subagent,
829}
830
831impl OpenRouterServerToolKind {
832 pub const ALL: [OpenRouterServerToolKind; 8] = [
834 Self::WebSearch,
835 Self::WebFetch,
836 Self::Datetime,
837 Self::ImageGeneration,
838 Self::ApplyPatch,
839 Self::Fusion,
840 Self::Advisor,
841 Self::Subagent,
842 ];
843
844 pub fn name(&self) -> &'static str {
846 match self {
847 Self::WebSearch => "web_search",
848 Self::WebFetch => "web_fetch",
849 Self::Datetime => "datetime",
850 Self::ImageGeneration => "image_generation",
851 Self::ApplyPatch => "apply_patch",
852 Self::Fusion => "fusion",
853 Self::Advisor => "advisor",
854 Self::Subagent => "subagent",
855 }
856 }
857
858 pub fn display_name(&self) -> &'static str {
860 match self {
861 Self::WebSearch => "Web Search",
862 Self::WebFetch => "Web Fetch",
863 Self::Datetime => "Date & Time",
864 Self::ImageGeneration => "Image Generation",
865 Self::ApplyPatch => "Apply Patch",
866 Self::Fusion => "Fusion",
867 Self::Advisor => "Advisor",
868 Self::Subagent => "Subagent",
869 }
870 }
871
872 pub fn wire_type(&self) -> String {
875 format!("openrouter:{}", self.name())
876 }
877
878 pub fn from_name(name: &str) -> Option<Self> {
880 Self::ALL.into_iter().find(|kind| kind.name() == name)
881 }
882}
883
884#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
888#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
889pub struct OpenRouterServerTool {
890 pub kind: OpenRouterServerToolKind,
891 #[serde(default, skip_serializing_if = "Option::is_none")]
892 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
893 pub parameters: Option<serde_json::Value>,
894}
895
896impl OpenRouterServerTool {
897 pub fn new(kind: OpenRouterServerToolKind) -> Self {
899 Self {
900 kind,
901 parameters: None,
902 }
903 }
904
905 pub fn with_parameters(kind: OpenRouterServerToolKind, parameters: serde_json::Value) -> Self {
907 Self {
908 kind,
909 parameters: Some(parameters),
910 }
911 }
912}
913
914#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
917#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
918pub struct OpenRouterRoutingConfig {
919 #[serde(default, skip_serializing_if = "Vec::is_empty")]
921 pub models: Vec<String>,
922 #[serde(default, skip_serializing_if = "Option::is_none")]
925 pub route: Option<OpenRouterRoute>,
926 #[serde(default, skip_serializing_if = "Option::is_none")]
928 pub provider: Option<OpenRouterProviderRouting>,
929 #[serde(default, skip_serializing_if = "Option::is_none")]
931 pub plugins: Option<OpenRouterPluginConfig>,
932 #[serde(default, skip_serializing_if = "Option::is_none")]
936 pub capacity_strategy: Option<OpenRouterCapacityStrategy>,
937 #[serde(default, skip_serializing_if = "Vec::is_empty")]
941 pub presets: Vec<OpenRouterRoutingPreset>,
942 #[serde(default, skip_serializing_if = "Vec::is_empty")]
945 pub server_tools: Vec<OpenRouterServerTool>,
946}
947
948impl OpenRouterRoutingConfig {
949 pub fn is_empty(&self) -> bool {
950 self.models.is_empty()
951 && self.route.is_none()
952 && self.provider.is_none()
953 && self.plugins.as_ref().is_none_or(|p| p.is_empty())
954 && matches!(
955 self.capacity_strategy,
956 None | Some(OpenRouterCapacityStrategy::SharedCapacity)
957 )
958 && self.presets.is_empty()
959 && self.server_tools.is_empty()
960 }
961
962 pub fn fallback_models(models: impl IntoIterator<Item = impl Into<String>>) -> Self {
964 let models = models.into_iter().map(Into::into).collect::<Vec<_>>();
965 let route = (!models.is_empty()).then_some(OpenRouterRoute::Fallback);
966 Self {
967 models,
968 route,
969 provider: None,
970 plugins: None,
971 capacity_strategy: None,
972 presets: vec![],
973 server_tools: vec![],
974 }
975 }
976
977 pub fn validate_for_primary_model(
978 &self,
979 primary_model: &str,
980 ) -> std::result::Result<(), String> {
981 if self.route == Some(OpenRouterRoute::Fallback) && self.models.is_empty() {
982 return Err(
983 "OpenRouter fallback routing requires at least one model in `models`".to_string(),
984 );
985 }
986
987 if let Some(first_model) = self.models.first()
988 && first_model != primary_model
989 {
990 return Err(format!(
991 "OpenRouter routing models[0] ('{first_model}') must match primary model ('{primary_model}')"
992 ));
993 }
994
995 Ok(())
996 }
997
998 pub fn apply_capacity_strategy(&self) -> std::result::Result<Self, String> {
1008 match self.capacity_strategy {
1009 None | Some(OpenRouterCapacityStrategy::SharedCapacity) => Ok(self.clone()),
1010 Some(OpenRouterCapacityStrategy::ByokFirst) => {
1011 let mut result = self.clone();
1012 let provider = result.provider.get_or_insert_with(Default::default);
1013 if provider.allow_fallbacks.is_none() {
1014 provider.allow_fallbacks = Some(true);
1015 }
1016 Ok(result)
1017 }
1018 Some(OpenRouterCapacityStrategy::ByokOnly) => {
1019 let only_is_empty = self.provider.as_ref().is_none_or(|p| p.only.is_empty());
1020 if only_is_empty {
1021 return Err(
1022 "OpenRouter BYOK-only strategy requires provider.only to list at least \
1023 one upstream provider slug. Configure the provider list to match the \
1024 BYOK providers registered in your OpenRouter workspace."
1025 .to_string(),
1026 );
1027 }
1028 let mut result = self.clone();
1029 let provider = result.provider.get_or_insert_with(Default::default);
1030 provider.allow_fallbacks = Some(false);
1031 Ok(result)
1032 }
1033 }
1034 }
1035
1036 pub fn apply_presets(&self) -> std::result::Result<Self, String> {
1046 if self.presets.is_empty() {
1047 return Ok(self.clone());
1048 }
1049
1050 let mut derived = OpenRouterProviderRouting::default();
1051
1052 for preset in &self.presets {
1053 match preset {
1054 OpenRouterRoutingPreset::CheapestWithTools => {
1055 derived.require_parameters = Some(true);
1056 derived.sort = Some(OpenRouterProviderSort::Simple(
1057 OpenRouterProviderSortBy::Price,
1058 ));
1059 }
1060 OpenRouterRoutingPreset::LowestLatencyReview => {
1061 derived.sort = Some(OpenRouterProviderSort::Simple(
1062 OpenRouterProviderSortBy::Throughput,
1063 ));
1064 }
1065 OpenRouterRoutingPreset::ZdrOnly => {
1066 derived.zdr = Some(true);
1067 }
1068 OpenRouterRoutingPreset::ByokFirst => {
1069 if derived.allow_fallbacks.is_none() {
1070 derived.allow_fallbacks = Some(true);
1071 }
1072 }
1073 OpenRouterRoutingPreset::NoDataCollection => {
1074 derived.data_collection = Some(OpenRouterDataCollection::Deny);
1075 }
1076 OpenRouterRoutingPreset::StrictJson
1077 | OpenRouterRoutingPreset::ReasoningRequired => {
1078 derived.require_parameters = Some(true);
1079 }
1080 OpenRouterRoutingPreset::MaxPrice {
1081 prompt_usd_per_million,
1082 completion_usd_per_million,
1083 } => {
1084 if prompt_usd_per_million.is_some_and(|v| v < 0.0)
1085 || completion_usd_per_million.is_some_and(|v| v < 0.0)
1086 {
1087 return Err(
1088 "MaxPrice preset values must be non-negative USD per million tokens"
1089 .to_string(),
1090 );
1091 }
1092 if prompt_usd_per_million.is_some() || completion_usd_per_million.is_some() {
1093 let mp = derived.max_price.get_or_insert_with(Default::default);
1094 if let Some(p) = prompt_usd_per_million {
1095 mp.prompt = Some(p / 1_000_000.0);
1096 }
1097 if let Some(c) = completion_usd_per_million {
1098 mp.completion = Some(c / 1_000_000.0);
1099 }
1100 }
1101 }
1102 }
1103 }
1104
1105 let merged = merge_provider_routing(derived, self.provider.clone().unwrap_or_default());
1107
1108 let mut result = self.clone();
1109 result.presets = vec![];
1110 result.provider = if merged.is_empty() {
1111 None
1112 } else {
1113 Some(merged)
1114 };
1115 Ok(result)
1116 }
1117}
1118
1119fn merge_provider_routing(
1123 derived: OpenRouterProviderRouting,
1124 explicit: OpenRouterProviderRouting,
1125) -> OpenRouterProviderRouting {
1126 OpenRouterProviderRouting {
1127 order: if !explicit.order.is_empty() {
1128 explicit.order
1129 } else {
1130 derived.order
1131 },
1132 only: if !explicit.only.is_empty() {
1133 explicit.only
1134 } else {
1135 derived.only
1136 },
1137 ignore: if !explicit.ignore.is_empty() {
1138 explicit.ignore
1139 } else {
1140 derived.ignore
1141 },
1142 allow_fallbacks: explicit.allow_fallbacks.or(derived.allow_fallbacks),
1143 require_parameters: explicit.require_parameters.or(derived.require_parameters),
1144 data_collection: explicit.data_collection.or(derived.data_collection),
1145 zdr: explicit.zdr.or(derived.zdr),
1146 enforce_distillable_text: explicit
1147 .enforce_distillable_text
1148 .or(derived.enforce_distillable_text),
1149 quantizations: if !explicit.quantizations.is_empty() {
1150 explicit.quantizations
1151 } else {
1152 derived.quantizations
1153 },
1154 sort: explicit.sort.or(derived.sort),
1155 max_price: explicit.max_price.or(derived.max_price),
1156 }
1157}
1158
1159#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1161#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1162#[serde(rename_all = "snake_case")]
1163pub enum OpenRouterRoute {
1164 Fallback,
1165}
1166
1167#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1169#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1170pub struct OpenRouterProviderRouting {
1171 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1173 pub order: Vec<String>,
1174 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1176 pub only: Vec<String>,
1177 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1179 pub ignore: Vec<String>,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1182 pub allow_fallbacks: Option<bool>,
1183 #[serde(default, skip_serializing_if = "Option::is_none")]
1185 pub require_parameters: Option<bool>,
1186 #[serde(default, skip_serializing_if = "Option::is_none")]
1188 pub data_collection: Option<OpenRouterDataCollection>,
1189 #[serde(default, skip_serializing_if = "Option::is_none")]
1191 pub zdr: Option<bool>,
1192 #[serde(default, skip_serializing_if = "Option::is_none")]
1194 pub enforce_distillable_text: Option<bool>,
1195 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1197 pub quantizations: Vec<String>,
1198 #[serde(default, skip_serializing_if = "Option::is_none")]
1200 pub sort: Option<OpenRouterProviderSort>,
1201 #[serde(default, skip_serializing_if = "Option::is_none")]
1203 pub max_price: Option<OpenRouterMaxPrice>,
1204}
1205
1206impl OpenRouterProviderRouting {
1207 pub fn is_empty(&self) -> bool {
1208 self.order.is_empty()
1209 && self.only.is_empty()
1210 && self.ignore.is_empty()
1211 && self.allow_fallbacks.is_none()
1212 && self.require_parameters.is_none()
1213 && self.data_collection.is_none()
1214 && self.zdr.is_none()
1215 && self.enforce_distillable_text.is_none()
1216 && self.quantizations.is_empty()
1217 && self.sort.is_none()
1218 && self.max_price.is_none()
1219 }
1220}
1221
1222#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1224#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1225#[serde(rename_all = "snake_case")]
1226pub enum OpenRouterDataCollection {
1227 Allow,
1228 Deny,
1229}
1230
1231#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1233#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1234#[serde(untagged)]
1235pub enum OpenRouterProviderSort {
1236 Simple(OpenRouterProviderSortBy),
1237 Advanced(OpenRouterProviderSortOptions),
1238}
1239
1240#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1242#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1243#[serde(rename_all = "snake_case")]
1244pub enum OpenRouterProviderSortBy {
1245 Price,
1246 Throughput,
1247 Latency,
1248}
1249
1250#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1252#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1253pub struct OpenRouterProviderSortOptions {
1254 pub by: OpenRouterProviderSortBy,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1256 pub partition: Option<OpenRouterSortPartition>,
1257}
1258
1259#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1261#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1262#[serde(rename_all = "snake_case")]
1263pub enum OpenRouterSortPartition {
1264 Model,
1265 None,
1266}
1267
1268#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1271#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1272pub struct OpenRouterMaxPrice {
1273 #[serde(default, skip_serializing_if = "Option::is_none")]
1274 pub prompt: Option<f64>,
1275 #[serde(default, skip_serializing_if = "Option::is_none")]
1276 pub completion: Option<f64>,
1277 #[serde(default, skip_serializing_if = "Option::is_none")]
1278 pub request: Option<f64>,
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1280 pub image: Option<f64>,
1281}
1282
1283#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1289#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1290pub struct OpenRouterWebSearchPlugin {
1291 #[serde(default, skip_serializing_if = "Option::is_none")]
1293 pub max_results: Option<u32>,
1294 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 pub search_prompt: Option<String>,
1297}
1298
1299#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1304#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1305pub struct OpenRouterFilePlugin {}
1306
1307#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1312#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1313pub struct OpenRouterPluginConfig {
1314 #[serde(default, skip_serializing_if = "Option::is_none")]
1316 pub web: Option<OpenRouterWebSearchPlugin>,
1317 #[serde(default, skip_serializing_if = "Option::is_none")]
1319 pub file: Option<OpenRouterFilePlugin>,
1320}
1321
1322impl OpenRouterPluginConfig {
1323 pub fn is_empty(&self) -> bool {
1324 self.web.is_none() && self.file.is_none()
1325 }
1326}
1327
1328pub const OPENROUTER_HTTP_REFERER_METADATA_KEY: &str = "openrouter.http_referer";
1330pub const OPENROUTER_X_TITLE_METADATA_KEY: &str = "openrouter.x_title";
1332
1333#[derive(Debug, Clone)]
1335pub struct LlmCallConfig {
1336 pub model: String,
1337 pub temperature: Option<f32>,
1338 pub max_tokens: Option<u32>,
1339 pub tools: Vec<ToolDefinition>,
1340 pub reasoning_effort: Option<String>,
1342 pub speed: Option<String>,
1346 pub verbosity: Option<String>,
1350 pub metadata: HashMap<String, String>,
1354 pub previous_response_id: Option<String>,
1357 pub provider_opaque_context: Option<ProviderOpaqueContext>,
1363 pub tool_search: Option<ToolSearchConfig>,
1365 pub prompt_cache: Option<PromptCacheConfig>,
1367 pub openrouter_routing: Option<OpenRouterRoutingConfig>,
1369 pub parallel_tool_calls: Option<bool>,
1376 pub volatile_suffix_len: usize,
1386}
1387
1388impl LlmCallConfig {
1389 pub fn resolved_parallel_tool_calls(&self, supported: bool) -> Option<bool> {
1399 if supported {
1400 self.parallel_tool_calls
1401 } else {
1402 None
1403 }
1404 }
1405}
1406
1407#[derive(Debug, Clone)]
1412pub struct LlmResponse {
1413 pub text: String,
1414 pub thinking: Option<String>,
1416 pub thinking_signature: Option<String>,
1418 pub tool_calls: Option<Vec<ToolCall>>,
1419 pub metadata: LlmCompletionMetadata,
1420}
1421
1422pub struct LlmCallConfigBuilder {
1428 config: LlmCallConfig,
1429}
1430
1431impl LlmCallConfigBuilder {
1432 pub fn from_config(config: LlmCallConfig) -> Self {
1434 Self { config }
1435 }
1436
1437 pub fn reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1439 self.config.reasoning_effort = Some(effort.into());
1440 self
1441 }
1442
1443 pub fn speed(mut self, speed: impl Into<String>) -> Self {
1445 self.config.speed = Some(speed.into());
1446 self
1447 }
1448
1449 pub fn verbosity(mut self, verbosity: impl Into<String>) -> Self {
1451 self.config.verbosity = Some(verbosity.into());
1452 self
1453 }
1454
1455 pub fn model(mut self, model: impl Into<String>) -> Self {
1457 self.config.model = model.into();
1458 self
1459 }
1460
1461 pub fn temperature(mut self, temp: f32) -> Self {
1463 self.config.temperature = Some(temp);
1464 self
1465 }
1466
1467 pub fn max_tokens(mut self, tokens: u32) -> Self {
1469 self.config.max_tokens = Some(tokens);
1470 self
1471 }
1472
1473 pub fn tools(mut self, tools: Vec<ToolDefinition>) -> Self {
1475 self.config.tools = tools;
1476 self
1477 }
1478
1479 pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
1484 self.config.metadata = metadata;
1485 self
1486 }
1487
1488 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1490 self.config.metadata.insert(key.into(), value.into());
1491 self
1492 }
1493
1494 pub fn previous_response_id(mut self, id: Option<String>) -> Self {
1496 self.config.previous_response_id = id;
1497 self
1498 }
1499
1500 pub fn provider_opaque_context(mut self, context: Option<ProviderOpaqueContext>) -> Self {
1502 self.config.provider_opaque_context = context;
1503 self
1504 }
1505
1506 pub fn tool_search(mut self, config: ToolSearchConfig) -> Self {
1508 self.config.tool_search = Some(config);
1509 self
1510 }
1511
1512 pub fn prompt_cache(mut self, config: PromptCacheConfig) -> Self {
1514 self.config.prompt_cache = Some(config);
1515 self
1516 }
1517
1518 pub fn openrouter_routing(mut self, config: OpenRouterRoutingConfig) -> Self {
1520 self.config.openrouter_routing = (!config.is_empty()).then_some(config);
1521 self
1522 }
1523
1524 pub fn parallel_tool_calls(mut self, parallel_tool_calls: Option<bool>) -> Self {
1526 self.config.parallel_tool_calls = parallel_tool_calls;
1527 self
1528 }
1529
1530 pub fn volatile_suffix_len(mut self, len: usize) -> Self {
1534 self.config.volatile_suffix_len = len;
1535 self
1536 }
1537
1538 pub fn build(self) -> LlmCallConfig {
1540 self.config
1541 }
1542}
1543
1544pub use crate::provider::DriverId;
1553
1554#[derive(Clone, Default, PartialEq, Eq)]
1560pub struct ProviderMetadata {
1561 pub refresh_token: Option<String>,
1563 pub account_id: Option<String>,
1565 pub extra: Option<serde_json::Value>,
1567}
1568
1569impl std::fmt::Debug for ProviderMetadata {
1570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1571 f.debug_struct("ProviderMetadata")
1572 .field(
1573 "refresh_token",
1574 &self.refresh_token.as_ref().map(|_| "<configured>"),
1575 )
1576 .field("account_id", &self.account_id)
1577 .field("extra", &self.extra.as_ref().map(|_| "<configured>"))
1578 .finish()
1579 }
1580}
1581
1582#[derive(Clone)]
1584pub struct ProviderConfig {
1585 pub provider: crate::runtime_provider::ProviderKey,
1587 pub provider_type: DriverId,
1589 pub api_key: Option<String>,
1591 pub base_url: Option<String>,
1593 pub metadata: ProviderMetadata,
1595}
1596
1597impl ProviderConfig {
1598 pub fn new(provider_type: DriverId) -> Self {
1600 let provider = crate::runtime_provider::ProviderKey::new(provider_type.as_str());
1601 Self {
1602 provider,
1603 provider_type,
1604 api_key: None,
1605 base_url: None,
1606 metadata: ProviderMetadata::default(),
1607 }
1608 }
1609
1610 pub fn for_provider(
1613 provider: impl Into<crate::runtime_provider::ProviderKey>,
1614 provider_type: DriverId,
1615 ) -> Self {
1616 Self {
1617 provider: provider.into(),
1618 provider_type,
1619 api_key: None,
1620 base_url: None,
1621 metadata: ProviderMetadata::default(),
1622 }
1623 }
1624
1625 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1627 self.api_key = Some(api_key.into());
1628 self
1629 }
1630
1631 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
1633 self.base_url = Some(base_url.into());
1634 self
1635 }
1636
1637 pub fn with_metadata(mut self, metadata: ProviderMetadata) -> Self {
1639 self.metadata = metadata;
1640 self
1641 }
1642}
1643
1644impl std::fmt::Debug for ProviderConfig {
1645 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1646 f.debug_struct("ProviderConfig")
1647 .field("provider", &self.provider)
1648 .field("provider_type", &self.provider_type)
1649 .field("auth", &self.api_key.as_ref().map(|_| "<configured>"))
1650 .field("base_url", &self.base_url.as_ref().map(|_| "<configured>"))
1651 .field(
1652 "metadata",
1653 &self.metadata.extra.as_ref().map(|_| "<configured>"),
1654 )
1655 .finish()
1656 }
1657}
1658
1659#[derive(Clone)]
1665pub struct DriverConfig {
1666 pub provider: crate::runtime_provider::ProviderKey,
1668 pub provider_type: DriverId,
1670 pub api_key: Option<String>,
1676 pub credentials: std::collections::BTreeMap<String, String>,
1682 pub base_url: Option<String>,
1684 pub metadata: ProviderMetadata,
1686}
1687
1688impl DriverConfig {
1689 pub fn from_provider_config(config: &ProviderConfig) -> Self {
1695 Self {
1696 provider: config.provider.clone(),
1697 provider_type: config.provider_type.clone(),
1698 credentials: crate::credential_schema::parse_credential_document(
1699 config.api_key.as_deref(),
1700 ),
1701 api_key: config.api_key.clone(),
1702 base_url: config.base_url.clone(),
1703 metadata: config.metadata.clone(),
1704 }
1705 }
1706
1707 pub fn credential(&self, name: &str) -> Option<&str> {
1709 self.credentials
1710 .get(name)
1711 .map(String::as_str)
1712 .filter(|s| !s.is_empty())
1713 }
1714}
1715
1716impl std::fmt::Debug for DriverConfig {
1717 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1718 f.debug_struct("DriverConfig")
1719 .field("provider", &self.provider)
1720 .field("provider_type", &self.provider_type)
1721 .field("auth", &self.api_key.as_ref().map(|_| "<configured>"))
1722 .field(
1723 "credential_fields",
1724 &self.credentials.keys().collect::<Vec<_>>(),
1725 )
1726 .field("base_url", &self.base_url.as_ref().map(|_| "<configured>"))
1727 .finish()
1728 }
1729}
1730
1731pub type BoxedChatDriver = Box<dyn ChatDriver>;
1736
1737#[derive(Debug, Clone)]
1743pub struct EmbedRequest {
1744 pub texts: Vec<String>,
1746 pub model: String,
1748}
1749
1750#[derive(Debug, Clone)]
1752pub struct EmbedResponse {
1753 pub embeddings: Vec<Vec<f32>>,
1755 pub usage_tokens: Option<u32>,
1758}
1759
1760#[derive(Debug, thiserror::Error)]
1762pub enum EmbeddingsDriverError {
1763 #[error("embeddings provider returned an error: {0}")]
1764 Provider(String),
1765 #[error("embeddings request failed: {0}")]
1766 Transport(String),
1767}
1768
1769#[async_trait]
1775pub trait EmbeddingsDriver: Send + Sync {
1776 async fn embed(
1778 &self,
1779 endpoint: &crate::runtime_provider::ProviderEndpoint,
1780 request: EmbedRequest,
1781 ) -> std::result::Result<EmbedResponse, EmbeddingsDriverError>;
1782}
1783
1784#[async_trait]
1785impl EmbeddingsDriver for Box<dyn EmbeddingsDriver> {
1786 async fn embed(
1787 &self,
1788 endpoint: &crate::runtime_provider::ProviderEndpoint,
1789 request: EmbedRequest,
1790 ) -> std::result::Result<EmbedResponse, EmbeddingsDriverError> {
1791 (**self).embed(endpoint, request).await
1792 }
1793}
1794
1795pub type BoxedEmbeddingsDriver = Box<dyn EmbeddingsDriver>;
1797
1798pub type EmbeddingsDriverFactory =
1800 Arc<dyn Fn(&DriverConfig) -> BoxedEmbeddingsDriver + Send + Sync>;
1801
1802pub type DriverFactory = Arc<dyn Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync>;
1811
1812#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1818#[serde(rename_all = "snake_case")]
1819pub enum ServiceKind {
1820 Chat,
1822 Embeddings,
1824 Realtime,
1826 Images,
1828 Rerank,
1830}
1831
1832impl std::fmt::Display for ServiceKind {
1833 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1834 let s = match self {
1835 ServiceKind::Chat => "chat",
1836 ServiceKind::Embeddings => "embeddings",
1837 ServiceKind::Realtime => "realtime",
1838 ServiceKind::Images => "images",
1839 ServiceKind::Rerank => "rerank",
1840 };
1841 f.write_str(s)
1842 }
1843}
1844
1845#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1858pub enum DriverOAuthFlow {
1859 OpenRouterPkce,
1867}
1868
1869#[derive(Debug, Clone)]
1874pub struct DriverOAuthConfig {
1875 pub authorize_url: String,
1877 pub token_url: String,
1879 pub flow: DriverOAuthFlow,
1881}
1882
1883impl DriverOAuthConfig {
1884 pub fn openrouter() -> Self {
1886 Self {
1887 authorize_url: "https://openrouter.ai/auth".to_string(),
1888 token_url: "https://openrouter.ai/api/v1/auth/keys".to_string(),
1889 flow: DriverOAuthFlow::OpenRouterPkce,
1890 }
1891 }
1892}
1893
1894#[derive(Clone)]
1901pub struct DriverDescriptor {
1902 pub id: DriverId,
1904 pub display_name: String,
1906 pub services: Vec<ServiceKind>,
1908 pub credential_schema: CredentialFormSchema,
1910 pub oauth: Option<DriverOAuthConfig>,
1913 pub chat: Option<DriverFactory>,
1915 pub embeddings: Option<EmbeddingsDriverFactory>,
1917}
1918
1919impl DriverDescriptor {
1920 pub fn chat_only<F>(id: impl Into<DriverId>, factory: F) -> Self
1925 where
1926 F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
1927 {
1928 let id = id.into();
1929 Self {
1930 display_name: default_display_name(&id),
1931 credential_schema: default_credential_schema(&id),
1932 services: vec![ServiceKind::Chat],
1933 oauth: None,
1934 chat: Some(Arc::new(factory)),
1935 embeddings: None,
1936 id,
1937 }
1938 }
1939
1940 pub fn supports(&self, service: ServiceKind) -> bool {
1942 self.services.contains(&service)
1943 }
1944}
1945
1946impl std::fmt::Debug for DriverDescriptor {
1947 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1948 f.debug_struct("DriverDescriptor")
1949 .field("id", &self.id)
1950 .field("display_name", &self.display_name)
1951 .field("services", &self.services)
1952 .field("oauth", &self.oauth.is_some())
1953 .field("chat", &self.chat.is_some())
1954 .field("embeddings", &self.embeddings.is_some())
1955 .finish()
1956 }
1957}
1958
1959fn default_display_name(id: &DriverId) -> String {
1960 id.as_str().replace(['_', '-'], " ")
1961}
1962
1963fn default_credential_schema(id: &DriverId) -> CredentialFormSchema {
1964 if id == &DriverId::LlmSim {
1965 CredentialFormSchema::empty()
1966 } else {
1967 CredentialFormSchema::api_key(String::new())
1968 }
1969}
1970
1971#[derive(Clone, Default)]
1991pub struct DriverRegistry {
1992 descriptors: HashMap<DriverId, DriverDescriptor>,
1993 providers: crate::runtime_provider::RuntimeProviderRegistry,
1994}
1995
1996impl DriverRegistry {
1997 pub fn new() -> Self {
1999 Self {
2000 descriptors: HashMap::new(),
2001 providers: crate::runtime_provider::RuntimeProviderRegistry::new(),
2002 }
2003 }
2004
2005 pub fn register_provider(
2007 &mut self,
2008 provider: crate::runtime_provider::RuntimeProvider,
2009 ) -> Result<()> {
2010 self.providers.register(provider)
2011 }
2012
2013 pub fn replace_provider(
2015 &mut self,
2016 provider: crate::runtime_provider::RuntimeProvider,
2017 ) -> Option<Arc<crate::runtime_provider::RuntimeProvider>> {
2018 self.providers.replace(provider)
2019 }
2020
2021 pub fn provider(
2023 &self,
2024 id: &crate::runtime_provider::ProviderKey,
2025 ) -> Option<Arc<crate::runtime_provider::RuntimeProvider>> {
2026 self.providers.get(id)
2027 }
2028
2029 pub fn register_descriptor(&mut self, descriptor: DriverDescriptor) {
2035 if self.descriptors.contains_key(&descriptor.id) {
2036 panic!(
2037 "driver already registered for provider '{}'; \
2038 use register_descriptor_or_replace to overwrite intentionally",
2039 descriptor.id
2040 );
2041 }
2042 self.descriptors.insert(descriptor.id.clone(), descriptor);
2043 }
2044
2045 pub fn register_descriptor_or_replace(&mut self, descriptor: DriverDescriptor) {
2047 self.descriptors.insert(descriptor.id.clone(), descriptor);
2048 }
2049
2050 pub fn register<F>(&mut self, provider_type: impl Into<DriverId>, factory: F)
2056 where
2057 F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
2058 {
2059 self.register_descriptor(DriverDescriptor::chat_only(provider_type, factory));
2060 }
2061
2062 pub fn register_or_replace<F>(&mut self, provider_type: impl Into<DriverId>, factory: F)
2067 where
2068 F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
2069 {
2070 self.register_descriptor_or_replace(DriverDescriptor::chat_only(provider_type, factory));
2071 }
2072
2073 pub fn register_external<F>(&mut self, id: impl AsRef<str>, factory: F)
2078 where
2079 F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
2080 {
2081 let mut descriptor = DriverDescriptor::chat_only(DriverId::external(id), factory);
2082 descriptor.credential_schema = CredentialFormSchema::empty();
2083 self.register_descriptor(descriptor);
2084 }
2085
2086 pub fn create_chat_driver(&self, config: &ProviderConfig) -> Result<BoxedChatDriver> {
2095 if let Some(provider) = self.providers.get(&config.provider) {
2096 return Ok((*provider).clone().into_boxed_driver());
2097 }
2098 let descriptor = self.descriptors.get(&config.provider_type).ok_or_else(|| {
2099 AgentLoopError::driver_not_registered(config.provider_type.to_string())
2100 })?;
2101 let requires_api_key = descriptor
2102 .credential_schema
2103 .fields
2104 .iter()
2105 .any(|field| field.name == "api_key" && field.required && field.group.is_none());
2106 if requires_api_key && config.api_key.is_none() {
2107 return Err(AgentLoopError::llm(
2108 "API key is required. Configure the API key in provider settings.",
2109 ));
2110 }
2111
2112 let factory = descriptor.chat.as_ref().ok_or_else(|| {
2114 AgentLoopError::llm(format!(
2115 "Provider driver '{}' does not implement the chat service.",
2116 config.provider_type
2117 ))
2118 })?;
2119
2120 let driver_config = DriverConfig::from_provider_config(config);
2122 Ok(factory(&driver_config))
2123 }
2124
2125 pub fn has_driver(&self, provider_type: &DriverId) -> bool {
2127 self.descriptors.contains_key(provider_type)
2128 }
2129
2130 pub fn descriptor(&self, provider_type: &DriverId) -> Option<&DriverDescriptor> {
2132 self.descriptors.get(provider_type)
2133 }
2134
2135 pub fn supports(&self, provider_type: &DriverId, service: ServiceKind) -> bool {
2137 self.descriptors
2138 .get(provider_type)
2139 .is_some_and(|d| d.supports(service))
2140 }
2141
2142 pub fn providers_for(&self, service: ServiceKind) -> Vec<DriverId> {
2144 self.descriptors
2145 .values()
2146 .filter(|d| d.supports(service))
2147 .map(|d| d.id.clone())
2148 .collect()
2149 }
2150
2151 pub fn registered_providers(&self) -> Vec<DriverId> {
2153 self.descriptors.keys().cloned().collect()
2154 }
2155
2156 pub fn registered_provider_ids(&self) -> Vec<String> {
2158 self.providers.ids()
2159 }
2160
2161 pub fn create_embeddings_driver(
2169 &self,
2170 config: &ProviderConfig,
2171 ) -> std::result::Result<BoxedEmbeddingsDriver, EmbeddingsDriverError> {
2172 let requires_api_key = config.provider_type != DriverId::LlmSim;
2173 if requires_api_key && config.api_key.is_none() {
2174 return Err(EmbeddingsDriverError::Provider(
2175 "API key is required. Configure the API key in provider settings.".to_string(),
2176 ));
2177 }
2178 let descriptor = self.descriptors.get(&config.provider_type).ok_or_else(|| {
2179 EmbeddingsDriverError::Provider(format!(
2180 "No driver registered for provider '{}'",
2181 config.provider_type
2182 ))
2183 })?;
2184 let factory = descriptor.embeddings.as_ref().ok_or_else(|| {
2185 EmbeddingsDriverError::Provider(format!(
2186 "Provider driver '{}' does not implement the embeddings service.",
2187 config.provider_type
2188 ))
2189 })?;
2190 let driver_config = DriverConfig::from_provider_config(config);
2191 Ok(factory(&driver_config))
2192 }
2193}
2194
2195const MAX_TOOL_RESULT_BYTES: usize = 64 * 1024;
2200
2201const TRUNCATION_SUFFIX: &str =
2202 "\n\n[Output truncated — exceeded 64 KiB limit. Try quiet flags, pipes, or redirect to file.]";
2203
2204pub fn truncate_tool_result(text: String) -> String {
2205 if text.len() <= MAX_TOOL_RESULT_BYTES {
2206 return text;
2207 }
2208 let content_budget = MAX_TOOL_RESULT_BYTES.saturating_sub(TRUNCATION_SUFFIX.len());
2209 let mut end = content_budget;
2210 while end > 0 && !text.is_char_boundary(end) {
2211 end -= 1;
2212 }
2213 let mut truncated = text[..end].to_string();
2214 truncated.push_str(TRUNCATION_SUFFIX);
2215 truncated
2216}
2217
2218#[cfg(test)]
2223mod tests {
2224 use super::*;
2225 use crate::runtime_provider::ProviderEndpoint;
2226
2227 #[test]
2228 fn test_disjoint_prompt_tokens_subtracts_cached_subset() {
2229 assert_eq!(disjoint_prompt_tokens(1000, Some(800)), 200);
2232 assert_eq!(disjoint_prompt_tokens(1000, None), 1000);
2234 assert_eq!(disjoint_prompt_tokens(1000, Some(0)), 1000);
2235 assert_eq!(disjoint_prompt_tokens(800, Some(1000)), 0);
2237 }
2238
2239 #[test]
2240 fn test_chat_driver_defaults_are_conservative_and_boxed_capabilities_forward() {
2241 struct DefaultDriver;
2243 #[async_trait]
2244 impl ChatDriver for DefaultDriver {
2245 async fn chat_completion_stream(
2246 &self,
2247 _endpoint: &ProviderEndpoint,
2248 _messages: Vec<LlmMessage>,
2249 _config: &LlmCallConfig,
2250 ) -> Result<LlmResponseStream> {
2251 unreachable!()
2252 }
2253 }
2254 assert!(!DefaultDriver.supports_parallel_tool_calls("any-model"));
2255 assert!(!DefaultDriver.supports_stateful_responses());
2256
2257 struct StatefulDriver;
2258 #[async_trait]
2259 impl ChatDriver for StatefulDriver {
2260 async fn chat_completion_stream(
2261 &self,
2262 _endpoint: &ProviderEndpoint,
2263 _messages: Vec<LlmMessage>,
2264 _config: &LlmCallConfig,
2265 ) -> Result<LlmResponseStream> {
2266 unreachable!()
2267 }
2268
2269 fn supports_stateful_responses(&self) -> bool {
2270 true
2271 }
2272 }
2273 let boxed: BoxedChatDriver = Box::new(StatefulDriver);
2274 assert!(boxed.supports_stateful_responses());
2275 }
2276
2277 #[test]
2278 fn test_fold_system_messages_none_when_absent() {
2279 let messages = vec![
2280 LlmMessage::text(LlmMessageRole::User, "hi"),
2281 LlmMessage::text(LlmMessageRole::Assistant, "ok"),
2282 ];
2283 assert_eq!(fold_system_messages(&messages), None);
2284 }
2285
2286 #[test]
2287 fn test_fold_system_messages_single() {
2288 let messages = vec![
2289 LlmMessage::text(LlmMessageRole::System, "AGENT-PROMPT"),
2290 LlmMessage::text(LlmMessageRole::User, "hi"),
2291 ];
2292 assert_eq!(
2293 fold_system_messages(&messages),
2294 Some("AGENT-PROMPT".to_string())
2295 );
2296 }
2297
2298 #[test]
2299 fn test_fold_system_messages_accumulates_in_order() {
2300 let messages = vec![
2304 LlmMessage::text(LlmMessageRole::System, "A"),
2305 LlmMessage::text(LlmMessageRole::User, "hi"),
2306 LlmMessage::text(LlmMessageRole::Assistant, "ok"),
2307 LlmMessage::text(LlmMessageRole::System, "B"),
2308 ];
2309 assert_eq!(fold_system_messages(&messages), Some("A\n\nB".to_string()));
2310 }
2311
2312 #[test]
2313 fn test_fold_system_messages_concatenates_parts() {
2314 let messages = vec![LlmMessage::parts(
2315 LlmMessageRole::System,
2316 vec![
2317 LlmContentPart::text("foo"),
2318 LlmContentPart::image("data:image/png;base64,xxx"),
2319 LlmContentPart::text("bar"),
2320 ],
2321 )];
2322 assert_eq!(fold_system_messages(&messages), Some("foobar".to_string()));
2323 }
2324
2325 #[test]
2326 fn test_openrouter_fallback_models_empty_is_empty() {
2327 let routing = OpenRouterRoutingConfig::fallback_models(std::iter::empty::<String>());
2328
2329 assert!(routing.is_empty());
2330 assert_eq!(routing.route, None);
2331 }
2332
2333 #[test]
2334 fn test_openrouter_routing_validates_primary_model() {
2335 let routing = OpenRouterRoutingConfig::fallback_models([
2336 "openai/gpt-5-mini",
2337 "anthropic/claude-sonnet-4.5",
2338 ]);
2339
2340 assert!(
2341 routing
2342 .validate_for_primary_model("openai/gpt-5-mini")
2343 .is_ok()
2344 );
2345 let err = routing
2346 .validate_for_primary_model("anthropic/claude-sonnet-4.5")
2347 .unwrap_err();
2348 assert!(err.contains("models[0]"));
2349 }
2350
2351 #[test]
2352 fn test_openrouter_routing_rejects_fallback_without_models() {
2353 let routing = OpenRouterRoutingConfig {
2354 route: Some(OpenRouterRoute::Fallback),
2355 ..Default::default()
2356 };
2357
2358 let err = routing
2359 .validate_for_primary_model("openai/gpt-5-mini")
2360 .unwrap_err();
2361 assert!(err.contains("requires at least one model"));
2362 }
2363
2364 #[test]
2365 fn test_openrouter_routing_serializes_request_fields() {
2366 let routing = OpenRouterRoutingConfig {
2367 models: vec![
2368 "openai/gpt-5-mini".to_string(),
2369 "anthropic/claude-sonnet-4.5".to_string(),
2370 ],
2371 route: Some(OpenRouterRoute::Fallback),
2372 provider: Some(OpenRouterProviderRouting {
2373 order: vec!["anthropic".to_string(), "openai".to_string()],
2374 allow_fallbacks: Some(false),
2375 require_parameters: Some(true),
2376 data_collection: Some(OpenRouterDataCollection::Deny),
2377 zdr: Some(true),
2378 sort: Some(OpenRouterProviderSort::Advanced(
2379 OpenRouterProviderSortOptions {
2380 by: OpenRouterProviderSortBy::Throughput,
2381 partition: Some(OpenRouterSortPartition::None),
2382 },
2383 )),
2384 max_price: Some(OpenRouterMaxPrice {
2385 prompt: Some(1.0),
2386 completion: Some(2.0),
2387 ..Default::default()
2388 }),
2389 ..Default::default()
2390 }),
2391 ..Default::default()
2392 };
2393
2394 let json = serde_json::to_value(routing).unwrap();
2395
2396 assert_eq!(
2397 json,
2398 serde_json::json!({
2399 "models": [
2400 "openai/gpt-5-mini",
2401 "anthropic/claude-sonnet-4.5"
2402 ],
2403 "route": "fallback",
2404 "provider": {
2405 "order": ["anthropic", "openai"],
2406 "allow_fallbacks": false,
2407 "require_parameters": true,
2408 "data_collection": "deny",
2409 "zdr": true,
2410 "sort": {
2411 "by": "throughput",
2412 "partition": "none"
2413 },
2414 "max_price": {
2415 "prompt": 1.0,
2416 "completion": 2.0
2417 }
2418 }
2419 })
2420 );
2421 }
2422
2423 #[test]
2424 fn test_provider_type_parsing() {
2425 assert_eq!("openai".parse::<DriverId>().unwrap(), DriverId::OpenAI);
2426 assert_eq!(
2427 "openrouter".parse::<DriverId>().unwrap(),
2428 DriverId::OpenRouter
2429 );
2430 assert_eq!(
2431 "openai_completions".parse::<DriverId>().unwrap(),
2432 DriverId::OpenAICompletions
2433 );
2434 assert_eq!(
2435 "azure_openai".parse::<DriverId>().unwrap(),
2436 DriverId::AzureOpenAI
2437 );
2438 assert_eq!(
2439 "anthropic".parse::<DriverId>().unwrap(),
2440 DriverId::Anthropic
2441 );
2442 assert_eq!("gemini".parse::<DriverId>().unwrap(), DriverId::Gemini);
2443 assert_eq!(
2445 "ollama".parse::<DriverId>().unwrap(),
2446 DriverId::external("ollama")
2447 );
2448 assert_eq!(
2449 "custom".parse::<DriverId>().unwrap(),
2450 DriverId::external("custom")
2451 );
2452 }
2453
2454 #[test]
2455 fn test_external_provider_id_is_case_insensitive() {
2456 assert_eq!("OpenAI".parse::<DriverId>().unwrap(), DriverId::OpenAI);
2459 assert_eq!(
2460 "Ollama".parse::<DriverId>().unwrap(),
2461 "ollama".parse::<DriverId>().unwrap()
2462 );
2463 assert_eq!(DriverId::external("OpenAI-Codex").as_str(), "openai-codex");
2464 assert_eq!(
2466 DriverId::external("MyProvider"),
2467 "myprovider".parse::<DriverId>().unwrap()
2468 );
2469 }
2470
2471 #[test]
2472 fn test_provider_type_display() {
2473 assert_eq!(DriverId::OpenAI.to_string(), "openai");
2474 assert_eq!(DriverId::OpenRouter.to_string(), "openrouter");
2475 assert_eq!(DriverId::AzureOpenAI.to_string(), "azure_openai");
2476 assert_eq!(
2477 DriverId::OpenAICompletions.to_string(),
2478 "openai_completions"
2479 );
2480 assert_eq!(DriverId::Anthropic.to_string(), "anthropic");
2481 assert_eq!(DriverId::Gemini.to_string(), "gemini");
2482 }
2483
2484 #[test]
2485 fn test_provider_config_builder() {
2486 let config = ProviderConfig::new(DriverId::Anthropic)
2487 .with_api_key("test-key")
2488 .with_base_url("https://custom.api.com");
2489
2490 assert_eq!(config.provider_type, DriverId::Anthropic);
2491 assert_eq!(config.api_key, Some("test-key".to_string()));
2492 assert_eq!(config.base_url, Some("https://custom.api.com".to_string()));
2493 }
2494
2495 #[test]
2496 fn provider_config_debug_redacts_runtime_values() {
2497 let config = ProviderConfig::new(DriverId::OpenAI)
2498 .with_api_key("secret-key")
2499 .with_base_url("https://user:password@example.test/v1?token=secret")
2500 .with_metadata(ProviderMetadata {
2501 refresh_token: Some("refresh-secret".into()),
2502 account_id: Some("account-1".into()),
2503 extra: Some(serde_json::json!({ "client_secret": "metadata-secret" })),
2504 });
2505 let debug = format!("{config:?}");
2506 for secret in [
2507 "secret-key",
2508 "password",
2509 "token=secret",
2510 "refresh-secret",
2511 "metadata-secret",
2512 ] {
2513 assert!(!debug.contains(secret), "debug output exposed {secret}");
2514 }
2515 }
2516
2517 #[test]
2518 fn test_driver_registry_requires_api_key() {
2519 let mut registry = DriverRegistry::new();
2521 registry.register(DriverId::OpenAI, |_config| {
2522 struct MockDriver;
2524 #[async_trait]
2525 impl ChatDriver for MockDriver {
2526 async fn chat_completion_stream(
2527 &self,
2528 _endpoint: &ProviderEndpoint,
2529 _messages: Vec<LlmMessage>,
2530 _config: &LlmCallConfig,
2531 ) -> Result<LlmResponseStream> {
2532 unimplemented!()
2533 }
2534 }
2535 Box::new(MockDriver)
2536 });
2537
2538 let config = ProviderConfig::new(DriverId::OpenAI);
2540 let result = registry.create_chat_driver(&config);
2541 assert!(result.is_err());
2542
2543 let config_with_key = ProviderConfig::new(DriverId::OpenAI).with_api_key("test-key");
2545 let result = registry.create_chat_driver(&config_with_key);
2546 assert!(result.is_ok());
2547 }
2548
2549 #[test]
2550 fn test_driver_registry_returns_error_for_unregistered_provider() {
2551 let registry = DriverRegistry::new();
2552 let config = ProviderConfig::new(DriverId::Anthropic).with_api_key("test-key");
2553
2554 let result = registry.create_chat_driver(&config);
2555
2556 if let Err(AgentLoopError::DriverNotRegistered(provider)) = result {
2558 assert_eq!(provider, "anthropic");
2559 } else {
2560 panic!("Expected DriverNotRegistered error");
2561 }
2562 }
2563
2564 #[test]
2565 fn test_driver_registry_registration() {
2566 let mut registry = DriverRegistry::new();
2567
2568 assert!(!registry.has_driver(&DriverId::OpenAI));
2569 assert!(!registry.has_driver(&DriverId::Anthropic));
2570
2571 registry.register(DriverId::OpenAI, |_config| {
2572 struct MockDriver;
2573 #[async_trait]
2574 impl ChatDriver for MockDriver {
2575 async fn chat_completion_stream(
2576 &self,
2577 _endpoint: &ProviderEndpoint,
2578 _messages: Vec<LlmMessage>,
2579 _config: &LlmCallConfig,
2580 ) -> Result<LlmResponseStream> {
2581 unimplemented!()
2582 }
2583 }
2584 Box::new(MockDriver)
2585 });
2586
2587 assert!(registry.has_driver(&DriverId::OpenAI));
2588 assert!(!registry.has_driver(&DriverId::Anthropic));
2589 }
2590
2591 #[test]
2592 fn test_register_external_and_create_driver_without_api_key() {
2593 struct MockDriver;
2594 #[async_trait]
2595 impl ChatDriver for MockDriver {
2596 async fn chat_completion_stream(
2597 &self,
2598 _endpoint: &ProviderEndpoint,
2599 _messages: Vec<LlmMessage>,
2600 _config: &LlmCallConfig,
2601 ) -> Result<LlmResponseStream> {
2602 unimplemented!()
2603 }
2604 }
2605
2606 let mut registry = DriverRegistry::new();
2607 registry.register_external("openai-codex", |config| {
2608 assert_eq!(config.provider_type, DriverId::external("openai-codex"));
2610 Box::new(MockDriver)
2611 });
2612
2613 assert!(registry.has_driver(&DriverId::external("openai-codex")));
2614
2615 let config = ProviderConfig::new(DriverId::external("openai-codex")).with_metadata(
2617 ProviderMetadata {
2618 refresh_token: Some("rt".into()),
2619 ..Default::default()
2620 },
2621 );
2622 assert!(registry.create_chat_driver(&config).is_ok());
2623 }
2624
2625 #[test]
2626 fn test_register_defaults_to_chat_only_descriptor() {
2627 struct MockDriver;
2628 #[async_trait]
2629 impl ChatDriver for MockDriver {
2630 async fn chat_completion_stream(
2631 &self,
2632 _endpoint: &ProviderEndpoint,
2633 _messages: Vec<LlmMessage>,
2634 _config: &LlmCallConfig,
2635 ) -> Result<LlmResponseStream> {
2636 unimplemented!()
2637 }
2638 }
2639
2640 let mut registry = DriverRegistry::new();
2641 registry.register(DriverId::Anthropic, |_config| Box::new(MockDriver));
2642
2643 let descriptor = registry.descriptor(&DriverId::Anthropic).unwrap();
2644 assert_eq!(descriptor.display_name, "anthropic");
2645 assert_eq!(descriptor.services, vec![ServiceKind::Chat]);
2646 assert!(descriptor.chat.is_some());
2647 assert_eq!(descriptor.credential_schema.fields.len(), 1);
2649 assert_eq!(descriptor.credential_schema.fields[0].name, "api_key");
2650 assert!(descriptor.credential_schema.fields[0].required);
2651
2652 registry.register(DriverId::LlmSim, |_config| Box::new(MockDriver));
2654 let sim = registry.descriptor(&DriverId::LlmSim).unwrap();
2655 assert!(sim.credential_schema.fields.is_empty());
2656 }
2657
2658 #[test]
2659 fn test_descriptor_services_and_lookup() {
2660 struct MockDriver;
2661 #[async_trait]
2662 impl ChatDriver for MockDriver {
2663 async fn chat_completion_stream(
2664 &self,
2665 _endpoint: &ProviderEndpoint,
2666 _messages: Vec<LlmMessage>,
2667 _config: &LlmCallConfig,
2668 ) -> Result<LlmResponseStream> {
2669 unimplemented!()
2670 }
2671 }
2672
2673 let mut registry = DriverRegistry::new();
2674 registry.register_descriptor(DriverDescriptor {
2675 services: vec![ServiceKind::Chat, ServiceKind::Realtime],
2676 ..DriverDescriptor::chat_only(DriverId::OpenAI, |_config| Box::new(MockDriver))
2677 });
2678 registry.register(DriverId::Anthropic, |_config| Box::new(MockDriver));
2679
2680 assert!(registry.supports(&DriverId::OpenAI, ServiceKind::Chat));
2681 assert!(registry.supports(&DriverId::OpenAI, ServiceKind::Realtime));
2682 assert!(!registry.supports(&DriverId::Anthropic, ServiceKind::Realtime));
2683 assert!(!registry.supports(&DriverId::Gemini, ServiceKind::Chat));
2684
2685 let realtime = registry.providers_for(ServiceKind::Realtime);
2686 assert_eq!(realtime, vec![DriverId::OpenAI]);
2687 let mut chat = registry.providers_for(ServiceKind::Chat);
2688 chat.sort_by_key(|p| p.to_string());
2689 assert_eq!(chat, vec![DriverId::Anthropic, DriverId::OpenAI]);
2690 }
2691
2692 #[test]
2693 fn test_create_chat_driver_fails_without_chat_factory() {
2694 let mut registry = DriverRegistry::new();
2695 registry.register_descriptor(DriverDescriptor {
2696 id: DriverId::external("embeddings-only"),
2697 display_name: "Embeddings Only".to_string(),
2698 services: vec![ServiceKind::Embeddings],
2699 credential_schema: CredentialFormSchema::empty(),
2700 oauth: None,
2701 chat: None,
2702 embeddings: None,
2703 });
2704
2705 let config = ProviderConfig::new(DriverId::external("embeddings-only"));
2706 let err = match registry.create_chat_driver(&config) {
2707 Ok(_) => panic!("expected error for missing chat factory"),
2708 Err(err) => err,
2709 };
2710 assert!(
2711 err.to_string()
2712 .contains("does not implement the chat service"),
2713 "unexpected error: {err}"
2714 );
2715 }
2716
2717 #[test]
2718 #[should_panic(expected = "already registered")]
2719 fn test_register_duplicate_panics() {
2720 struct MockDriver;
2721 #[async_trait]
2722 impl ChatDriver for MockDriver {
2723 async fn chat_completion_stream(
2724 &self,
2725 _endpoint: &ProviderEndpoint,
2726 _messages: Vec<LlmMessage>,
2727 _config: &LlmCallConfig,
2728 ) -> Result<LlmResponseStream> {
2729 unimplemented!()
2730 }
2731 }
2732
2733 let mut registry = DriverRegistry::new();
2734 registry.register(DriverId::OpenAI, |_config| Box::new(MockDriver));
2735 registry.register(DriverId::OpenAI, |_config| Box::new(MockDriver));
2737 }
2738
2739 #[test]
2740 fn test_register_or_replace_overwrites() {
2741 struct MockDriver;
2742 #[async_trait]
2743 impl ChatDriver for MockDriver {
2744 async fn chat_completion_stream(
2745 &self,
2746 _endpoint: &ProviderEndpoint,
2747 _messages: Vec<LlmMessage>,
2748 _config: &LlmCallConfig,
2749 ) -> Result<LlmResponseStream> {
2750 unimplemented!()
2751 }
2752 }
2753
2754 let mut registry = DriverRegistry::new();
2755 registry.register(DriverId::LlmSim, |_config| Box::new(MockDriver));
2756 registry.register_or_replace(DriverId::LlmSim, |_config| Box::new(MockDriver));
2758 assert!(registry.has_driver(&DriverId::LlmSim));
2759 }
2760
2761 #[test]
2762 fn test_prepend_text_prefix_simple_text() {
2763 let mut msg = LlmMessage::text(LlmMessageRole::User, "Hello bot");
2764 msg.prepend_text_prefix("[Alice] ");
2765 assert_eq!(msg.content_as_text(), "[Alice] Hello bot");
2766 }
2767
2768 #[test]
2769 fn test_prepend_text_prefix_parts() {
2770 let mut msg = LlmMessage::parts(
2771 LlmMessageRole::User,
2772 vec![
2773 LlmContentPart::Text {
2774 text: "Hello".to_string(),
2775 },
2776 LlmContentPart::Image {
2777 url: "data:image/png;base64,abc".to_string(),
2778 },
2779 ],
2780 );
2781 msg.prepend_text_prefix("[Bob] ");
2782 match &msg.content {
2783 LlmMessageContent::Parts(parts) => {
2784 if let LlmContentPart::Text { text } = &parts[0] {
2785 assert_eq!(text, "[Bob] Hello");
2786 } else {
2787 panic!("Expected text part");
2788 }
2789 }
2790 _ => panic!("Expected parts content"),
2791 }
2792 }
2793
2794 #[test]
2795 fn test_prepend_text_prefix_parts_no_text() {
2796 let mut msg = LlmMessage::parts(
2797 LlmMessageRole::User,
2798 vec![LlmContentPart::Image {
2799 url: "data:image/png;base64,abc".to_string(),
2800 }],
2801 );
2802 msg.prepend_text_prefix("[Eve] ");
2803 match &msg.content {
2804 LlmMessageContent::Parts(parts) => {
2805 assert_eq!(parts.len(), 2);
2806 if let LlmContentPart::Text { text } = &parts[0] {
2807 assert_eq!(text, "[Eve] ");
2808 } else {
2809 panic!("Expected prepended text part");
2810 }
2811 }
2812 _ => panic!("Expected parts content"),
2813 }
2814 }
2815
2816 #[test]
2817 fn test_openrouter_plugin_config_is_empty() {
2818 assert!(OpenRouterPluginConfig::default().is_empty());
2819 assert!(
2820 !OpenRouterPluginConfig {
2821 web: Some(OpenRouterWebSearchPlugin::default()),
2822 file: None,
2823 }
2824 .is_empty()
2825 );
2826 assert!(
2827 !OpenRouterPluginConfig {
2828 web: None,
2829 file: Some(OpenRouterFilePlugin {}),
2830 }
2831 .is_empty()
2832 );
2833 }
2834
2835 #[test]
2836 fn test_openrouter_routing_is_empty_with_plugins() {
2837 let with_plugins = OpenRouterRoutingConfig {
2838 plugins: Some(OpenRouterPluginConfig {
2839 web: Some(OpenRouterWebSearchPlugin::default()),
2840 file: None,
2841 }),
2842 ..Default::default()
2843 };
2844 assert!(!with_plugins.is_empty());
2845
2846 let empty_plugins = OpenRouterRoutingConfig {
2847 plugins: Some(OpenRouterPluginConfig::default()),
2848 ..Default::default()
2849 };
2850 assert!(empty_plugins.is_empty());
2851 }
2852
2853 #[test]
2854 fn test_openrouter_web_search_plugin_serialization() {
2855 let plugin = OpenRouterWebSearchPlugin {
2856 max_results: Some(10),
2857 search_prompt: Some("search for Rust crates".to_string()),
2858 };
2859 let json = serde_json::to_value(&plugin).unwrap();
2860 assert_eq!(json["max_results"], 10);
2861 assert_eq!(json["search_prompt"], "search for Rust crates");
2862 }
2863
2864 #[test]
2865 fn test_openrouter_web_search_plugin_omits_none_fields() {
2866 let plugin = OpenRouterWebSearchPlugin::default();
2867 let json = serde_json::to_value(&plugin).unwrap();
2868 assert!(json.get("max_results").is_none());
2869 assert!(json.get("search_prompt").is_none());
2870 }
2871
2872 #[test]
2873 fn test_capacity_strategy_shared_capacity_is_noop() {
2874 let base = OpenRouterRoutingConfig {
2875 models: vec!["openai/gpt-5-mini".to_string()],
2876 capacity_strategy: Some(OpenRouterCapacityStrategy::SharedCapacity),
2877 ..Default::default()
2878 };
2879 let result = base.apply_capacity_strategy().unwrap();
2880 assert_eq!(
2881 result.capacity_strategy,
2882 Some(OpenRouterCapacityStrategy::SharedCapacity)
2883 );
2884 assert!(result.provider.is_none());
2885 }
2886
2887 #[test]
2888 fn test_capacity_strategy_none_is_noop() {
2889 let base = OpenRouterRoutingConfig {
2890 models: vec!["openai/gpt-5-mini".to_string()],
2891 capacity_strategy: None,
2892 ..Default::default()
2893 };
2894 let result = base.apply_capacity_strategy().unwrap();
2895 assert!(result.provider.is_none());
2896 }
2897
2898 #[test]
2899 fn test_capacity_strategy_byok_first_sets_allow_fallbacks() {
2900 let base = OpenRouterRoutingConfig {
2901 models: vec!["openai/gpt-5-mini".to_string()],
2902 capacity_strategy: Some(OpenRouterCapacityStrategy::ByokFirst),
2903 ..Default::default()
2904 };
2905 let result = base.apply_capacity_strategy().unwrap();
2906 let provider = result.provider.as_ref().expect("provider set by ByokFirst");
2907 assert_eq!(provider.allow_fallbacks, Some(true));
2908 }
2909
2910 #[test]
2911 fn test_capacity_strategy_byok_first_preserves_explicit_allow_fallbacks() {
2912 let base = OpenRouterRoutingConfig {
2914 models: vec!["openai/gpt-5-mini".to_string()],
2915 capacity_strategy: Some(OpenRouterCapacityStrategy::ByokFirst),
2916 provider: Some(OpenRouterProviderRouting {
2917 allow_fallbacks: Some(false),
2918 ..Default::default()
2919 }),
2920 ..Default::default()
2921 };
2922 let result = base.apply_capacity_strategy().unwrap();
2923 let provider = result.provider.as_ref().unwrap();
2924 assert_eq!(provider.allow_fallbacks, Some(false));
2925 }
2926
2927 #[test]
2928 fn test_capacity_strategy_byok_only_requires_provider_only() {
2929 let base = OpenRouterRoutingConfig {
2930 models: vec!["openai/gpt-5-mini".to_string()],
2931 capacity_strategy: Some(OpenRouterCapacityStrategy::ByokOnly),
2932 ..Default::default()
2933 };
2934 let err = base.apply_capacity_strategy().unwrap_err();
2935 assert!(
2936 err.contains("provider.only"),
2937 "error should mention provider.only: {err}"
2938 );
2939 }
2940
2941 #[test]
2942 fn test_capacity_strategy_byok_only_disables_fallbacks() {
2943 let base = OpenRouterRoutingConfig {
2944 models: vec!["openai/gpt-5-mini".to_string()],
2945 capacity_strategy: Some(OpenRouterCapacityStrategy::ByokOnly),
2946 provider: Some(OpenRouterProviderRouting {
2947 only: vec!["my-byok-provider".to_string()],
2948 ..Default::default()
2949 }),
2950 ..Default::default()
2951 };
2952 let result = base.apply_capacity_strategy().unwrap();
2953 let provider = result.provider.as_ref().unwrap();
2954 assert_eq!(provider.allow_fallbacks, Some(false));
2955 assert_eq!(provider.only, vec!["my-byok-provider"]);
2956 }
2957
2958 #[test]
2959 fn test_capacity_strategy_byok_only_not_empty_in_is_empty() {
2960 let with_strategy = OpenRouterRoutingConfig {
2961 capacity_strategy: Some(OpenRouterCapacityStrategy::ByokOnly),
2962 ..Default::default()
2963 };
2964 assert!(!with_strategy.is_empty());
2965
2966 let byok_first = OpenRouterRoutingConfig {
2967 capacity_strategy: Some(OpenRouterCapacityStrategy::ByokFirst),
2968 ..Default::default()
2969 };
2970 assert!(!byok_first.is_empty());
2971
2972 let shared = OpenRouterRoutingConfig {
2973 capacity_strategy: Some(OpenRouterCapacityStrategy::SharedCapacity),
2974 ..Default::default()
2975 };
2976 assert!(shared.is_empty());
2977 }
2978
2979 #[test]
2986 fn test_preset_no_presets_is_noop() {
2987 let base = OpenRouterRoutingConfig {
2988 models: vec!["openai/gpt-5-mini".to_string()],
2989 ..Default::default()
2990 };
2991 let result = base.apply_presets().unwrap();
2992 assert_eq!(result, base);
2993 }
2994
2995 #[test]
2996 fn test_preset_cheapest_with_tools_sets_require_parameters_and_sort_price() {
2997 let base = OpenRouterRoutingConfig {
2998 presets: vec![OpenRouterRoutingPreset::CheapestWithTools],
2999 ..Default::default()
3000 };
3001 let result = base.apply_presets().unwrap();
3002 assert!(result.presets.is_empty(), "presets cleared after apply");
3003 let provider = result.provider.expect("provider set by preset");
3004 assert_eq!(provider.require_parameters, Some(true));
3005 assert_eq!(
3006 provider.sort,
3007 Some(OpenRouterProviderSort::Simple(
3008 OpenRouterProviderSortBy::Price
3009 ))
3010 );
3011 }
3012
3013 #[test]
3014 fn test_preset_lowest_latency_review_sets_sort_throughput() {
3015 let base = OpenRouterRoutingConfig {
3016 presets: vec![OpenRouterRoutingPreset::LowestLatencyReview],
3017 ..Default::default()
3018 };
3019 let result = base.apply_presets().unwrap();
3020 let provider = result.provider.expect("provider set by preset");
3021 assert_eq!(
3022 provider.sort,
3023 Some(OpenRouterProviderSort::Simple(
3024 OpenRouterProviderSortBy::Throughput
3025 ))
3026 );
3027 }
3028
3029 #[test]
3030 fn test_preset_zdr_only_sets_zdr() {
3031 let base = OpenRouterRoutingConfig {
3032 presets: vec![OpenRouterRoutingPreset::ZdrOnly],
3033 ..Default::default()
3034 };
3035 let result = base.apply_presets().unwrap();
3036 let provider = result.provider.expect("provider set");
3037 assert_eq!(provider.zdr, Some(true));
3038 }
3039
3040 #[test]
3041 fn test_preset_byok_first_sets_allow_fallbacks() {
3042 let base = OpenRouterRoutingConfig {
3043 presets: vec![OpenRouterRoutingPreset::ByokFirst],
3044 ..Default::default()
3045 };
3046 let result = base.apply_presets().unwrap();
3047 let provider = result.provider.expect("provider set");
3048 assert_eq!(provider.allow_fallbacks, Some(true));
3049 }
3050
3051 #[test]
3052 fn test_preset_no_data_collection_sets_data_collection_deny() {
3053 let base = OpenRouterRoutingConfig {
3054 presets: vec![OpenRouterRoutingPreset::NoDataCollection],
3055 ..Default::default()
3056 };
3057 let result = base.apply_presets().unwrap();
3058 let provider = result.provider.expect("provider set");
3059 assert_eq!(
3060 provider.data_collection,
3061 Some(OpenRouterDataCollection::Deny)
3062 );
3063 }
3064
3065 #[test]
3066 fn test_preset_strict_json_sets_require_parameters() {
3067 let base = OpenRouterRoutingConfig {
3068 presets: vec![OpenRouterRoutingPreset::StrictJson],
3069 ..Default::default()
3070 };
3071 let result = base.apply_presets().unwrap();
3072 let provider = result.provider.expect("provider set");
3073 assert_eq!(provider.require_parameters, Some(true));
3074 }
3075
3076 #[test]
3077 fn test_preset_reasoning_required_sets_require_parameters() {
3078 let base = OpenRouterRoutingConfig {
3079 presets: vec![OpenRouterRoutingPreset::ReasoningRequired],
3080 ..Default::default()
3081 };
3082 let result = base.apply_presets().unwrap();
3083 let provider = result.provider.expect("provider set");
3084 assert_eq!(provider.require_parameters, Some(true));
3085 }
3086
3087 #[test]
3088 fn test_preset_max_price_converts_usd_per_million() {
3089 let base = OpenRouterRoutingConfig {
3090 presets: vec![OpenRouterRoutingPreset::MaxPrice {
3091 prompt_usd_per_million: Some(5.0),
3092 completion_usd_per_million: Some(15.0),
3093 }],
3094 ..Default::default()
3095 };
3096 let result = base.apply_presets().unwrap();
3097 let provider = result.provider.expect("provider set");
3098 let max_price = provider.max_price.expect("max_price set");
3099 let prompt = max_price.prompt.expect("prompt set");
3101 assert!((prompt - 5.0 / 1_000_000.0).abs() < f64::EPSILON);
3102 let completion = max_price.completion.expect("completion set");
3103 assert!((completion - 15.0 / 1_000_000.0).abs() < f64::EPSILON);
3104 }
3105
3106 #[test]
3107 fn test_preset_max_price_rejects_negative_values() {
3108 let base = OpenRouterRoutingConfig {
3109 presets: vec![OpenRouterRoutingPreset::MaxPrice {
3110 prompt_usd_per_million: Some(-1.0),
3111 completion_usd_per_million: None,
3112 }],
3113 ..Default::default()
3114 };
3115 let err = base.apply_presets().unwrap_err();
3116 assert!(
3117 err.contains("non-negative"),
3118 "error should mention non-negative: {err}"
3119 );
3120 }
3121
3122 #[test]
3123 fn test_preset_max_price_both_none_no_provider_field() {
3124 let base = OpenRouterRoutingConfig {
3125 presets: vec![OpenRouterRoutingPreset::MaxPrice {
3126 prompt_usd_per_million: None,
3127 completion_usd_per_million: None,
3128 }],
3129 ..Default::default()
3130 };
3131 let result = base.apply_presets().unwrap();
3132 assert!(
3133 result.provider.is_none(),
3134 "MaxPrice with no dimensions should not produce a provider field"
3135 );
3136 }
3137
3138 #[test]
3139 fn test_preset_explicit_provider_overrides_preset() {
3140 let base = OpenRouterRoutingConfig {
3141 presets: vec![OpenRouterRoutingPreset::CheapestWithTools],
3142 provider: Some(OpenRouterProviderRouting {
3143 sort: Some(OpenRouterProviderSort::Simple(
3145 OpenRouterProviderSortBy::Throughput,
3146 )),
3147 ..Default::default()
3148 }),
3149 ..Default::default()
3150 };
3151 let result = base.apply_presets().unwrap();
3152 let provider = result.provider.expect("provider set");
3153 assert_eq!(
3155 provider.sort,
3156 Some(OpenRouterProviderSort::Simple(
3157 OpenRouterProviderSortBy::Throughput
3158 ))
3159 );
3160 assert_eq!(provider.require_parameters, Some(true));
3162 }
3163
3164 #[test]
3165 fn test_preset_multiple_presets_combined() {
3166 let base = OpenRouterRoutingConfig {
3167 presets: vec![
3168 OpenRouterRoutingPreset::ZdrOnly,
3169 OpenRouterRoutingPreset::NoDataCollection,
3170 OpenRouterRoutingPreset::LowestLatencyReview,
3171 ],
3172 ..Default::default()
3173 };
3174 let result = base.apply_presets().unwrap();
3175 let provider = result.provider.expect("provider set");
3176 assert_eq!(provider.zdr, Some(true));
3177 assert_eq!(
3178 provider.data_collection,
3179 Some(OpenRouterDataCollection::Deny)
3180 );
3181 assert_eq!(
3182 provider.sort,
3183 Some(OpenRouterProviderSort::Simple(
3184 OpenRouterProviderSortBy::Throughput
3185 ))
3186 );
3187 }
3188
3189 #[test]
3190 fn test_preset_later_preset_overrides_sort() {
3191 let base = OpenRouterRoutingConfig {
3192 presets: vec![
3193 OpenRouterRoutingPreset::CheapestWithTools, OpenRouterRoutingPreset::LowestLatencyReview, ],
3196 ..Default::default()
3197 };
3198 let result = base.apply_presets().unwrap();
3199 let provider = result.provider.expect("provider set");
3200 assert_eq!(
3202 provider.sort,
3203 Some(OpenRouterProviderSort::Simple(
3204 OpenRouterProviderSortBy::Throughput
3205 ))
3206 );
3207 assert_eq!(provider.require_parameters, Some(true));
3209 }
3210
3211 #[test]
3212 fn test_preset_non_empty_in_is_empty() {
3213 let with_preset = OpenRouterRoutingConfig {
3214 presets: vec![OpenRouterRoutingPreset::ZdrOnly],
3215 ..Default::default()
3216 };
3217 assert!(!with_preset.is_empty());
3218
3219 let without = OpenRouterRoutingConfig::default();
3220 assert!(without.is_empty());
3221 }
3222}