From d8586cdc0f5ae6fd3e4b2b552de148a76514c8e5 Mon Sep 17 00:00:00 2001 From: Ric Orna Date: Thu, 19 Feb 2026 17:48:24 -0600 Subject: [PATCH] feat: add MiniMax as native provider Add MiniMax as a new LLM provider with full ProviderConfig integration: legacy key auto-mapping, API routing (Anthropic-compatible via api.minimax.io), routing defaults (MiniMax-M1-80k), and onboarding flow. MiniMax exposes an Anthropic messages endpoint, so it dispatches through the Anthropic API path rather than the OpenAI-compatible path used by most other providers. Frontend gains a MiniMax entry in the provider list with brand icon from lobehub. --- interface/src/api/client.ts | 1 + interface/src/lib/providerIcons.tsx | 2 ++ interface/src/routes/Settings.tsx | 8 ++++++ src/api/models.rs | 10 +++++++ src/api/providers.rs | 12 +++++++-- src/config.rs | 42 +++++++++++++++++++++++++++-- src/llm/providers.rs | 6 ++++- src/llm/routing.rs | 2 ++ 8 files changed, 78 insertions(+), 5 deletions(-) diff --git a/interface/src/api/client.ts b/interface/src/api/client.ts index 379925559..66fceb487 100644 --- a/interface/src/api/client.ts +++ b/interface/src/api/client.ts @@ -656,6 +656,7 @@ export interface ProviderStatus { ollama: boolean; opencode_zen: boolean; nvidia: boolean; + minimax: boolean; } export interface ProvidersResponse { diff --git a/interface/src/lib/providerIcons.tsx b/interface/src/lib/providerIcons.tsx index 7f6b81544..2b253dde8 100644 --- a/interface/src/lib/providerIcons.tsx +++ b/interface/src/lib/providerIcons.tsx @@ -9,6 +9,7 @@ import Fireworks from "@lobehub/icons/es/Fireworks"; import Together from "@lobehub/icons/es/Together"; import XAI from "@lobehub/icons/es/XAI"; import ZAI from "@lobehub/icons/es/ZAI"; +import Minimax from "@lobehub/icons/es/Minimax"; interface IconProps { size?: number; @@ -107,6 +108,7 @@ export function ProviderIcon({ provider, className = "text-ink-faint", size = 24 ollama: OllamaIcon, "opencode-zen": OpenCodeZenIcon, nvidia: NvidiaIcon, + minimax: Minimax, }; const IconComponent = iconMap[provider.toLowerCase()]; diff --git a/interface/src/routes/Settings.tsx b/interface/src/routes/Settings.tsx index adeb6d532..c1d5b271d 100644 --- a/interface/src/routes/Settings.tsx +++ b/interface/src/routes/Settings.tsx @@ -159,6 +159,14 @@ const PROVIDERS = [ envVar: "NVIDIA_API_KEY", defaultModel: "nvidia/meta/llama-3.1-405b-instruct", }, + { + id: "minimax", + name: "MiniMax", + description: "MiniMax M1 (Anthropic message format)", + placeholder: "eyJ...", + envVar: "MINIMAX_API_KEY", + defaultModel: "MiniMax-M1-80k", + }, { id: "ollama", name: "Ollama", diff --git a/src/api/models.rs b/src/api/models.rs index f5a384940..2b66d052a 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -149,6 +149,15 @@ fn extra_models() -> Vec { tool_call: true, reasoning: false, }, + // MiniMax + ModelInfo { + id: "minimax/MiniMax-M1-80k".into(), + name: "MiniMax M1 80K".into(), + provider: "minimax".into(), + context_window: Some(80000), + tool_call: true, + reasoning: false, + }, ] } @@ -274,6 +283,7 @@ pub(super) async fn configured_providers(config_path: &std::path::Path) -> Vec<& if has_key("xai_key", "XAI_API_KEY") { providers.push("xai"); } if has_key("mistral_key", "MISTRAL_API_KEY") { providers.push("mistral"); } if has_key("opencode_zen_key", "OPENCODE_ZEN_API_KEY") { providers.push("opencode-zen"); } + if has_key("minimax_key", "MINIMAX_API_KEY") { providers.push("minimax"); } providers } diff --git a/src/api/providers.rs b/src/api/providers.rs index 76f602b84..5f6dbbe93 100644 --- a/src/api/providers.rs +++ b/src/api/providers.rs @@ -19,6 +19,7 @@ pub(super) struct ProviderStatus { xai: bool, mistral: bool, opencode_zen: bool, + minimax: bool, } #[derive(Serialize)] @@ -44,7 +45,7 @@ pub(super) async fn get_providers( ) -> Result, StatusCode> { let config_path = state.config_path.read().await.clone(); - let (anthropic, openai, openrouter, zhipu, groq, together, fireworks, deepseek, xai, mistral, opencode_zen) = if config_path.exists() { + let (anthropic, openai, openrouter, zhipu, groq, together, fireworks, deepseek, xai, mistral, opencode_zen, minimax) = if config_path.exists() { let content = tokio::fs::read_to_string(&config_path) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -78,6 +79,7 @@ pub(super) async fn get_providers( has_key("xai_key", "XAI_API_KEY"), has_key("mistral_key", "MISTRAL_API_KEY"), has_key("opencode_zen_key", "OPENCODE_ZEN_API_KEY"), + has_key("minimax_key", "MINIMAX_API_KEY"), ) } else { ( @@ -92,6 +94,7 @@ pub(super) async fn get_providers( std::env::var("XAI_API_KEY").is_ok(), std::env::var("MISTRAL_API_KEY").is_ok(), std::env::var("OPENCODE_ZEN_API_KEY").is_ok(), + std::env::var("MINIMAX_API_KEY").is_ok(), ) }; @@ -107,6 +110,7 @@ pub(super) async fn get_providers( xai, mistral, opencode_zen, + minimax, }; let has_any = providers.anthropic || providers.openai @@ -118,7 +122,8 @@ pub(super) async fn get_providers( || providers.deepseek || providers.xai || providers.mistral - || providers.opencode_zen; + || providers.opencode_zen + || providers.minimax; Ok(Json(ProvidersResponse { providers, has_any })) } @@ -139,6 +144,7 @@ pub(super) async fn update_provider( "xai" => "xai_key", "mistral" => "mistral_key", "opencode-zen" => "opencode_zen_key", + "minimax" => "minimax_key", _ => { return Ok(Json(ProviderUpdateResponse { success: false, @@ -208,6 +214,7 @@ pub(super) async fn update_provider( "xai" => has_provider_key("xai_key", "XAI_API_KEY"), "mistral" => has_provider_key("mistral_key", "MISTRAL_API_KEY"), "opencode-zen" => has_provider_key("opencode_zen_key", "OPENCODE_ZEN_API_KEY"), + "minimax" => has_provider_key("minimax_key", "MINIMAX_API_KEY"), _ => false, }; @@ -273,6 +280,7 @@ pub(super) async fn delete_provider( "xai" => "xai_key", "mistral" => "mistral_key", "opencode-zen" => "opencode_zen_key", + "minimax" => "minimax_key", _ => { return Ok(Json(ProviderUpdateResponse { success: false, diff --git a/src/config.rs b/src/config.rs index a96067e76..e489ff3ba 100644 --- a/src/config.rs +++ b/src/config.rs @@ -145,6 +145,7 @@ pub struct LlmConfig { pub ollama_base_url: Option, pub opencode_zen_key: Option, pub nvidia_key: Option, + pub minimax_key: Option, pub providers: HashMap, } @@ -165,6 +166,7 @@ impl LlmConfig { || self.ollama_base_url.is_some() || self.opencode_zen_key.is_some() || self.nvidia_key.is_some() + || self.minimax_key.is_some() || !self.providers.is_empty() } } @@ -172,6 +174,7 @@ impl LlmConfig { const ANTHROPIC_PROVIDER_BASE_URL: &str = "https://api.anthropic.com"; const OPENAI_PROVIDER_BASE_URL: &str = "https://api.openai.com"; const OPENROUTER_PROVIDER_BASE_URL: &str = "https://openrouter.ai/api"; +const MINIMAX_PROVIDER_BASE_URL: &str = "https://api.minimax.io/anthropic"; /// Defaults inherited by all agents. Individual agents can override any field. #[derive(Debug, Clone)] @@ -1100,6 +1103,7 @@ struct TomlLlmConfigFields { ollama_base_url: Option, opencode_zen_key: Option, nvidia_key: Option, + minimax_key: Option, #[serde(default)] providers: HashMap, #[serde(default)] @@ -1123,6 +1127,7 @@ struct TomlLlmConfig { ollama_base_url: Option, opencode_zen_key: Option, nvidia_key: Option, + minimax_key: Option, providers: HashMap, } @@ -1171,6 +1176,7 @@ impl<'de> Deserialize<'de> for TomlLlmConfig { ollama_base_url: fields.ollama_base_url, opencode_zen_key: fields.opencode_zen_key, nvidia_key: fields.nvidia_key, + minimax_key: fields.minimax_key, providers: fields.providers, }) } @@ -1509,8 +1515,9 @@ impl Config { || std::env::var("NVIDIA_API_KEY").is_ok() || std::env::var("OLLAMA_API_KEY").is_ok() || std::env::var("OLLAMA_BASE_URL").is_ok() - || std::env::var("OPENCODE_ZEN_API_KEY").is_ok(); - + || std::env::var("OPENCODE_ZEN_API_KEY").is_ok() + || std::env::var("MINIMAX_API_KEY").is_ok(); + // If we have any legacy keys, no onboarding needed if has_legacy_keys { return false; @@ -1571,6 +1578,7 @@ impl Config { ollama_base_url: std::env::var("OLLAMA_BASE_URL").ok(), opencode_zen_key: std::env::var("OPENCODE_ZEN_API_KEY").ok(), nvidia_key: std::env::var("NVIDIA_API_KEY").ok(), + minimax_key: std::env::var("MINIMAX_API_KEY").ok(), providers: HashMap::new(), }; @@ -1608,6 +1616,17 @@ impl Config { }); } + if let Some(minimax_key) = llm.minimax_key.clone() { + llm.providers + .entry("minimax".to_string()) + .or_insert_with(|| ProviderConfig { + api_type: ApiType::Anthropic, + base_url: MINIMAX_PROVIDER_BASE_URL.to_string(), + api_key: minimax_key, + name: None, + }); + } + // Note: We allow boot without provider keys now. System starts in setup mode. // Agents are initialized later when keys are added via API. @@ -1784,6 +1803,12 @@ impl Config { .as_deref() .and_then(resolve_env_value) .or_else(|| std::env::var("NVIDIA_API_KEY").ok()), + minimax_key: toml + .llm + .minimax_key + .as_deref() + .and_then(resolve_env_value) + .or_else(|| std::env::var("MINIMAX_API_KEY").ok()), providers: toml .llm .providers @@ -1836,6 +1861,17 @@ impl Config { }); } + if let Some(minimax_key) = llm.minimax_key.clone() { + llm.providers + .entry("minimax".to_string()) + .or_insert_with(|| ProviderConfig { + api_type: ApiType::Anthropic, + base_url: MINIMAX_PROVIDER_BASE_URL.to_string(), + api_key: minimax_key, + name: None, + }); + } + // Note: We allow boot without provider keys now. System starts in setup mode. // Agents are initialized later when keys are added via API. @@ -2882,6 +2918,7 @@ pub fn run_onboarding() -> anyhow::Result> { "Mistral AI", "Ollama", "OpenCode Zen", + "MiniMax", ]; let provider_idx = Select::new() .with_prompt("Which LLM provider do you want to use?") @@ -2902,6 +2939,7 @@ pub fn run_onboarding() -> anyhow::Result> { 9 => ("Mistral AI API key", "mistral_key", "mistral"), 10 => ("Ollama base URL", "ollama_base_url", "ollama"), 11 => ("OpenCode Zen API key", "opencode_zen_key", "opencode-zen"), + 12 => ("MiniMax API key", "minimax_key", "minimax"), _ => unreachable!(), }; let is_secret = provider_id != "ollama"; diff --git a/src/llm/providers.rs b/src/llm/providers.rs index d37a64796..bc23fa1d6 100644 --- a/src/llm/providers.rs +++ b/src/llm/providers.rs @@ -24,6 +24,10 @@ pub async fn init_providers(config: &LlmConfig) -> Result<()> { if config.opencode_zen_key.is_some() { tracing::info!("OpenCode Zen provider configured"); } - + + if config.minimax_key.is_some() { + tracing::info!("MiniMax provider configured"); + } + Ok(()) } diff --git a/src/llm/routing.rs b/src/llm/routing.rs index db12ff24c..47b3ad4c5 100644 --- a/src/llm/routing.rs +++ b/src/llm/routing.rs @@ -144,6 +144,7 @@ pub fn defaults_for_provider(provider: &str) -> RoutingConfig { "mistral" => RoutingConfig::for_model("mistral/mistral-large-latest".into()), "nvidia" => RoutingConfig::for_model("nvidia/meta/llama-3.1-405b-instruct".into()), "opencode-zen" => RoutingConfig::for_model("opencode-zen/kimi-k2.5".into()), + "minimax" => RoutingConfig::for_model("minimax/MiniMax-M1-80k".into()), _ => RoutingConfig::default(), } } @@ -163,6 +164,7 @@ pub fn provider_to_prefix(provider: &str) -> &str { "mistral" => "mistral/", "nvidia" => "nvidia/", "opencode-zen" => "opencode-zen/", + "minimax" => "minimax/", _ => "", } }