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