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/assets/models_dev.bundled.json b/crates/config/assets/models_dev.bundled.json index 7de94bc4c..d4ee86674 100644 --- a/crates/config/assets/models_dev.bundled.json +++ b/crates/config/assets/models_dev.bundled.json @@ -6,7 +6,7 @@ "source": "Compact offline seed of verified in-repo defaults (context/output from crates/tui/src/models.rs; USD pricing from crates/tui/src/pricing.rs) for providers CodeWhale ships with. It is intentionally smaller than a full Models.dev dump; live refresh supersedes these rows on (provider, wire_model_id) identity.", "honesty": "Pricing is intentionally OMITTED where the repo does not publish a trustworthy per-token rate: DeepSeek-native rows (priced via the time-aware DeepSeek table elsewhere, kept UnknownOrStale at the route layer), aggregator-hosted DeepSeek rows (aggregator account terms, not DeepSeek Platform pricing), and Xiaomi MiMo rows (published PAYG rates apply only to sk- pay-as-you-go keys; the catalog cannot distinguish that billing surface from credit/quota Token Plan keys, so MiMo stays unpriced). Absent pricing surfaces as PricingSku::UnknownOrStale, never a fabricated zero.", "default_rows": "Each provider's `default: true` wire id equals that provider's built-in DEFAULT_*_MODEL so RouteResolver::new() and the descriptor stay in agreement when offline.", - "coverage": "14 providers, 42 chat offerings (offline seed only)." + "coverage": "15 providers, 44 chat offerings (offline seed only)." }, "models": { "deepseek-v4-pro": { @@ -144,20 +144,63 @@ "name": "MiniMax M3", "family": "minimax", "default": true, + "attachment": true, "reasoning": true, + "reasoning_options": [ + { "type": "thinking", "values": ["adaptive", "disabled"], "default": "adaptive" } + ], + "tool_call": true, + "modalities": { "input": ["text", "image", "video"], "output": ["text"] }, + "limit": { "context": 1000000, "output": 524288 } + }, + "MiniMax-M2.7": { + "id": "MiniMax-M2.7", + "name": "MiniMax M2.7", + "family": "minimax", + "reasoning": true, + "reasoning_options": [ + { "type": "thinking", "values": ["always_on"], "default": "always_on" } + ], "tool_call": true, "modalities": { "input": ["text"], "output": ["text"] }, - "limit": { "context": 1000000, "output": 524288 }, - "cost": { "input": 0.30, "output": 1.20, "cache_read": 0.06 } + "limit": { "context": 204800, "output": 204800 }, + "cost": { "input": 0.30, "output": 1.20, "cache_read": 0.06, "cache_write": 0.375 } + } + } + }, + "minimax-anthropic": { + "id": "minimax-anthropic", + "name": "MiniMax (Anthropic-compatible)", + "api": "https://api.minimax.io/anthropic", + "npm": "@ai-sdk/anthropic", + "env": ["MINIMAX_API_KEY"], + "models": { + "MiniMax-M3": { + "id": "MiniMax-M3", + "name": "MiniMax M3", + "family": "minimax", + "default": true, + "attachment": true, + "reasoning": true, + "reasoning_options": [ + { "type": "thinking", "values": ["adaptive", "disabled"], "default": "disabled" } + ], + "tool_call": true, + "modalities": { "input": ["text", "image", "video"], "output": ["text"] }, + "limit": { "context": 1000000, "output": 524288 } }, - "minimax-m2.7": { - "id": "minimax-m2.7", + "MiniMax-M2.7": { + "id": "MiniMax-M2.7", "name": "MiniMax M2.7", "family": "minimax", "reasoning": true, + "reasoning_options": [ + { "type": "thinking", "values": ["always_on"], "default": "always_on" } + ], "tool_call": true, "modalities": { "input": ["text"], "output": ["text"] }, - "limit": { "context": 204800, "output": 204800 } + "limit": { "context": 204800, "output": 204800 }, + "cost": { "input": 0.30, "output": 1.20, "cache_read": 0.06, "cache_write": 0.375 } } } }, diff --git a/crates/config/src/catalog/tests.rs b/crates/config/src/catalog/tests.rs index e64eac275..43bab7f61 100644 --- a/crates/config/src/catalog/tests.rs +++ b/crates/config/src/catalog/tests.rs @@ -587,6 +587,39 @@ fn bundled_asset_yields_real_chat_offerings_for_key_models() { let kimi = find(&rows, "moonshot", "kimi-k2.7-code"); assert_eq!(kimi.limit.as_ref().and_then(|l| l.context), Some(262_144)); + let minimax_m3 = find(&rows, "minimax-anthropic", "MiniMax-M3"); + assert_eq!( + minimax_m3.limit.as_ref().and_then(|limit| limit.context), + Some(1_000_000) + ); + let input_modalities = minimax_m3 + .modalities + .as_ref() + .expect("M3 modalities") + .input + .iter() + .map(String::as_str) + .collect::>(); + assert_eq!(input_modalities, ["text", "image", "video"]); + assert_eq!( + minimax_m3.reasoning_options[0] + .get("default") + .and_then(serde_json::Value::as_str), + Some("disabled") + ); + + let minimax_m2_7 = find(&rows, "minimax-anthropic", "MiniMax-M2.7"); + assert_eq!( + minimax_m2_7.limit.as_ref().and_then(|limit| limit.context), + Some(204_800) + ); + assert_eq!( + minimax_m2_7.reasoning_options[0] + .get("default") + .and_then(serde_json::Value::as_str), + Some("always_on") + ); + // Audio/TTS rows are absent (the asset only ships chat models, but assert // the filter contract anyway). assert!( @@ -629,6 +662,18 @@ fn bundled_asset_pricing_is_honest() { assert_eq!(cost.input, Some(1.40)); assert_eq!(cost.output, Some(4.40)); assert_eq!(cost.cache_read, Some(0.26)); + + // M3 has input-length and service tiers that the flat catalog cost shape + // cannot represent, so the bundled route row stays honestly unpriced. + let minimax_m3 = find(&rows, "minimax-anthropic", "MiniMax-M3"); + assert!(minimax_m3.cost.is_none()); + + let minimax_m2_7 = find(&rows, "minimax-anthropic", "MiniMax-M2.7"); + let cost = minimax_m2_7.cost.as_ref().expect("M2.7 is priced"); + assert_eq!(cost.input, Some(0.30)); + assert_eq!(cost.output, Some(1.20)); + assert_eq!(cost.cache_read, Some(0.06)); + assert_eq!(cost.cache_write, Some(0.375)); } #[test] 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/route/tests.rs b/crates/config/src/route/tests.rs index 1e54dfc10..39874a9e1 100644 --- a/crates/config/src/route/tests.rs +++ b/crates/config/src/route/tests.rs @@ -161,9 +161,10 @@ fn descriptor_protocol_matches_provider_wire() { ); let expected = match kind { ProviderKind::OpenaiCodex => RequestProtocol::Responses, - ProviderKind::DeepseekAnthropic | ProviderKind::Anthropic | ProviderKind::Openmodel => { - RequestProtocol::AnthropicMessages - } + ProviderKind::DeepseekAnthropic + | ProviderKind::Anthropic + | ProviderKind::MinimaxAnthropic + | ProviderKind::Openmodel => RequestProtocol::AnthropicMessages, _ => RequestProtocol::ChatCompletions, }; assert_eq!(d.protocol(), expected, "{kind:?} protocol mismatch"); @@ -362,6 +363,24 @@ fn resolver_carries_models_dev_limits_into_ready_candidate() { assert!(out.limits.has_known_limit()); } +#[test] +fn minimax_anthropic_routes_use_catalog_limits_and_messages_protocol() { + let resolver = RouteResolver::new(); + + for (model, context) in [("MiniMax-M3", 1_000_000), ("MiniMax-M2.7", 204_800)] { + let route = resolver + .resolve(&req(Some(ProviderKind::MinimaxAnthropic), Some(model))) + .expect("MiniMax Messages route should resolve"); + + assert_eq!(route.provider_kind, ProviderKind::MinimaxAnthropic); + assert_eq!(route.wire_model_id.as_str(), model); + assert_eq!(route.protocol, RequestProtocol::AnthropicMessages); + assert_eq!(route.endpoint.protocol, RequestProtocol::AnthropicMessages); + assert_eq!(route.endpoint.base_url, "https://api.minimax.io/anthropic"); + assert_eq!(route.limits.context_tokens, Some(context)); + } +} + #[test] fn resolver_keeps_limits_provider_scoped_for_same_canonical_model() { let r = models_dev_route_resolver(); 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 7e20dbf95..004b89e4c 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -935,7 +935,10 @@ 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 ) } @@ -1987,10 +1990,12 @@ pub(super) fn apply_reasoning_effort( // #3024: Ollama OpenAI-compat endpoint accepts think param. body["think"] = json!(false); } - ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic | ApiProvider::Openmodel => { - // #3014: thinking/effort shaping happens natively inside - // client/anthropic.rs (adaptive thinking + output_config), - // not via OpenAI-dialect fields. + ApiProvider::Anthropic + | ApiProvider::DeepseekAnthropic + | ApiProvider::MinimaxAnthropic + | ApiProvider::Openmodel => { + // Thinking shaping happens in the Messages adapter, which + // applies each provider's supported control fields. } ApiProvider::NvidiaNim => { body["chat_template_kwargs"] = json!({ @@ -2072,10 +2077,12 @@ pub(super) fn apply_reasoning_effort( // #3024: Ollama think param. body["think"] = json!(true); } - ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic | ApiProvider::Openmodel => { - // #3014: thinking/effort shaping happens natively inside - // client/anthropic.rs (adaptive thinking + output_config), - // not via OpenAI-dialect fields. + ApiProvider::Anthropic + | ApiProvider::DeepseekAnthropic + | ApiProvider::MinimaxAnthropic + | ApiProvider::Openmodel => { + // Thinking shaping happens in the Messages adapter, which + // applies each provider's supported control fields. } ApiProvider::NvidiaNim => { body["chat_template_kwargs"] = json!({ @@ -2144,10 +2151,12 @@ pub(super) fn apply_reasoning_effort( // #3024: Ollama think param. body["think"] = json!(true); } - ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic | ApiProvider::Openmodel => { - // #3014: thinking/effort shaping happens natively inside - // client/anthropic.rs (adaptive thinking + output_config), - // not via OpenAI-dialect fields. + ApiProvider::Anthropic + | ApiProvider::DeepseekAnthropic + | ApiProvider::MinimaxAnthropic + | ApiProvider::Openmodel => { + // Thinking shaping happens in the Messages adapter, which + // applies each provider's supported control fields. } ApiProvider::NvidiaNim => { body["chat_template_kwargs"] = json!({ @@ -2379,6 +2388,24 @@ mod tests { .expect("deepseek anthropic client") } + fn minimax_anthropic_client_with_base_url(base_url: String) -> 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(base_url), + ..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 { @@ -2623,6 +2650,14 @@ mod tests { api_url("https://api.deepseek.com/beta", "models"), "https://api.deepseek.com/v1/models" ); + assert_eq!( + api_url("https://api.minimax.io/anthropic", "models"), + "https://api.minimax.io/anthropic/v1/models" + ); + assert_eq!( + api_url("https://api.minimaxi.com/anthropic", "models"), + "https://api.minimaxi.com/anthropic/v1/models" + ); // explicit v versions other than /v1 should be preserved assert_eq!( api_url( @@ -2856,6 +2891,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(); @@ -2955,6 +3015,78 @@ mod tests { ); } + #[tokio::test] + async fn minimax_anthropic_health_check_uses_models_endpoint() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/anthropic/v1/models")) + .and(header("x-api-key", "minimax-test")) + .and(header("anthropic-version", "2023-06-01")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({"data": []}))) + .expect(1) + .mount(&server) + .await; + let client = minimax_anthropic_client_with_base_url(format!("{}/anthropic", server.uri())); + + assert!(client.health_check().await.expect("health check")); + } + + #[tokio::test] + async fn minimax_anthropic_request_uses_messages_endpoint() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/anthropic/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_with_base_url(format!("{}/anthropic", server.uri())); + 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); + let requests = server.received_requests().await.expect("recorded requests"); + let body: Value = serde_json::from_slice(&requests[0].body).expect("request JSON"); + assert_eq!( + body.pointer("/thinking/type").and_then(Value::as_str), + Some("disabled") + ); + assert!(body.get("output_config").is_none(), "{body}"); + } + #[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 01c63a96d..e0b98960e 100644 --- a/crates/tui/src/client/anthropic.rs +++ b/crates/tui/src/client/anthropic.rs @@ -27,6 +27,7 @@ use anyhow::{Context, Result}; use serde_json::{Value, json}; +use crate::config::ApiProvider; use crate::llm_client::StreamEventBox; use crate::logging; use crate::models::{ContentBlock, MessageRequest, MessageResponse, StreamEvent, Usage}; @@ -116,25 +117,31 @@ impl DeepSeekClient { body["tool_choice"] = anthropic_tool_choice(tool_choice); } - // Thinking + effort shaping. "off" omits thinking entirely; any other - // tier enables adaptive thinking, with `output_config.effort` only on - // models the capability matrix marks as thinking-capable. + // Thinking + effort shaping. MiniMax supports adaptive/disabled but + // not Anthropic's output_config effort field; native Anthropic routes + // keep the existing effort mapping. let thinking_capable = crate::models::model_supports_reasoning(&request.model); + let is_minimax = self.api_provider == ApiProvider::MinimaxAnthropic; let effort = request .reasoning_effort .as_deref() .map(|raw| raw.trim().to_ascii_lowercase()); match effort.as_deref() { + Some("off" | "disabled" | "none" | "false") if is_minimax && thinking_capable => { + body["thinking"] = json!({ "type": "disabled" }); + } Some("off" | "disabled" | "none" | "false") => {} Some(level) if thinking_capable => { body["thinking"] = json!({ "type": "adaptive" }); - let mapped = match level { - "low" | "minimal" => "low", - "medium" | "mid" => "medium", - "max" | "xhigh" | "highest" => "max", - _ => "high", - }; - body["output_config"] = json!({ "effort": mapped }); + if !is_minimax { + let mapped = match level { + "low" | "minimal" => "low", + "medium" | "mid" => "medium", + "max" | "xhigh" | "highest" => "max", + _ => "high", + }; + body["output_config"] = json!({ "effort": mapped }); + } } None if thinking_capable => { body["thinking"] = json!({ "type": "adaptive" }); @@ -620,6 +627,22 @@ mod tests { DeepSeekClient::new(&config).expect("anthropic client constructs") } + fn minimax_test_client() -> DeepSeekClient { + let _ = rustls::crypto::ring::default_provider().install_default(); + let config = crate::config::Config { + provider: Some("minimax-anthropic".to_string()), + providers: Some(crate::config::ProvidersConfig { + minimax_anthropic: crate::config::ProviderConfig { + api_key: Some("test-key".to_string()), + ..Default::default() + }, + ..Default::default() + }), + ..Default::default() + }; + DeepSeekClient::new(&config).expect("MiniMax Messages client constructs") + } + #[test] fn body_keeps_native_cache_control_on_system_and_tools() { let client = test_client(); @@ -706,6 +729,26 @@ mod tests { assert!(body.get("output_config").is_none(), "{body}"); } + #[test] + fn minimax_body_uses_supported_thinking_controls() { + let client = minimax_test_client(); + let body = + client.build_anthropic_body(&request_with("MiniMax-M3", Some("off"), None, None), true); + assert_eq!( + body.pointer("/thinking/type").and_then(Value::as_str), + Some("disabled") + ); + assert!(body.get("output_config").is_none(), "{body}"); + + let body = client + .build_anthropic_body(&request_with("MiniMax-M3", Some("high"), None, None), true); + assert_eq!( + body.pointer("/thinking/type").and_then(Value::as_str), + Some("adaptive") + ); + assert!(body.get("output_config").is_none(), "{body}"); + } + #[test] fn body_drops_sampling_params_for_models_that_reject_them() { let client = test_client(); @@ -992,5 +1035,13 @@ 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" + ); + assert_eq!( + anthropic_messages_url("https://api.minimaxi.com/anthropic"), + "https://api.minimaxi.com/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 4f02197ba..7651240b0 100644 --- a/crates/tui/src/pricing.rs +++ b/crates/tui/src/pricing.rs @@ -189,6 +189,10 @@ 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-M3 uses the lower standard tier for metadata-only lookups; + // cost estimation selects the correct tier from total input usage. + "minimax-m3" => Some(minimax_m3_standard_pricing(false)), + "minimax-m2.7" => Some(usd_pricing_with_write(0.06, 0.30, 1.20, 0.375)), // 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 @@ -213,7 +217,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 @@ -289,6 +292,25 @@ fn usd_pricing( } } +const MINIMAX_M3_LONG_CONTEXT_THRESHOLD: u32 = 512_000; + +fn minimax_m3_standard_pricing(long_context: bool) -> ModelPricing { + if long_context { + usd_only_pricing(0.12, 0.60, 2.40) + } else { + usd_only_pricing(0.06, 0.30, 1.20) + } +} + +fn pricing_for_model_and_usage(model: &str, usage: &Usage) -> Option { + if model.trim().eq_ignore_ascii_case("minimax-m3") { + return Some(minimax_m3_standard_pricing( + usage.input_tokens > MINIMAX_M3_LONG_CONTEXT_THRESHOLD, + )); + } + pricing_for_model(model) +} + /// Claude Sonnet 5 pricing (https://platform.claude.com/docs/en/about-claude/pricing): /// introductory 2.00 / 10.00 (cache-read 0.20, cache-write 2.50) through /// 2026-08-31 UTC, then the standard 3.00 / 15.00 (cache-read 0.30, @@ -349,7 +371,7 @@ pub fn calculate_turn_cost_from_usage(model: &str, usage: &Usage) -> Option /// Calculate cost from provider usage in both official currencies. #[must_use] pub fn calculate_turn_cost_estimate_from_usage(model: &str, usage: &Usage) -> Option { - let pricing = pricing_for_model(model)?; + let pricing = pricing_for_model_and_usage(model, usage)?; Some(CostEstimate { usd: calculate_turn_cost_from_usage_with_pricing(pricing.usd, usage), cny: pricing @@ -452,6 +474,12 @@ pub fn calculate_cache_savings(model: &str, cache_hit_tokens: u32) -> Option 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 495720508..1cb76aa9a 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -11971,6 +11971,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, @@ -12096,6 +12097,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..d9be3e2f7 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. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled. - `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..0793c1cf1 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. Official M3 input modalities are text, image, and video; M2.7 is text-only. | +| `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. Keep the `/anthropic` suffix because CodeWhale appends `/v1/messages`; the route uses `x-api-key`. M3 supports adaptive or disabled thinking. M2.7 always keeps thinking enabled. | | `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,11 +467,32 @@ 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 | | Other recognized DeepSeek model IDs | 128,000 unless the model name carries an explicit `Nk` hint | 4,096 | no unless V4/reasoner logic matches | DeepSeek/NIM only | DeepSeek beta only | +MiniMax M3 uses input-length and service tiers. CodeWhale omits +`service_tier`, so requests use the standard tier and cost estimates select the +correct standard rate from total input usage. Priority rates are listed to keep +the official tier structure visible. Prices are USD per million tokens. + +| Model / service tier | Input length | Input | Output | Cache read | Cache write | +| --- | --- | ---: | ---: | ---: | ---: | +| `MiniMax-M3` standard | up to 512,000 input tokens | $0.30 | $1.20 | $0.06 | not published | +| `MiniMax-M3` standard | over 512,000 input tokens | $0.60 | $2.40 | $0.12 | not published | +| `MiniMax-M3` priority | up to 512,000 input tokens | $0.45 | $1.80 | $0.09 | not published | +| `MiniMax-M3` priority | over 512,000 input tokens | $0.90 | $3.60 | $0.18 | not published | +| `MiniMax-M2.7` standard | all supported inputs | $0.30 | $1.20 | $0.06 | $0.375 | + +These values come from the [MiniMax pay-as-you-go pricing +guide](https://platform.minimax.io/docs/guides/pricing-paygo). M3 thinking is +adaptive or disabled; the OpenAI-compatible API defaults to adaptive and the +Anthropic-compatible API defaults to disabled. M2.7 thinking cannot be +disabled. CodeWhale sends explicit controls when the user selects a reasoning +mode. + Tool-call support is tracked separately by the static `ModelRegistry` and by the endpoint's ability to accept OpenAI-compatible `tools` payloads. A custom OpenAI-compatible or local endpoint can still reject tool calls even if 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" },