Skip to main content

rig_core/providers/
minimax.rs

1//! MiniMax API clients and Rig integrations.
2//!
3//! MiniMax exposes both OpenAI-compatible and Anthropic-compatible chat APIs,
4//! with distinct global and China entrypoints.
5//!
6//! # OpenAI-compatible example
7//! ```no_run
8//! use rig_core::client::CompletionClient;
9//! use rig_core::providers::minimax;
10//!
11//! let client = minimax::Client::new("YOUR_API_KEY").expect("Failed to build client");
12//! let model = client.completion_model(minimax::MINIMAX_M2_7);
13//! ```
14//!
15//! # Anthropic-compatible example
16//! ```no_run
17//! use rig_core::client::CompletionClient;
18//! use rig_core::providers::minimax;
19//!
20//! let client = minimax::AnthropicClient::new("YOUR_API_KEY").expect("Failed to build client");
21//! let model = client.completion_model(minimax::MINIMAX_M2);
22//! ```
23
24use crate::client::{
25    self, BearerAuth, Capabilities, Capable, DebugExt, Nothing, Provider, ProviderBuilder,
26    ProviderClient,
27};
28use crate::http_client::{self, HttpClientExt};
29use crate::providers::anthropic::client::{
30    AnthropicBuilder as AnthropicCompatBuilder, AnthropicKey, finish_anthropic_builder,
31};
32
33/// Global OpenAI-compatible base URL.
34pub const GLOBAL_API_BASE_URL: &str = "https://api.minimax.io/v1";
35/// China OpenAI-compatible base URL.
36pub const CHINA_API_BASE_URL: &str = "https://api.minimaxi.com/v1";
37/// Global Anthropic-compatible base URL.
38pub const GLOBAL_ANTHROPIC_API_BASE_URL: &str = "https://api.minimax.io/anthropic";
39/// China Anthropic-compatible base URL.
40pub const CHINA_ANTHROPIC_API_BASE_URL: &str = "https://api.minimaxi.com/anthropic";
41
42/// `MiniMax-M2.7`
43pub const MINIMAX_M2_7: &str = "MiniMax-M2.7";
44/// `MiniMax-M2.7-highspeed`
45pub const MINIMAX_M2_7_HIGHSPEED: &str = "MiniMax-M2.7-highspeed";
46/// `MiniMax-M2.5`
47pub const MINIMAX_M2_5: &str = "MiniMax-M2.5";
48/// `MiniMax-M2.5-highspeed`
49pub const MINIMAX_M2_5_HIGHSPEED: &str = "MiniMax-M2.5-highspeed";
50/// `MiniMax-M2.1`
51pub const MINIMAX_M2_1: &str = "MiniMax-M2.1";
52/// `MiniMax-M2.1-highspeed`
53pub const MINIMAX_M2_1_HIGHSPEED: &str = "MiniMax-M2.1-highspeed";
54/// `MiniMax-M2`
55pub const MINIMAX_M2: &str = "MiniMax-M2";
56
57#[derive(Debug, Default, Clone, Copy)]
58pub struct MiniMaxExt;
59
60#[derive(Debug, Default, Clone, Copy)]
61pub struct MiniMaxBuilder;
62
63#[derive(Debug, Default, Clone)]
64pub struct MiniMaxAnthropicBuilder {
65    anthropic: AnthropicCompatBuilder,
66}
67
68#[derive(Debug, Default, Clone, Copy)]
69pub struct MiniMaxAnthropicExt;
70
71type MiniMaxApiKey = BearerAuth;
72
73pub type Client<H = reqwest::Client> = client::Client<MiniMaxExt, H>;
74pub type ClientBuilder<H = crate::markers::Missing> =
75    client::ClientBuilder<MiniMaxBuilder, MiniMaxApiKey, H>;
76
77pub type AnthropicClient<H = reqwest::Client> = client::Client<MiniMaxAnthropicExt, H>;
78pub type AnthropicClientBuilder<H = crate::markers::Missing> =
79    client::ClientBuilder<MiniMaxAnthropicBuilder, AnthropicKey, H>;
80
81impl Provider for MiniMaxExt {
82    type Builder = MiniMaxBuilder;
83
84    const VERIFY_PATH: &'static str = "/models";
85}
86
87impl Provider for MiniMaxAnthropicExt {
88    type Builder = MiniMaxAnthropicBuilder;
89
90    const VERIFY_PATH: &'static str = "/v1/models";
91}
92
93impl<H> Capabilities<H> for MiniMaxExt {
94    type Completion = Capable<super::openai::completion::GenericCompletionModel<MiniMaxExt, H>>;
95    type Embeddings = Nothing;
96    type Transcription = Nothing;
97    type ModelListing = Nothing;
98    #[cfg(feature = "image")]
99    type ImageGeneration = Nothing;
100    #[cfg(feature = "audio")]
101    type AudioGeneration = Nothing;
102    type Rerank = Nothing;
103}
104
105impl<H> Capabilities<H> for MiniMaxAnthropicExt {
106    type Completion =
107        Capable<super::anthropic::completion::GenericCompletionModel<MiniMaxAnthropicExt, H>>;
108    type Embeddings = Nothing;
109    type Transcription = Nothing;
110    type ModelListing = Nothing;
111    #[cfg(feature = "image")]
112    type ImageGeneration = Nothing;
113    #[cfg(feature = "audio")]
114    type AudioGeneration = Nothing;
115    type Rerank = Nothing;
116}
117
118impl DebugExt for MiniMaxExt {}
119impl DebugExt for MiniMaxAnthropicExt {}
120
121impl super::openai::completion::OpenAICompatibleProvider for MiniMaxExt {
122    const PROVIDER_NAME: &'static str = "minimax";
123
124    type StreamingUsage = super::openai::Usage;
125
126    type Response = super::openai::CompletionResponse;
127}
128
129impl ProviderBuilder for MiniMaxBuilder {
130    type Extension<H>
131        = MiniMaxExt
132    where
133        H: HttpClientExt;
134    type ApiKey = MiniMaxApiKey;
135
136    const BASE_URL: &'static str = GLOBAL_API_BASE_URL;
137
138    fn build<H>(
139        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
140    ) -> http_client::Result<Self::Extension<H>>
141    where
142        H: HttpClientExt,
143    {
144        Ok(MiniMaxExt)
145    }
146}
147
148impl ProviderBuilder for MiniMaxAnthropicBuilder {
149    type Extension<H>
150        = MiniMaxAnthropicExt
151    where
152        H: HttpClientExt;
153    type ApiKey = AnthropicKey;
154
155    const BASE_URL: &'static str = GLOBAL_ANTHROPIC_API_BASE_URL;
156
157    fn build<H>(
158        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
159    ) -> http_client::Result<Self::Extension<H>>
160    where
161        H: HttpClientExt,
162    {
163        Ok(MiniMaxAnthropicExt)
164    }
165
166    fn finish<H>(
167        &self,
168        builder: client::ClientBuilder<Self, AnthropicKey, H>,
169    ) -> http_client::Result<client::ClientBuilder<Self, AnthropicKey, H>> {
170        finish_anthropic_builder(&self.anthropic, builder)
171    }
172}
173
174impl super::anthropic::completion::AnthropicCompatibleProvider for MiniMaxAnthropicExt {
175    const PROVIDER_NAME: &'static str = "minimax";
176
177    fn default_max_tokens(_model: &str) -> Option<u64> {
178        Some(4096)
179    }
180}
181
182impl ProviderClient for Client {
183    type Input = MiniMaxApiKey;
184    type Error = crate::client::ProviderClientError;
185
186    fn from_env() -> Result<Self, Self::Error> {
187        let api_key = crate::client::required_env_var("MINIMAX_API_KEY")?;
188        let mut builder = Self::builder().api_key(api_key);
189
190        if let Some(base_url) = crate::client::optional_env_var("MINIMAX_API_BASE")? {
191            builder = builder.base_url(base_url);
192        }
193
194        builder.build().map_err(Into::into)
195    }
196
197    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
198        Self::new(input).map_err(Into::into)
199    }
200}
201
202impl ProviderClient for AnthropicClient {
203    type Input = String;
204    type Error = crate::client::ProviderClientError;
205
206    fn from_env() -> Result<Self, Self::Error> {
207        let api_key = crate::client::required_env_var("MINIMAX_API_KEY")?;
208        let mut builder = Self::builder().api_key(api_key);
209
210        if let Some(base_url) =
211            anthropic_base_override("MINIMAX_ANTHROPIC_API_BASE", "MINIMAX_API_BASE")?
212        {
213            builder = builder.base_url(base_url);
214        }
215
216        builder.build().map_err(Into::into)
217    }
218
219    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
220        Self::builder().api_key(input).build().map_err(Into::into)
221    }
222}
223
224fn anthropic_base_override(
225    primary_env: &'static str,
226    fallback_env: &'static str,
227) -> crate::client::ProviderClientResult<Option<String>> {
228    let primary = crate::client::optional_env_var(primary_env)?;
229    let fallback = crate::client::optional_env_var(fallback_env)?;
230
231    Ok(resolve_anthropic_base_override(
232        primary.as_deref(),
233        fallback.as_deref(),
234    ))
235}
236
237fn resolve_anthropic_base_override(
238    primary: Option<&str>,
239    fallback: Option<&str>,
240) -> Option<String> {
241    primary
242        .map(str::to_owned)
243        .or_else(|| fallback.and_then(normalize_anthropic_base_url))
244}
245
246fn normalize_anthropic_base_url(base_url: &str) -> Option<String> {
247    if base_url.contains("/anthropic") {
248        return Some(base_url.to_owned());
249    }
250
251    match base_url.trim_end_matches('/') {
252        GLOBAL_API_BASE_URL => Some(GLOBAL_ANTHROPIC_API_BASE_URL.to_owned()),
253        CHINA_API_BASE_URL => Some(CHINA_ANTHROPIC_API_BASE_URL.to_owned()),
254        _ => {
255            let mut url = url::Url::parse(base_url).ok()?;
256            if !matches!(url.path(), "/v1" | "/v1/") {
257                return None;
258            }
259            url.set_path("/anthropic");
260            Some(url.to_string())
261        }
262    }
263}
264
265impl<H> ClientBuilder<H> {
266    pub fn global(self) -> Self {
267        self.base_url(GLOBAL_API_BASE_URL)
268    }
269
270    pub fn china(self) -> Self {
271        self.base_url(CHINA_API_BASE_URL)
272    }
273}
274
275impl<H> AnthropicClientBuilder<H> {
276    pub fn global(self) -> Self {
277        self.base_url(GLOBAL_ANTHROPIC_API_BASE_URL)
278    }
279
280    pub fn china(self) -> Self {
281        self.base_url(CHINA_ANTHROPIC_API_BASE_URL)
282    }
283
284    pub fn anthropic_version(self, anthropic_version: &str) -> Self {
285        self.over_ext(|mut ext| {
286            ext.anthropic.anthropic_version = anthropic_version.into();
287            ext
288        })
289    }
290
291    pub fn anthropic_betas(self, anthropic_betas: &[&str]) -> Self {
292        self.over_ext(|mut ext| {
293            ext.anthropic
294                .anthropic_betas
295                .extend(anthropic_betas.iter().copied().map(String::from));
296            ext
297        })
298    }
299
300    pub fn anthropic_beta(self, anthropic_beta: &str) -> Self {
301        self.over_ext(|mut ext| {
302            ext.anthropic.anthropic_betas.push(anthropic_beta.into());
303            ext
304        })
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::{
311        CHINA_ANTHROPIC_API_BASE_URL, CHINA_API_BASE_URL, GLOBAL_ANTHROPIC_API_BASE_URL,
312        GLOBAL_API_BASE_URL, normalize_anthropic_base_url, resolve_anthropic_base_override,
313    };
314
315    #[test]
316    fn test_client_initialization() {
317        let _client = crate::providers::minimax::Client::new("dummy-key").expect("Client::new()");
318        let _client_from_builder = crate::providers::minimax::Client::builder()
319            .api_key("dummy-key")
320            .build()
321            .expect("Client::builder()");
322        let _anthropic_client = crate::providers::minimax::AnthropicClient::new("dummy-key")
323            .expect("AnthropicClient::new()");
324        let _anthropic_client_from_builder = crate::providers::minimax::AnthropicClient::builder()
325            .api_key("dummy-key")
326            .build()
327            .expect("AnthropicClient::builder()");
328    }
329
330    #[test]
331    fn normalize_openai_bases_to_anthropic_bases() {
332        assert_eq!(
333            normalize_anthropic_base_url(GLOBAL_API_BASE_URL).as_deref(),
334            Some(GLOBAL_ANTHROPIC_API_BASE_URL)
335        );
336        assert_eq!(
337            normalize_anthropic_base_url(CHINA_API_BASE_URL).as_deref(),
338            Some(CHINA_ANTHROPIC_API_BASE_URL)
339        );
340        assert_eq!(
341            normalize_anthropic_base_url("https://proxy.example.com/v1").as_deref(),
342            Some("https://proxy.example.com/anthropic")
343        );
344    }
345
346    #[test]
347    fn normalize_preserves_existing_anthropic_base() {
348        assert_eq!(
349            normalize_anthropic_base_url(CHINA_ANTHROPIC_API_BASE_URL).as_deref(),
350            Some(CHINA_ANTHROPIC_API_BASE_URL)
351        );
352    }
353
354    #[test]
355    fn anthropic_primary_override_wins() {
356        let override_url = resolve_anthropic_base_override(
357            Some("https://primary.example.com/anthropic"),
358            Some(CHINA_API_BASE_URL),
359        );
360
361        assert_eq!(
362            override_url.as_deref(),
363            Some("https://primary.example.com/anthropic")
364        );
365    }
366}