Skip to main content

rig/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::client::CompletionClient;
9//! use rig::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::client::CompletionClient;
18//! use rig::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 = reqwest::Client> =
75    client::ClientBuilder<MiniMaxBuilder, MiniMaxApiKey, H>;
76
77pub type AnthropicClient<H = reqwest::Client> = client::Client<MiniMaxAnthropicExt, H>;
78pub type AnthropicClientBuilder<H = reqwest::Client> =
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}
103
104impl<H> Capabilities<H> for MiniMaxAnthropicExt {
105    type Completion =
106        Capable<super::anthropic::completion::GenericCompletionModel<MiniMaxAnthropicExt, H>>;
107    type Embeddings = Nothing;
108    type Transcription = Nothing;
109    type ModelListing = Nothing;
110    #[cfg(feature = "image")]
111    type ImageGeneration = Nothing;
112    #[cfg(feature = "audio")]
113    type AudioGeneration = Nothing;
114}
115
116impl DebugExt for MiniMaxExt {}
117impl DebugExt for MiniMaxAnthropicExt {}
118
119impl ProviderBuilder for MiniMaxBuilder {
120    type Extension<H>
121        = MiniMaxExt
122    where
123        H: HttpClientExt;
124    type ApiKey = MiniMaxApiKey;
125
126    const BASE_URL: &'static str = GLOBAL_API_BASE_URL;
127
128    fn build<H>(
129        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
130    ) -> http_client::Result<Self::Extension<H>>
131    where
132        H: HttpClientExt,
133    {
134        Ok(MiniMaxExt)
135    }
136}
137
138impl ProviderBuilder for MiniMaxAnthropicBuilder {
139    type Extension<H>
140        = MiniMaxAnthropicExt
141    where
142        H: HttpClientExt;
143    type ApiKey = AnthropicKey;
144
145    const BASE_URL: &'static str = GLOBAL_ANTHROPIC_API_BASE_URL;
146
147    fn build<H>(
148        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
149    ) -> http_client::Result<Self::Extension<H>>
150    where
151        H: HttpClientExt,
152    {
153        Ok(MiniMaxAnthropicExt)
154    }
155
156    fn finish<H>(
157        &self,
158        builder: client::ClientBuilder<Self, AnthropicKey, H>,
159    ) -> http_client::Result<client::ClientBuilder<Self, AnthropicKey, H>> {
160        finish_anthropic_builder(&self.anthropic, builder)
161    }
162}
163
164impl super::anthropic::completion::AnthropicCompatibleProvider for MiniMaxAnthropicExt {
165    const PROVIDER_NAME: &'static str = "minimax";
166
167    fn default_max_tokens(_model: &str) -> Option<u64> {
168        Some(4096)
169    }
170}
171
172impl ProviderClient for Client {
173    type Input = MiniMaxApiKey;
174    type Error = crate::client::ProviderClientError;
175
176    fn from_env() -> Result<Self, Self::Error> {
177        let api_key = crate::client::required_env_var("MINIMAX_API_KEY")?;
178        let mut builder = Self::builder().api_key(api_key);
179
180        if let Some(base_url) = crate::client::optional_env_var("MINIMAX_API_BASE")? {
181            builder = builder.base_url(base_url);
182        }
183
184        builder.build().map_err(Into::into)
185    }
186
187    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
188        Self::new(input).map_err(Into::into)
189    }
190}
191
192impl ProviderClient for AnthropicClient {
193    type Input = String;
194    type Error = crate::client::ProviderClientError;
195
196    fn from_env() -> Result<Self, Self::Error> {
197        let api_key = crate::client::required_env_var("MINIMAX_API_KEY")?;
198        let mut builder = Self::builder().api_key(api_key);
199
200        if let Some(base_url) =
201            anthropic_base_override("MINIMAX_ANTHROPIC_API_BASE", "MINIMAX_API_BASE")?
202        {
203            builder = builder.base_url(base_url);
204        }
205
206        builder.build().map_err(Into::into)
207    }
208
209    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
210        Self::builder().api_key(input).build().map_err(Into::into)
211    }
212}
213
214fn anthropic_base_override(
215    primary_env: &'static str,
216    fallback_env: &'static str,
217) -> crate::client::ProviderClientResult<Option<String>> {
218    let primary = crate::client::optional_env_var(primary_env)?;
219    let fallback = crate::client::optional_env_var(fallback_env)?;
220
221    Ok(resolve_anthropic_base_override(
222        primary.as_deref(),
223        fallback.as_deref(),
224    ))
225}
226
227fn resolve_anthropic_base_override(
228    primary: Option<&str>,
229    fallback: Option<&str>,
230) -> Option<String> {
231    primary
232        .map(str::to_owned)
233        .or_else(|| fallback.and_then(normalize_anthropic_base_url))
234}
235
236fn normalize_anthropic_base_url(base_url: &str) -> Option<String> {
237    if base_url.contains("/anthropic") {
238        return Some(base_url.to_owned());
239    }
240
241    match base_url.trim_end_matches('/') {
242        GLOBAL_API_BASE_URL => Some(GLOBAL_ANTHROPIC_API_BASE_URL.to_owned()),
243        CHINA_API_BASE_URL => Some(CHINA_ANTHROPIC_API_BASE_URL.to_owned()),
244        _ => {
245            let mut url = url::Url::parse(base_url).ok()?;
246            if !matches!(url.path(), "/v1" | "/v1/") {
247                return None;
248            }
249            url.set_path("/anthropic");
250            Some(url.to_string())
251        }
252    }
253}
254
255impl<H> ClientBuilder<H> {
256    pub fn global(self) -> Self {
257        self.base_url(GLOBAL_API_BASE_URL)
258    }
259
260    pub fn china(self) -> Self {
261        self.base_url(CHINA_API_BASE_URL)
262    }
263}
264
265impl<H> AnthropicClientBuilder<H> {
266    pub fn global(self) -> Self {
267        self.base_url(GLOBAL_ANTHROPIC_API_BASE_URL)
268    }
269
270    pub fn china(self) -> Self {
271        self.base_url(CHINA_ANTHROPIC_API_BASE_URL)
272    }
273
274    pub fn anthropic_version(self, anthropic_version: &str) -> Self {
275        self.over_ext(|mut ext| {
276            ext.anthropic.anthropic_version = anthropic_version.into();
277            ext
278        })
279    }
280
281    pub fn anthropic_betas(self, anthropic_betas: &[&str]) -> Self {
282        self.over_ext(|mut ext| {
283            ext.anthropic
284                .anthropic_betas
285                .extend(anthropic_betas.iter().copied().map(String::from));
286            ext
287        })
288    }
289
290    pub fn anthropic_beta(self, anthropic_beta: &str) -> Self {
291        self.over_ext(|mut ext| {
292            ext.anthropic.anthropic_betas.push(anthropic_beta.into());
293            ext
294        })
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::{
301        CHINA_ANTHROPIC_API_BASE_URL, CHINA_API_BASE_URL, GLOBAL_ANTHROPIC_API_BASE_URL,
302        GLOBAL_API_BASE_URL, normalize_anthropic_base_url, resolve_anthropic_base_override,
303    };
304
305    #[test]
306    fn test_client_initialization() {
307        let _client = crate::providers::minimax::Client::new("dummy-key").expect("Client::new()");
308        let _client_from_builder = crate::providers::minimax::Client::builder()
309            .api_key("dummy-key")
310            .build()
311            .expect("Client::builder()");
312        let _anthropic_client = crate::providers::minimax::AnthropicClient::new("dummy-key")
313            .expect("AnthropicClient::new()");
314        let _anthropic_client_from_builder = crate::providers::minimax::AnthropicClient::builder()
315            .api_key("dummy-key")
316            .build()
317            .expect("AnthropicClient::builder()");
318    }
319
320    #[test]
321    fn normalize_openai_bases_to_anthropic_bases() {
322        assert_eq!(
323            normalize_anthropic_base_url(GLOBAL_API_BASE_URL).as_deref(),
324            Some(GLOBAL_ANTHROPIC_API_BASE_URL)
325        );
326        assert_eq!(
327            normalize_anthropic_base_url(CHINA_API_BASE_URL).as_deref(),
328            Some(CHINA_ANTHROPIC_API_BASE_URL)
329        );
330        assert_eq!(
331            normalize_anthropic_base_url("https://proxy.example.com/v1").as_deref(),
332            Some("https://proxy.example.com/anthropic")
333        );
334    }
335
336    #[test]
337    fn normalize_preserves_existing_anthropic_base() {
338        assert_eq!(
339            normalize_anthropic_base_url(CHINA_ANTHROPIC_API_BASE_URL).as_deref(),
340            Some(CHINA_ANTHROPIC_API_BASE_URL)
341        );
342    }
343
344    #[test]
345    fn anthropic_primary_override_wins() {
346        let override_url = resolve_anthropic_base_override(
347            Some("https://primary.example.com/anthropic"),
348            Some(CHINA_API_BASE_URL),
349        );
350
351        assert_eq!(
352            override_url.as_deref(),
353            Some("https://primary.example.com/anthropic")
354        );
355    }
356}