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