Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions crates/agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
55 changes: 49 additions & 6 deletions crates/config/assets/models_dev.bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -144,20 +144,63 @@
"name": "MiniMax M3",
"family": "minimax",
"default": true,
"attachment": true,
"reasoning": true,
"reasoning_options": [
{ "type": "thinking", "values": ["adaptive", "disabled"], "default": "adaptive" }
],
"tool_call": true,
"modalities": { "input": ["text", "image", "video"], "output": ["text"] },
"limit": { "context": 1000000, "output": 524288 }
},
"MiniMax-M2.7": {
"id": "MiniMax-M2.7",
"name": "MiniMax M2.7",
"family": "minimax",
"reasoning": true,
"reasoning_options": [
{ "type": "thinking", "values": ["always_on"], "default": "always_on" }
],
"tool_call": true,
"modalities": { "input": ["text"], "output": ["text"] },
"limit": { "context": 1000000, "output": 524288 },
"cost": { "input": 0.30, "output": 1.20, "cache_read": 0.06 }
"limit": { "context": 204800, "output": 204800 },
"cost": { "input": 0.30, "output": 1.20, "cache_read": 0.06, "cache_write": 0.375 }
}
}
},
"minimax-anthropic": {
"id": "minimax-anthropic",
"name": "MiniMax (Anthropic-compatible)",
"api": "https://api.minimax.io/anthropic",
"npm": "@ai-sdk/anthropic",
"env": ["MINIMAX_API_KEY"],
"models": {
"MiniMax-M3": {
"id": "MiniMax-M3",
"name": "MiniMax M3",
"family": "minimax",
"default": true,
"attachment": true,
"reasoning": true,
"reasoning_options": [
{ "type": "thinking", "values": ["adaptive", "disabled"], "default": "disabled" }
],
"tool_call": true,
"modalities": { "input": ["text", "image", "video"], "output": ["text"] },
"limit": { "context": 1000000, "output": 524288 }
},
"minimax-m2.7": {
"id": "minimax-m2.7",
"MiniMax-M2.7": {
"id": "MiniMax-M2.7",
"name": "MiniMax M2.7",
"family": "minimax",
"reasoning": true,
"reasoning_options": [
{ "type": "thinking", "values": ["always_on"], "default": "always_on" }
],
"tool_call": true,
"modalities": { "input": ["text"], "output": ["text"] },
"limit": { "context": 204800, "output": 204800 }
"limit": { "context": 204800, "output": 204800 },
"cost": { "input": 0.30, "output": 1.20, "cache_read": 0.06, "cache_write": 0.375 }
}
}
},
Expand Down
45 changes: 45 additions & 0 deletions crates/config/src/catalog/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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]
Expand Down
29 changes: 25 additions & 4 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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();
}
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
Loading