diff --git a/README.md b/README.md index 644c92ad3..96679ce67 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ you want isn't here, that's a good issue to open. - **Open models, hosted:** `deepseek` (the default), `openrouter`, `huggingface` (Inference Providers), `moonshot` (Kimi), `zai` (GLM), - `minimax`, `volcengine` (Ark), `nvidia-nim`, `together`, `fireworks`, + `minimax` / `minimax-anthropic`, `volcengine` (Ark), `nvidia-nim`, `together`, `fireworks`, `novita`, `siliconflow` / `siliconflow-CN`, `arcee`, `xiaomi-mimo`, `openmodel`, `deepinfra`, `stepfun`, `atlascloud`, `qianfan`, `wanjie-ark`, plus a generic `openai`-compatible route for any gateway. diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs index fdda57429..1bdf193d2 100644 --- a/crates/agent/src/lib.rs +++ b/crates/agent/src/lib.rs @@ -776,6 +776,28 @@ impl Default for ModelRegistry { supports_tools: true, supports_reasoning: true, }, + ModelInfo { + id: "MiniMax-M3".to_string(), + provider: ProviderKind::MinimaxAnthropic, + aliases: vec![ + "minimax-anthropic".to_string(), + "minimax-anthropic-m3".to_string(), + "minimax-m3".to_string(), + ], + supports_tools: true, + supports_reasoning: true, + }, + ModelInfo { + id: "MiniMax-M2.7".to_string(), + provider: ProviderKind::MinimaxAnthropic, + aliases: vec![ + "minimax-anthropic-m2.7".to_string(), + "minimax-anthropic-m2-7".to_string(), + "minimax-m2.7".to_string(), + ], + supports_tools: true, + supports_reasoning: true, + }, ModelInfo { id: "MiniMax-M2.7-highspeed".to_string(), provider: ProviderKind::Minimax, @@ -1561,6 +1583,7 @@ mod tests { (ProviderKind::Zai, "GLM-5.2"), (ProviderKind::Stepfun, "step-3.7-flash"), (ProviderKind::Minimax, "MiniMax-M2.1"), + (ProviderKind::MinimaxAnthropic, "MiniMax-M3"), (ProviderKind::Openmodel, "deepseek-v4-flash"), (ProviderKind::Meta, "muse-spark-1.1"), (ProviderKind::Xai, "grok-4.5"), @@ -1661,6 +1684,25 @@ mod tests { } } + #[test] + fn minimax_anthropic_models_resolve_when_provider_hinted() { + let registry = ModelRegistry::default(); + + for (alias, expected) in [ + ("minimax-anthropic", "MiniMax-M3"), + ("minimax-m3", "MiniMax-M3"), + ("minimax-m2.7", "MiniMax-M2.7"), + ] { + let resolved = registry.resolve(Some(alias), Some(ProviderKind::MinimaxAnthropic)); + + assert_eq!(resolved.resolved.provider, ProviderKind::MinimaxAnthropic); + assert_eq!(resolved.resolved.id, expected); + assert!(!resolved.used_fallback); + assert!(resolved.resolved.supports_tools); + assert!(resolved.resolved.supports_reasoning); + } + } + #[test] fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() { let registry = ModelRegistry::default(); diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 9bc29cfbe..1b49d05bb 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -61,6 +61,12 @@ enum ProviderArg { Zai, Stepfun, Minimax, + #[value( + alias = "minimax_anthropic", + alias = "mini-max-anthropic", + alias = "mini_max_anthropic" + )] + MinimaxAnthropic, #[value(alias = "deep-infra", alias = "deep_infra")] Deepinfra, #[value(alias = "fugu", alias = "sakana-ai", alias = "sakana_ai")] @@ -107,6 +113,7 @@ impl From for ProviderKind { ProviderArg::Zai => ProviderKind::Zai, ProviderArg::Stepfun => ProviderKind::Stepfun, ProviderArg::Minimax => ProviderKind::Minimax, + ProviderArg::MinimaxAnthropic => ProviderKind::MinimaxAnthropic, ProviderArg::Deepinfra => ProviderKind::Deepinfra, ProviderArg::Sakana => ProviderKind::Sakana, ProviderArg::LongCat => ProviderKind::LongCat, @@ -4336,6 +4343,8 @@ mod tests { ("zai", ProviderArg::Zai), ("stepfun", ProviderArg::Stepfun), ("minimax", ProviderArg::Minimax), + ("minimax-anthropic", ProviderArg::MinimaxAnthropic), + ("minimax_anthropic", ProviderArg::MinimaxAnthropic), ("deepinfra", ProviderArg::Deepinfra), ("deep-infra", ProviderArg::Deepinfra), ("siliconflow-cn", ProviderArg::SiliconflowCn), diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 67fd3ed40..a813b8999 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -224,6 +224,15 @@ pub struct ProvidersToml { alias = "minimax" )] pub minimax: ProviderConfigToml, + #[serde( + default, + skip_serializing_if = "ProviderConfigToml::is_empty", + alias = "minimax-anthropic", + alias = "minimaxAnthropic", + alias = "mini-max-anthropic", + alias = "mini_max_anthropic" + )] + pub minimax_anthropic: ProviderConfigToml, #[serde( default, skip_serializing_if = "ProviderConfigToml::is_empty", @@ -369,6 +378,7 @@ impl ProvidersToml { ProviderKind::Zai => &self.zai, ProviderKind::Stepfun => &self.stepfun, ProviderKind::Minimax => &self.minimax, + ProviderKind::MinimaxAnthropic => &self.minimax_anthropic, ProviderKind::Deepinfra => &self.deepinfra, ProviderKind::Sakana => &self.sakana, ProviderKind::LongCat => &self.longcat, @@ -407,6 +417,7 @@ impl ProvidersToml { ProviderKind::Zai => &mut self.zai, ProviderKind::Stepfun => &mut self.stepfun, ProviderKind::Minimax => &mut self.minimax, + ProviderKind::MinimaxAnthropic => &mut self.minimax_anthropic, ProviderKind::Deepinfra => &mut self.deepinfra, ProviderKind::Sakana => &mut self.sakana, ProviderKind::LongCat => &mut self.longcat, @@ -2260,6 +2271,7 @@ impl ConfigToml { ProviderKind::Zai => DEFAULT_ZAI_BASE_URL.to_string(), ProviderKind::Stepfun => DEFAULT_STEPFUN_BASE_URL.to_string(), ProviderKind::Minimax => DEFAULT_MINIMAX_BASE_URL.to_string(), + ProviderKind::MinimaxAnthropic => DEFAULT_MINIMAX_ANTHROPIC_BASE_URL.to_string(), ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_BASE_URL.to_string(), ProviderKind::Sakana => DEFAULT_SAKANA_BASE_URL.to_string(), ProviderKind::LongCat => DEFAULT_LONGCAT_BASE_URL.to_string(), @@ -2495,8 +2507,10 @@ fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String { { return canonical.to_string(); } - if matches!(provider, ProviderKind::Minimax) - && let Some(canonical) = canonical_minimax_model_id(model) + if matches!( + provider, + ProviderKind::Minimax | ProviderKind::MinimaxAnthropic + ) && let Some(canonical) = canonical_minimax_model_id(model) { return canonical.to_string(); } @@ -2515,6 +2529,7 @@ fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String { | ProviderKind::Zai | ProviderKind::Stepfun | ProviderKind::Minimax + | ProviderKind::MinimaxAnthropic | ProviderKind::Qianfan | ProviderKind::Ollama | ProviderKind::Meta @@ -2840,7 +2855,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str { ProviderKind::Openmodel => DEFAULT_OPENMODEL_MODEL, ProviderKind::Zai => DEFAULT_ZAI_MODEL, ProviderKind::Stepfun => DEFAULT_STEPFUN_MODEL, - ProviderKind::Minimax => DEFAULT_MINIMAX_MODEL, + ProviderKind::Minimax | ProviderKind::MinimaxAnthropic => DEFAULT_MINIMAX_MODEL, ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_MODEL, ProviderKind::Sakana => DEFAULT_SAKANA_MODEL, ProviderKind::LongCat => DEFAULT_LONGCAT_MODEL, @@ -2880,6 +2895,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str { ProviderKind::Zai => DEFAULT_ZAI_BASE_URL, ProviderKind::Stepfun => DEFAULT_STEPFUN_BASE_URL, ProviderKind::Minimax => DEFAULT_MINIMAX_BASE_URL, + ProviderKind::MinimaxAnthropic => DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_BASE_URL, ProviderKind::Sakana => DEFAULT_SAKANA_BASE_URL, ProviderKind::LongCat => DEFAULT_LONGCAT_BASE_URL, @@ -4423,6 +4439,7 @@ struct EnvRuntimeOverrides { stepfun_base_url: Option, stepfun_model: Option, minimax_base_url: Option, + minimax_anthropic_base_url: Option, minimax_model: Option, deepinfra_base_url: Option, deepinfra_model: Option, @@ -4657,6 +4674,9 @@ impl EnvRuntimeOverrides { minimax_base_url: std::env::var("MINIMAX_BASE_URL") .ok() .filter(|v| !v.trim().is_empty()), + minimax_anthropic_base_url: std::env::var("MINIMAX_ANTHROPIC_BASE_URL") + .ok() + .filter(|v| !v.trim().is_empty()), minimax_model: std::env::var("MINIMAX_MODEL") .ok() .filter(|v| !v.trim().is_empty()), @@ -4749,6 +4769,7 @@ impl EnvRuntimeOverrides { ProviderKind::Zai => self.zai_base_url.clone(), ProviderKind::Stepfun => self.stepfun_base_url.clone(), ProviderKind::Minimax => self.minimax_base_url.clone(), + ProviderKind::MinimaxAnthropic => self.minimax_anthropic_base_url.clone(), ProviderKind::Deepinfra => self.deepinfra_base_url.clone(), ProviderKind::Sakana => self.sakana_base_url.clone(), ProviderKind::LongCat => self.longcat_base_url.clone(), @@ -4781,7 +4802,7 @@ impl EnvRuntimeOverrides { ProviderKind::Openmodel => self.openmodel_model.clone(), ProviderKind::Zai => self.zai_model.clone(), ProviderKind::Stepfun => self.stepfun_model.clone(), - ProviderKind::Minimax => self.minimax_model.clone(), + ProviderKind::Minimax | ProviderKind::MinimaxAnthropic => self.minimax_model.clone(), ProviderKind::Deepinfra => self.deepinfra_model.clone(), ProviderKind::Sakana => self.sakana_model.clone(), ProviderKind::LongCat => self.longcat_model.clone(), diff --git a/crates/config/src/provider.rs b/crates/config/src/provider.rs index 7517ea8fa..7b768cb5b 100644 --- a/crates/config/src/provider.rs +++ b/crates/config/src/provider.rs @@ -11,17 +11,17 @@ use super::{ DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL, DEFAULT_FIREWORKS_BASE_URL, DEFAULT_FIREWORKS_MODEL, DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL, DEFAULT_LONGCAT_BASE_URL, DEFAULT_LONGCAT_MODEL, DEFAULT_META_BASE_URL, DEFAULT_META_MODEL, - DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL, DEFAULT_MOONSHOT_BASE_URL, - DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL, DEFAULT_NOVITA_MODEL, - DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_OLLAMA_BASE_URL, - DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL, - DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENMODEL_BASE_URL, - DEFAULT_OPENMODEL_MODEL, DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, - DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, - DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL, - DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL, - DEFAULT_STEPFUN_MODEL, DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, - DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL, + DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL, + DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL, + DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL, + DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL, + DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL, + DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL, DEFAULT_OPENROUTER_BASE_URL, + DEFAULT_OPENROUTER_MODEL, DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL, + DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, + DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, + DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL, DEFAULT_TOGETHER_BASE_URL, + DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL, DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL, DEFAULT_ZAI_MODEL, ProviderKind, @@ -581,6 +581,51 @@ provider!( aliases: ["mini-max", "mini_max"] ); +/// MiniMax route that speaks the Anthropic Messages wire protocol. +pub struct MinimaxAnthropic; + +impl Provider for MinimaxAnthropic { + fn id(&self) -> &'static str { + "minimax-anthropic" + } + + fn kind(&self) -> ProviderKind { + ProviderKind::MinimaxAnthropic + } + + fn display_name(&self) -> &'static str { + "MiniMax (Anthropic-compatible)" + } + + fn default_base_url(&self) -> &'static str { + DEFAULT_MINIMAX_ANTHROPIC_BASE_URL + } + + fn default_model(&self) -> &'static str { + DEFAULT_MINIMAX_MODEL + } + + fn env_vars(&self) -> &'static [&'static str] { + &["MINIMAX_API_KEY"] + } + + fn provider_config_key(&self) -> &'static str { + "minimax_anthropic" + } + + fn aliases(&self) -> &'static [&'static str] { + &[ + "minimax_anthropic", + "mini-max-anthropic", + "mini_max_anthropic", + ] + } + + fn wire(&self) -> WireFormat { + WireFormat::AnthropicMessages + } +} + provider!( Deepinfra, Deepinfra, @@ -728,6 +773,7 @@ static OPENMODEL: Openmodel = Openmodel; static ZAI: Zai = Zai; static STEPFUN: Stepfun = Stepfun; static MINIMAX: Minimax = Minimax; +static MINIMAX_ANTHROPIC: MinimaxAnthropic = MinimaxAnthropic; static DEEPINFRA: Deepinfra = Deepinfra; static SAKANA: Sakana = Sakana; static LONGCAT: LongCat = LongCat; @@ -735,7 +781,7 @@ static META: Meta = Meta; static XAI: Xai = Xai; static CUSTOM: Custom = Custom; -static PROVIDER_REGISTRY: [&dyn Provider; 33] = [ +static PROVIDER_REGISTRY: [&dyn Provider; 34] = [ &DEEPSEEK, &DEEPSEEK_ANTHROPIC, &NVIDIA_NIM, @@ -763,6 +809,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 33] = [ &ZAI, &STEPFUN, &MINIMAX, + &MINIMAX_ANTHROPIC, &DEEPINFRA, &SAKANA, &LONGCAT, diff --git a/crates/config/src/provider_defaults.rs b/crates/config/src/provider_defaults.rs index 8d081cdff..6a6824db2 100644 --- a/crates/config/src/provider_defaults.rs +++ b/crates/config/src/provider_defaults.rs @@ -124,6 +124,7 @@ pub(crate) const MINIMAX_M2_1_MODEL: &str = "MiniMax-M2.1"; pub(crate) const MINIMAX_M2_1_HIGHSPEED_MODEL: &str = "MiniMax-M2.1-highspeed"; pub(crate) const MINIMAX_M2_MODEL: &str = "MiniMax-M2"; pub(crate) const DEFAULT_MINIMAX_BASE_URL: &str = "https://api.minimax.io/v1"; +pub(crate) const DEFAULT_MINIMAX_ANTHROPIC_BASE_URL: &str = "https://api.minimax.io/anthropic"; pub(crate) const DEFAULT_DEEPINFRA_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro"; pub(crate) const DEFAULT_DEEPINFRA_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash"; pub(crate) const DEFAULT_DEEPINFRA_BASE_URL: &str = "https://api.deepinfra.com/v1/openai"; diff --git a/crates/config/src/provider_kind.rs b/crates/config/src/provider_kind.rs index 431e04f11..6ad00bf24 100644 --- a/crates/config/src/provider_kind.rs +++ b/crates/config/src/provider_kind.rs @@ -99,6 +99,12 @@ pub enum ProviderKind { Stepfun, #[serde(alias = "mini-max", alias = "mini_max", alias = "minimax")] Minimax, + #[serde( + alias = "minimax_anthropic", + alias = "mini-max-anthropic", + alias = "mini_max_anthropic" + )] + MinimaxAnthropic, #[serde(alias = "deep-infra", alias = "deep_infra")] Deepinfra, #[serde(alias = "sakana-ai", alias = "sakana_ai", alias = "fugu")] @@ -127,7 +133,7 @@ pub enum ProviderKind { } impl ProviderKind { - pub const ALL: [Self; 33] = [ + pub const ALL: [Self; 34] = [ Self::Deepseek, Self::DeepseekAnthropic, Self::NvidiaNim, @@ -155,6 +161,7 @@ impl ProviderKind { Self::Zai, Self::Stepfun, Self::Minimax, + Self::MinimaxAnthropic, Self::Deepinfra, Self::Sakana, Self::LongCat, diff --git a/crates/config/src/tests.rs b/crates/config/src/tests.rs index e10830da6..4c3d43462 100644 --- a/crates/config/src/tests.rs +++ b/crates/config/src/tests.rs @@ -845,6 +845,7 @@ struct EnvGuard { stepfun_model: Option, minimax_api_key: Option, minimax_base_url: Option, + minimax_anthropic_base_url: Option, minimax_model: Option, sakana_api_key: Option, fugu_api_key: Option, @@ -977,6 +978,7 @@ impl EnvGuard { stepfun_model: env::var_os("STEPFUN_MODEL"), minimax_api_key: env::var_os("MINIMAX_API_KEY"), minimax_base_url: env::var_os("MINIMAX_BASE_URL"), + minimax_anthropic_base_url: env::var_os("MINIMAX_ANTHROPIC_BASE_URL"), minimax_model: env::var_os("MINIMAX_MODEL"), sakana_api_key: env::var_os("SAKANA_API_KEY"), fugu_api_key: env::var_os("FUGU_API_KEY"), @@ -1095,6 +1097,7 @@ impl EnvGuard { env::remove_var("STEPFUN_MODEL"); env::remove_var("MINIMAX_API_KEY"); env::remove_var("MINIMAX_BASE_URL"); + env::remove_var("MINIMAX_ANTHROPIC_BASE_URL"); env::remove_var("MINIMAX_MODEL"); env::remove_var("SAKANA_API_KEY"); env::remove_var("FUGU_API_KEY"); @@ -1248,6 +1251,10 @@ impl Drop for EnvGuard { Self::restore_var("STEPFUN_MODEL", self.stepfun_model.take()); Self::restore_var("MINIMAX_API_KEY", self.minimax_api_key.take()); Self::restore_var("MINIMAX_BASE_URL", self.minimax_base_url.take()); + Self::restore_var( + "MINIMAX_ANTHROPIC_BASE_URL", + self.minimax_anthropic_base_url.take(), + ); Self::restore_var("MINIMAX_MODEL", self.minimax_model.take()); Self::restore_var("SAKANA_API_KEY", self.sakana_api_key.take()); Self::restore_var("FUGU_API_KEY", self.fugu_api_key.take()); @@ -4156,6 +4163,11 @@ fn zai_stepfun_minimax_and_sakana_default_to_first_party_routes() { DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL, ), + ( + ProviderKind::MinimaxAnthropic, + DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, + DEFAULT_MINIMAX_MODEL, + ), ( ProviderKind::Sakana, DEFAULT_SAKANA_BASE_URL, @@ -4242,6 +4254,29 @@ fn minimax_env_model_override_canonicalizes_known_aliases() { assert_eq!(resolved.model, "MiniMax-M2.5-highspeed"); } +#[test] +fn minimax_anthropic_env_overrides_use_messages_base_url() { + let _lock = env_lock(); + let _env = EnvGuard::without_deepseek_runtime_overrides(); + unsafe { + env::set_var("CODEWHALE_PROVIDER", "minimax-anthropic"); + env::set_var( + "MINIMAX_ANTHROPIC_BASE_URL", + "https://messages.minimax.example/anthropic", + ); + env::set_var("MINIMAX_MODEL", "MiniMax-M2.7"); + } + + let resolved = ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default()); + + assert_eq!(resolved.provider, ProviderKind::MinimaxAnthropic); + assert_eq!( + resolved.base_url, + "https://messages.minimax.example/anthropic" + ); + assert_eq!(resolved.model, "MiniMax-M2.7"); +} + #[test] fn sakana_env_overrides_resolve_fugu_route() { let _lock = env_lock(); diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index c3b4ded3c..6f636a69e 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -935,12 +935,18 @@ fn is_auth_dialect_header(header_name: &HeaderName) -> bool { fn api_provider_uses_anthropic_messages(api_provider: ApiProvider) -> bool { matches!( api_provider, - ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic | ApiProvider::Openmodel + ApiProvider::Anthropic + | ApiProvider::DeepseekAnthropic + | ApiProvider::MinimaxAnthropic + | ApiProvider::Openmodel ) } fn api_provider_skips_models_probe(api_provider: ApiProvider) -> bool { - matches!(api_provider, ApiProvider::DeepseekAnthropic) + matches!( + api_provider, + ApiProvider::DeepseekAnthropic | ApiProvider::MinimaxAnthropic + ) } /// Verify a provider API key by hitting the `/models` endpoint @@ -1987,7 +1993,10 @@ pub(super) fn apply_reasoning_effort( // #3024: Ollama OpenAI-compat endpoint accepts think param. body["think"] = json!(false); } - ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic | ApiProvider::Openmodel => { + ApiProvider::Anthropic + | ApiProvider::DeepseekAnthropic + | ApiProvider::MinimaxAnthropic + | ApiProvider::Openmodel => { // #3014: thinking/effort shaping happens natively inside // client/anthropic.rs (adaptive thinking + output_config), // not via OpenAI-dialect fields. @@ -2072,7 +2081,10 @@ pub(super) fn apply_reasoning_effort( // #3024: Ollama think param. body["think"] = json!(true); } - ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic | ApiProvider::Openmodel => { + ApiProvider::Anthropic + | ApiProvider::DeepseekAnthropic + | ApiProvider::MinimaxAnthropic + | ApiProvider::Openmodel => { // #3014: thinking/effort shaping happens natively inside // client/anthropic.rs (adaptive thinking + output_config), // not via OpenAI-dialect fields. @@ -2144,7 +2156,10 @@ pub(super) fn apply_reasoning_effort( // #3024: Ollama think param. body["think"] = json!(true); } - ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic | ApiProvider::Openmodel => { + ApiProvider::Anthropic + | ApiProvider::DeepseekAnthropic + | ApiProvider::MinimaxAnthropic + | ApiProvider::Openmodel => { // #3014: thinking/effort shaping happens natively inside // client/anthropic.rs (adaptive thinking + output_config), // not via OpenAI-dialect fields. @@ -2378,6 +2393,24 @@ mod tests { .expect("deepseek anthropic client") } + fn minimax_anthropic_client(server: &MockServer) -> DeepSeekClient { + let _ = rustls::crypto::ring::default_provider().install_default(); + let providers = ProvidersConfig { + minimax_anthropic: ProviderConfig { + api_key: Some("minimax-test".to_string()), + base_url: Some(server.uri()), + ..ProviderConfig::default() + }, + ..ProvidersConfig::default() + }; + DeepSeekClient::new(&Config { + provider: Some("minimax-anthropic".to_string()), + providers: Some(providers), + ..Config::default() + }) + .expect("minimax anthropic client") + } + fn zai_client_for_test() -> DeepSeekClient { let _ = rustls::crypto::ring::default_provider().install_default(); let providers = ProvidersConfig { @@ -2855,6 +2888,31 @@ mod tests { ); } + #[test] + fn minimax_anthropic_uses_anthropic_header_dialect() { + let headers = DeepSeekClient::default_headers_for_provider( + "minimax-test", + &HashMap::new(), + ApiProvider::MinimaxAnthropic, + crate::config::DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, + ) + .expect("headers"); + + assert_eq!( + headers + .get("x-api-key") + .and_then(|value| value.to_str().ok()), + Some("minimax-test") + ); + assert_eq!( + headers + .get("anthropic-version") + .and_then(|value| value.to_str().ok()), + Some("2023-06-01") + ); + assert!(headers.get(AUTHORIZATION).is_none()); + } + #[test] fn openmodel_uses_bearer_auth_with_anthropic_version() { let mut extra = HashMap::new(); @@ -2954,6 +3012,55 @@ mod tests { ); } + #[tokio::test] + async fn minimax_anthropic_request_uses_messages_endpoint() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/v1/messages")) + .and(header("x-api-key", "minimax-test")) + .and(header("anthropic-version", "2023-06-01")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "id": "msg_1", + "type": "message", + "role": "assistant", + "content": [{"type": "text", "text": "ok"}], + "model": "MiniMax-M3", + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": {"input_tokens": 3, "output_tokens": 1} + }))) + .expect(1) + .mount(&server) + .await; + + let client = minimax_anthropic_client(&server); + let response = client + .create_message(MessageRequest { + model: "MiniMax-M3".to_string(), + messages: vec![Message { + role: "user".to_string(), + content: vec![ContentBlock::Text { + text: "hello".to_string(), + cache_control: None, + }], + }], + max_tokens: 32, + system: None, + tools: None, + tool_choice: None, + metadata: None, + thinking: None, + reasoning_effort: Some("off".to_string()), + stream: Some(false), + temperature: None, + top_p: None, + }) + .await + .expect("message succeeds"); + + assert_eq!(response.content.len(), 1); + } + #[tokio::test] async fn deepseek_anthropic_fim_fails_without_http_request() { let server = MockServer::start().await; diff --git a/crates/tui/src/client/anthropic.rs b/crates/tui/src/client/anthropic.rs index b1b59d83d..791382edc 100644 --- a/crates/tui/src/client/anthropic.rs +++ b/crates/tui/src/client/anthropic.rs @@ -960,5 +960,9 @@ mod tests { anthropic_messages_url("https://api.deepseek.com/anthropic"), "https://api.deepseek.com/anthropic/v1/messages" ); + assert_eq!( + anthropic_messages_url("https://api.minimax.io/anthropic"), + "https://api.minimax.io/anthropic/v1/messages" + ); } } diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 2ca44bfef..f3c26f208 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -68,6 +68,7 @@ pub enum ApiProvider { Zai, Stepfun, Minimax, + MinimaxAnthropic, Deepinfra, Sakana, LongCat, @@ -203,7 +204,9 @@ impl ApiProvider { Self::Openmodel => "https://docs.openmodel.ai/en/docs/guides/api-key", Self::Zai => "https://z.ai/model-api", Self::Stepfun => "https://platform.stepfun.ai/", - Self::Minimax => "https://platform.minimax.io/docs/guides/quickstart-preparation", + Self::Minimax | Self::MinimaxAnthropic => { + "https://platform.minimax.io/docs/guides/quickstart-preparation" + } Self::Deepinfra => "https://deepinfra.com/dash/api_keys", Self::Sakana => "https://api.sakana.ai/", Self::LongCat => "https://longcat.chat/platform", @@ -224,7 +227,7 @@ impl ApiProvider { /// `ApiProvider` discriminant → `ProviderKind` lookup. /// Index 1 is `None` for the legacy `DeepseekCN` variant. - const KIND_LOOKUP: [Option; 34] = [ + const KIND_LOOKUP: [Option; 35] = [ Some(codewhale_config::ProviderKind::Deepseek), None, // DeepseekCN Some(codewhale_config::ProviderKind::DeepseekAnthropic), @@ -253,6 +256,7 @@ impl ApiProvider { Some(codewhale_config::ProviderKind::Zai), Some(codewhale_config::ProviderKind::Stepfun), Some(codewhale_config::ProviderKind::Minimax), + Some(codewhale_config::ProviderKind::MinimaxAnthropic), Some(codewhale_config::ProviderKind::Deepinfra), Some(codewhale_config::ProviderKind::Sakana), Some(codewhale_config::ProviderKind::LongCat), @@ -262,7 +266,7 @@ impl ApiProvider { ]; /// `ProviderKind` discriminant → `ApiProvider` lookup. - const FROM_KIND_LOOKUP: [Self; 33] = [ + const FROM_KIND_LOOKUP: [Self; 34] = [ Self::Deepseek, Self::DeepseekAnthropic, Self::NvidiaNim, @@ -290,6 +294,7 @@ impl ApiProvider { Self::Zai, Self::Stepfun, Self::Minimax, + Self::MinimaxAnthropic, Self::Deepinfra, Self::Sakana, Self::LongCat, @@ -462,7 +467,10 @@ pub enum RequestPayloadMode { /// in the API payload (after normalization / provider-specific mapping). #[must_use] pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> ProviderCapability { - if matches!(provider, ApiProvider::Anthropic | ApiProvider::Openmodel) { + if matches!( + provider, + ApiProvider::Anthropic | ApiProvider::MinimaxAnthropic | ApiProvider::Openmodel + ) { return ProviderCapability { provider, resolved_model: resolved_model.to_string(), @@ -584,7 +592,7 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi let request_payload_mode = if matches!( provider, - ApiProvider::DeepseekAnthropic | ApiProvider::Openmodel + ApiProvider::DeepseekAnthropic | ApiProvider::MinimaxAnthropic | ApiProvider::Openmodel ) { RequestPayloadMode::AnthropicMessages } else { @@ -1028,7 +1036,7 @@ pub fn canonical_model_id_for_provider(provider: ApiProvider, model: &str) -> Op ApiProvider::Arcee => canonical_arcee_model_id(trimmed), ApiProvider::Moonshot => canonical_moonshot_model_id(trimmed), ApiProvider::Zai => canonical_zai_model_id(trimmed), - ApiProvider::Minimax => canonical_minimax_model_id(trimmed), + ApiProvider::Minimax | ApiProvider::MinimaxAnthropic => canonical_minimax_model_id(trimmed), _ => None, }; if let Some(canonical) = family_canonical { @@ -1176,7 +1184,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati DEFAULT_ANTHROPIC_MODEL, ANTHROPIC_HAIKU_MODEL, ], - ApiProvider::Minimax => vec![ + ApiProvider::Minimax | ApiProvider::MinimaxAnthropic => vec![ DEFAULT_MINIMAX_MODEL, MINIMAX_M2_7_MODEL, MINIMAX_M2_7_HIGHSPEED_MODEL, @@ -2639,6 +2647,14 @@ pub struct ProvidersConfig { pub stepfun: ProviderConfig, #[serde(default)] pub minimax: ProviderConfig, + #[serde( + default, + alias = "minimax-anthropic", + alias = "minimaxAnthropic", + alias = "mini-max-anthropic", + alias = "mini_max_anthropic" + )] + pub minimax_anthropic: ProviderConfig, #[serde(default, alias = "sakana-ai", alias = "sakana_ai", alias = "fugu")] pub sakana: ProviderConfig, #[serde( @@ -2709,6 +2725,7 @@ impl ProvidersConfig { ("providers.zai", &self.zai), ("providers.stepfun", &self.stepfun), ("providers.minimax", &self.minimax), + ("providers.minimax_anthropic", &self.minimax_anthropic), ("providers.sakana", &self.sakana), ("providers.meta", &self.meta), ("providers.xai", &self.xai), @@ -3084,6 +3101,7 @@ impl Config { ApiProvider::Zai => &providers.zai, ApiProvider::Stepfun => &providers.stepfun, ApiProvider::Minimax => &providers.minimax, + ApiProvider::MinimaxAnthropic => &providers.minimax_anthropic, ApiProvider::Sakana => &providers.sakana, ApiProvider::LongCat => &providers.longcat, ApiProvider::Meta => &providers.meta, @@ -3147,6 +3165,7 @@ impl Config { ApiProvider::Zai => &mut providers.zai, ApiProvider::Stepfun => &mut providers.stepfun, ApiProvider::Minimax => &mut providers.minimax, + ApiProvider::MinimaxAnthropic => &mut providers.minimax_anthropic, ApiProvider::Sakana => &mut providers.sakana, ApiProvider::LongCat => &mut providers.longcat, ApiProvider::Meta => &mut providers.meta, @@ -3341,7 +3360,7 @@ impl Config { ApiProvider::Zai => DEFAULT_ZAI_MODEL, ApiProvider::Stepfun => DEFAULT_STEPFUN_MODEL, ApiProvider::Anthropic => DEFAULT_ANTHROPIC_MODEL, - ApiProvider::Minimax => DEFAULT_MINIMAX_MODEL, + ApiProvider::Minimax | ApiProvider::MinimaxAnthropic => DEFAULT_MINIMAX_MODEL, ApiProvider::Sakana => DEFAULT_SAKANA_MODEL, ApiProvider::LongCat => DEFAULT_LONGCAT_MODEL, ApiProvider::Meta => DEFAULT_META_MODEL, @@ -3398,6 +3417,7 @@ impl Config { | ApiProvider::Zai | ApiProvider::Stepfun | ApiProvider::Minimax + | ApiProvider::MinimaxAnthropic | ApiProvider::Sakana | ApiProvider::LongCat | ApiProvider::Meta @@ -3461,6 +3481,7 @@ impl Config { ApiProvider::Stepfun => DEFAULT_STEPFUN_BASE_URL, ApiProvider::Anthropic => DEFAULT_ANTHROPIC_BASE_URL, ApiProvider::Minimax => DEFAULT_MINIMAX_BASE_URL, + ApiProvider::MinimaxAnthropic => DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, ApiProvider::Sakana => DEFAULT_SAKANA_BASE_URL, ApiProvider::LongCat => DEFAULT_LONGCAT_BASE_URL, ApiProvider::Meta => DEFAULT_META_BASE_URL, @@ -4639,6 +4660,13 @@ fn apply_env_overrides(config: &mut Config) { .minimax .base_url = Some(value); } + ApiProvider::MinimaxAnthropic => { + config + .providers + .get_or_insert_with(ProvidersConfig::default) + .minimax_anthropic + .base_url = Some(value); + } ApiProvider::Sakana => { config .providers @@ -4915,6 +4943,7 @@ fn apply_env_overrides(config: &mut Config) { ApiProvider::Zai => &mut providers.zai, ApiProvider::Stepfun => &mut providers.stepfun, ApiProvider::Minimax => &mut providers.minimax, + ApiProvider::MinimaxAnthropic => &mut providers.minimax_anthropic, ApiProvider::Sakana => &mut providers.sakana, ApiProvider::LongCat => &mut providers.longcat, ApiProvider::Meta => &mut providers.meta, @@ -5161,6 +5190,7 @@ fn apply_env_overrides(config: &mut Config) { ApiProvider::Zai => &mut providers.zai, ApiProvider::Stepfun => &mut providers.stepfun, ApiProvider::Minimax => &mut providers.minimax, + ApiProvider::MinimaxAnthropic => &mut providers.minimax_anthropic, ApiProvider::Sakana => &mut providers.sakana, ApiProvider::LongCat => &mut providers.longcat, ApiProvider::Meta => &mut providers.meta, @@ -5944,6 +5974,10 @@ fn merge_providers( zai: merge_provider_config(base.zai, override_cfg.zai), stepfun: merge_provider_config(base.stepfun, override_cfg.stepfun), minimax: merge_provider_config(base.minimax, override_cfg.minimax), + minimax_anthropic: merge_provider_config( + base.minimax_anthropic, + override_cfg.minimax_anthropic, + ), sakana: merge_provider_config(base.sakana, override_cfg.sakana), longcat: merge_provider_config(base.longcat, override_cfg.longcat), meta: merge_provider_config(base.meta, override_cfg.meta), diff --git a/crates/tui/src/config/models.rs b/crates/tui/src/config/models.rs index 4ca57502b..44c42f76e 100644 --- a/crates/tui/src/config/models.rs +++ b/crates/tui/src/config/models.rs @@ -162,6 +162,7 @@ pub const MINIMAX_M2_1_MODEL: &str = "MiniMax-M2.1"; pub const MINIMAX_M2_1_HIGHSPEED_MODEL: &str = "MiniMax-M2.1-highspeed"; pub const MINIMAX_M2_MODEL: &str = "MiniMax-M2"; pub const DEFAULT_MINIMAX_BASE_URL: &str = "https://api.minimax.io/v1"; +pub const DEFAULT_MINIMAX_ANTHROPIC_BASE_URL: &str = "https://api.minimax.io/anthropic"; pub const DEFAULT_SAKANA_MODEL: &str = "fugu"; pub const SAKANA_FUGU_ULTRA_MODEL: &str = "fugu-ultra-20260615"; pub const DEFAULT_SAKANA_BASE_URL: &str = "https://api.sakana.ai/v1"; diff --git a/crates/tui/src/config/tests.rs b/crates/tui/src/config/tests.rs index f7cdd3a02..2ca4e5b8a 100644 --- a/crates/tui/src/config/tests.rs +++ b/crates/tui/src/config/tests.rs @@ -66,6 +66,11 @@ fn deepseek_api_key_reads_metadata_env_vars_for_newer_providers() -> Result<()> (ApiProvider::Zai, "ZAI_API_KEY", "zai-env-key"), (ApiProvider::Stepfun, "STEPFUN_API_KEY", "stepfun-env-key"), (ApiProvider::Minimax, "MINIMAX_API_KEY", "minimax-env-key"), + ( + ApiProvider::MinimaxAnthropic, + "MINIMAX_API_KEY", + "minimax-env-key", + ), ( ApiProvider::Deepinfra, "DEEPINFRA_API_KEY", @@ -3941,6 +3946,14 @@ fn model_completion_names_for_minimax_include_direct_chat_models() { ); } +#[test] +fn model_completion_names_for_minimax_anthropic_include_target_models() { + let models = model_completion_names_for_provider(ApiProvider::MinimaxAnthropic); + + assert!(models.contains(&DEFAULT_MINIMAX_MODEL)); + assert!(models.contains(&MINIMAX_M2_7_MODEL)); +} + #[test] fn model_completion_names_for_sakana_include_fugu_models() { assert_eq!( @@ -7285,6 +7298,19 @@ fn provider_capability_minimax_direct_models_use_api_docs_shape() { } } +#[test] +fn provider_capability_minimax_anthropic_uses_messages_shape() { + for model in [DEFAULT_MINIMAX_MODEL, MINIMAX_M2_7_MODEL] { + let cap = provider_capability(ApiProvider::MinimaxAnthropic, model); + assert!(cap.thinking_supported, "{model}"); + assert!(!cap.cache_telemetry_supported, "{model}"); + assert_eq!( + cap.request_payload_mode, + RequestPayloadMode::AnthropicMessages + ); + } +} + #[test] fn provider_capability_wanjie_ark_reasoner_has_thinking_no_cache() { let cap = provider_capability(ApiProvider::WanjieArk, DEFAULT_WANJIE_ARK_MODEL); diff --git a/crates/tui/src/config_persistence.rs b/crates/tui/src/config_persistence.rs index 218209ccd..f42aa6574 100644 --- a/crates/tui/src/config_persistence.rs +++ b/crates/tui/src/config_persistence.rs @@ -378,6 +378,7 @@ fn provider_base_url_table_key(provider: ApiProvider) -> anyhow::Result<&'static ApiProvider::Zai => Ok("zai"), ApiProvider::Stepfun => Ok("stepfun"), ApiProvider::Minimax => Ok("minimax"), + ApiProvider::MinimaxAnthropic => Ok("minimax_anthropic"), ApiProvider::Sakana => Ok("sakana"), ApiProvider::LongCat => Ok("longcat"), ApiProvider::Meta => Ok("meta"), diff --git a/crates/tui/src/pricing.rs b/crates/tui/src/pricing.rs index 76ba7127c..bcd523218 100644 --- a/crates/tui/src/pricing.rs +++ b/crates/tui/src/pricing.rs @@ -186,6 +186,11 @@ fn known_pricing_for_model(model_lower: &str) -> Option { // Moonshot K2.7 Code cache-read rate per // https://platform.kimi.ai/docs/pricing/chat-k27-code "moonshotai/kimi-k2.7-code" | "kimi-k2.7-code" => Some(usd_only_pricing(0.19, 0.95, 4.00)), + // MiniMax cache-read pricing. Cache-write is not represented by + // CurrencyPricing yet, so only the supported fields are populated. + "minimax/minimax-m3" | "minimax-m3" | "minimax/minimax-m2.7" | "minimax-m2.7" => { + Some(usd_only_pricing(0.06, 0.30, 1.20)) + } // gpt-5-codex is deprecated upstream on the ChatGPT-OAuth path // (successor: gpt-5.3-codex); API usage is still billed at these rates. // https://developers.openai.com/api/docs/models/gpt-5.3-codex @@ -210,7 +215,6 @@ fn known_pricing_for_model(model_lower: &str) -> Option { "z-ai/glm-5.1" | "glm-5.1" => Some(usd_only_pricing(0.26, 1.40, 4.40)), // GLM-5 Turbo pricing per https://docs.z.ai/guides/overview/pricing "z-ai/glm-5-turbo" | "glm-5-turbo" => Some(usd_only_pricing(0.24, 1.20, 4.00)), - "minimax/minimax-m3" | "minimax-m3" => Some(usd_only_pricing(0.06, 0.30, 1.20)), // Arcee publishes no cache rate for Trinity Large Thinking, so the // cache-hit rate equals the input rate (no-discount representation). // https://docs.arcee.ai/get-started/pricing @@ -467,6 +471,16 @@ mod tests { } } + #[test] + fn minimax_target_models_use_cache_read_discount() { + for model in ["minimax-m3", "minimax-m2.7"] { + let pricing = pricing_for_model_at(model, Utc::now()).expect(model); + assert_eq!(pricing.usd.input_cache_hit_per_million, 0.06, "{model}"); + assert_eq!(pricing.usd.input_cache_miss_per_million, 0.30, "{model}"); + assert_eq!(pricing.usd.output_per_million, 1.20, "{model}"); + } + } + #[test] fn curated_usd_only_models_have_pricing_and_accrue_cost() { let usage = Usage { diff --git a/crates/tui/src/tui/provider_picker.rs b/crates/tui/src/tui/provider_picker.rs index b628a903a..ad8f48c17 100644 --- a/crates/tui/src/tui/provider_picker.rs +++ b/crates/tui/src/tui/provider_picker.rs @@ -921,6 +921,7 @@ fn default_reasoning_stream_visibility(provider: ApiProvider) -> ProviderReasoni | ApiProvider::Volcengine | ApiProvider::Arcee | ApiProvider::Minimax + | ApiProvider::MinimaxAnthropic | ApiProvider::Sglang | ApiProvider::Vllm | ApiProvider::Zai diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index db839d70f..eb6cd1e0e 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -9763,7 +9763,9 @@ fn render(f: &mut Frame, app: &mut App, config: &Config) { crate::config::ApiProvider::OpenaiCodex => Some("Codex"), crate::config::ApiProvider::Zai => Some("Z.ai"), crate::config::ApiProvider::Stepfun => Some("StepFun"), - crate::config::ApiProvider::Minimax => Some("MiniMax"), + crate::config::ApiProvider::Minimax | crate::config::ApiProvider::MinimaxAnthropic => { + Some("MiniMax") + } crate::config::ApiProvider::Sakana => Some("Sakana"), crate::config::ApiProvider::LongCat => Some("Meituan LongCat"), crate::config::ApiProvider::Meta => Some("Meta"), @@ -11793,6 +11795,7 @@ fn mirror_saved_api_key_in_config(config: &mut Config, provider: ApiProvider, ap ApiProvider::Zai => &mut providers.zai, ApiProvider::Stepfun => &mut providers.stepfun, ApiProvider::Minimax => &mut providers.minimax, + ApiProvider::MinimaxAnthropic => &mut providers.minimax_anthropic, ApiProvider::Sakana => &mut providers.sakana, ApiProvider::LongCat => &mut providers.longcat, ApiProvider::Meta => &mut providers.meta, @@ -11918,6 +11921,7 @@ fn set_provider_auth_mode_in_memory(config: &mut Config, provider: ApiProvider, ApiProvider::Zai => &mut providers.zai, ApiProvider::Stepfun => &mut providers.stepfun, ApiProvider::Minimax => &mut providers.minimax, + ApiProvider::MinimaxAnthropic => &mut providers.minimax_anthropic, ApiProvider::Sakana => &mut providers.sakana, ApiProvider::LongCat => &mut providers.longcat, ApiProvider::Meta => &mut providers.meta, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e7d0a4ab0..fa2d04e6e 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1198,6 +1198,7 @@ If you are upgrading from older releases: ### Core keys (used by the TUI/engine) - `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `huggingface`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, or `sakana`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = "token-plan-cn"` for China and `mode = "token-plan-ams"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets ChatGPT/Codex OAuth; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`. +- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because CodeWhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. - `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and Ollama can omit it. - `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://chatgpt.com/backend-api` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `base_url = "https://token-plan-cn.xiaomimimo.com/v1"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = "https://token-plan-ams.xiaomimimo.com/v1"` for Europe/Amsterdam accounts. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features. - `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than CodeWhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index f7996412a..2a00a5e17 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -235,6 +235,7 @@ the same links where possible. | `zai` | [Z.ai model API](https://z.ai/model-api) | | `stepfun` | [StepFun Open Platform](https://platform.stepfun.ai/) | | `minimax` | [MiniMax prerequisites](https://platform.minimax.io/docs/guides/quickstart-preparation) | +| `minimax-anthropic` | [MiniMax prerequisites](https://platform.minimax.io/docs/guides/quickstart-preparation) | | `huggingface` | [Hugging Face tokens](https://huggingface.co/settings/tokens) | | `deepinfra` | [DeepInfra API keys](https://deepinfra.com/dash/api_keys) | | `together` | [Together API keys](https://api.together.ai/settings/api-keys) | @@ -268,7 +269,8 @@ the same links where possible. | `moonshot` | `[providers.moonshot]` | `MOONSHOT_API_KEY`, `KIMI_API_KEY` | `MOONSHOT_BASE_URL`, `KIMI_BASE_URL`; default `https://api.moonshot.ai/v1` | `kimi-k2.7-code`, `kimi-k2.6`; Kimi Code path uses `kimi-for-coding` at `https://api.kimi.com/coding/v1` | Moonshot/Kimi route. `kimi` and `kimi-k2` aliases select `kimi-k2.7-code`; `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, and `KIMI_MODEL` are accepted. Kimi thinking streams through `reasoning_content`; CodeWhale keeps it in Thinking cells and replays it for thinking/tool-call continuity. `[providers.moonshot] auth_mode = "kimi_oauth"` reads Kimi Code OAuth credentials from `KIMI_CODE_HOME`/`~/.kimi-code`, with legacy `KIMI_SHARE_DIR`/`~/.kimi` fallback. | | `zai` | `[providers.zai]` | `ZAI_API_KEY`, `Z_AI_API_KEY` | `ZAI_BASE_URL`, `Z_AI_BASE_URL`; default `https://api.z.ai/api/coding/paas/v4`; general API `https://api.z.ai/api/paas/v4` | `GLM-5.2` default; `GLM-5.1`, `GLM-5-Turbo` available | Z.AI GLM Coding Plan route. `GLM-5.2` is the default; set `model = "GLM-5.1"` or `ZAI_MODEL=GLM-5.1` for the smaller model, or `GLM-5-Turbo` for the fast variant used by faster/explore sub-agents. | | `stepfun` | `[providers.stepfun]` | `STEPFUN_API_KEY`, `STEP_API_KEY` | `STEPFUN_BASE_URL`, `STEP_BASE_URL`; default `https://api.stepfun.ai/v1`; Coding Plan endpoint `https://api.stepfun.com/step_plan/v1` | `step-3.7-flash` | StepFun / StepFlash direct OpenAI-compatible route. Set `[providers.stepfun].base_url` or `STEP_BASE_URL` to the Coding Plan URL when using that plan. `STEPFUN_MODEL` and `STEP_MODEL` are accepted. | -| `minimax` | `[providers.minimax]` | `MINIMAX_API_KEY` | `MINIMAX_BASE_URL`; default `https://api.minimax.io/v1`; Anthropic-compatible routes are `https://api.minimax.io/anthropic` globally and `https://api.minimaxi.com/anthropic` in China | `MiniMax-M3`, `MiniMax-M2.7`, `MiniMax-M2.7-highspeed`, `MiniMax-M2.5`, `MiniMax-M2.5-highspeed`, `MiniMax-M2.1`, `MiniMax-M2.1-highspeed`, `MiniMax-M2` | MiniMax direct OpenAI-compatible route. CodeWhale sends `reasoning_split = true` so MiniMax thinking arrives separately from answer text, and direct MiniMax IDs stay distinct from OpenRouter namespaced IDs such as `minimax/minimax-m3`. | +| `minimax` | `[providers.minimax]` | `MINIMAX_API_KEY` | `MINIMAX_BASE_URL`; default `https://api.minimax.io/v1`; China `https://api.minimaxi.com/v1` | `MiniMax-M3`, `MiniMax-M2.7`, `MiniMax-M2.7-highspeed`, `MiniMax-M2.5`, `MiniMax-M2.5-highspeed`, `MiniMax-M2.1`, `MiniMax-M2.1-highspeed`, `MiniMax-M2` | MiniMax direct OpenAI-compatible route. CodeWhale sends `reasoning_split = true` so MiniMax thinking arrives separately from answer text, and direct MiniMax IDs stay distinct from namespaced gateway IDs. | +| `minimax-anthropic` | `[providers.minimax_anthropic]` | `MINIMAX_API_KEY` | `MINIMAX_ANTHROPIC_BASE_URL`; default `https://api.minimax.io/anthropic`; China `https://api.minimaxi.com/anthropic` | `MiniMax-M3`, `MiniMax-M2.7` | MiniMax direct Anthropic-compatible Messages route. The configured Base URL keeps the `/anthropic` suffix; CodeWhale appends `/v1/messages`, uses `x-api-key`, and sends adaptive thinking controls through the Messages adapter. | | `sglang` | `[providers.sglang]` | Optional `SGLANG_API_KEY` | `SGLANG_BASE_URL`; default `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted OpenAI-compatible route. Localhost deployments commonly omit auth. `SGLANG_MODEL` is accepted. | | `vllm` | `[providers.vllm]` | Optional `VLLM_API_KEY` | `VLLM_BASE_URL`; default `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted vLLM OpenAI-compatible route. Localhost deployments commonly omit auth. `VLLM_MODEL` is accepted. | | `ollama` | `[providers.ollama]` | Optional `OLLAMA_API_KEY` | `OLLAMA_BASE_URL`; default `http://localhost:11434/v1` | `deepseek-coder:1.3b`; provider-hinted custom tags pass through | Self-hosted Ollama OpenAI-compatible route. Localhost deployments commonly omit auth. `OLLAMA_MODEL` is accepted. | @@ -403,6 +405,7 @@ endpoint when the endpoint supports model listing. | `zai` | `GLM-5.2`, `GLM-5.1`, `GLM-5-Turbo`; provider-hinted custom model IDs pass through | yes | yes | | `stepfun` | `step-3.7-flash` | yes | no | | `minimax` | `MiniMax-M3`, `MiniMax-M2.7`, `MiniMax-M2.7-highspeed`, `MiniMax-M2.5`, `MiniMax-M2.5-highspeed`, `MiniMax-M2.1`, `MiniMax-M2.1-highspeed`, `MiniMax-M2` | yes | yes | +| `minimax-anthropic` | `MiniMax-M3`, `MiniMax-M2.7` | yes | yes | | `sglang` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes | | `vllm` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes | | `ollama` | `deepseek-coder:1.3b`; custom tags pass through when provider hint is `ollama` | yes | no | @@ -431,7 +434,8 @@ metadata, not a live API probe. Current fields are: `thinking_supported`, `cache_telemetry_supported`, and `request_payload_mode`. Most shipped providers use the Chat Completions request payload mode. Native -Anthropic and OpenModel use Messages, and `openai-codex` uses Responses. +Messages routes, including `minimax-anthropic`, use `/v1/messages`, and +`openai-codex` uses Responses. For OpenAI-compatible gateways or self-hosted runtimes whose real window differs from the static table, set `[providers.] context_window = N`. @@ -463,6 +467,7 @@ context-pressure checks, compaction, and output-cap budgeting. | Direct Z.AI `GLM-5-Turbo` | 202,752 | 131,072 | yes | no | faster/explore sub-agent sibling | | Direct MiniMax `MiniMax-M3` | 1,000,000 | 524,288 | yes | no | not documented in code | | Direct MiniMax M2.x models | 204,800 | 4,096 fallback until MiniMax output metadata is promoted | yes | no | not documented in code | +| MiniMax Messages route (`MiniMax-M3`, `MiniMax-M2.7`) | model-specific values above | model-specific values above | yes | no | route uses `/anthropic/v1/messages` | | Generic `openai` and AtlasCloud | 128,000 | 4,096 | no in doctor capability metadata | no | not documented in code | | Ollama | 8,192 | 4,096 | no | no | not documented in code | | Hugging Face Inference Providers V4 model IDs | 131,072 | 4,096 | yes | no | not documented in code | diff --git a/web/lib/facts-drift.ts b/web/lib/facts-drift.ts index c6534f2c2..30b4f128f 100644 --- a/web/lib/facts-drift.ts +++ b/web/lib/facts-drift.ts @@ -103,6 +103,7 @@ function deriveProvidersFromConfig(cfg: string): ProviderFact[] { Zai: { id: "zai", label: "Z.ai", env: "ZAI_API_KEY / Z_AI_API_KEY" }, Stepfun: { id: "stepfun", label: "StepFun", env: "STEPFUN_API_KEY / STEP_API_KEY" }, Minimax: { id: "minimax", label: "MiniMax", env: "MINIMAX_API_KEY" }, + MinimaxAnthropic: { id: "minimax-anthropic", label: "MiniMax (Anthropic-compatible)", env: "MINIMAX_API_KEY" }, Openmodel: { id: "openmodel", label: "OpenModel", env: "OPENMODEL_API_KEY" }, Sakana: { id: "sakana", label: "Sakana AI", env: "FUGU_API_KEY / SAKANA_API_KEY" }, LongCat: { id: "longcat", label: "LongCat", env: "LONGCAT_API_KEY" }, diff --git a/web/lib/facts.generated.ts b/web/lib/facts.generated.ts index 3d8eb23df..7a3ac65a1 100644 --- a/web/lib/facts.generated.ts +++ b/web/lib/facts.generated.ts @@ -18,7 +18,7 @@ export interface RepoFacts { } export const FACTS: RepoFacts = { - "generatedAt": "2026-07-10T00:34:57.187Z", + "generatedAt": "2026-07-12T16:27:50.979Z", "version": "0.8.68", "crates": [ "agent", @@ -183,6 +183,11 @@ export const FACTS: RepoFacts = { "label": "MiniMax", "env": "MINIMAX_API_KEY" }, + { + "id": "minimax-anthropic", + "label": "MiniMax (Anthropic-compatible)", + "env": "MINIMAX_API_KEY" + }, { "id": "deepinfra", "label": "DeepInfra", diff --git a/web/scripts/facts-lib.mjs b/web/scripts/facts-lib.mjs index 1ea33ccd0..e1c59b33f 100644 --- a/web/scripts/facts-lib.mjs +++ b/web/scripts/facts-lib.mjs @@ -87,6 +87,7 @@ const PROVIDER_LABEL_MAP = { Zai: { id: "zai", label: "Z.ai", env: "ZAI_API_KEY / Z_AI_API_KEY" }, Stepfun: { id: "stepfun", label: "StepFun", env: "STEPFUN_API_KEY / STEP_API_KEY" }, Minimax: { id: "minimax", label: "MiniMax", env: "MINIMAX_API_KEY" }, + MinimaxAnthropic: { id: "minimax-anthropic", label: "MiniMax (Anthropic-compatible)", env: "MINIMAX_API_KEY" }, Openmodel: { id: "openmodel", label: "OpenModel", env: "OPENMODEL_API_KEY" }, Sakana: { id: "sakana", label: "Sakana AI", env: "FUGU_API_KEY / SAKANA_API_KEY" }, LongCat: { id: "longcat", label: "Meituan LongCat", env: "LONGCAT_API_KEY" },