1use crate::config::TimeoutsConfig;
2use crate::config::core::{GeminiPromptCacheSettings, PromptCachingConfig};
3use crate::llm::cgp::{CanDescribeProvider, ProviderMetadataProvider};
4use crate::llm::factory::{self, ProviderConfig as FactoryProviderConfig};
5use crate::llm::provider::{LLMError, LLMProvider};
6use crate::llm::provider_builder::ProviderConfig as LegacyProviderConfig;
7
8macro_rules! define_provider_config {
9 ($(#[$meta:meta])* $name:ident, $key:literal, $display:literal, $default_model:expr, $api_base:expr, $env_var:expr, $prompt_cache_settings:ty) => {
10 $(#[$meta])*
11 pub struct $name;
12
13 impl ProviderMetadataProvider<$name> for $name {
14 const PROVIDER_KEY: &'static str = $key;
15 const DISPLAY_NAME: &'static str = $display;
16 const DEFAULT_MODEL: &'static str = $default_model;
17 const API_BASE_URL: &'static str = $api_base;
18 const BASE_URL_ENV_VAR: Option<&'static str> = $env_var;
19 }
20
21 impl LegacyProviderConfig for $name {
22 const PROVIDER_KEY: &'static str = <Self as CanDescribeProvider>::PROVIDER_KEY;
23 const DISPLAY_NAME: &'static str = <Self as CanDescribeProvider>::DISPLAY_NAME;
24 const DEFAULT_MODEL: &'static str = <Self as CanDescribeProvider>::DEFAULT_MODEL;
25 const API_BASE_URL: &'static str = <Self as CanDescribeProvider>::API_BASE_URL;
26 const BASE_URL_ENV_VAR: Option<&'static str> =
27 <Self as CanDescribeProvider>::BASE_URL_ENV_VAR;
28
29 type PromptCacheSettings = $prompt_cache_settings;
30 }
31 };
32}
33
34define_provider_config!(
35 GeminiProviderConfig,
37 "gemini",
38 "Gemini",
39 crate::config::constants::models::google::GEMINI_3_FLASH_PREVIEW,
40 crate::config::constants::urls::GEMINI_API_BASE,
41 Some(crate::config::constants::env_vars::GEMINI_BASE_URL),
42 GeminiPromptCacheSettings
43);
44define_provider_config!(
45 AnthropicProviderConfig,
47 "anthropic",
48 "Anthropic",
49 crate::config::constants::models::anthropic::DEFAULT_MODEL,
50 crate::config::constants::urls::ANTHROPIC_API_BASE,
51 Some(crate::config::constants::env_vars::ANTHROPIC_BASE_URL),
52 ()
53);
54define_provider_config!(
55 CopilotProviderConfig,
57 "copilot",
58 "GitHub Copilot",
59 crate::config::constants::models::copilot::DEFAULT_MODEL,
60 "",
61 None,
62 ()
63);
64define_provider_config!(
65 OpenAIProviderConfig,
67 "openai",
68 "OpenAI",
69 crate::config::constants::models::openai::DEFAULT_MODEL,
70 crate::config::constants::urls::OPENAI_API_BASE,
71 Some(crate::config::constants::env_vars::OPENAI_BASE_URL),
72 ()
73);
74define_provider_config!(
75 HuggingFaceProviderConfig,
77 "huggingface",
78 "HuggingFace",
79 crate::config::constants::models::huggingface::DEFAULT_MODEL,
80 crate::config::constants::urls::HUGGINGFACE_API_BASE,
81 Some(crate::config::constants::env_vars::HUGGINGFACE_BASE_URL),
82 ()
83);
84define_provider_config!(
85 DeepSeekProviderConfig,
87 "deepseek",
88 "DeepSeek",
89 crate::config::constants::models::deepseek::DEEPSEEK_FLASH,
90 crate::config::constants::urls::DEEPSEEK_API_BASE,
91 Some(crate::config::constants::env_vars::DEEPSEEK_BASE_URL),
92 ()
93);
94define_provider_config!(
95 MetaProviderConfig,
97 "meta",
98 "Meta AI",
99 crate::config::constants::models::meta::DEFAULT_MODEL,
100 crate::config::constants::urls::META_API_BASE,
101 Some(crate::config::constants::env_vars::META_BASE_URL),
102 ()
103);
104define_provider_config!(
105 MistralProviderConfig,
107 "mistral",
108 "Mistral",
109 crate::config::constants::models::mistral::MISTRAL_LARGE_3,
110 crate::config::constants::urls::MISTRAL_API_BASE,
111 Some(crate::config::constants::env_vars::MISTRAL_BASE_URL),
112 ()
113);
114define_provider_config!(
115 MoonshotProviderConfig,
117 "moonshot",
118 "Moonshot",
119 crate::config::constants::models::moonshot::DEFAULT_MODEL,
120 crate::config::constants::urls::MOONSHOT_API_BASE,
121 Some(crate::config::constants::env_vars::MOONSHOT_BASE_URL),
122 ()
123);
124define_provider_config!(
125 ZAIProviderConfig,
127 "zai",
128 "Z.AI",
129 crate::config::constants::models::zai::DEFAULT_MODEL,
130 crate::config::constants::urls::Z_AI_API_BASE,
131 Some(crate::config::constants::env_vars::ZAI_BASE_URL),
132 ()
133);
134define_provider_config!(
135 OpenRouterProviderConfig,
137 "openrouter",
138 "OpenRouter",
139 "openrouter/auto",
140 crate::config::constants::urls::OPENROUTER_API_BASE,
141 Some(crate::config::constants::env_vars::OPENROUTER_BASE_URL),
142 ()
143);
144define_provider_config!(
145 OpenResponsesProviderConfig,
147 "openresponses",
148 "OpenResponses",
149 crate::config::constants::models::openresponses::DEFAULT_MODEL,
150 crate::config::constants::urls::OPENRESPONSES_API_BASE,
151 Some(crate::config::constants::env_vars::OPENRESPONSES_BASE_URL),
152 ()
153);
154define_provider_config!(
155 OllamaProviderConfig,
157 "ollama",
158 "Ollama",
159 "gpt-oss:20b",
160 "http://localhost:11434",
161 None,
162 ()
163);
164define_provider_config!(
165 OllamaCloudProviderConfig,
167 "ollama-cloud",
168 "Ollama Cloud",
169 crate::config::constants::models::ollama::DEFAULT_CLOUD_MODEL,
170 crate::config::constants::urls::OLLAMA_CLOUD_API_BASE,
171 Some(crate::config::constants::env_vars::OLLAMA_BASE_URL),
172 ()
173);
174define_provider_config!(
175 LmStudioProviderConfig,
177 "lmstudio",
178 "LM Studio",
179 crate::config::constants::models::lmstudio::DEFAULT_MODEL,
180 crate::config::constants::urls::LMSTUDIO_API_BASE,
181 Some(crate::config::constants::env_vars::LMSTUDIO_BASE_URL),
182 ()
183);
184define_provider_config!(
185 LlamaCppProviderConfig,
187 "llamacpp",
188 "llama.cpp",
189 crate::config::constants::models::llamacpp::DEFAULT_MODEL,
190 crate::config::constants::urls::LLAMACPP_API_BASE,
191 Some(crate::config::constants::env_vars::LLAMACPP_BASE_URL),
192 ()
193);
194define_provider_config!(
195 MiMoProviderConfig,
197 "mimo",
198 "Xiaomi MiMo",
199 crate::config::constants::models::mimo::DEFAULT_MODEL,
200 crate::config::constants::urls::MIMO_API_BASE,
201 Some(crate::config::constants::env_vars::MIMO_BASE_URL),
202 ()
203);
204define_provider_config!(
205 MinimaxProviderConfig,
207 "minimax",
208 "Minimax",
209 crate::config::constants::models::minimax::DEFAULT_MODEL,
210 crate::config::constants::urls::MINIMAX_API_BASE,
211 Some(crate::config::constants::env_vars::MINIMAX_BASE_URL),
212 ()
213);
214define_provider_config!(
215 OpenCodeZenProviderConfig,
217 "opencode-zen",
218 "OpenCode Zen",
219 crate::config::constants::models::opencode_zen::DEFAULT_MODEL,
220 crate::config::constants::urls::OPENCODE_ZEN_API_BASE,
221 Some(crate::config::constants::env_vars::OPENCODE_ZEN_BASE_URL),
222 ()
223);
224define_provider_config!(
225 OpenCodeGoProviderConfig,
227 "opencode-go",
228 "OpenCode Go",
229 crate::config::constants::models::opencode_go::DEFAULT_MODEL,
230 crate::config::constants::urls::OPENCODE_GO_API_BASE,
231 Some(crate::config::constants::env_vars::OPENCODE_GO_BASE_URL),
232 ()
233);
234define_provider_config!(
235 QwenProviderConfig,
237 "qwen",
238 "Qwen",
239 crate::config::constants::models::qwen::DEFAULT_MODEL,
240 crate::config::constants::urls::QWEN_API_BASE,
241 Some(crate::config::constants::env_vars::QWEN_BASE_URL),
242 ()
243);
244define_provider_config!(
245 StepFunProviderConfig,
247 "stepfun",
248 "StepFun",
249 crate::config::constants::models::stepfun::DEFAULT_MODEL,
250 crate::config::constants::urls::STEPFUN_API_BASE,
251 Some(crate::config::constants::env_vars::STEPFUN_BASE_URL),
252 ()
253);
254define_provider_config!(
255 EvolinkProviderConfig,
257 "evolink",
258 "Evolink",
259 crate::config::constants::models::evolink::DEFAULT_MODEL,
260 crate::config::constants::urls::EVOLINK_API_BASE,
261 Some(crate::config::constants::env_vars::EVOLINK_BASE_URL),
262 ()
263);
264define_provider_config!(
265 XAIProviderConfig,
267 "xai",
268 "xAI",
269 crate::config::constants::models::xai::DEFAULT_MODEL,
270 crate::config::constants::urls::XAI_API_BASE,
271 Some(crate::config::constants::env_vars::XAI_BASE_URL),
272 ()
273);
274define_provider_config!(
275 PoolsideProviderConfig,
277 "poolside",
278 "Poolside",
279 crate::config::constants::models::poolside::DEFAULT_MODEL,
280 crate::config::constants::urls::POOLSIDE_API_BASE,
281 Some(crate::config::constants::env_vars::POOLSIDE_BASE_URL),
282 ()
283);
284define_provider_config!(
285 VercelProviderConfig,
287 "vercel",
288 "Vercel AI Gateway",
289 crate::config::constants::models::vercel::DEFAULT_MODEL,
290 crate::config::constants::urls::VERCEL_AI_GATEWAY_API_BASE,
291 Some(crate::config::constants::env_vars::VERCEL_AI_GATEWAY_BASE_URL),
292 ()
293);
294define_provider_config!(
295 NvidiaProviderConfig,
297 "nvidia",
298 "NVIDIA",
299 crate::config::constants::models::nvidia::DEFAULT_MODEL,
300 crate::config::constants::urls::NVIDIA_API_BASE,
301 Some(crate::config::constants::env_vars::NVIDIA_BASE_URL),
302 ()
303);
304define_provider_config!(
305 MergeGatewayProviderConfig,
307 "merge-gateway",
308 "Merge Gateway",
309 crate::config::constants::models::merge_gateway::DEFAULT_MODEL,
310 crate::config::constants::urls::MERGE_GATEWAY_NATIVE_API_BASE,
311 Some(crate::config::constants::env_vars::MERGE_GATEWAY_BASE_URL),
312 ()
313);
314
315#[macro_export]
317macro_rules! create_provider_builder {
318 ($config_type:ty) => {
319 $crate::llm::provider_builder::ProviderBuilder::<$config_type>::new()
320 };
321}
322
323fn non_empty(value: Option<String>) -> Option<String> {
324 value.and_then(|value| {
325 let trimmed = value.trim();
326 if trimmed.is_empty() {
327 None
328 } else if trimmed.len() == value.len() {
329 Some(value)
330 } else {
331 Some(trimmed.to_string())
332 }
333 })
334}
335
336pub fn create_provider_unified(
339 provider_name: &str,
340 api_key: Option<String>,
341 model: Option<String>,
342 base_url: Option<String>,
343 prompt_cache: Option<PromptCachingConfig>,
344 timeouts: Option<TimeoutsConfig>,
345) -> Result<Box<dyn LLMProvider>, LLMError> {
346 factory::create_provider_with_config(
347 provider_name,
348 FactoryProviderConfig {
349 api_key: non_empty(api_key),
350 openai_chatgpt_auth: None,
351 copilot_auth: None,
352 base_url: non_empty(base_url),
353 model: non_empty(model),
354 prompt_cache,
355 timeouts,
356 openai: None,
357 anthropic: None,
358 model_behavior: None,
359 workspace_root: None,
360 },
361 )
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::llm::provider_builder::ProviderBuilder;
368
369 #[test]
370 fn unified_provider_creation_supports_huggingface_through_factory() {
371 let provider = create_provider_unified(
372 "huggingface",
373 Some("test-key".to_string()),
374 Some("openai/gpt-oss-20b".to_string()),
375 None,
376 None,
377 None,
378 )
379 .expect("shim should route through the factory");
380
381 assert_eq!(provider.name(), "huggingface");
382 }
383
384 #[test]
385 fn builder_shim_routes_through_factory() {
386 let provider = ProviderBuilder::<OpenAIProviderConfig>::new()
387 .api_key("test-key".to_string())
388 .model(crate::config::constants::models::openai::DEFAULT_MODEL.to_string())
389 .try_build()
390 .expect("builder shim should resolve via the factory");
391
392 assert_eq!(provider.name(), "openai");
393 }
394
395 #[test]
396 fn legacy_provider_config_create_provider_routes_through_factory() {
397 let provider = <OpenAIProviderConfig as crate::llm::provider_builder::ProviderConfig>::create_provider(
398 "test-key".to_string(),
399 crate::config::constants::models::openai::DEFAULT_MODEL.to_string(),
400 crate::config::constants::urls::OPENAI_API_BASE.to_string(),
401 false,
402 (),
403 TimeoutsConfig::default(),
404 );
405
406 assert_eq!(provider.name(), "openai");
407 }
408
409 #[test]
410 fn unified_provider_creation_matches_factory_behavior() {
411 let shim = create_provider_unified(
412 "ollama",
413 None,
414 Some("gpt-oss:20b".to_string()),
415 Some("http://localhost:11434".to_string()),
416 None,
417 Some(TimeoutsConfig::default()),
418 )
419 .expect("shim provider should build");
420
421 let factory = factory::create_provider_with_config(
422 "ollama",
423 FactoryProviderConfig {
424 api_key: None,
425 openai_chatgpt_auth: None,
426 copilot_auth: None,
427 base_url: Some("http://localhost:11434".to_string()),
428 model: Some("gpt-oss:20b".to_string()),
429 prompt_cache: None,
430 timeouts: Some(TimeoutsConfig::default()),
431 openai: None,
432 anthropic: None,
433 model_behavior: None,
434 workspace_root: None,
435 },
436 )
437 .expect("factory provider should build");
438
439 assert_eq!(shim.name(), factory.name());
440 assert_eq!(shim.supported_models(), factory.supported_models());
441 }
442}