Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions rust/crates/api/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum ProviderClient {
ClawApi(ClawApiClient),
Xai(OpenAiCompatClient),
OpenAi(OpenAiCompatClient),
MiniMax(OpenAiCompatClient),
}

impl ProviderClient {
Expand All @@ -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(),
)?)),
}
}

Expand All @@ -55,6 +59,7 @@ impl ProviderClient {
Self::ClawApi(_) => ProviderKind::ClawApi,
Self::Xai(_) => ProviderKind::Xai,
Self::OpenAi(_) => ProviderKind::OpenAi,
Self::MiniMax(_) => ProviderKind::MiniMax,
}
}

Expand All @@ -64,7 +69,9 @@ impl ProviderClient {
) -> Result<MessageResponse, ApiError> {
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
}
}
}

Expand All @@ -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)
}
}
}
}
Expand Down Expand Up @@ -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};
Expand All @@ -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);
}
}
5 changes: 3 additions & 2 deletions rust/crates/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
49 changes: 49 additions & 0 deletions rust/crates/api/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub enum ProviderKind {
ClawApi,
Xai,
OpenAi,
MiniMax,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -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]
Expand All @@ -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)
Expand All @@ -181,6 +201,14 @@ pub fn metadata_for_model(model: &str) -> Option<ProviderMetadata> {
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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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
);
}
}
14 changes: 14 additions & 0 deletions rust/crates/api/src/providers/openai_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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]
Expand All @@ -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,
_ => &[],
}
}
Expand Down
43 changes: 42 additions & 1 deletion rust/crates/api/tests/provider_client_integration.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
Expand Down