1use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7
8use super::*;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(deny_unknown_fields)]
18pub struct ManagedSupplyProviderDef {
19 pub version: u32,
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct ProviderDef {
24 pub display_name: Option<String>,
25 pub icon: Option<String>,
26 pub protocol: Option<String>,
30 pub managed_supply: Option<ManagedSupplyProviderDef>,
35 pub base_url: String,
36 pub base_url_env: Option<String>,
37 pub region_env: Option<String>,
40 pub regions: BTreeMap<String, ProviderRegionDef>,
41 pub auth_style: String,
42 pub auth_header: Option<String>,
43 pub auth_env: AuthEnv,
44 pub credential_resolution: String,
54 pub extra_headers: BTreeMap<String, String>,
55 pub chat_endpoint: String,
56 pub completion_endpoint: Option<String>,
57 pub command: Option<String>,
58 pub args: Vec<String>,
59 pub env: BTreeMap<String, String>,
60 pub cwd: Option<String>,
61 pub mcp_servers: Vec<serde_json::Value>,
62 pub healthcheck: Option<HealthcheckDef>,
63 pub local_runtime: Option<LocalRuntimeDef>,
67 pub cache_usage_accounting: Option<bool>,
70 pub stream_usage_accounting: Option<bool>,
75 pub features: Vec<String>,
76 pub fallback: Option<String>,
78 pub retry_count: Option<u32>,
80 pub retry_delay_ms: Option<u64>,
82 pub rpm: Option<u32>,
84 pub rate_limits: Option<RateLimitsDef>,
88 pub cost_per_1k_in: Option<f64>,
90 pub cost_per_1k_out: Option<f64>,
92 pub latency_p50_ms: Option<u64>,
94 pub performance: Option<ServingPerformanceDef>,
96 #[doc(hidden)]
97 pub auth_style_explicit: bool,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
101pub struct ProviderRegionDef {
102 pub base_url: String,
103 #[serde(default)]
104 pub label: Option<String>,
105 #[serde(default)]
106 pub source_url: Option<String>,
107 #[serde(default)]
108 pub last_verified: Option<String>,
109 #[serde(default)]
110 pub notes: Option<String>,
111}
112
113#[derive(Debug, Clone, Deserialize)]
114struct ProviderDefWire {
115 #[serde(default)]
116 display_name: Option<String>,
117 #[serde(default)]
118 icon: Option<String>,
119 #[serde(default)]
120 protocol: Option<String>,
121 #[serde(default)]
122 managed_supply: Option<ManagedSupplyProviderDef>,
123 #[serde(default)]
124 base_url: String,
125 #[serde(default)]
126 base_url_env: Option<String>,
127 #[serde(default)]
128 region_env: Option<String>,
129 #[serde(default)]
130 regions: BTreeMap<String, ProviderRegionDef>,
131 #[serde(default)]
132 auth_style: Option<String>,
133 #[serde(default)]
134 auth_header: Option<String>,
135 #[serde(default)]
136 auth_env: AuthEnv,
137 #[serde(default)]
138 credential_resolution: Option<String>,
139 #[serde(default)]
140 extra_headers: BTreeMap<String, String>,
141 #[serde(default)]
142 chat_endpoint: String,
143 #[serde(default)]
144 completion_endpoint: Option<String>,
145 #[serde(default)]
146 command: Option<String>,
147 #[serde(default)]
148 args: Vec<String>,
149 #[serde(default)]
150 env: BTreeMap<String, String>,
151 #[serde(default)]
152 cwd: Option<String>,
153 #[serde(default)]
154 mcp_servers: Vec<serde_json::Value>,
155 #[serde(default)]
156 healthcheck: Option<HealthcheckDef>,
157 #[serde(default)]
158 local_runtime: Option<LocalRuntimeDef>,
159 #[serde(default)]
160 cache_usage_accounting: Option<bool>,
161 #[serde(default)]
162 stream_usage_accounting: Option<bool>,
163 #[serde(default)]
164 features: Vec<String>,
165 #[serde(default)]
166 fallback: Option<String>,
167 #[serde(default)]
168 retry_count: Option<u32>,
169 #[serde(default)]
170 retry_delay_ms: Option<u64>,
171 #[serde(default)]
172 rpm: Option<u32>,
173 #[serde(default)]
174 rate_limits: Option<RateLimitsDef>,
175 #[serde(default)]
176 cost_per_1k_in: Option<f64>,
177 #[serde(default)]
178 cost_per_1k_out: Option<f64>,
179 #[serde(default)]
180 latency_p50_ms: Option<u64>,
181 #[serde(default)]
182 performance: Option<ServingPerformanceDef>,
183}
184
185impl<'de> Deserialize<'de> for ProviderDef {
186 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
187 where
188 D: serde::Deserializer<'de>,
189 {
190 let wire = ProviderDefWire::deserialize(deserializer)?;
191 let auth_style_explicit = wire.auth_style.is_some();
192 Ok(Self {
193 display_name: wire.display_name,
194 icon: wire.icon,
195 protocol: wire.protocol,
196 managed_supply: wire.managed_supply,
197 base_url: wire.base_url,
198 base_url_env: wire.base_url_env,
199 region_env: wire.region_env,
200 regions: wire.regions,
201 auth_style: wire.auth_style.unwrap_or_else(default_bearer),
202 auth_header: wire.auth_header,
203 auth_env: wire.auth_env,
204 credential_resolution: wire
205 .credential_resolution
206 .unwrap_or_else(default_credential_resolution),
207 extra_headers: wire.extra_headers,
208 chat_endpoint: wire.chat_endpoint,
209 completion_endpoint: wire.completion_endpoint,
210 command: wire.command,
211 args: wire.args,
212 env: wire.env,
213 cwd: wire.cwd,
214 mcp_servers: wire.mcp_servers,
215 healthcheck: wire.healthcheck,
216 local_runtime: wire.local_runtime,
217 cache_usage_accounting: wire.cache_usage_accounting,
218 stream_usage_accounting: wire.stream_usage_accounting,
219 features: wire.features,
220 fallback: wire.fallback,
221 retry_count: wire.retry_count,
222 retry_delay_ms: wire.retry_delay_ms,
223 rpm: wire.rpm,
224 rate_limits: wire.rate_limits,
225 cost_per_1k_in: wire.cost_per_1k_in,
226 cost_per_1k_out: wire.cost_per_1k_out,
227 latency_p50_ms: wire.latency_p50_ms,
228 performance: wire.performance,
229 auth_style_explicit,
230 })
231 }
232}
233
234impl Default for ProviderDef {
235 fn default() -> Self {
236 Self {
237 display_name: None,
238 icon: None,
239 protocol: None,
240 managed_supply: None,
241 base_url: String::new(),
242 base_url_env: None,
243 region_env: None,
244 regions: BTreeMap::new(),
245 auth_style: default_bearer(),
246 auth_header: None,
247 auth_env: AuthEnv::None,
248 credential_resolution: default_credential_resolution(),
249 extra_headers: BTreeMap::new(),
250 chat_endpoint: String::new(),
251 completion_endpoint: None,
252 command: None,
253 args: Vec::new(),
254 env: BTreeMap::new(),
255 cwd: None,
256 mcp_servers: Vec::new(),
257 healthcheck: None,
258 local_runtime: None,
259 cache_usage_accounting: None,
260 stream_usage_accounting: None,
261 features: Vec::new(),
262 fallback: None,
263 retry_count: None,
264 retry_delay_ms: None,
265 rpm: None,
266 rate_limits: None,
267 cost_per_1k_in: None,
268 cost_per_1k_out: None,
269 latency_p50_ms: None,
270 performance: None,
271 auth_style_explicit: false,
272 }
273 }
274}
275
276impl ProviderDef {
277 pub(crate) fn merge_from(&mut self, overlay: &ProviderDef) {
278 merge_option(&mut self.display_name, &overlay.display_name);
279 merge_option(&mut self.icon, &overlay.icon);
280 merge_option(&mut self.protocol, &overlay.protocol);
281 merge_option(&mut self.managed_supply, &overlay.managed_supply);
282 merge_string(&mut self.base_url, &overlay.base_url);
283 merge_option(&mut self.base_url_env, &overlay.base_url_env);
284 merge_option(&mut self.region_env, &overlay.region_env);
285 self.regions.extend(overlay.regions.clone());
286 let overlay_uses_default_auth_style = overlay.auth_style == default_bearer();
287 if overlay.auth_style_explicit
288 || !overlay_uses_default_auth_style
289 || self.auth_style == default_bearer()
290 {
291 self.auth_style = overlay.auth_style.clone();
292 self.auth_style_explicit |=
293 overlay.auth_style_explicit || !overlay_uses_default_auth_style;
294 }
295 merge_option(&mut self.auth_header, &overlay.auth_header);
296 if !overlay.auth_env.is_none() {
297 self.auth_env = overlay.auth_env.clone();
298 }
299 if overlay.credential_resolution != default_credential_resolution() {
300 self.credential_resolution = overlay.credential_resolution.clone();
301 }
302 self.extra_headers.extend(overlay.extra_headers.clone());
303 merge_string(&mut self.chat_endpoint, &overlay.chat_endpoint);
304 merge_option(&mut self.completion_endpoint, &overlay.completion_endpoint);
305 merge_option(&mut self.command, &overlay.command);
306 merge_vec(&mut self.args, &overlay.args);
307 self.env.extend(overlay.env.clone());
308 merge_option(&mut self.cwd, &overlay.cwd);
309 merge_vec(&mut self.mcp_servers, &overlay.mcp_servers);
310 merge_option(&mut self.healthcheck, &overlay.healthcheck);
311 merge_option(&mut self.local_runtime, &overlay.local_runtime);
312 merge_option(
313 &mut self.cache_usage_accounting,
314 &overlay.cache_usage_accounting,
315 );
316 merge_option(
317 &mut self.stream_usage_accounting,
318 &overlay.stream_usage_accounting,
319 );
320 merge_vec(&mut self.features, &overlay.features);
321 merge_option(&mut self.fallback, &overlay.fallback);
322 merge_option(&mut self.retry_count, &overlay.retry_count);
323 merge_option(&mut self.retry_delay_ms, &overlay.retry_delay_ms);
324 merge_option(&mut self.rpm, &overlay.rpm);
325 merge_option(&mut self.rate_limits, &overlay.rate_limits);
326 merge_option(&mut self.cost_per_1k_in, &overlay.cost_per_1k_in);
327 merge_option(&mut self.cost_per_1k_out, &overlay.cost_per_1k_out);
328 merge_option(&mut self.latency_p50_ms, &overlay.latency_p50_ms);
329 merge_option(&mut self.performance, &overlay.performance);
330 }
331}
332
333fn merge_option<T: Clone>(base: &mut Option<T>, overlay: &Option<T>) {
334 if overlay.is_some() {
335 *base = overlay.clone();
336 }
337}
338
339fn merge_string(base: &mut String, overlay: &str) {
340 if !overlay.is_empty() {
341 *base = overlay.to_string();
342 }
343}
344
345fn merge_vec<T: Clone>(base: &mut Vec<T>, overlay: &[T]) {
346 if !overlay.is_empty() {
347 *base = overlay.to_vec();
348 }
349}
350
351fn default_bearer() -> String {
352 "bearer".to_string()
353}
354
355fn default_credential_resolution() -> String {
356 "env".to_string()
357}
358
359impl ProviderDef {
360 pub fn is_credential_resolution_platform_managed(&self) -> bool {
367 self.credential_resolution == "platform_managed"
368 }
369}
370
371#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
374#[serde(untagged)]
375pub enum AuthEnv {
376 #[default]
377 None,
378 Single(String),
379 Multiple(Vec<String>),
380}
381
382impl AuthEnv {
383 fn is_none(&self) -> bool {
384 matches!(self, AuthEnv::None)
385 }
386}
387
388pub fn resolve_base_url(pdef: &ProviderDef) -> String {
393 if let Some(env_name) = &pdef.base_url_env {
394 if let Some(val) = crate::stdlib::process::session_env_var(env_name)
395 .ok()
396 .flatten()
397 {
398 let trimmed = val.trim().trim_matches('"').trim_matches('\'');
400 if !trimmed.is_empty() {
401 return trimmed.to_string();
402 }
403 }
404 }
405 if let Some(env_name) = &pdef.region_env {
406 if let Some(val) = crate::stdlib::process::session_env_var(env_name)
407 .ok()
408 .flatten()
409 {
410 let region = val.trim().trim_matches('"').trim_matches('\'');
411 if !region.is_empty() {
412 let endpoint = pdef
413 .regions
414 .get(region)
415 .or_else(|| pdef.regions.get(®ion.to_ascii_lowercase()));
416 if let Some(endpoint) = endpoint {
417 let base_url = endpoint.base_url.trim();
418 if !base_url.is_empty() {
419 return base_url.to_string();
420 }
421 }
422 }
423 }
424 }
425 pdef.base_url.clone()
426}