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