llm/
provider_connection.rs1use 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 #[serde(default, rename = "url", skip_serializing_if = "Option::is_none")]
32 pub base_url: Option<String>,
33 #[serde(default, rename = "auth", skip_serializing_if = "Option::is_none")]
36 pub auth_mode: Option<ProviderAuthMode>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub request_model: Option<String>,
40 #[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}