vtcode_core/llm/providers/
minimax.rs1use crate::config::TimeoutsConfig;
2use crate::config::constants::{env_vars, models, urls};
3use crate::config::core::{AnthropicConfig, ModelConfig, PromptCachingConfig};
4use crate::llm::provider::{LLMError, LLMProvider, LLMRequest, LLMResponse, LLMStream, Message};
5use async_trait::async_trait;
6use std::env;
7
8use super::anthropic::AnthropicProvider;
9use super::common::{ensure_model, impl_llm_client, resolve_model};
10
11pub struct MinimaxProvider {
12 inner: AnthropicProvider,
13 model: String,
14}
15
16impl MinimaxProvider {
17 pub fn new(api_key: String) -> Self {
18 Self::from_config(
19 Some(api_key),
20 Some(models::minimax::DEFAULT_MODEL.to_string()),
21 None,
22 None,
23 None,
24 None,
25 None,
26 )
27 }
28
29 pub fn with_model(api_key: String, model: String) -> Self {
30 Self::from_config(Some(api_key), Some(model), None, None, None, None, None)
31 }
32
33 pub fn new_with_client(
34 api_key: String,
35 model: String,
36 http_client: reqwest::Client,
37 base_url: String,
38 timeouts: TimeoutsConfig,
39 ) -> Self {
40 let resolved_model = resolve_model(Some(model), models::minimax::DEFAULT_MODEL);
41 let resolved_base = resolve_minimax_base_url(Some(base_url));
42
43 Self {
44 inner: AnthropicProvider::new_with_client(
45 api_key,
46 resolved_model.clone(),
47 http_client,
48 resolved_base,
49 timeouts,
50 ),
51 model: resolved_model,
52 }
53 }
54
55 pub fn from_config(
56 api_key: Option<String>,
57 model: Option<String>,
58 base_url: Option<String>,
59 prompt_cache: Option<PromptCachingConfig>,
60 timeouts: Option<TimeoutsConfig>,
61 anthropic: Option<AnthropicConfig>,
62 model_behavior: Option<ModelConfig>,
63 ) -> Self {
64 let resolved_model = resolve_model(model, models::minimax::DEFAULT_MODEL);
65 let resolved_base = resolve_minimax_base_url(base_url);
66
67 Self {
68 inner: AnthropicProvider::from_config(
69 api_key,
70 Some(resolved_model.clone()),
71 Some(resolved_base),
72 prompt_cache,
73 timeouts,
74 anthropic,
75 model_behavior,
76 ),
77 model: resolved_model,
78 }
79 }
80}
81
82fn resolve_minimax_base_url(base_url: Option<String>) -> String {
83 fn sanitize(value: &str) -> Option<String> {
84 let trimmed = value.trim();
85 if trimmed.is_empty() {
86 None
87 } else {
88 Some(trimmed.trim_end_matches('/').to_string())
89 }
90 }
91
92 fn is_official_minimax_host(url: &str) -> bool {
93 let lower = url.to_ascii_lowercase();
94 [
95 "://api.minimax.io",
96 "://platform.minimax.io",
97 "api.minimax.io",
98 "platform.minimax.io",
99 ]
100 .iter()
101 .any(|marker| lower.contains(marker))
102 }
103
104 let resolved = base_url
105 .and_then(|value| sanitize(&value))
106 .or_else(|| {
107 env::var(env_vars::MINIMAX_BASE_URL)
108 .ok()
109 .and_then(|value| sanitize(&value))
110 })
111 .or_else(|| {
112 env::var(env_vars::ANTHROPIC_BASE_URL)
113 .ok()
114 .and_then(|value| sanitize(&value))
115 })
116 .or_else(|| sanitize(urls::MINIMAX_API_BASE))
117 .unwrap_or_else(|| urls::MINIMAX_API_BASE.trim_end_matches('/').to_string());
118
119 let mut normalized = resolved;
120
121 if normalized.ends_with("/messages") {
122 normalized = normalized
123 .trim_end_matches("/messages")
124 .trim_end_matches('/')
125 .to_string();
126 }
127
128 if let Some(pos) = normalized.find("/v1/") {
129 normalized = normalized[..pos + 3].to_string();
130 }
131
132 let mut without_v1 = normalized.trim_end_matches('/').to_string();
133 if without_v1.ends_with("/v1") {
134 without_v1 = without_v1
135 .trim_end_matches("/v1")
136 .trim_end_matches('/')
137 .to_string();
138 }
139
140 if is_official_minimax_host(&without_v1)
141 && !without_v1.to_ascii_lowercase().contains("/anthropic")
142 {
143 without_v1 = format!("{}/anthropic", without_v1.trim_end_matches('/'));
144 }
145
146 format!("{}/v1", without_v1.trim_end_matches('/'))
147}
148
149#[async_trait]
150impl LLMProvider for MinimaxProvider {
151 fn name(&self) -> &str {
152 "minimax"
153 }
154
155 fn supports_streaming(&self) -> bool {
156 self.inner.supports_streaming()
157 }
158
159 fn supports_reasoning(&self, model: &str) -> bool {
160 self.inner.supports_reasoning(model)
161 }
162
163 fn supports_reasoning_effort(&self, model: &str) -> bool {
164 self.inner.supports_reasoning_effort(model)
165 }
166
167 fn supports_tools(&self, model: &str) -> bool {
168 self.inner.supports_tools(model)
169 }
170
171 fn supports_parallel_tool_config(&self, model: &str) -> bool {
172 self.inner.supports_parallel_tool_config(model)
173 }
174
175 fn supports_structured_output(&self, model: &str) -> bool {
176 self.inner.supports_structured_output(model)
177 }
178
179 fn supports_context_caching(&self, model: &str) -> bool {
180 self.inner.supports_context_caching(model)
181 }
182
183 fn supports_vision(&self, model: &str) -> bool {
184 self.inner.supports_vision(model)
185 }
186
187 fn supports_responses_compaction(&self, model: &str) -> bool {
188 self.inner.supports_responses_compaction(model)
189 }
190
191 fn effective_context_size(&self, model: &str) -> usize {
192 self.inner.effective_context_size(model)
193 }
194
195 async fn compact_history(
196 &self,
197 model: &str,
198 history: &[Message],
199 ) -> Result<Vec<Message>, LLMError> {
200 self.inner.compact_history(model, history).await
201 }
202
203 async fn generate(&self, mut request: LLMRequest) -> Result<LLMResponse, LLMError> {
204 ensure_model(&mut request, &self.model);
205 self.inner.generate(request).await
206 }
207
208 async fn stream(&self, mut request: LLMRequest) -> Result<LLMStream, LLMError> {
209 ensure_model(&mut request, &self.model);
210 self.inner.stream(request).await
211 }
212
213 fn supported_models(&self) -> Vec<String> {
214 models::minimax::SUPPORTED_MODELS
215 .iter()
216 .map(|s| s.to_string())
217 .collect()
218 }
219
220 fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError> {
221 self.inner.validate_request(request)
222 }
223}
224
225impl_llm_client!(MinimaxProvider);
226
227#[cfg(test)]
228mod tests {
229 use super::{MinimaxProvider, resolve_minimax_base_url};
230 use crate::config::constants::models;
231 use crate::llm::client::LLMClient;
232 use crate::llm::provider::LLMProvider;
233
234 #[test]
235 fn resolve_minimax_base_url_defaults_to_anthropic_v1() {
236 assert_eq!(
237 resolve_minimax_base_url(None),
238 "https://api.minimax.io/anthropic/v1"
239 );
240 }
241
242 #[test]
243 fn resolve_minimax_base_url_normalizes_root_host_to_anthropic_v1() {
244 assert_eq!(
245 resolve_minimax_base_url(Some("https://api.minimax.io".to_string())),
246 "https://api.minimax.io/anthropic/v1"
247 );
248 assert_eq!(
249 resolve_minimax_base_url(Some("https://api.minimax.io/v1".to_string())),
250 "https://api.minimax.io/anthropic/v1"
251 );
252 }
253
254 #[test]
255 fn resolve_minimax_base_url_keeps_explicit_anthropic_path() {
256 assert_eq!(
257 resolve_minimax_base_url(Some("https://api.minimax.io/anthropic".to_string())),
258 "https://api.minimax.io/anthropic/v1"
259 );
260 assert_eq!(
261 resolve_minimax_base_url(Some(
262 "https://api.minimax.io/anthropic/v1/messages".to_string()
263 )),
264 "https://api.minimax.io/anthropic/v1"
265 );
266 }
267
268 #[test]
269 fn resolve_minimax_base_url_respects_custom_proxy_path() {
270 assert_eq!(
271 resolve_minimax_base_url(Some("https://proxy.example.com/minimax".to_string())),
272 "https://proxy.example.com/minimax/v1"
273 );
274 }
275
276 #[test]
277 fn minimax_provider_preserves_provider_name_and_default_model() {
278 let provider = MinimaxProvider::from_config(
279 Some("test-key".to_string()),
280 None,
281 None,
282 None,
283 None,
284 None,
285 None,
286 );
287
288 assert_eq!(provider.name(), "minimax");
289 assert_eq!(provider.model_id(), models::minimax::DEFAULT_MODEL);
290 assert_eq!(
291 provider.supported_models(),
292 vec![
293 models::minimax::MINIMAX_M3.to_string(),
294 models::minimax::MINIMAX_M2_7.to_string(),
295 models::minimax::MINIMAX_M2_5.to_string(),
296 ]
297 );
298 }
299
300 #[test]
301 fn minimax_provider_blank_model_falls_back_to_default() {
302 let provider = MinimaxProvider::from_config(
303 Some("test-key".to_string()),
304 Some(" ".to_string()),
305 None,
306 None,
307 None,
308 None,
309 None,
310 );
311
312 assert_eq!(provider.model_id(), models::minimax::DEFAULT_MODEL);
313 }
314
315 #[test]
316 fn minimax_provider_supports_streaming() {
317 let provider = MinimaxProvider::from_config(
318 Some("test-key".to_string()),
319 None,
320 None,
321 None,
322 None,
323 None,
324 None,
325 );
326
327 assert!(provider.supports_streaming());
328 }
329}