1use 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
33pub const GLOBAL_API_BASE_URL: &str = "https://api.minimax.io/v1";
35pub const CHINA_API_BASE_URL: &str = "https://api.minimaxi.com/v1";
37pub const GLOBAL_ANTHROPIC_API_BASE_URL: &str = "https://api.minimax.io/anthropic";
39pub const CHINA_ANTHROPIC_API_BASE_URL: &str = "https://api.minimaxi.com/anthropic";
41
42pub const MINIMAX_M2_7: &str = "MiniMax-M2.7";
44pub const MINIMAX_M2_7_HIGHSPEED: &str = "MiniMax-M2.7-highspeed";
46pub const MINIMAX_M2_5: &str = "MiniMax-M2.5";
48pub const MINIMAX_M2_5_HIGHSPEED: &str = "MiniMax-M2.5-highspeed";
50pub const MINIMAX_M2_1: &str = "MiniMax-M2.1";
52pub const MINIMAX_M2_1_HIGHSPEED: &str = "MiniMax-M2.1-highspeed";
54pub 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}