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