mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 17:10:44 -10:00
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:
24 files changed
+667
-63
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user