Skip to main content

harn_vm/llm_config/
provider_def.rs

1//! Provider serving definitions: the `ProviderDef` runtime shape, its wire
2//! deserialization form, overlay merge, auth-env selector, and base-URL
3//! resolution.
4use 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    /// Provider protocol. Omitted providers use Harn's normal HTTP provider
15    /// path; `acp` launches an Agent Client Protocol server and drives it as
16    /// an agent-backed provider.
17    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    /// How this provider's credentials are resolved. `"env"` (default) means
24    /// the generic `auth_env` lookup is authoritative: missing env vars are a
25    /// hard "missing API key" error. `"platform_managed"` means the provider's
26    /// own shim resolves credentials through a multi-step chain the generic
27    /// `auth_env` lookup cannot see (e.g. Bedrock's AWS credential chain —
28    /// env/profile/container/instance-role — or Vertex's bearer token /
29    /// service-account JSON / ADC). Callers must skip the generic `auth_env`
30    /// requirement for these providers and let the shim fail on its own if
31    /// credentials are truly absent, instead of hardcoding provider names.
32    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    /// Local runtime lifecycle metadata used by `harn local launch/stop`.
43    /// This is intentionally separate from provider process fields such as
44    /// `command`/`args`, which are used for ACP or external provider adapters.
45    pub local_runtime: Option<LocalRuntimeDef>,
46    pub features: Vec<String>,
47    /// Fallback provider name to try if this provider fails.
48    pub fallback: Option<String>,
49    /// Number of retries before falling back (default 0).
50    pub retry_count: Option<u32>,
51    /// Delay between retries in milliseconds (default 1000).
52    pub retry_delay_ms: Option<u64>,
53    /// Maximum requests per minute. None = unlimited.
54    pub rpm: Option<u32>,
55    /// Rich provider quota metadata. `rpm` remains as a legacy shorthand;
56    /// when both are present, this nested shape is the authoritative catalog
57    /// record and callers can still read the flattened `rpm`.
58    pub rate_limits: Option<RateLimitsDef>,
59    /// Provider/catalog pricing in USD per 1k input tokens.
60    pub cost_per_1k_in: Option<f64>,
61    /// Provider/catalog pricing in USD per 1k output tokens.
62    pub cost_per_1k_out: Option<f64>,
63    /// Observed or configured p50 latency in milliseconds.
64    pub latency_p50_ms: Option<u64>,
65    /// Optional provider-level serving performance observations.
66    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    /// Whether this provider resolves its own credentials through a
288    /// multi-step chain (AWS SigV4 credential chain, GCP ADC / service
289    /// account JSON, etc.) rather than the generic `auth_env` lookup.
290    /// Callers that would otherwise hardcode a provider-name match (e.g.
291    /// "does this provider need the generic missing-API-key error") should
292    /// read this instead.
293    pub fn is_credential_resolution_platform_managed(&self) -> bool {
294        self.credential_resolution == "platform_managed"
295    }
296}
297
298/// Auth env var name(s) for the provider. Can be a single string or an array
299/// (tried in order until one is set).
300#[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
315/// Resolve the effective base URL for a provider, checking the `base_url_env`
316/// override first, then falling back to the configured `base_url`.
317pub 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            // Strip surrounding quotes that some .env parsers leave intact.
321            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}