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 ($name:ident, $key:literal, $display:literal, $default_model:expr, $api_base:expr, $env_var:expr, $prompt_cache_settings:ty) => {
10 pub struct $name;
11
12 impl ProviderMetadataProvider<$name> for $name {
13 const PROVIDER_KEY: &'static str = $key;
14 const DISPLAY_NAME: &'static str = $display;
15 const DEFAULT_MODEL: &'static str = $default_model;
16 const API_BASE_URL: &'static str = $api_base;
17 const BASE_URL_ENV_VAR: Option<&'static str> = $env_var;
18 }
19
20 impl LegacyProviderConfig for $name {
21 const PROVIDER_KEY: &'static str = <Self as CanDescribeProvider>::PROVIDER_KEY;
22 const DISPLAY_NAME: &'static str = <Self as CanDescribeProvider>::DISPLAY_NAME;
23 const DEFAULT_MODEL: &'static str = <Self as CanDescribeProvider>::DEFAULT_MODEL;
24 const API_BASE_URL: &'static str = <Self as CanDescribeProvider>::API_BASE_URL;
25 const BASE_URL_ENV_VAR: Option<&'static str> =
26 <Self as CanDescribeProvider>::BASE_URL_ENV_VAR;
27
28 type PromptCacheSettings = $prompt_cache_settings;
29 }
30 };
31}
32
33define_provider_config!(
34 GeminiProviderConfig,
35 "gemini",
36 "Gemini",
37 crate::config::constants::models::google::GEMINI_3_FLASH_PREVIEW,
38 crate::config::constants::urls::GEMINI_API_BASE,
39 Some(crate::config::constants::env_vars::GEMINI_BASE_URL),
40 GeminiPromptCacheSettings
41);
42define_provider_config!(
43 AnthropicProviderConfig,
44 "anthropic",
45 "Anthropic",
46 crate::config::constants::models::anthropic::DEFAULT_MODEL,
47 crate::config::constants::urls::ANTHROPIC_API_BASE,
48 Some(crate::config::constants::env_vars::ANTHROPIC_BASE_URL),
49 ()
50);
51define_provider_config!(
52 CopilotProviderConfig,
53 "copilot",
54 "GitHub Copilot",
55 crate::config::constants::models::copilot::DEFAULT_MODEL,
56 "",
57 None,
58 ()
59);
60define_provider_config!(
61 OpenAIProviderConfig,
62 "openai",
63 "OpenAI",
64 crate::config::constants::models::openai::DEFAULT_MODEL,
65 crate::config::constants::urls::OPENAI_API_BASE,
66 Some(crate::config::constants::env_vars::OPENAI_BASE_URL),
67 ()
68);
69define_provider_config!(
70 HuggingFaceProviderConfig,
71 "huggingface",
72 "HuggingFace",
73 crate::config::constants::models::huggingface::DEFAULT_MODEL,
74 crate::config::constants::urls::HUGGINGFACE_API_BASE,
75 Some(crate::config::constants::env_vars::HUGGINGFACE_BASE_URL),
76 ()
77);
78define_provider_config!(
79 DeepSeekProviderConfig,
80 "deepseek",
81 "DeepSeek",
82 crate::config::constants::models::deepseek::DEEPSEEK_V4_PRO,
83 crate::config::constants::urls::DEEPSEEK_API_BASE,
84 Some(crate::config::constants::env_vars::DEEPSEEK_BASE_URL),
85 ()
86);
87define_provider_config!(
88 MistralProviderConfig,
89 "mistral",
90 "Mistral",
91 crate::config::constants::models::mistral::MISTRAL_LARGE_3,
92 crate::config::constants::urls::MISTRAL_API_BASE,
93 Some(crate::config::constants::env_vars::MISTRAL_BASE_URL),
94 ()
95);
96define_provider_config!(
97 MoonshotProviderConfig,
98 "moonshot",
99 "Moonshot",
100 crate::config::constants::models::moonshot::DEFAULT_MODEL,
101 crate::config::constants::urls::MOONSHOT_API_BASE,
102 Some(crate::config::constants::env_vars::MOONSHOT_BASE_URL),
103 ()
104);
105define_provider_config!(
106 ZAIProviderConfig,
107 "zai",
108 "Z.AI",
109 crate::config::constants::models::zai::DEFAULT_MODEL,
110 crate::config::constants::urls::Z_AI_API_BASE,
111 Some(crate::config::constants::env_vars::ZAI_BASE_URL),
112 ()
113);
114define_provider_config!(
115 OpenRouterProviderConfig,
116 "openrouter",
117 "OpenRouter",
118 "openrouter/auto",
119 crate::config::constants::urls::OPENROUTER_API_BASE,
120 Some(crate::config::constants::env_vars::OPENROUTER_BASE_URL),
121 ()
122);
123define_provider_config!(
124 OpenResponsesProviderConfig,
125 "openresponses",
126 "OpenResponses",
127 crate::config::constants::models::openresponses::DEFAULT_MODEL,
128 crate::config::constants::urls::OPENRESPONSES_API_BASE,
129 Some(crate::config::constants::env_vars::OPENRESPONSES_BASE_URL),
130 ()
131);
132define_provider_config!(
133 OllamaProviderConfig,
134 "ollama",
135 "Ollama",
136 "gpt-oss:20b",
137 "http://localhost:11434",
138 None,
139 ()
140);
141define_provider_config!(
142 LmStudioProviderConfig,
143 "lmstudio",
144 "LM Studio",
145 crate::config::constants::models::lmstudio::DEFAULT_MODEL,
146 crate::config::constants::urls::LMSTUDIO_API_BASE,
147 Some(crate::config::constants::env_vars::LMSTUDIO_BASE_URL),
148 ()
149);
150define_provider_config!(
151 LlamaCppProviderConfig,
152 "llamacpp",
153 "llama.cpp",
154 crate::config::constants::models::llamacpp::DEFAULT_MODEL,
155 crate::config::constants::urls::LLAMACPP_API_BASE,
156 Some(crate::config::constants::env_vars::LLAMACPP_BASE_URL),
157 ()
158);
159define_provider_config!(
160 MiMoProviderConfig,
161 "mimo",
162 "Xiaomi MiMo",
163 crate::config::constants::models::mimo::DEFAULT_MODEL,
164 crate::config::constants::urls::MIMO_API_BASE,
165 Some(crate::config::constants::env_vars::MIMO_BASE_URL),
166 ()
167);
168define_provider_config!(
169 MinimaxProviderConfig,
170 "minimax",
171 "Minimax",
172 crate::config::constants::models::minimax::DEFAULT_MODEL,
173 crate::config::constants::urls::MINIMAX_API_BASE,
174 Some(crate::config::constants::env_vars::MINIMAX_BASE_URL),
175 ()
176);
177define_provider_config!(
178 OpenCodeZenProviderConfig,
179 "opencode-zen",
180 "OpenCode Zen",
181 crate::config::constants::models::opencode_zen::DEFAULT_MODEL,
182 crate::config::constants::urls::OPENCODE_ZEN_API_BASE,
183 Some(crate::config::constants::env_vars::OPENCODE_ZEN_BASE_URL),
184 ()
185);
186define_provider_config!(
187 OpenCodeGoProviderConfig,
188 "opencode-go",
189 "OpenCode Go",
190 crate::config::constants::models::opencode_go::DEFAULT_MODEL,
191 crate::config::constants::urls::OPENCODE_GO_API_BASE,
192 Some(crate::config::constants::env_vars::OPENCODE_GO_BASE_URL),
193 ()
194);
195define_provider_config!(
196 QwenProviderConfig,
197 "qwen",
198 "Qwen",
199 crate::config::constants::models::qwen::DEFAULT_MODEL,
200 crate::config::constants::urls::QWEN_API_BASE,
201 Some(crate::config::constants::env_vars::QWEN_BASE_URL),
202 ()
203);
204define_provider_config!(
205 StepFunProviderConfig,
206 "stepfun",
207 "StepFun",
208 crate::config::constants::models::stepfun::DEFAULT_MODEL,
209 crate::config::constants::urls::STEPFUN_API_BASE,
210 Some(crate::config::constants::env_vars::STEPFUN_BASE_URL),
211 ()
212);
213define_provider_config!(
214 EvolinkProviderConfig,
215 "evolink",
216 "Evolink",
217 crate::config::constants::models::evolink::DEFAULT_MODEL,
218 crate::config::constants::urls::EVOLINK_API_BASE,
219 Some(crate::config::constants::env_vars::EVOLINK_BASE_URL),
220 ()
221);
222define_provider_config!(
223 PoolsideProviderConfig,
224 "poolside",
225 "Poolside",
226 crate::config::constants::models::poolside::DEFAULT_MODEL,
227 crate::config::constants::urls::POOLSIDE_API_BASE,
228 Some(crate::config::constants::env_vars::POOLSIDE_BASE_URL),
229 ()
230);
231
232#[macro_export]
234macro_rules! create_provider_builder {
235 ($config_type:ty) => {
236 $crate::llm::provider_builder::ProviderBuilder::<$config_type>::new()
237 };
238}
239
240fn non_empty(value: Option<String>) -> Option<String> {
241 value.and_then(|value| {
242 let trimmed = value.trim();
243 if trimmed.is_empty() {
244 None
245 } else if trimmed.len() == value.len() {
246 Some(value)
247 } else {
248 Some(trimmed.to_string())
249 }
250 })
251}
252
253pub fn create_provider_unified(
256 provider_name: &str,
257 api_key: Option<String>,
258 model: Option<String>,
259 base_url: Option<String>,
260 prompt_cache: Option<PromptCachingConfig>,
261 timeouts: Option<TimeoutsConfig>,
262) -> Result<Box<dyn LLMProvider>, LLMError> {
263 factory::create_provider_with_config(
264 provider_name,
265 FactoryProviderConfig {
266 api_key: non_empty(api_key),
267 openai_chatgpt_auth: None,
268 copilot_auth: None,
269 base_url: non_empty(base_url),
270 model: non_empty(model),
271 prompt_cache,
272 timeouts,
273 openai: None,
274 anthropic: None,
275 model_behavior: None,
276 workspace_root: None,
277 },
278 )
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::llm::provider_builder::ProviderBuilder;
285
286 #[test]
287 fn unified_provider_creation_supports_huggingface_through_factory() {
288 let provider = create_provider_unified(
289 "huggingface",
290 Some("test-key".to_string()),
291 Some("openai/gpt-oss-20b".to_string()),
292 None,
293 None,
294 None,
295 )
296 .expect("shim should route through the factory");
297
298 assert_eq!(provider.name(), "huggingface");
299 }
300
301 #[test]
302 fn builder_shim_routes_through_factory() {
303 let provider = ProviderBuilder::<OpenAIProviderConfig>::new()
304 .api_key("test-key".to_string())
305 .model(crate::config::constants::models::openai::DEFAULT_MODEL.to_string())
306 .try_build()
307 .expect("builder shim should resolve via the factory");
308
309 assert_eq!(provider.name(), "openai");
310 }
311
312 #[test]
313 fn legacy_provider_config_create_provider_routes_through_factory() {
314 let provider =
315 <OpenAIProviderConfig as crate::llm::provider_builder::ProviderConfig>::create_provider(
316 "test-key".to_string(),
317 crate::config::constants::models::openai::DEFAULT_MODEL.to_string(),
318 crate::config::constants::urls::OPENAI_API_BASE.to_string(),
319 false,
320 (),
321 TimeoutsConfig::default(),
322 );
323
324 assert_eq!(provider.name(), "openai");
325 }
326
327 #[test]
328 fn unified_provider_creation_matches_factory_behavior() {
329 let shim = create_provider_unified(
330 "ollama",
331 None,
332 Some("gpt-oss:20b".to_string()),
333 Some("http://localhost:11434".to_string()),
334 None,
335 Some(TimeoutsConfig::default()),
336 )
337 .expect("shim provider should build");
338
339 let factory = factory::create_provider_with_config(
340 "ollama",
341 FactoryProviderConfig {
342 api_key: None,
343 openai_chatgpt_auth: None,
344 copilot_auth: None,
345 base_url: Some("http://localhost:11434".to_string()),
346 model: Some("gpt-oss:20b".to_string()),
347 prompt_cache: None,
348 timeouts: Some(TimeoutsConfig::default()),
349 openai: None,
350 anthropic: None,
351 model_behavior: None,
352 workspace_root: None,
353 },
354 )
355 .expect("factory provider should build");
356
357 assert_eq!(shim.name(), factory.name());
358 assert_eq!(shim.supported_models(), factory.supported_models());
359 }
360}