Skip to main content

ares_llm/
config.rs

1use serde::{Deserialize, Serialize};
2
3// ============= Provider Configuration =============
4
5/// LLM provider configuration.
6///
7/// Internally tagged with `type`. Existing `openai` / `azure` / `anthropic` /
8/// `bedrock` / `ollama` shapes are unchanged. Additional variants match genai
9/// `AdapterKind::as_lower_str` names. `type = "bedrock"` remains ARES Bedrock
10/// (Bearer via `AWS_BEARER_TOKEN_BEDROCK`); genai's Bedrock API kind is
11/// `bedrock_api`.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(tag = "type")]
14#[non_exhaustive]
15pub enum ProviderConfig {
16    /// OpenAI API (or compatible endpoints, including NVIDIA NIM).
17    #[serde(rename = "openai")]
18    OpenAI {
19        /// Environment variable containing API key.
20        api_key_env: String,
21        /// API base URL (default: `https://api.openai.com/v1`).
22        #[serde(default = "default_openai_base")]
23        api_base: String,
24        /// Default model to use with this provider.
25        default_model: String,
26    },
27    /// Azure AI Foundry OpenAI-compatible chat completions.
28    #[serde(rename = "azure")]
29    Azure {
30        /// Environment variable containing the Foundry API key.
31        #[serde(default = "default_azure_api_key_env")]
32        api_key_env: String,
33        /// Environment variable containing the Foundry base URL.
34        #[serde(default = "default_azure_base_url_env")]
35        base_url_env: String,
36        /// Default Foundry model id.
37        #[serde(default = "default_azure_default_model")]
38        default_model: String,
39    },
40    /// Anthropic Claude API.
41    #[serde(rename = "anthropic")]
42    Anthropic {
43        /// Environment variable containing API key (default: `ANTHROPIC_API_KEY`).
44        #[serde(default = "default_anthropic_api_key_env")]
45        api_key_env: String,
46        /// Default model (default: `claude-3-5-sonnet-20241022`).
47        #[serde(default = "default_anthropic_default_model")]
48        default_model: String,
49    },
50    /// AWS Bedrock Claude API (ARES bearer token).
51    #[serde(rename = "bedrock")]
52    Bedrock {
53        /// Environment variable containing the Bedrock bearer token.
54        #[serde(default = "default_bedrock_api_key_env")]
55        api_key_env: String,
56        /// Environment variable containing the AWS region.
57        #[serde(default = "default_bedrock_region_env")]
58        region_env: String,
59        /// Default Bedrock model id.
60        #[serde(default = "default_bedrock_default_model")]
61        default_model: String,
62    },
63    /// Local Ollama server.
64    #[serde(rename = "ollama")]
65    Ollama {
66        /// Dummy env var; unused at runtime.
67        #[serde(default = "default_ollama_api_key_env")]
68        api_key_env: String,
69        /// Base URL of the Ollama server.
70        #[serde(default = "default_ollama_base_url")]
71        base_url: String,
72        /// Default model id (e.g. `ministral-3:3b`).
73        #[serde(default = "default_ollama_default_model")]
74        default_model: String,
75    },
76    /// Google Vertex AI.
77    #[serde(rename = "vertex")]
78    Vertex {
79        /// Environment variable containing the Vertex API key.
80        #[serde(default = "default_vertex_api_key_env")]
81        api_key_env: String,
82        /// Environment variable containing the GCP project id.
83        #[serde(default = "default_vertex_project_env")]
84        project_env: String,
85        /// Environment variable containing the Vertex location.
86        #[serde(default = "default_vertex_location_env")]
87        location_env: String,
88        /// Default model.
89        #[serde(default)]
90        default_model: String,
91    },
92    /// Bring-your-own OpenAI-compatible endpoint (`GENAI_{n}_*`).
93    #[serde(rename = "custom")]
94    Custom {
95        /// Custom adapter index (`GENAI_{index}_ENDPOINT`).
96        index: u8,
97        /// Endpoint URL.
98        endpoint: String,
99        /// Optional environment variable containing an API key.
100        #[serde(default)]
101        api_key_env: Option<String>,
102        /// Default model.
103        #[serde(default)]
104        default_model: String,
105    },
106    #[serde(rename = "openai_resp")]
107    OpenAIResp {
108        #[serde(default = "default_openai_api_key_env")]
109        api_key_env: String,
110        #[serde(default)]
111        api_base: Option<String>,
112        #[serde(default)]
113        default_model: String,
114    },
115    #[serde(rename = "gemini")]
116    Gemini {
117        #[serde(default = "default_gemini_api_key_env")]
118        api_key_env: String,
119        #[serde(default)]
120        api_base: Option<String>,
121        #[serde(default)]
122        default_model: String,
123    },
124    #[serde(rename = "fireworks")]
125    Fireworks {
126        #[serde(default = "default_fireworks_api_key_env")]
127        api_key_env: String,
128        #[serde(default)]
129        api_base: Option<String>,
130        #[serde(default)]
131        default_model: String,
132    },
133    #[serde(rename = "together")]
134    Together {
135        #[serde(default = "default_together_api_key_env")]
136        api_key_env: String,
137        #[serde(default)]
138        api_base: Option<String>,
139        #[serde(default)]
140        default_model: String,
141    },
142    #[serde(rename = "groq")]
143    Groq {
144        #[serde(default = "default_groq_api_key_env")]
145        api_key_env: String,
146        #[serde(default)]
147        api_base: Option<String>,
148        #[serde(default)]
149        default_model: String,
150    },
151    #[serde(rename = "aihubmix")]
152    Aihubmix {
153        #[serde(default = "default_aihubmix_api_key_env")]
154        api_key_env: String,
155        #[serde(default)]
156        api_base: Option<String>,
157        #[serde(default)]
158        default_model: String,
159    },
160    #[serde(rename = "kimi")]
161    Kimi {
162        #[serde(default = "default_kimi_api_key_env")]
163        api_key_env: String,
164        #[serde(default)]
165        api_base: Option<String>,
166        #[serde(default)]
167        default_model: String,
168    },
169    #[serde(rename = "mimo")]
170    Mimo {
171        #[serde(default = "default_mimo_api_key_env")]
172        api_key_env: String,
173        #[serde(default)]
174        api_base: Option<String>,
175        #[serde(default)]
176        default_model: String,
177    },
178    #[serde(rename = "moonshot")]
179    Moonshot {
180        #[serde(default = "default_moonshot_api_key_env")]
181        api_key_env: String,
182        #[serde(default)]
183        api_base: Option<String>,
184        #[serde(default)]
185        default_model: String,
186    },
187    #[serde(rename = "nebius")]
188    Nebius {
189        #[serde(default = "default_nebius_api_key_env")]
190        api_key_env: String,
191        #[serde(default)]
192        api_base: Option<String>,
193        #[serde(default)]
194        default_model: String,
195    },
196    #[serde(rename = "xai")]
197    Xai {
198        #[serde(default = "default_xai_api_key_env")]
199        api_key_env: String,
200        #[serde(default)]
201        api_base: Option<String>,
202        #[serde(default)]
203        default_model: String,
204    },
205    #[serde(rename = "deepseek")]
206    DeepSeek {
207        #[serde(default = "default_deepseek_api_key_env")]
208        api_key_env: String,
209        #[serde(default)]
210        api_base: Option<String>,
211        #[serde(default)]
212        default_model: String,
213    },
214    #[serde(rename = "zai")]
215    Zai {
216        #[serde(default = "default_zai_api_key_env")]
217        api_key_env: String,
218        #[serde(default)]
219        api_base: Option<String>,
220        #[serde(default)]
221        default_model: String,
222    },
223    #[serde(rename = "bigmodel")]
224    BigModel {
225        #[serde(default = "default_bigmodel_api_key_env")]
226        api_key_env: String,
227        #[serde(default)]
228        api_base: Option<String>,
229        #[serde(default)]
230        default_model: String,
231    },
232    #[serde(rename = "aliyun")]
233    Aliyun {
234        #[serde(default = "default_aliyun_api_key_env")]
235        api_key_env: String,
236        #[serde(default)]
237        api_base: Option<String>,
238        #[serde(default)]
239        default_model: String,
240    },
241    #[serde(rename = "qwen_cloud")]
242    QwenCloud {
243        #[serde(default = "default_qwen_cloud_api_key_env")]
244        api_key_env: String,
245        #[serde(default)]
246        api_base: Option<String>,
247        #[serde(default)]
248        default_model: String,
249    },
250    #[serde(rename = "baidu")]
251    Baidu {
252        #[serde(default = "default_baidu_api_key_env")]
253        api_key_env: String,
254        #[serde(default)]
255        api_base: Option<String>,
256        #[serde(default)]
257        default_model: String,
258    },
259    #[serde(rename = "cohere")]
260    Cohere {
261        #[serde(default = "default_cohere_api_key_env")]
262        api_key_env: String,
263        #[serde(default)]
264        api_base: Option<String>,
265        #[serde(default)]
266        default_model: String,
267    },
268    #[serde(rename = "ollama_cloud")]
269    OllamaCloud {
270        #[serde(default = "default_ollama_cloud_api_key_env")]
271        api_key_env: String,
272        #[serde(default)]
273        api_base: Option<String>,
274        #[serde(default)]
275        default_model: String,
276    },
277    #[serde(rename = "omlx")]
278    Omlx {
279        #[serde(default = "default_omlx_api_key_env")]
280        api_key_env: String,
281        #[serde(default)]
282        api_base: Option<String>,
283        #[serde(default)]
284        default_model: String,
285    },
286    #[serde(rename = "github_copilot", alias = "github")]
287    GithubCopilot {
288        #[serde(default = "default_github_copilot_api_key_env")]
289        api_key_env: String,
290        #[serde(default)]
291        api_base: Option<String>,
292        #[serde(default)]
293        default_model: String,
294    },
295    #[serde(rename = "opencode_go")]
296    OpenCodeGo {
297        #[serde(default = "default_opencode_go_api_key_env")]
298        api_key_env: String,
299        #[serde(default)]
300        api_base: Option<String>,
301        #[serde(default)]
302        default_model: String,
303    },
304    #[serde(rename = "bedrock_api")]
305    BedrockApi {
306        #[serde(default = "default_bedrock_api_kind_api_key_env")]
307        api_key_env: String,
308        #[serde(default)]
309        api_base: Option<String>,
310        #[serde(default)]
311        default_model: String,
312    },
313    #[serde(rename = "open_router", alias = "openrouter")]
314    OpenRouter {
315        #[serde(default = "default_open_router_api_key_env")]
316        api_key_env: String,
317        #[serde(default)]
318        api_base: Option<String>,
319        #[serde(default)]
320        default_model: String,
321    },
322    #[serde(rename = "atlascloud")]
323    AtlasCloud {
324        #[serde(default = "default_atlascloud_api_key_env")]
325        api_key_env: String,
326        #[serde(default)]
327        api_base: Option<String>,
328        #[serde(default)]
329        default_model: String,
330    },
331    #[serde(rename = "minimax")]
332    MiniMax {
333        #[serde(default = "default_minimax_api_key_env")]
334        api_key_env: String,
335        #[serde(default)]
336        api_base: Option<String>,
337        #[serde(default)]
338        default_model: String,
339    },
340}
341
342macro_rules! type_name_match {
343    ($self:expr, $($variant:ident => $name:literal),* $(,)?) => {
344        match $self {
345            $(ProviderConfig::$variant { .. } => $name,)*
346        }
347    };
348}
349
350impl ProviderConfig {
351    /// Returns the provider type discriminator (genai lower string / ARES type).
352    pub fn type_name(&self) -> &'static str {
353        type_name_match!(
354            self,
355            OpenAI => "openai",
356            Azure => "azure",
357            Anthropic => "anthropic",
358            Bedrock => "bedrock",
359            Ollama => "ollama",
360            Vertex => "vertex",
361            Custom => "custom",
362            OpenAIResp => "openai_resp",
363            Gemini => "gemini",
364            Fireworks => "fireworks",
365            Together => "together",
366            Groq => "groq",
367            Aihubmix => "aihubmix",
368            Kimi => "kimi",
369            Mimo => "mimo",
370            Moonshot => "moonshot",
371            Nebius => "nebius",
372            Xai => "xai",
373            DeepSeek => "deepseek",
374            Zai => "zai",
375            BigModel => "bigmodel",
376            Aliyun => "aliyun",
377            QwenCloud => "qwen_cloud",
378            Baidu => "baidu",
379            Cohere => "cohere",
380            OllamaCloud => "ollama_cloud",
381            Omlx => "omlx",
382            GithubCopilot => "github_copilot",
383            OpenCodeGo => "opencode_go",
384            BedrockApi => "bedrock_api",
385            OpenRouter => "open_router",
386            AtlasCloud => "atlascloud",
387            MiniMax => "minimax",
388        )
389    }
390
391    /// Default model id configured for this provider.
392    pub fn default_model(&self) -> &str {
393        match self {
394            ProviderConfig::OpenAI { default_model, .. }
395            | ProviderConfig::Azure { default_model, .. }
396            | ProviderConfig::Anthropic { default_model, .. }
397            | ProviderConfig::Bedrock { default_model, .. }
398            | ProviderConfig::Ollama { default_model, .. }
399            | ProviderConfig::Vertex { default_model, .. }
400            | ProviderConfig::Custom { default_model, .. }
401            | ProviderConfig::OpenAIResp { default_model, .. }
402            | ProviderConfig::Gemini { default_model, .. }
403            | ProviderConfig::Fireworks { default_model, .. }
404            | ProviderConfig::Together { default_model, .. }
405            | ProviderConfig::Groq { default_model, .. }
406            | ProviderConfig::Aihubmix { default_model, .. }
407            | ProviderConfig::Kimi { default_model, .. }
408            | ProviderConfig::Mimo { default_model, .. }
409            | ProviderConfig::Moonshot { default_model, .. }
410            | ProviderConfig::Nebius { default_model, .. }
411            | ProviderConfig::Xai { default_model, .. }
412            | ProviderConfig::DeepSeek { default_model, .. }
413            | ProviderConfig::Zai { default_model, .. }
414            | ProviderConfig::BigModel { default_model, .. }
415            | ProviderConfig::Aliyun { default_model, .. }
416            | ProviderConfig::QwenCloud { default_model, .. }
417            | ProviderConfig::Baidu { default_model, .. }
418            | ProviderConfig::Cohere { default_model, .. }
419            | ProviderConfig::OllamaCloud { default_model, .. }
420            | ProviderConfig::Omlx { default_model, .. }
421            | ProviderConfig::GithubCopilot { default_model, .. }
422            | ProviderConfig::OpenCodeGo { default_model, .. }
423            | ProviderConfig::BedrockApi { default_model, .. }
424            | ProviderConfig::OpenRouter { default_model, .. }
425            | ProviderConfig::AtlasCloud { default_model, .. }
426            | ProviderConfig::MiniMax { default_model, .. } => default_model,
427        }
428    }
429}
430
431impl std::str::FromStr for ProviderConfig {
432    type Err = String;
433
434    /// Parse a provider type name into a default `ProviderConfig` variant.
435    /// `nvidia` stays an OpenAI-compat alias at the NIM base.
436    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
437        let key = s.trim().to_lowercase();
438        let key = match key.as_str() {
439            "openrouter" => "open_router",
440            "github" => "github_copilot",
441            other => other,
442        };
443        match key {
444            "openai" => Ok(ProviderConfig::OpenAI {
445                api_key_env: default_openai_api_key_env(),
446                api_base: default_openai_base(),
447                default_model: "gpt-4".to_string(),
448            }),
449            "nvidia" => Ok(ProviderConfig::OpenAI {
450                api_key_env: default_nvidia_api_key_env(),
451                api_base: default_nvidia_api_base(),
452                default_model: default_nvidia_default_model(),
453            }),
454            "azure" => Ok(ProviderConfig::Azure {
455                api_key_env: default_azure_api_key_env(),
456                base_url_env: default_azure_base_url_env(),
457                default_model: default_azure_default_model(),
458            }),
459            "anthropic" => Ok(ProviderConfig::Anthropic {
460                api_key_env: default_anthropic_api_key_env(),
461                default_model: default_anthropic_default_model(),
462            }),
463            "bedrock" => Ok(ProviderConfig::Bedrock {
464                api_key_env: default_bedrock_api_key_env(),
465                region_env: default_bedrock_region_env(),
466                default_model: default_bedrock_default_model(),
467            }),
468            "ollama" => Ok(ProviderConfig::Ollama {
469                api_key_env: default_ollama_api_key_env(),
470                base_url: default_ollama_base_url(),
471                default_model: default_ollama_default_model(),
472            }),
473            "vertex" => Ok(ProviderConfig::Vertex {
474                api_key_env: default_vertex_api_key_env(),
475                project_env: default_vertex_project_env(),
476                location_env: default_vertex_location_env(),
477                default_model: String::new(),
478            }),
479            "custom" => Ok(ProviderConfig::Custom {
480                index: 1,
481                endpoint: String::new(),
482                api_key_env: None,
483                default_model: String::new(),
484            }),
485            other => {
486                let json = format!(r#"{{"type":"{other}"}}"#);
487                serde_json::from_str(&json).map_err(|_| {
488                    format!(
489                        "Unknown provider type: {other}. Use: openai (or nvidia), azure, anthropic, bedrock, ollama, or a genai adapter kind"
490                    )
491                })
492            }
493        }
494    }
495}
496
497fn default_openai_base() -> String {
498    "https://api.openai.com/v1".to_string()
499}
500
501fn default_openai_api_key_env() -> String {
502    "OPENAI_API_KEY".to_string()
503}
504
505fn default_nvidia_api_key_env() -> String {
506    "NVIDIA_API_KEY".to_string()
507}
508
509fn default_nvidia_api_base() -> String {
510    "https://integrate.api.nvidia.com/v1".to_string()
511}
512
513fn default_nvidia_default_model() -> String {
514    "nvidia/nemotron-3-ultra-550b-a55b".to_string()
515}
516
517fn default_azure_api_key_env() -> String {
518    "AZURE_FOUNDRY_API_KEY".to_string()
519}
520
521fn default_azure_base_url_env() -> String {
522    "AZURE_FOUNDRY_BASE_URL".to_string()
523}
524
525fn default_azure_default_model() -> String {
526    "DeepSeek-V4-Flash".to_string()
527}
528
529fn default_anthropic_api_key_env() -> String {
530    "ANTHROPIC_API_KEY".to_string()
531}
532
533fn default_anthropic_default_model() -> String {
534    "claude-3-5-sonnet-20241022".to_string()
535}
536
537fn default_bedrock_api_key_env() -> String {
538    "AWS_BEARER_TOKEN_BEDROCK".to_string()
539}
540
541fn default_bedrock_region_env() -> String {
542    "AWS_REGION".to_string()
543}
544
545fn default_bedrock_default_model() -> String {
546    "us.anthropic.claude-haiku-4-5-20251001-v1:0".to_string()
547}
548
549fn default_ollama_api_key_env() -> String {
550    "OLLAMA_API_KEY".to_string()
551}
552
553fn default_ollama_base_url() -> String {
554    "http://localhost:11434".to_string()
555}
556
557fn default_ollama_default_model() -> String {
558    "ministral-3:3b".to_string()
559}
560
561fn default_vertex_api_key_env() -> String {
562    "VERTEX_API_KEY".to_string()
563}
564
565fn default_vertex_project_env() -> String {
566    "VERTEX_PROJECT_ID".to_string()
567}
568
569fn default_vertex_location_env() -> String {
570    "VERTEX_LOCATION".to_string()
571}
572
573fn default_gemini_api_key_env() -> String {
574    "GEMINI_API_KEY".to_string()
575}
576fn default_fireworks_api_key_env() -> String {
577    "FIREWORKS_API_KEY".to_string()
578}
579fn default_together_api_key_env() -> String {
580    "TOGETHER_API_KEY".to_string()
581}
582fn default_groq_api_key_env() -> String {
583    "GROQ_API_KEY".to_string()
584}
585fn default_aihubmix_api_key_env() -> String {
586    "AIHUBMIX_API_KEY".to_string()
587}
588fn default_kimi_api_key_env() -> String {
589    "KIMI_API_KEY".to_string()
590}
591fn default_mimo_api_key_env() -> String {
592    "MIMO_API_KEY".to_string()
593}
594fn default_moonshot_api_key_env() -> String {
595    "MOONSHOT_API_KEY".to_string()
596}
597fn default_nebius_api_key_env() -> String {
598    "NEBIUS_API_KEY".to_string()
599}
600fn default_xai_api_key_env() -> String {
601    "XAI_API_KEY".to_string()
602}
603fn default_deepseek_api_key_env() -> String {
604    "DEEPSEEK_API_KEY".to_string()
605}
606fn default_zai_api_key_env() -> String {
607    "ZAI_API_KEY".to_string()
608}
609fn default_bigmodel_api_key_env() -> String {
610    "BIGMODEL_API_KEY".to_string()
611}
612fn default_aliyun_api_key_env() -> String {
613    "ALIYUN_API_KEY".to_string()
614}
615fn default_qwen_cloud_api_key_env() -> String {
616    "QWEN_CLOUD_API_KEY".to_string()
617}
618fn default_baidu_api_key_env() -> String {
619    "BAIDU_API_KEY".to_string()
620}
621fn default_cohere_api_key_env() -> String {
622    "COHERE_API_KEY".to_string()
623}
624fn default_ollama_cloud_api_key_env() -> String {
625    "OLLAMA_API_KEY".to_string()
626}
627fn default_omlx_api_key_env() -> String {
628    "OMLX_API_KEY".to_string()
629}
630fn default_github_copilot_api_key_env() -> String {
631    "GITHUB_TOKEN".to_string()
632}
633fn default_opencode_go_api_key_env() -> String {
634    "OPENCODE_GO_API_KEY".to_string()
635}
636fn default_bedrock_api_kind_api_key_env() -> String {
637    "BEDROCK_API_KEY".to_string()
638}
639fn default_open_router_api_key_env() -> String {
640    "OPEN_ROUTER_API_KEY".to_string()
641}
642fn default_atlascloud_api_key_env() -> String {
643    "ATLASCLOUD_API_KEY".to_string()
644}
645fn default_minimax_api_key_env() -> String {
646    "MINIMAX_API_KEY".to_string()
647}
648
649// ============= Model Configuration =============
650
651/// Model configuration referencing a provider.
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct ModelConfig {
654    /// Reference to a provider name defined in \[providers\].
655    pub provider: String,
656
657    /// Model name/identifier to use with the provider.
658    pub model: String,
659
660    /// Sampling temperature (0.0 = deterministic, 1.0+ = creative). Default: 0.7.
661    #[serde(default = "default_temperature")]
662    pub temperature: f32,
663
664    /// Maximum tokens to generate (default: 512).
665    #[serde(default = "default_model_max_tokens")]
666    pub max_tokens: u32,
667}
668
669fn default_temperature() -> f32 {
670    0.7
671}
672
673fn default_model_max_tokens() -> u32 {
674    512
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680    use std::str::FromStr;
681
682    #[test]
683    fn parses_gemini_groq_openai_resp_custom() {
684        let gemini: ProviderConfig = serde_json::from_str(r#"{"type":"gemini"}"#).unwrap();
685        assert_eq!(gemini.type_name(), "gemini");
686        assert!(matches!(gemini, ProviderConfig::Gemini { .. }));
687
688        let groq: ProviderConfig = serde_json::from_str(r#"{"type":"groq"}"#).unwrap();
689        assert_eq!(groq.type_name(), "groq");
690
691        let resp: ProviderConfig = serde_json::from_str(r#"{"type":"openai_resp"}"#).unwrap();
692        assert_eq!(resp.type_name(), "openai_resp");
693
694        let custom: ProviderConfig = serde_json::from_str(
695            r#"{"type":"custom","index":2,"endpoint":"http://127.0.0.1:8000/v1"}"#,
696        )
697        .unwrap();
698        match custom {
699            ProviderConfig::Custom {
700                index, endpoint, ..
701            } => {
702                assert_eq!(index, 2);
703                assert_eq!(endpoint, "http://127.0.0.1:8000/v1");
704            }
705            other => panic!("expected Custom, got {}", other.type_name()),
706        }
707
708        assert_eq!(
709            ProviderConfig::from_str("gemini").unwrap().type_name(),
710            "gemini"
711        );
712        assert_eq!(ProviderConfig::from_str("groq").unwrap().type_name(), "groq");
713        assert_eq!(
714            ProviderConfig::from_str("openai_resp").unwrap().type_name(),
715            "openai_resp"
716        );
717        assert_eq!(
718            ProviderConfig::from_str("custom").unwrap().type_name(),
719            "custom"
720        );
721        assert_eq!(
722            ProviderConfig::from_str("openrouter").unwrap().type_name(),
723            "open_router"
724        );
725        assert_eq!(
726            ProviderConfig::from_str("github").unwrap().type_name(),
727            "github_copilot"
728        );
729        assert_eq!(
730            ProviderConfig::from_str("nvidia").unwrap().type_name(),
731            "openai"
732        );
733        assert!(ProviderConfig::from_str("not-a-provider").is_err());
734    }
735}