Skip to main content

llm/
provider_connection.rs

1use std::collections::BTreeMap;
2use std::num::NonZeroU64;
3use std::time::Duration;
4
5pub(crate) const DEFAULT_STREAM_IDLE_TIMEOUT: Duration = Duration::from_mins(5);
6
7#[derive(
8    Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
9)]
10#[serde(rename_all = "kebab-case")]
11pub enum ProviderAuthMode {
12    #[default]
13    Default,
14    None,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq, Hash)]
18pub struct ProviderConnectionConfig {
19    pub base_url: Option<String>,
20    pub auth_mode: ProviderAuthMode,
21    pub request_model: Option<String>,
22    pub inference_profile_arn: Option<String>,
23    pub idle_timeout: Duration,
24}
25
26#[doc = include_str!("docs/provider_connection_override.md")]
27#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct ProviderConnectionOverride {
30    /// Base URL override for the provider's API endpoint.
31    #[serde(default, rename = "url", skip_serializing_if = "Option::is_none")]
32    pub base_url: Option<String>,
33    /// Authentication mode. `default` uses the provider's normal credential
34    /// chain; `none` disables auth, for local or unauthenticated servers.
35    #[serde(default, rename = "auth", skip_serializing_if = "Option::is_none")]
36    pub auth_mode: Option<ProviderAuthMode>,
37    /// Provider-specific model or deployment target sent in requests without changing catalog identity.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub request_model: Option<String>,
40    /// AWS Bedrock application inference profile ARN to route requests through.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub inference_profile_arn: Option<String>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub idle_timeout_secs: Option<NonZeroU64>,
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
48#[serde(transparent)]
49pub struct ProviderConnectionOverrides {
50    providers: BTreeMap<String, ProviderConnectionOverride>,
51}
52
53impl ProviderConnectionConfig {
54    pub fn from_override(value: ProviderConnectionOverride) -> Self {
55        Self {
56            base_url: value.base_url,
57            auth_mode: value.auth_mode.unwrap_or_default(),
58            request_model: value.request_model,
59            inference_profile_arn: value.inference_profile_arn,
60            idle_timeout: value
61                .idle_timeout_secs
62                .map_or(DEFAULT_STREAM_IDLE_TIMEOUT, |secs| Duration::from_secs(secs.get())),
63        }
64    }
65}
66
67impl Default for ProviderConnectionConfig {
68    fn default() -> Self {
69        Self {
70            base_url: None,
71            auth_mode: ProviderAuthMode::default(),
72            request_model: None,
73            inference_profile_arn: None,
74            idle_timeout: DEFAULT_STREAM_IDLE_TIMEOUT,
75        }
76    }
77}
78
79impl ProviderConnectionOverride {
80    pub fn url(url: impl Into<String>) -> Self {
81        Self { base_url: Some(url.into()), ..Self::default() }
82    }
83
84    pub fn auth(auth_mode: ProviderAuthMode) -> Self {
85        Self { auth_mode: Some(auth_mode), ..Self::default() }
86    }
87
88    pub fn request_model(model: impl Into<String>) -> Self {
89        Self { request_model: Some(model.into()), ..Self::default() }
90    }
91
92    pub fn inference_profile_arn(arn: impl Into<String>) -> Self {
93        Self { inference_profile_arn: Some(arn.into()), ..Self::default() }
94    }
95
96    pub fn idle_timeout_secs(secs: NonZeroU64) -> Self {
97        Self { idle_timeout_secs: Some(secs), ..Self::default() }
98    }
99
100    pub fn merge(&mut self, override_value: Self) {
101        if override_value.base_url.is_some() {
102            self.base_url = override_value.base_url;
103        }
104        if override_value.auth_mode.is_some() {
105            self.auth_mode = override_value.auth_mode;
106        }
107        if override_value.request_model.is_some() {
108            self.request_model = override_value.request_model;
109        }
110        if override_value.inference_profile_arn.is_some() {
111            self.inference_profile_arn = override_value.inference_profile_arn;
112        }
113        if override_value.idle_timeout_secs.is_some() {
114            self.idle_timeout_secs = override_value.idle_timeout_secs;
115        }
116    }
117}
118
119impl ProviderConnectionOverrides {
120    pub fn new(providers: BTreeMap<String, ProviderConnectionOverride>) -> Self {
121        Self { providers }
122    }
123
124    pub fn is_empty(&self) -> bool {
125        self.providers.is_empty()
126    }
127
128    pub fn merge(&mut self, overrides: ProviderConnectionOverrides) {
129        for (provider, override_value) in overrides.providers {
130            self.providers
131                .entry(provider)
132                .and_modify(|existing| existing.merge(override_value.clone()))
133                .or_insert(override_value);
134        }
135    }
136
137    pub fn config_for(&self, provider: &str) -> ProviderConnectionConfig {
138        self.providers.get(provider).cloned().map(ProviderConnectionConfig::from_override).unwrap_or_default()
139    }
140
141    pub fn into_inner(self) -> BTreeMap<String, ProviderConnectionOverride> {
142        self.providers
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn deserializes_and_merges_request_model() {
152        let mut first: ProviderConnectionOverrides =
153            serde_json::from_str(r#"{"azure-foundry":{"requestModel":"first"}}"#).unwrap();
154        first.merge(ProviderConnectionOverrides::new(BTreeMap::from([(
155            "azure-foundry".to_string(),
156            ProviderConnectionOverride::request_model("second"),
157        )])));
158
159        assert_eq!(first.config_for("azure-foundry").request_model.as_deref(), Some("second"));
160    }
161    #[test]
162    fn deserializes_bedrock_inference_profile_arn() {
163        let overrides: ProviderConnectionOverrides = serde_json::from_str(
164            r#"{"bedrock":{"inferenceProfileArn":"arn:aws:bedrock:us-west-2:000000000000:application-inference-profile/000000000000"}}"#,
165        )
166        .unwrap();
167
168        let config = overrides.config_for("bedrock");
169
170        assert_eq!(
171            config.inference_profile_arn.as_deref(),
172            Some("arn:aws:bedrock:us-west-2:000000000000:application-inference-profile/000000000000")
173        );
174    }
175
176    #[test]
177    fn idle_timeout_defaults_and_deserializes() {
178        let overrides: ProviderConnectionOverrides =
179            serde_json::from_str(r#"{"ollama":{"idleTimeoutSecs":900}}"#).unwrap();
180
181        assert_eq!(overrides.config_for("ollama").idle_timeout, Duration::from_mins(15));
182        assert_eq!(overrides.config_for("anthropic").idle_timeout, Duration::from_mins(5));
183        assert!(serde_json::from_str::<ProviderConnectionOverrides>(r#"{"ollama":{"idleTimeoutSecs":0}}"#).is_err());
184    }
185
186    #[test]
187    fn merge_replaces_inference_profile_arn() {
188        let mut first = ProviderConnectionOverride::inference_profile_arn("arn:first");
189
190        first.merge(ProviderConnectionOverride::inference_profile_arn("arn:second"));
191
192        assert_eq!(first.inference_profile_arn.as_deref(), Some("arn:second"));
193    }
194
195    #[test]
196    fn provider_overrides_merge_inference_profile_arn() {
197        let mut first = ProviderConnectionOverrides::new(BTreeMap::from([(
198            "bedrock".to_string(),
199            ProviderConnectionOverride::inference_profile_arn("arn:first"),
200        )]));
201        let second = ProviderConnectionOverrides::new(BTreeMap::from([(
202            "bedrock".to_string(),
203            ProviderConnectionOverride::inference_profile_arn("arn:second"),
204        )]));
205
206        first.merge(second);
207
208        assert_eq!(first.config_for("bedrock").inference_profile_arn.as_deref(), Some("arn:second"));
209    }
210}