diff --git a/rust/crates/api/src/client.rs b/rust/crates/api/src/client.rs index b596777..893d6ce 100644 --- a/rust/crates/api/src/client.rs +++ b/rust/crates/api/src/client.rs @@ -23,6 +23,7 @@ pub enum ProviderClient { ClawApi(ClawApiClient), Xai(OpenAiCompatClient), OpenAi(OpenAiCompatClient), + MiniMax(OpenAiCompatClient), } impl ProviderClient { @@ -46,6 +47,9 @@ impl ProviderClient { ProviderKind::OpenAi => Ok(Self::OpenAi(OpenAiCompatClient::from_env( OpenAiCompatConfig::openai(), )?)), + ProviderKind::MiniMax => Ok(Self::MiniMax(OpenAiCompatClient::from_env( + OpenAiCompatConfig::minimax(), + )?)), } } @@ -55,6 +59,7 @@ impl ProviderClient { Self::ClawApi(_) => ProviderKind::ClawApi, Self::Xai(_) => ProviderKind::Xai, Self::OpenAi(_) => ProviderKind::OpenAi, + Self::MiniMax(_) => ProviderKind::MiniMax, } } @@ -64,7 +69,9 @@ impl ProviderClient { ) -> Result { match self { Self::ClawApi(client) => send_via_provider(client, request).await, - Self::Xai(client) | Self::OpenAi(client) => send_via_provider(client, request).await, + Self::Xai(client) | Self::OpenAi(client) | Self::MiniMax(client) => { + send_via_provider(client, request).await + } } } @@ -76,9 +83,11 @@ impl ProviderClient { Self::ClawApi(client) => stream_via_provider(client, request) .await .map(MessageStream::ClawApi), - Self::Xai(client) | Self::OpenAi(client) => stream_via_provider(client, request) - .await - .map(MessageStream::OpenAiCompat), + Self::Xai(client) | Self::OpenAi(client) | Self::MiniMax(client) => { + stream_via_provider(client, request) + .await + .map(MessageStream::OpenAiCompat) + } } } } @@ -119,6 +128,11 @@ pub fn read_xai_base_url() -> String { openai_compat::read_base_url(OpenAiCompatConfig::xai()) } +#[must_use] +pub fn read_minimax_base_url() -> String { + openai_compat::read_base_url(OpenAiCompatConfig::minimax()) +} + #[cfg(test)] mod tests { use crate::providers::{detect_provider_kind, resolve_model_alias, ProviderKind}; @@ -137,5 +151,6 @@ mod tests { detect_provider_kind("claude-sonnet-4-6"), ProviderKind::ClawApi ); + assert_eq!(detect_provider_kind("MiniMax-M2.7"), ProviderKind::MiniMax); } } diff --git a/rust/crates/api/src/lib.rs b/rust/crates/api/src/lib.rs index 3306f53..9498934 100644 --- a/rust/crates/api/src/lib.rs +++ b/rust/crates/api/src/lib.rs @@ -5,8 +5,9 @@ mod sse; mod types; pub use client::{ - oauth_token_is_expired, read_base_url, read_xai_base_url, resolve_saved_oauth_token, - resolve_startup_auth_source, MessageStream, OAuthTokenSet, ProviderClient, + oauth_token_is_expired, read_base_url, read_minimax_base_url, read_xai_base_url, + resolve_saved_oauth_token, resolve_startup_auth_source, MessageStream, OAuthTokenSet, + ProviderClient, }; pub use error::ApiError; pub use providers::claw_provider::{AuthSource, ClawApiClient, ClawApiClient as ApiClient}; diff --git a/rust/crates/api/src/providers/mod.rs b/rust/crates/api/src/providers/mod.rs index 192afd6..38e6dac 100644 --- a/rust/crates/api/src/providers/mod.rs +++ b/rust/crates/api/src/providers/mod.rs @@ -28,6 +28,7 @@ pub enum ProviderKind { ClawApi, Xai, OpenAi, + MiniMax, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -138,6 +139,24 @@ const MODEL_REGISTRY: &[(&str, ProviderMetadata)] = &[ default_base_url: openai_compat::DEFAULT_XAI_BASE_URL, }, ), + ( + "minimax-m2.7", + ProviderMetadata { + provider: ProviderKind::MiniMax, + auth_env: "MINIMAX_API_KEY", + base_url_env: "MINIMAX_BASE_URL", + default_base_url: openai_compat::DEFAULT_MINIMAX_BASE_URL, + }, + ), + ( + "minimax-m2.7-highspeed", + ProviderMetadata { + provider: ProviderKind::MiniMax, + auth_env: "MINIMAX_API_KEY", + base_url_env: "MINIMAX_BASE_URL", + default_base_url: openai_compat::DEFAULT_MINIMAX_BASE_URL, + }, + ), ]; #[must_use] @@ -161,6 +180,7 @@ pub fn resolve_model_alias(model: &str) -> String { _ => trimmed, }, ProviderKind::OpenAi => trimmed, + ProviderKind::MiniMax => trimmed, }) }) .map_or_else(|| trimmed.to_string(), ToOwned::to_owned) @@ -181,6 +201,14 @@ pub fn metadata_for_model(model: &str) -> Option { default_base_url: openai_compat::DEFAULT_XAI_BASE_URL, }); } + if lower.starts_with("minimax") { + return Some(ProviderMetadata { + provider: ProviderKind::MiniMax, + auth_env: "MINIMAX_API_KEY", + base_url_env: "MINIMAX_BASE_URL", + default_base_url: openai_compat::DEFAULT_MINIMAX_BASE_URL, + }); + } None } @@ -198,6 +226,9 @@ pub fn detect_provider_kind(model: &str) -> ProviderKind { if openai_compat::has_api_key("XAI_API_KEY") { return ProviderKind::Xai; } + if openai_compat::has_api_key("MINIMAX_API_KEY") { + return ProviderKind::MiniMax; + } ProviderKind::ClawApi } @@ -236,4 +267,22 @@ mod tests { assert_eq!(max_tokens_for_model("opus"), 32_000); assert_eq!(max_tokens_for_model("grok-3"), 64_000); } + + #[test] + fn resolves_minimax_models() { + assert_eq!(resolve_model_alias("MiniMax-M2.7"), "MiniMax-M2.7"); + assert_eq!( + resolve_model_alias("MiniMax-M2.7-highspeed"), + "MiniMax-M2.7-highspeed" + ); + } + + #[test] + fn detects_minimax_provider_from_model_name() { + assert_eq!(detect_provider_kind("MiniMax-M2.7"), ProviderKind::MiniMax); + assert_eq!( + detect_provider_kind("MiniMax-M2.7-highspeed"), + ProviderKind::MiniMax + ); + } } diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index e8210ae..a810600 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -16,6 +16,7 @@ use super::{Provider, ProviderFuture}; pub const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1"; pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1"; +pub const DEFAULT_MINIMAX_BASE_URL: &str = "https://api.minimax.io/v1"; const REQUEST_ID_HEADER: &str = "request-id"; const ALT_REQUEST_ID_HEADER: &str = "x-request-id"; const DEFAULT_INITIAL_BACKOFF: Duration = Duration::from_millis(200); @@ -32,6 +33,7 @@ pub struct OpenAiCompatConfig { const XAI_ENV_VARS: &[&str] = &["XAI_API_KEY"]; const OPENAI_ENV_VARS: &[&str] = &["OPENAI_API_KEY"]; +const MINIMAX_ENV_VARS: &[&str] = &["MINIMAX_API_KEY"]; impl OpenAiCompatConfig { #[must_use] @@ -53,11 +55,23 @@ impl OpenAiCompatConfig { default_base_url: DEFAULT_OPENAI_BASE_URL, } } + + #[must_use] + pub const fn minimax() -> Self { + Self { + provider_name: "MiniMax", + api_key_env: "MINIMAX_API_KEY", + base_url_env: "MINIMAX_BASE_URL", + default_base_url: DEFAULT_MINIMAX_BASE_URL, + } + } + #[must_use] pub fn credential_env_vars(self) -> &'static [&'static str] { match self.provider_name { "xAI" => XAI_ENV_VARS, "OpenAI" => OPENAI_ENV_VARS, + "MiniMax" => MINIMAX_ENV_VARS, _ => &[], } } diff --git a/rust/crates/api/tests/provider_client_integration.rs b/rust/crates/api/tests/provider_client_integration.rs index abeebdd..9065334 100644 --- a/rust/crates/api/tests/provider_client_integration.rs +++ b/rust/crates/api/tests/provider_client_integration.rs @@ -1,7 +1,9 @@ use std::ffi::OsString; use std::sync::{Mutex, OnceLock}; -use api::{read_xai_base_url, ApiError, AuthSource, ProviderClient, ProviderKind}; +use api::{ + read_minimax_base_url, read_xai_base_url, ApiError, AuthSource, ProviderClient, ProviderKind, +}; #[test] fn provider_client_routes_grok_aliases_through_xai() { @@ -53,6 +55,45 @@ fn read_xai_base_url_prefers_env_override() { assert_eq!(read_xai_base_url(), "https://example.xai.test/v1"); } +#[test] +fn provider_client_routes_minimax_models() { + let _lock = env_lock(); + let _minimax_api_key = EnvVarGuard::set("MINIMAX_API_KEY", Some("minimax-test-key")); + + let client = ProviderClient::from_model("MiniMax-M2.7").expect("MiniMax model should resolve"); + assert_eq!(client.provider_kind(), ProviderKind::MiniMax); + + let client = ProviderClient::from_model("MiniMax-M2.7-highspeed") + .expect("MiniMax highspeed model should resolve"); + assert_eq!(client.provider_kind(), ProviderKind::MiniMax); +} + +#[test] +fn provider_client_reports_missing_minimax_credentials() { + let _lock = env_lock(); + let _minimax_api_key = EnvVarGuard::set("MINIMAX_API_KEY", None); + + let error = ProviderClient::from_model("MiniMax-M2.7") + .expect_err("MiniMax requests without MINIMAX_API_KEY should fail fast"); + + match error { + ApiError::MissingCredentials { provider, env_vars } => { + assert_eq!(provider, "MiniMax"); + assert_eq!(env_vars, &["MINIMAX_API_KEY"]); + } + other => panic!("expected missing MiniMax credentials, got {other:?}"), + } +} + +#[test] +fn read_minimax_base_url_prefers_env_override() { + let _lock = env_lock(); + let _minimax_base_url = + EnvVarGuard::set("MINIMAX_BASE_URL", Some("https://example.minimax.test/v1")); + + assert_eq!(read_minimax_base_url(), "https://example.minimax.test/v1"); +} + fn env_lock() -> std::sync::MutexGuard<'static, ()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(()))