feat: add MiniMax Messages provider

Register MiniMax-M3 and MiniMax-M2.7 for the OpenAI-compatible and Messages routes. Add regional endpoint guidance, request tests, catalog limits, and tier-aware pricing.
This commit is contained in:
octo-patch committed 2026-07-13 21:17:28 +08:00
1 parent 02f05b03e9
commit 47163da80f
24 files changed
+667 -63

No files matched your search

+42
View File
@@ -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();
+9
View File
@@ -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<ProviderArg> 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),
+50 -7
View File
@@ -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"], "output": ["text"] },
"limit": { "context": 1000000, "output": 524288 },
"cost": { "input": 0.30, "output": 1.20, "cache_read": 0.06 }
"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 }
}
}
},
"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",
"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 },
"cost": { "input": 0.30, "output": 1.20, "cache_read": 0.06, "cache_write": 0.375 }
}
}
},
+45
View File
@@ -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::<Vec<_>>();
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]
+25 -4
View File
@@ -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<String>,
stepfun_model: Option<String>,
minimax_base_url: Option<String>,
minimax_anthropic_base_url: Option<String>,
minimax_model: Option<String>,
deepinfra_base_url: Option<String>,
deepinfra_model: Option<String>,
@@ -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(),
+59 -12
View File
@@ -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,
+1
View File
@@ -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";
+8 -1
View File
@@ -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,
+22 -3
View File
@@ -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();
+35
View File
@@ -845,6 +845,7 @@ struct EnvGuard {
stepfun_model: Option<OsString>,
minimax_api_key: Option<OsString>,
minimax_base_url: Option<OsString>,
minimax_anthropic_base_url: Option<OsString>,
minimax_model: Option<OsString>,
sakana_api_key: Option<OsString>,
fugu_api_key: Option<OsString>,
@@ -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();
+145 -13
View File
@@ -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<N> 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;
+61 -10
View File
@@ -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"
);
}
}
+42 -8
View File
@@ -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<codewhale_config::ProviderKind>; 34] = [
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 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),
+1
View File
@@ -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";
+26
View File
@@ -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);
+1
View File
@@ -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"),
+56 -2
View File
@@ -189,6 +189,10 @@ fn known_pricing_for_model(model_lower: &str) -> Option<ModelPricing> {
// 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<ModelPricing> {
"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<ModelPricing> {
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<f64>
/// Calculate cost from provider usage in both official currencies.
#[must_use]
pub fn calculate_turn_cost_estimate_from_usage(model: &str, usage: &Usage) -> Option<CostEstimate> {
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<Cos
if cache_hit_tokens == 0 {
return None;
}
// M3's cache-read savings depend on whether total input crosses 512k;
// this helper receives only cache-hit tokens, so an estimate would guess
// the tier. The full turn-cost path has total input and remains precise.
if model.trim().eq_ignore_ascii_case("minimax-m3") {
return None;
}
let pricing = pricing_for_model(model)?;
let tokens = cache_hit_tokens as f64 / 1_000_000.0;
Some(CostEstimate {
@@ -537,6 +565,32 @@ mod tests {
}
}
#[test]
fn minimax_m3_standard_pricing_tracks_the_512k_input_boundary() {
for (input_tokens, cache_read, input, output) in
[(512_000, 0.06, 0.30, 1.20), (512_001, 0.12, 0.60, 2.40)]
{
let usage = Usage {
input_tokens,
..Usage::default()
};
let pricing = pricing_for_model_and_usage("MiniMax-M3", &usage).expect("M3 pricing");
assert_eq!(pricing.usd.input_cache_hit_per_million, cache_read);
assert_eq!(pricing.usd.input_cache_miss_per_million, input);
assert_eq!(pricing.usd.output_per_million, output);
}
assert!(calculate_cache_savings("MiniMax-M3", 1).is_none());
}
#[test]
fn minimax_m2_7_preserves_cache_read_and_write_rates() {
let pricing = pricing_for_model_at("MiniMax-M2.7", Utc::now()).expect("M2.7 pricing");
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.06);
assert_eq!(pricing.usd.input_cache_miss_per_million, 0.30);
assert_eq!(pricing.usd.output_per_million, 1.20);
assert_eq!(pricing.usd.cache_write_per_million, Some(0.375));
}
#[test]
fn curated_usd_only_models_have_pricing_and_accrue_cost() {
let usage = Usage {
+1
View File
@@ -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
+2
View File
@@ -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,
+1
View File
@@ -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.<name>]` 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.
+27 -2
View File
@@ -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.<name>] 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
+1
View File
@@ -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" },
+6 -1
View File
@@ -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",
+1
View File
@@ -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" },