feat: add Mistral AI as a first-class provider route

Wire Mistral AI / la Plateforme into the shared provider registry, TUI
provider enum, provider-scoped config/env overrides, static model
registry, context-window metadata, reasoning wiring, docs, and
examples. The route uses Mistral's OpenAI-compatible Chat Completions
endpoint at https://api.mistral.ai/v1 with 'mistral-code-latest' as
the default model (Codestral coding model, 256K context).

Model IDs verified live against https://api.mistral.ai/v1/models: the
static registry ships 'mistral-code-latest' (accepts 'codestral-latest'
as alias for backward compatibility), 'mistral-medium-latest',
'mistral-small-latest', 'magistral-small-latest', and
'mistral-large-latest'. All models report 262144 (256K) context on
/v1/models except mistral-code-latest at 256000; earlier drafts of
this PR had those windows reversed.

Reasoning is wired end-to-end for the three models that advertise
'reasoning: true' on /v1/models — mistral-medium-latest,
mistral-small-latest, and magistral-small-latest. Codewhale sends
'reasoning_effort' (Mistral currently accepts 'none' or 'high' only;
intermediate tiers return HTTP 400 code 3051), parses the polymorphic
'content: [{type: thinking, thinking: [{type: text, text: ...}],
closed: bool}, {type: text, text: ...}]' shape emitted by reasoning
models, and replays the thinking trace back into multi-turn history
per docs.mistral.ai/capabilities/reasoning. Non-reasoning models
(mistral-code-latest, mistral-large-latest) never receive the field
because Mistral would reject it. FIM (/v1/fim/completions) is not
wired.

Provider aliases: mistral-ai, mistralai, la-plateforme. Env vars:
MISTRAL_API_KEY, MISTRAL_BASE_URL, MISTRAL_MODEL. Auth via API key
from https://console.mistral.ai/api-keys, config, or 'codewhale auth
set'.

Test env-poisoning: EnvGuard captures/removes/restores MISTRAL_* so
tests stay reproducible when a user has these vars exported in their
shell.

Validation:
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features --locked (with
  the documented allow list) -- No issues found
- cargo test --workspace --all-features --locked -- 22 pre-existing
  failures in crates/tui git-shell tests (worktree init failing on
  'git commit' in isolated tempdirs), verified identical count on
  origin/main at 91bca01a9 and unrelated to this change
- python3 scripts/check-provider-registry.py -- passed
- codewhale --provider mistral --model mistral-medium-latest exec
  against api.mistral.ai returned a correct reasoning-mode response
- codewhale --provider mistral --model mistral-large-latest exec
  succeeded without HTTP 400 code 3051 (verifies the model-aware
  reasoning gate)
- TUI smoke previously validated: /status shows mistral +
  mistral-code-latest, /provider lists Mistral, tool call end-to-end

Assisted by Codex CLI for implementation and multiple Oracle review
passes (correctness + convention + Hunter's inline review) that
surfaced the ProviderArg clap enum gap, the ModelRegistry silent
fallthrough to DeepSeek, the Codestral context-window regression, the
EnvGuard env-poisoning flake, and the model-ID / context-window /
reasoning-support mistakes from the initial docs-slug pass now
corrected against the live /v1/models catalog.
This commit is contained in:
Xavier Pestel
2026-08-08 15:06:22 +02:00
parent aa47e142c9
commit f157d34a51
20 changed files with 681 additions and 37 deletions
+3
View File
@@ -19,3 +19,6 @@
# AtlasCloud OpenAI-compatible endpoint
# ATLASCLOUD_API_KEY=
# Mistral AI (la Plateforme) — https://console.mistral.ai/api-keys
# MISTRAL_API_KEY=
+24
View File
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Mistral AI (la Plateforme) is now a first-class OpenAI-compatible provider
route. Select it with `provider = "mistral"`, `CODEWHALE_PROVIDER=mistral`,
or `codewhale --provider mistral`. Aliases `mistral-ai`, `mistralai`, and
`la-plateforme` resolve to the same route. Authenticate with
`MISTRAL_API_KEY` (get one at <https://console.mistral.ai/api-keys>), or
via `[providers.mistral].api_key` / `codewhale auth set --provider mistral`.
Endpoint defaults to `https://api.mistral.ai/v1`; model defaults to
`mistral-code-latest` (Codestral coding model, 256K context; the historical
`codestral-latest` slug is accepted as an alias). The static model registry
ships `mistral-code-latest`, `mistral-medium-latest`, `mistral-small-latest`,
`magistral-small-latest`, and `mistral-large-latest` (all 256K context).
Reasoning is wired end-to-end for `mistral-medium-latest`,
`mistral-small-latest`, and `magistral-small-latest`: Codewhale sends
`reasoning_effort` (Mistral currently accepts `none` or `high` only —
intermediate tiers get HTTP 400 code 3051), parses the polymorphic
`content: [{type: thinking, thinking: [{type: text, text: ...}], closed:
bool}, {type: text, text: ...}]` shape emitted during reasoning, and
replays the thinking trace back into multi-turn history per
docs.mistral.ai/capabilities/reasoning. Non-reasoning models
(`mistral-code-latest`, `mistral-large-latest`) never receive the field
because Mistral would reject it. FIM (`/v1/fim/completions`) is not wired.
## [0.9.5] - 2026-08-08
Codewhale v0.9.5 consolidates the terminal application into one compiled
+9
View File
@@ -734,6 +734,15 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://api.x.ai/v1"
# model = "grok-4.5" # or grok-4.3, grok-build
# Mistral AI — la Plateforme (https://console.mistral.ai/)
# OpenAI-compatible Chat Completions route.
# Provider aliases: mistral, mistral-ai, mistralai, la-plateforme
# Env var aliases: MISTRAL_API_KEY, MISTRAL_BASE_URL, MISTRAL_MODEL
[providers.mistral]
# api_key = "YOUR_MISTRAL_API_KEY"
# base_url = "https://api.mistral.ai/v1"
# model = "mistral-code-latest" # or mistral-medium-latest, mistral-small-latest, magistral-small-latest, mistral-large-latest
# ─────────────────────────────────────────────────────────────────────────────────
# Alibaba Cloud Model Studio — Token Plan
# (https://bailian.console.aliyun.com/)
+45
View File
@@ -1168,6 +1168,51 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: false,
},
ModelInfo {
id: "mistral-code-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec![
"codestral".to_string(),
"codestral-latest".to_string(),
"mistral-code".to_string(),
],
supports_tools: true,
supports_reasoning: false,
},
ModelInfo {
id: "mistral-medium-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec![
"mistral-medium".to_string(),
"mistral-medium-3-5".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "mistral-small-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec![
"mistral-small".to_string(),
"mistral-small-2603".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "magistral-small-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec!["magistral".to_string(), "magistral-small".to_string()],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "mistral-large-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec!["mistral-large".to_string()],
supports_tools: true,
supports_reasoning: false,
},
];
Self::new(models)
}
+11 -2
View File
@@ -99,6 +99,14 @@ enum ProviderArg {
Meta,
#[value(alias = "x-ai", alias = "x_ai", alias = "grok")]
Xai,
#[value(
alias = "mistral-ai",
alias = "mistral_ai",
alias = "mistralai",
alias = "la-plateforme",
alias = "la_plateforme"
)]
Mistral,
}
impl From<ProviderArg> for ProviderKind {
@@ -137,6 +145,7 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::OpencodeZen => ProviderKind::OpencodeZen,
ProviderArg::Meta => ProviderKind::Meta,
ProviderArg::Xai => ProviderKind::Xai,
ProviderArg::Mistral => ProviderKind::Mistral,
}
}
}
@@ -7864,8 +7873,8 @@ mod tests {
.map(|provider| provider.kind())
.collect();
// Full registry keeps legacy dialect/plan kinds; ALL is the catalog surface.
assert_eq!(registry_kinds.len(), 41);
assert_eq!(ProviderKind::ALL.len(), 36);
assert_eq!(registry_kinds.len(), 42);
assert_eq!(ProviderKind::ALL.len(), 37);
for kind in ProviderKind::ALL {
assert!(
registry_kinds.contains(&kind),
+25
View File
@@ -400,6 +400,16 @@ pub struct ProvidersToml {
alias = "grok"
)]
pub xai: ProviderConfigToml,
#[serde(
default,
skip_serializing_if = "ProviderConfigToml::is_empty",
alias = "mistral-ai",
alias = "mistral_ai",
alias = "mistralai",
alias = "la-plateforme",
alias = "la_plateforme"
)]
pub mistral: ProviderConfigToml,
/// Jiangsu Telecom TokenHub — OpenAI-compatible AI gateway.
#[serde(
default,
@@ -627,6 +637,7 @@ impl ProvidersToml {
ProviderKind::OpencodeZen => &self.opencode_zen,
ProviderKind::Meta => &self.meta,
ProviderKind::Xai => &self.xai,
ProviderKind::Mistral => &self.mistral,
ProviderKind::Telecomjs => &self.telecomjs,
ProviderKind::ModelstudioTokenPlan => &self.modelstudio_token_plan,
ProviderKind::ModelstudioTokenPlanAnthropic => &self.modelstudio_token_plan_anthropic,
@@ -673,6 +684,7 @@ impl ProvidersToml {
ProviderKind::OpencodeZen => &mut self.opencode_zen,
ProviderKind::Meta => &mut self.meta,
ProviderKind::Xai => &mut self.xai,
ProviderKind::Mistral => &mut self.mistral,
ProviderKind::Telecomjs => &mut self.telecomjs,
ProviderKind::ModelstudioTokenPlan => &mut self.modelstudio_token_plan,
ProviderKind::ModelstudioTokenPlanAnthropic => {
@@ -3191,6 +3203,7 @@ impl ConfigToml {
ProviderKind::OpencodeZen => DEFAULT_OPENCODE_ZEN_BASE_URL.to_string(),
ProviderKind::Meta => DEFAULT_META_BASE_URL.to_string(),
ProviderKind::Xai => DEFAULT_XAI_BASE_URL.to_string(),
ProviderKind::Mistral => DEFAULT_MISTRAL_BASE_URL.to_string(),
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL.to_string(),
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
@@ -4251,6 +4264,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::OpencodeZen => DEFAULT_OPENCODE_ZEN_MODEL,
ProviderKind::Meta => DEFAULT_META_MODEL,
ProviderKind::Xai => DEFAULT_XAI_MODEL,
ProviderKind::Mistral => DEFAULT_MISTRAL_MODEL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_MODEL,
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
@@ -4298,6 +4312,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::OpencodeZen => DEFAULT_OPENCODE_ZEN_BASE_URL,
ProviderKind::Meta => DEFAULT_META_BASE_URL,
ProviderKind::Xai => DEFAULT_XAI_BASE_URL,
ProviderKind::Mistral => DEFAULT_MISTRAL_BASE_URL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
ProviderKind::ModelstudioTokenPlan => DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
ProviderKind::ModelstudioTokenPlanAnthropic => MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
@@ -6554,6 +6569,8 @@ struct EnvRuntimeOverrides {
meta_model: Option<String>,
xai_base_url: Option<String>,
xai_model: Option<String>,
mistral_base_url: Option<String>,
mistral_model: Option<String>,
telecomjs_base_url: Option<String>,
telecomjs_model: Option<String>,
modelstudio_token_plan_base_url: Option<String>,
@@ -6858,6 +6875,12 @@ impl EnvRuntimeOverrides {
xai_model: std::env::var("XAI_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
mistral_base_url: std::env::var("MISTRAL_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
mistral_model: std::env::var("MISTRAL_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
telecomjs_base_url: std::env::var("TELECOMJS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
@@ -6958,6 +6981,7 @@ impl EnvRuntimeOverrides {
ProviderKind::OpencodeZen => self.opencode_zen_base_url.clone(),
ProviderKind::Meta => self.meta_base_url.clone(),
ProviderKind::Xai => self.xai_base_url.clone(),
ProviderKind::Mistral => self.mistral_base_url.clone(),
ProviderKind::Telecomjs => self.telecomjs_base_url.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
self.modelstudio_token_plan_base_url.clone()
@@ -7000,6 +7024,7 @@ impl EnvRuntimeOverrides {
ProviderKind::OpencodeZen => self.opencode_zen_model.clone(),
ProviderKind::Meta => self.meta_model.clone(),
ProviderKind::Xai => self.xai_model.clone(),
ProviderKind::Mistral => self.mistral_model.clone(),
ProviderKind::Telecomjs => self.telecomjs_model.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
self.modelstudio_token_plan_model.clone()
+39 -19
View File
@@ -12,24 +12,25 @@ use super::{
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_ANTHROPIC_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL,
DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
DEFAULT_MODELSTUDIO_TOKEN_PLAN_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_OPENCODE_GO_BASE_URL, DEFAULT_OPENCODE_GO_MODEL,
DEFAULT_OPENCODE_ZEN_BASE_URL, DEFAULT_OPENCODE_ZEN_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_TELECOMJS_BASE_URL, DEFAULT_TELECOMJS_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, MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL,
MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL, ProviderKind,
DEFAULT_MISTRAL_BASE_URL, DEFAULT_MISTRAL_MODEL, DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_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_OPENCODE_GO_BASE_URL, DEFAULT_OPENCODE_GO_MODEL, DEFAULT_OPENCODE_ZEN_BASE_URL,
DEFAULT_OPENCODE_ZEN_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_TELECOMJS_BASE_URL, DEFAULT_TELECOMJS_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,
MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL, MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
ProviderKind,
};
/// Wire protocol spoken by a provider.
@@ -391,6 +392,12 @@ pub const fn credential_help(kind: ProviderKind) -> CredentialHelp {
docs_url: None,
guidance: "Use an xAI Console API key or Codewhale's native device login. Reading an existing Grok CLI file requires explicit provider-scoped read-only consent.",
},
ProviderKind::Mistral => CredentialHelp {
acquisition: ApiKey,
credential_url: Some("https://console.mistral.ai/api-keys"),
docs_url: Some("https://docs.mistral.ai/"),
guidance: "Create a Mistral API key in the Mistral Console (la Plateforme).",
},
ProviderKind::Telecomjs => CredentialHelp {
acquisition: ApiKey,
credential_url: Some("https://aigw.telecomjs.com/"),
@@ -905,6 +912,17 @@ provider!(
"qianfan",
aliases: ["baidu-qianfan", "baidu_qianfan", "baidu"]
);
provider!(
Mistral,
Mistral,
"mistral",
"Mistral AI",
DEFAULT_MISTRAL_BASE_URL,
DEFAULT_MISTRAL_MODEL,
["MISTRAL_API_KEY"],
"mistral",
aliases: ["mistral-ai", "mistral_ai", "mistralai", "la-plateforme", "la_plateforme"]
);
/// OpenAI Codex / ChatGPT OAuth provider using the Responses API.
pub struct OpenaiCodex;
@@ -1523,6 +1541,7 @@ static OPENCODE_GO: OpencodeGo = OpencodeGo;
static OPENCODE_ZEN: OpencodeZen = OpencodeZen;
static META: Meta = Meta;
static XAI: Xai = Xai;
static MISTRAL: Mistral = Mistral;
static TELECOMJS: Telecomjs = Telecomjs;
static MODELSTUDIO_TOKEN_PLAN: ModelstudioTokenPlan = ModelstudioTokenPlan;
static MODELSTUDIO_TOKEN_PLAN_ANTHROPIC: ModelstudioTokenPlanAnthropic =
@@ -1532,7 +1551,7 @@ static MODELSTUDIO_CODING_PLAN_ANTHROPIC: ModelstudioCodingPlanAnthropic =
ModelstudioCodingPlanAnthropic;
static CUSTOM: Custom = Custom;
static PROVIDER_REGISTRY: [&dyn Provider; 41] = [
static PROVIDER_REGISTRY: [&dyn Provider; 42] = [
&DEEPSEEK,
&DEEPSEEK_ANTHROPIC,
&NVIDIA_NIM,
@@ -1568,6 +1587,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 41] = [
&OPENCODE_ZEN,
&META,
&XAI,
&MISTRAL,
&TELECOMJS,
&MODELSTUDIO_TOKEN_PLAN,
&MODELSTUDIO_TOKEN_PLAN_ANTHROPIC,
+3
View File
@@ -167,6 +167,9 @@ pub(crate) const DEFAULT_META_BASE_URL: &str = "https://api.meta.ai/v1";
// xAI / Grok API-key route defaults
pub(crate) const DEFAULT_XAI_MODEL: &str = "grok-4.5";
pub(crate) const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1";
// Mistral AI (la Plateforme) defaults
pub(crate) const DEFAULT_MISTRAL_MODEL: &str = "mistral-code-latest";
pub(crate) const DEFAULT_MISTRAL_BASE_URL: &str = "https://api.mistral.ai/v1";
// TelecomJS (Jiangsu Telecom TokenHub) defaults
pub(crate) const DEFAULT_TELECOMJS_MODEL: &str = "deepseek-v4-pro";
pub(crate) const DEFAULT_TELECOMJS_BASE_URL: &str = "https://aigw.telecomjs.com/v1";
+11 -1
View File
@@ -131,6 +131,15 @@ pub enum ProviderKind {
Meta,
#[serde(alias = "x-ai", alias = "x_ai", alias = "grok")]
Xai,
/// Mistral AI — la Plateforme (OpenAI-compatible Chat Completions).
#[serde(
alias = "mistral-ai",
alias = "mistral_ai",
alias = "mistralai",
alias = "la-plateforme",
alias = "la_plateforme"
)]
Mistral,
/// Jiangsu Telecom TokenHub (OpenAI-compatible).
///
/// An AI gateway operated by Jiangsu Telecom that speaks the OpenAI Chat
@@ -195,7 +204,7 @@ impl ProviderKind {
/// stay on the enum for serde and `provider_for_kind`, but they are not
/// first-class catalog rows. Plan is `mode` / base_url; dialect is
/// `wire = openai|anthropic` on the primary provider config.
pub const ALL: [Self; 36] = [
pub const ALL: [Self; 37] = [
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
@@ -229,6 +238,7 @@ impl ProviderKind {
Self::OpencodeZen,
Self::Meta,
Self::Xai,
Self::Mistral,
Self::Telecomjs,
Self::ModelstudioTokenPlan,
Self::Custom,
+76 -2
View File
@@ -1157,6 +1157,9 @@ struct EnvGuard {
xai_api_key: Option<OsString>,
xai_base_url: Option<OsString>,
xai_model: Option<OsString>,
mistral_api_key: Option<OsString>,
mistral_base_url: Option<OsString>,
mistral_model: Option<OsString>,
telecomjs_api_key: Option<OsString>,
telecomjs_base_url: Option<OsString>,
telecomjs_model: Option<OsString>,
@@ -1193,6 +1196,9 @@ impl EnvGuard {
xai_api_key: env::var_os("XAI_API_KEY"),
xai_base_url: env::var_os("XAI_BASE_URL"),
xai_model: env::var_os("XAI_MODEL"),
mistral_api_key: env::var_os("MISTRAL_API_KEY"),
mistral_base_url: env::var_os("MISTRAL_BASE_URL"),
mistral_model: env::var_os("MISTRAL_MODEL"),
telecomjs_api_key: env::var_os("TELECOMJS_API_KEY"),
telecomjs_base_url: env::var_os("TELECOMJS_BASE_URL"),
telecomjs_model: env::var_os("TELECOMJS_MODEL"),
@@ -1322,6 +1328,9 @@ impl EnvGuard {
env::remove_var("XAI_API_KEY");
env::remove_var("XAI_BASE_URL");
env::remove_var("XAI_MODEL");
env::remove_var("MISTRAL_API_KEY");
env::remove_var("MISTRAL_BASE_URL");
env::remove_var("MISTRAL_MODEL");
env::remove_var("TELECOMJS_API_KEY");
env::remove_var("TELECOMJS_BASE_URL");
env::remove_var("TELECOMJS_MODEL");
@@ -1474,6 +1483,9 @@ impl Drop for EnvGuard {
Self::restore_var("XAI_API_KEY", self.xai_api_key.take());
Self::restore_var("XAI_BASE_URL", self.xai_base_url.take());
Self::restore_var("XAI_MODEL", self.xai_model.take());
Self::restore_var("MISTRAL_API_KEY", self.mistral_api_key.take());
Self::restore_var("MISTRAL_BASE_URL", self.mistral_base_url.take());
Self::restore_var("MISTRAL_MODEL", self.mistral_model.take());
Self::restore_var("TELECOMJS_API_KEY", self.telecomjs_api_key.take());
Self::restore_var("TELECOMJS_BASE_URL", self.telecomjs_base_url.take());
Self::restore_var("TELECOMJS_MODEL", self.telecomjs_model.take());
@@ -4113,6 +4125,21 @@ fn provider_kind_parses_openrouter_and_novita_aliases() {
assert_eq!(parsed.provider, ProviderKind::Sakana);
}
for alias in [
"mistral",
"mistral-ai",
"mistral_ai",
"mistralai",
"la-plateforme",
"la_plateforme",
] {
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::Mistral));
let parsed: ConfigToml =
toml::from_str(&format!("provider = \"{alias}\"")).expect("mistral alias");
assert_eq!(parsed.provider, ProviderKind::Mistral);
}
for alias in ["qianfan", "baidu-qianfan", "baidu_qianfan", "baidu"] {
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::Qianfan));
@@ -4474,6 +4501,53 @@ fn xai_api_key_provider_resolves_defaults_and_scopes_env_credentials() {
assert_eq!(resolved.model, "grok-4.3");
}
#[test]
fn mistral_provider_resolves_defaults_and_metadata() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let metadata = provider::resolve_provider("mistral").expect("mistral provider metadata");
assert_eq!(metadata.id(), "mistral");
assert_eq!(metadata.kind(), ProviderKind::Mistral);
assert_eq!(metadata.display_name(), "Mistral AI");
assert_eq!(metadata.provider_config_key(), "mistral");
assert_eq!(metadata.default_base_url(), "https://api.mistral.ai/v1");
assert_eq!(metadata.default_model(), "mistral-code-latest");
assert_eq!(metadata.env_vars(), &["MISTRAL_API_KEY"]);
assert_eq!(
metadata.wire_policy().fixed(),
Some(provider::WireFormat::ChatCompletions)
);
let help = metadata.credential_help();
assert_eq!(
help.acquisition,
provider::CredentialAcquisition::ApiKey,
"Mistral is a hosted API-key provider"
);
assert_eq!(
help.credential_url,
Some("https://console.mistral.ai/api-keys")
);
let config: ConfigToml = toml::from_str(
r#"
provider = "mistral-ai"
[providers.mistral]
api_key = "mistral-config-key"
model = "mistral-large-latest"
"#,
)
.expect("mistral provider table");
assert_eq!(config.provider, ProviderKind::Mistral);
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Mistral);
assert_eq!(resolved.base_url, "https://api.mistral.ai/v1");
assert_eq!(resolved.model, "mistral-large-latest");
assert_eq!(resolved.api_key.as_deref(), Some("mistral-config-key"));
}
#[test]
fn opencode_go_resolves_current_chat_completions_route() {
let _lock = env_lock();
@@ -4729,9 +4803,9 @@ fn meta_model_api_scopes_both_documented_key_names_to_official_endpoint() {
fn provider_metadata_registry_covers_every_provider_kind_once() {
let providers = provider::all_providers();
// Full registry keeps legacy dialect/plan kinds for provider_for_kind.
assert_eq!(providers.len(), 41);
assert_eq!(providers.len(), 42);
// Catalog surface is one identity per vendor (no dual-wire / plan rows).
assert_eq!(ProviderKind::ALL.len(), 36);
assert_eq!(ProviderKind::ALL.len(), 37);
assert!(ProviderKind::ALL.len() < providers.len());
let mut ids = std::collections::BTreeSet::new();
+1 -1
View File
@@ -413,7 +413,7 @@ fn every_legitimately_recorded_event_survives_the_drain() {
);
// Dialect kinds (`deepseek-anthropic`, the Model Studio plan variants) are
// absent from `ProviderKind::ALL`, which is the 36-row *catalog* subset,
// absent from `ProviderKind::ALL`, which is the 37-row *catalog* subset,
// but `ApiProvider::kind()` yields them for real routes. Narrowing the
// provider bound to the catalog would drop those users' `session_end`.
for kind in [
+3
View File
@@ -3172,6 +3172,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::OpencodeGo | ApiProvider::OpencodeZen => {}
ApiProvider::Meta => {}
ApiProvider::Xai => {}
ApiProvider::Mistral => {}
},
"low" | "minimal" | "medium" | "mid" | "high" | "" => match provider {
// DeepSeek first-party Chat Completions: the wire documents
@@ -3294,6 +3295,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::OpencodeGo | ApiProvider::OpencodeZen => {}
ApiProvider::Meta => {}
ApiProvider::Xai => {}
ApiProvider::Mistral => {}
},
"xhigh" | "max" | "highest" | "ultracode" => match provider {
ApiProvider::Deepseek
@@ -3376,6 +3378,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::OpencodeGo | ApiProvider::OpencodeZen => {}
ApiProvider::Meta => {}
ApiProvider::Xai => {}
ApiProvider::Mistral => {}
},
_ => {}
}
+352 -3
View File
@@ -486,6 +486,143 @@ pub(super) fn apply_route_reasoning_controls(
apply_direct_moonshot_k3_reasoning_effort(body, provider, base_url, model, effort);
apply_kimi_code_k3_reasoning_effort(body, provider, base_url, model, effort);
apply_zai_route_reasoning_controls(body, provider, base_url, model, effort);
apply_mistral_route_reasoning_controls(body, provider, model, effort);
}
fn mistral_model_supports_reasoning(model: &str) -> bool {
let model = model.trim().to_ascii_lowercase();
model.starts_with("mistral-medium")
|| model.starts_with("mistral-small")
|| model.starts_with("magistral")
}
fn mistral_reasoning_effort_wire_value(effort: &str) -> Option<&'static str> {
match effort.trim().to_ascii_lowercase().as_str() {
"off" | "disabled" | "none" | "false" => Some("none"),
"high" | "xhigh" | "max" | "highest" | "ultracode" => Some("high"),
_ => None,
}
}
/// Rewrite assistant messages that carry `reasoning_content` back into the
/// polymorphic `content: [{type: thinking, thinking: [{type: text, text: ...}],
/// closed: bool}, {type: text, text: ...}]` shape that Mistral la Plateforme
/// emits and accepts on replay. Mistral tolerates plain-string history in a
/// thinking-capable conversation, but replaying the original thinking trace
/// keeps multi-turn reasoning quality high per the official docs
/// (docs.mistral.ai/capabilities/reasoning). Non-assistant messages and
/// assistant messages without stored thinking are left untouched.
fn reshape_mistral_messages_for_reasoning_replay(messages: &mut [Value]) {
for message in messages.iter_mut() {
let Some(object) = message.as_object_mut() else {
continue;
};
if object.get("role").and_then(Value::as_str) != Some("assistant") {
continue;
}
let Some(reasoning) = object.remove("reasoning_content") else {
continue;
};
let reasoning_text = reasoning
.as_str()
.map(str::to_string)
.filter(|s| !s.trim().is_empty());
let Some(reasoning_text) = reasoning_text else {
continue;
};
let text_content = object
.get("content")
.and_then(Value::as_str)
.map(str::to_string);
let mut blocks = vec![json!({
"type": "thinking",
"thinking": [{"type": "text", "text": reasoning_text}],
"closed": true,
})];
if let Some(text) = text_content.filter(|s| !s.trim().is_empty()) {
blocks.push(json!({"type": "text", "text": text}));
}
object.insert("content".to_string(), Value::Array(blocks));
}
}
/// Extract thinking and text content from a Mistral polymorphic `content`
/// value. Mistral la Plateforme returns `content` as either a plain string
/// (default) or an array of typed blocks (`{type: "thinking", thinking:
/// [{type: "text", text: "..."}], closed: bool}` and `{type: "text", text:
/// "..."}`). This helper flattens the thinking sub-array into a single
/// string and returns any inline text separately. It ignores plain-string
/// `content` (returns `(None, None)`) so the shared string fallback still
/// runs for non-reasoning responses.
fn extract_mistral_polymorphic_content(value: &Value) -> (Option<String>, Option<String>) {
let Some(array) = value.get("content").and_then(Value::as_array) else {
return (None, None);
};
let mut thinking = String::new();
let mut text = String::new();
for block in array {
let Some(kind) = block.get("type").and_then(Value::as_str) else {
continue;
};
match kind {
"thinking" => {
if let Some(inner) = block.get("thinking").and_then(Value::as_array) {
for sub in inner {
if let Some(sub_text) = sub
.get("text")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
{
thinking.push_str(sub_text);
}
}
} else if let Some(inline) = block
.get("thinking")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
{
thinking.push_str(inline);
}
}
"text" => {
if let Some(sub_text) = block
.get("text")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
{
text.push_str(sub_text);
}
}
_ => {}
}
}
let thinking = (!thinking.is_empty()).then_some(thinking);
let text = (!text.is_empty()).then_some(text);
(thinking, text)
}
fn apply_mistral_route_reasoning_controls(
body: &mut Value,
provider: ApiProvider,
model: &str,
effort: Option<&str>,
) {
if provider != ApiProvider::Mistral {
return;
}
if let Some(object) = body.as_object_mut() {
object.remove("thinking");
object.remove("reasoning_effort");
}
if !mistral_model_supports_reasoning(model) {
return;
}
let Some(effort) = effort else {
return;
};
if let Some(wire) = mistral_reasoning_effort_wire_value(effort) {
body["reasoning_effort"] = json!(wire);
}
}
/// The direct K3 Chat Completions schema exposes fixed sampling behavior and
@@ -1230,6 +1367,9 @@ impl<'a> PromptBuilder<'a> {
if provider == ApiProvider::Minimax {
mirror_minimax_reasoning_details_for_messages(&mut messages);
}
if provider == ApiProvider::Mistral {
reshape_mistral_messages_for_reasoning_replay(&mut messages);
}
messages
}
@@ -2939,7 +3079,19 @@ pub(super) fn parse_chat_message(payload: &Value) -> Result<MessageResponse> {
thinking: reasoning.to_string(),
});
}
if let Some(text) = message.get("content").and_then(Value::as_str)
let (mistral_thinking, mistral_text) = extract_mistral_polymorphic_content(message);
if let Some(thinking) = mistral_thinking.filter(|s| !s.trim().is_empty()) {
content_blocks.push(ContentBlock::Thinking {
signature: None,
thinking,
});
}
if let Some(text) = mistral_text.filter(|s| !s.trim().is_empty()) {
content_blocks.push(ContentBlock::Text {
text,
cache_control: None,
});
} else if let Some(text) = message.get("content").and_then(Value::as_str)
&& !text.trim().is_empty()
{
content_blocks.push(ContentBlock::Text {
@@ -3288,11 +3440,29 @@ fn parse_sse_chunk_with_reasoning_style(
if let Some(delta) = delta {
let reasoning_text = reasoning_delta(delta, choice_index, reasoning_detail_buffers)
.filter(|s| !s.is_empty());
let content_text = delta
// Mistral la Plateforme streams reasoning as a polymorphic
// `delta.content` value: an array of typed {type: thinking|text}
// blocks while thinking, then a plain string once the final
// answer starts. Flatten thinking sub-blocks into a single
// reasoning delta and treat text sub-blocks as normal content
// before the shared string fallback below.
let (mistral_thinking, mistral_text) = extract_mistral_polymorphic_content(delta);
if let Some(reasoning) = mistral_thinking.as_deref() {
push_thinking_delta(
&mut events,
content_index,
text_started,
thinking_started,
reasoning.to_string(),
);
}
let content_text = mistral_text.or_else(|| {
delta
.get("content")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string);
.map(str::to_string)
});
// Handle reasoning_content / reasoning thinking deltas.
if reasoning_stream_style == ReasoningStreamStyle::SeparateField
@@ -6354,3 +6524,182 @@ mod image_block_wire_tests {
);
}
}
#[cfg(test)]
mod mistral_reasoning_tests {
use super::*;
#[test]
fn mistral_effort_wire_value_covers_codewhale_tiers() {
assert_eq!(mistral_reasoning_effort_wire_value("off"), Some("none"));
assert_eq!(
mistral_reasoning_effort_wire_value("disabled"),
Some("none")
);
assert_eq!(mistral_reasoning_effort_wire_value("none"), Some("none"));
assert_eq!(mistral_reasoning_effort_wire_value("false"), Some("none"));
assert_eq!(mistral_reasoning_effort_wire_value("high"), Some("high"));
assert_eq!(mistral_reasoning_effort_wire_value("xhigh"), Some("high"));
assert_eq!(mistral_reasoning_effort_wire_value("max"), Some("high"));
assert_eq!(
mistral_reasoning_effort_wire_value("ultracode"),
Some("high")
);
// Intermediate tiers must be omitted so the request falls back to
// Mistral's own default rather than 400 code 3051 on unsupported
// values like "low"/"medium" that the server does not accept today.
assert_eq!(mistral_reasoning_effort_wire_value("low"), None);
assert_eq!(mistral_reasoning_effort_wire_value("medium"), None);
assert_eq!(mistral_reasoning_effort_wire_value("mid"), None);
assert_eq!(mistral_reasoning_effort_wire_value("minimal"), None);
}
#[test]
fn mistral_model_gate_only_matches_reasoning_capable_families() {
assert!(mistral_model_supports_reasoning("mistral-medium-latest"));
assert!(mistral_model_supports_reasoning("mistral-medium-3-5"));
assert!(mistral_model_supports_reasoning("mistral-small-latest"));
assert!(mistral_model_supports_reasoning("mistral-small-2603"));
assert!(mistral_model_supports_reasoning("magistral-small-latest"));
assert!(mistral_model_supports_reasoning("MISTRAL-MEDIUM-LATEST"));
assert!(!mistral_model_supports_reasoning("mistral-code-latest"));
assert!(!mistral_model_supports_reasoning("codestral-latest"));
assert!(!mistral_model_supports_reasoning("mistral-large-latest"));
assert!(!mistral_model_supports_reasoning("mistral-nemo-2407"));
}
#[test]
fn mistral_route_shaper_writes_reasoning_only_for_supported_models() {
let mut body = json!({"model": "mistral-medium-latest"});
apply_mistral_route_reasoning_controls(
&mut body,
ApiProvider::Mistral,
"mistral-medium-latest",
Some("high"),
);
assert_eq!(body["reasoning_effort"], json!("high"));
let mut body = json!({"model": "mistral-code-latest"});
apply_mistral_route_reasoning_controls(
&mut body,
ApiProvider::Mistral,
"mistral-code-latest",
Some("high"),
);
assert!(
body.get("reasoning_effort").is_none(),
"non-reasoning models must never see reasoning_effort (Mistral 400s on 3051): {body}"
);
let mut body = json!({"model": "mistral-medium-latest", "reasoning_effort": "stale"});
apply_mistral_route_reasoning_controls(
&mut body,
ApiProvider::Mistral,
"mistral-medium-latest",
Some("low"),
);
assert!(
body.get("reasoning_effort").is_none(),
"intermediate tiers must be stripped rather than sent unsupported: {body}"
);
// Non-Mistral providers must not be touched by this shaper.
let mut body = json!({"model": "deepseek-v4-pro", "reasoning_effort": "high"});
apply_mistral_route_reasoning_controls(
&mut body,
ApiProvider::Deepseek,
"deepseek-v4-pro",
Some("high"),
);
assert_eq!(body["reasoning_effort"], json!("high"));
}
#[test]
fn extract_mistral_polymorphic_content_flattens_thinking_and_text() {
// Non-reasoning response: plain string content. Extractor returns
// (None, None) so the shared string fallback still runs.
let plain = json!({"content": "Hello world"});
assert_eq!(extract_mistral_polymorphic_content(&plain), (None, None));
// Missing content: no panic, returns (None, None).
let empty = json!({});
assert_eq!(extract_mistral_polymorphic_content(&empty), (None, None));
// Reasoning response: nested thinking array + text block.
let reasoning = json!({"content": [
{"type": "thinking", "thinking": [
{"type": "text", "text": "First "},
{"type": "text", "text": "second."},
], "closed": true},
{"type": "text", "text": "Final answer."},
]});
let (thinking, text) = extract_mistral_polymorphic_content(&reasoning);
assert_eq!(thinking.as_deref(), Some("First second."));
assert_eq!(text.as_deref(), Some("Final answer."));
// Thinking-only chunk (mid-stream) with no closing text yet.
let thinking_only = json!({"content": [
{"type": "thinking", "thinking": [{"type": "text", "text": "still thinking"}]},
]});
let (thinking, text) = extract_mistral_polymorphic_content(&thinking_only);
assert_eq!(thinking.as_deref(), Some("still thinking"));
assert_eq!(text, None);
}
#[test]
fn reshape_mistral_messages_reconstructs_polymorphic_shape_for_assistant_replay() {
// Assistant message with stored reasoning_content is reshaped into
// Mistral's polymorphic content-as-array shape.
let mut messages = vec![
json!({"role": "user", "content": "compute 3+4"}),
json!({
"role": "assistant",
"content": "The answer is 7.",
"reasoning_content": "Let me add 3 and 4 to get 7.",
}),
json!({"role": "user", "content": "now multiply by 2"}),
];
reshape_mistral_messages_for_reasoning_replay(&mut messages);
assert_eq!(messages[0]["role"], "user");
assert!(
messages[0]["content"].is_string(),
"user turns are left untouched: {}",
messages[0]
);
assert_eq!(messages[1]["role"], "assistant");
assert!(
messages[1].get("reasoning_content").is_none(),
"reasoning_content field must be removed after reshape: {}",
messages[1]
);
let content = messages[1]["content"]
.as_array()
.expect("assistant content is now an array");
assert_eq!(content.len(), 2);
assert_eq!(content[0]["type"], "thinking");
assert_eq!(content[0]["closed"], true);
assert_eq!(content[0]["thinking"][0]["type"], "text");
assert_eq!(
content[0]["thinking"][0]["text"],
"Let me add 3 and 4 to get 7."
);
assert_eq!(content[1]["type"], "text");
assert_eq!(content[1]["text"], "The answer is 7.");
// Assistant with no reasoning stays as-is (plain string content).
let mut plain = vec![json!({"role": "assistant", "content": "hi"})];
reshape_mistral_messages_for_reasoning_replay(&mut plain);
assert_eq!(plain[0]["content"], "hi");
// Empty reasoning is treated as absent — no reshape.
let mut empty = vec![json!({
"role": "assistant",
"content": "hi",
"reasoning_content": " ",
})];
reshape_mistral_messages_for_reasoning_replay(&mut empty);
assert_eq!(empty[0]["content"], "hi");
}
}
+48 -2
View File
@@ -85,6 +85,8 @@ pub enum ApiProvider {
OpencodeZen,
Meta,
Xai,
/// Mistral AI — la Plateforme (OpenAI-compatible Chat Completions).
Mistral,
/// Jiangsu Telecom TokenHub — OpenAI-compatible AI gateway.
Telecomjs,
/// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible Chat Completions).
@@ -314,7 +316,7 @@ impl ApiProvider {
/// `ApiProvider` discriminant → `ProviderKind` lookup.
/// Index 1 is `None` for the legacy `DeepseekCN` variant.
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 42] = [
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 43] = [
Some(codewhale_config::ProviderKind::Deepseek),
None, // DeepseekCN
Some(codewhale_config::ProviderKind::DeepseekAnthropic),
@@ -351,6 +353,7 @@ impl ApiProvider {
Some(codewhale_config::ProviderKind::OpencodeZen),
Some(codewhale_config::ProviderKind::Meta),
Some(codewhale_config::ProviderKind::Xai),
Some(codewhale_config::ProviderKind::Mistral),
Some(codewhale_config::ProviderKind::Telecomjs),
Some(codewhale_config::ProviderKind::ModelstudioTokenPlan),
Some(codewhale_config::ProviderKind::ModelstudioTokenPlanAnthropic),
@@ -360,7 +363,7 @@ impl ApiProvider {
];
/// `ProviderKind` discriminant → `ApiProvider` lookup.
const FROM_KIND_LOOKUP: [Self; 41] = [
const FROM_KIND_LOOKUP: [Self; 42] = [
Self::Deepseek,
Self::DeepseekAnthropic,
Self::NvidiaNim,
@@ -396,6 +399,7 @@ impl ApiProvider {
Self::OpencodeZen,
Self::Meta,
Self::Xai,
Self::Mistral,
Self::Telecomjs,
Self::ModelstudioTokenPlan,
Self::ModelstudioTokenPlanAnthropic,
@@ -1531,6 +1535,13 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
// No glm-5.3: Model Studio publishes no such row (2026-08-03).
"glm-5.2",
],
ApiProvider::Mistral => vec![
DEFAULT_MISTRAL_MODEL,
"mistral-medium-latest",
"mistral-small-latest",
"magistral-small-latest",
"mistral-large-latest",
],
// Custom endpoints expose no built-in completion names; the user
// supplies their own model id (#1519).
ApiProvider::Custom => Vec::new(),
@@ -3344,6 +3355,15 @@ pub struct ProvidersConfig {
pub meta: ProviderConfig,
#[serde(default, alias = "x-ai", alias = "x_ai", alias = "grok")]
pub xai: ProviderConfig,
#[serde(
default,
alias = "mistral-ai",
alias = "mistral_ai",
alias = "mistralai",
alias = "la-plateforme",
alias = "la_plateforme"
)]
pub mistral: ProviderConfig,
#[serde(
default,
alias = "telecom-js",
@@ -4789,6 +4809,7 @@ impl Config {
ApiProvider::OpencodeZen => &providers.opencode_zen,
ApiProvider::Meta => &providers.meta,
ApiProvider::Xai => &providers.xai,
ApiProvider::Mistral => &providers.mistral,
ApiProvider::Telecomjs => &providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
@@ -4864,6 +4885,7 @@ impl Config {
ApiProvider::OpencodeZen => &mut providers.opencode_zen,
ApiProvider::Meta => &mut providers.meta,
ApiProvider::Xai => &mut providers.xai,
ApiProvider::Mistral => &mut providers.mistral,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
@@ -5224,6 +5246,7 @@ impl Config {
ApiProvider::OpencodeZen => DEFAULT_OPENCODE_ZEN_MODEL,
ApiProvider::Meta => DEFAULT_META_MODEL,
ApiProvider::Xai => DEFAULT_XAI_MODEL,
ApiProvider::Mistral => DEFAULT_MISTRAL_MODEL,
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_MODEL,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
@@ -5341,6 +5364,7 @@ impl Config {
| ApiProvider::OpencodeZen
| ApiProvider::Meta
| ApiProvider::Xai
| ApiProvider::Mistral
| ApiProvider::Telecomjs
| ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
@@ -5445,6 +5469,7 @@ impl Config {
ApiProvider::OpencodeZen => DEFAULT_OPENCODE_ZEN_BASE_URL,
ApiProvider::Meta => DEFAULT_META_BASE_URL,
ApiProvider::Xai => DEFAULT_XAI_BASE_URL,
ApiProvider::Mistral => DEFAULT_MISTRAL_BASE_URL,
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
@@ -6979,6 +7004,7 @@ fn provider_env_base_url_override(provider: ApiProvider) -> Option<String> {
ApiProvider::Huggingface => &["HUGGINGFACE_BASE_URL", "HF_BASE_URL"],
ApiProvider::Meta => &["META_MODEL_API_BASE_URL", "MODEL_API_BASE_URL"],
ApiProvider::Xai => &["XAI_BASE_URL"],
ApiProvider::Mistral => &["MISTRAL_BASE_URL"],
ApiProvider::Telecomjs => &["TELECOMJS_BASE_URL"],
ApiProvider::ModelstudioTokenPlan | ApiProvider::ModelstudioTokenPlanAnthropic => {
&["MODELSTUDIO_TOKEN_PLAN_BASE_URL"]
@@ -7309,6 +7335,13 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
.xai
.base_url = Some(value);
}
ApiProvider::Mistral => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.mistral
.base_url = Some(value);
}
ApiProvider::Telecomjs => {
config
.providers
@@ -7543,6 +7576,16 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
.xai
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Mistral)
&& let Ok(value) = std::env::var("MISTRAL_BASE_URL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.mistral
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Telecomjs)
&& let Ok(value) = std::env::var("TELECOMJS_BASE_URL")
&& !value.trim().is_empty()
@@ -7658,6 +7701,7 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
ApiProvider::OpencodeZen => &mut providers.opencode_zen,
ApiProvider::Meta => &mut providers.meta,
ApiProvider::Xai => &mut providers.xai,
ApiProvider::Mistral => &mut providers.mistral,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
@@ -7991,6 +8035,7 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
ApiProvider::OpencodeZen => &mut providers.opencode_zen,
ApiProvider::Meta => &mut providers.meta,
ApiProvider::Xai => &mut providers.xai,
ApiProvider::Mistral => &mut providers.mistral,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
@@ -9370,6 +9415,7 @@ fn merge_providers(
opencode_zen: merge_provider_config(base.opencode_zen, override_cfg.opencode_zen),
meta: merge_provider_config(base.meta, override_cfg.meta),
xai: merge_provider_config(base.xai, override_cfg.xai),
mistral: merge_provider_config(base.mistral, override_cfg.mistral),
telecomjs: merge_provider_config(base.telecomjs, override_cfg.telecomjs),
modelstudio_token_plan: merge_provider_config(
base.modelstudio_token_plan,
+2
View File
@@ -220,6 +220,8 @@ pub const XAI_GROK_COMPOSER_2_5_FAST_MODEL: &str = "grok-composer-2.5-fast";
pub const XAI_GROK_4_20_0309_REASONING_MODEL: &str = "grok-4.20-0309-reasoning";
pub const XAI_GROK_4_20_0309_NON_REASONING_MODEL: &str = "grok-4.20-0309-non-reasoning";
pub const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1";
pub const DEFAULT_MISTRAL_MODEL: &str = "mistral-code-latest";
pub const DEFAULT_MISTRAL_BASE_URL: &str = "https://api.mistral.ai/v1";
pub const DEFAULT_TELECOMJS_MODEL: &str = "deepseek-v4-pro";
pub const DEFAULT_TELECOMJS_BASE_URL: &str = "https://aigw.telecomjs.com/v1";
// Alibaba Cloud Model Studio (DashScope) defaults
+1
View File
@@ -304,6 +304,7 @@ fn provider_base_url_table_key(provider: ApiProvider) -> anyhow::Result<&'static
ApiProvider::OpencodeZen => Ok("opencode_zen"),
ApiProvider::Meta => Ok("meta"),
ApiProvider::Xai => Ok("xai"),
ApiProvider::Mistral => Ok("mistral"),
ApiProvider::Telecomjs => Ok("telecomjs"),
ApiProvider::ModelstudioTokenPlan => Ok("modelstudio_token_plan"),
ApiProvider::ModelstudioTokenPlanAnthropic => Ok("modelstudio_token_plan_anthropic"),
+16
View File
@@ -384,6 +384,22 @@ fn known_context_window_for_model(model_lower: &str) -> Option<u32> {
"grok-composer-2.5-fast" => Some(200_000),
"grok-4.20-0309-reasoning" | "grok-4.20-0309-non-reasoning" => Some(2_000_000),
"muse-spark-1.1" | "muse-spark-1.2" | "muse-spark-1.2-contributor" => Some(1_000_000),
// Mistral la Plateforme text/reasoning models: all report 262144
// (256K) tokens on /v1/models as of 2026-08-08. Codestral coding
// model (mistral-code-latest) reports 256000 tokens on the same
// endpoint. IDs and windows verified live against
// https://api.mistral.ai/v1/models rather than model-card slugs.
"mistral-medium-latest"
| "mistral-medium-3-5"
| "mistral-medium-2604"
| "mistral-medium-3.5"
| "mistral-medium-3"
| "mistral-small-latest"
| "mistral-small-2603"
| "magistral-small-latest"
| "mistral-large-latest"
| "mistral-large-2512" => Some(262_144),
"mistral-code-latest" | "codestral-latest" | "codestral" | "mistral-code" => Some(256_000),
_ => None,
}
}
+1
View File
@@ -870,6 +870,7 @@ pub(crate) fn mirror_saved_api_key_in_config(
ApiProvider::OpencodeZen => &mut providers.opencode_zen,
ApiProvider::Meta => &mut providers.meta,
ApiProvider::Xai => &mut providers.xai,
ApiProvider::Mistral => &mut providers.mistral,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
+1 -1
View File
@@ -1637,7 +1637,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`, `sakana`, `longcat`, `opencode-go`, `meta`, `telecomjs`, or `xai`. 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`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go Chat Completions route at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.
- `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`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, or `xai`. 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`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go Chat Completions route at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.
- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.5`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model is resolved through the curated Zen catalog: GPT uses Responses, Claude/Qwen use Anthropic Messages, and the documented DeepSeek/MiniMax/GLM/Kimi/Grok/free rows use Chat Completions. Gemini and unknown models fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).
- `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.
+7 -3
View File
@@ -31,7 +31,7 @@ Sources to keep in sync:
## Provider Selection
The canonical provider IDs are the 36 entries of `ProviderKind::ALL`
The canonical provider IDs are the 37 entries of `ProviderKind::ALL`
(`crates/config/src/provider_kind.rs:198-234`), in that order:
`deepseek`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,
@@ -39,8 +39,8 @@ The canonical provider IDs are the 36 entries of `ProviderKind::ALL`
`siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `huggingface`,
`together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`,
`stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`,
`opencode-zen`, `meta`, `xai`, `telecomjs`, `modelstudio-token-plan`, and
`custom`.
`opencode-zen`, `meta`, `xai`, `mistral`, `telecomjs`, `modelstudio-token-plan`,
and `custom`.
`deepseek-anthropic` is *not* on this list — it is a wire dialect of
`deepseek`, reached with `wire = "anthropic"`, not a separate route to select.
@@ -127,6 +127,7 @@ the listed provider env vars.
| `meta` | `[providers.meta]` | OpenAI Chat Completions | `META_MODEL_API_KEY`, `MODEL_API_KEY` |
| `telecomjs` | `[providers.telecomjs]` | OpenAI Chat Completions | `TELECOMJS_API_KEY` |
| `xai` | `[providers.xai]` | OpenAI Chat Completions | `XAI_API_KEY` |
| `mistral` | `[providers.mistral]` | OpenAI Chat Completions | `MISTRAL_API_KEY` |
| `modelstudio-token-plan` | `[providers.modelstudio_token_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
| `modelstudio-token-plan-anthropic` | `[providers.modelstudio_token_plan_anthropic]` | Anthropic Messages | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
| `modelstudio-coding-plan` | `[providers.modelstudio_coding_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
@@ -339,6 +340,7 @@ configuration path instead of guessing a vendor page.
| `meta` | [Meta Model API](https://developer.meta.com/ai/) |
| `telecomjs` | [TelecomJS TokenHub](https://aigw.telecomjs.com/) |
| `xai` | [xAI Console](https://console.x.ai/) for an API key, Codewhale-owned device login, or explicitly consented read-only Grok CLI credentials. |
| `mistral` | [Mistral Console (la Plateforme)](https://console.mistral.ai/api-keys) |
| `modelstudio-token-plan`, `modelstudio-token-plan-anthropic`, `modelstudio-coding-plan`, `modelstudio-coding-plan-anthropic` | [Alibaba Cloud Model Studio (Bailian console)](https://bailian.console.aliyun.com/) — create or copy a Model Studio API key. |
| `custom` | Set the named provider's `base_url` and `api_key_env` or `api_key`; no canonical vendor credential page exists. |
@@ -426,6 +428,7 @@ Kimi remains API-key-only; external consent for Kimi is rejected.
| `opencode-zen` | `[providers.opencode_zen]` | `OPENCODE_ZEN_API_KEY`, fallback `OPENCODE_API_KEY` | `OPENCODE_ZEN_BASE_URL`; default `https://opencode.ai/zen/v1` | `gpt-5.5` (default); current documented GPT, Claude, Qwen, DeepSeek, MiniMax, GLM, Kimi, Grok, and free-model IDs | [OpenCode Zen](https://opencode.ai/docs/zen/) model-aware gateway. `OPENCODE_ZEN_MODEL` is accepted, and official `opencode/<model-id>` selectors normalize to bare wire IDs. GPT rows use `/responses`; Claude and Qwen rows use `/messages`; DeepSeek, MiniMax, GLM, Kimi, Grok, and the listed free rows use `/chat/completions`. Responses and Chat Completions authenticate with Bearer `Authorization`, while Anthropic Messages uses `x-api-key`; none of these routes use ChatGPT/Codex OAuth guidance or headers. Gemini currently fails closed because its model-specific Google wire protocol is not implemented. Unknown models also fail closed until their protocol is present in the curated catalog. |
| `meta` | `[providers.meta]` | `META_MODEL_API_KEY`, `MODEL_API_KEY` | `META_MODEL_API_BASE_URL`, `MODEL_API_BASE_URL`; default `https://api.meta.ai/v1` | `muse-spark-1.2` (default) | [Meta Model API](https://developer.meta.com/ai/resources/blog/build-with-muse-spark/) public-preview route using OpenAI-compatible Chat Completions. Muse Spark 1.2 keeps its wire ID, tool support, 1M context, 32K output metadata, and `none` through `xhigh` reasoning effort. `META_MODEL_API_MODEL` and `MODEL_API_MODEL` are accepted. Provider aliases: `meta-ai`, `meta_model_api`, `muse`, `muse-spark`. |
| `telecomjs` | `[providers.telecomjs]` | `TELECOMJS_API_KEY` | `TELECOMJS_BASE_URL`; default `https://aigw.telecomjs.com/v1` | `deepseek-v4-pro` conservative fallback; authenticated `/models` rows when a key is configured | TelecomJS TokenHub OpenAI-compatible Chat Completions route. Live catalogs are isolated by provider and key fingerprint, stale rows survive transient refresh failures, and unsupported reasoning request fields are omitted. `TELECOMJS_MODEL` is accepted. Provider aliases: `telecom-js`, `telecom_js`, `telecomjs-cn`, `tokenhub`. |
| `mistral` | `[providers.mistral]` | `MISTRAL_API_KEY` | `MISTRAL_BASE_URL`; default `https://api.mistral.ai/v1` | `mistral-code-latest` (default; `codestral-latest` accepted as alias), `mistral-medium-latest` (aliases: `mistral-medium-3-5`), `mistral-small-latest` (aliases: `mistral-small-2603`), `magistral-small-latest`, `mistral-large-latest` | Mistral AI (la Plateforme) route using the OpenAI-compatible Chat Completions endpoint. Reasoning is wired end-to-end for `mistral-medium-latest`, `mistral-small-latest`, and `magistral-small-latest`: Codewhale sends `reasoning_effort` (`none` or `high` only — Mistral rejects intermediate tiers with HTTP 400 code 3051), parses the polymorphic `content: [{type: thinking, thinking: [{type: text, text: ...}], closed: bool}, {type: text, text: ...}]` shape emitted by the reasoning models, and replays the thinking trace back into multi-turn history per docs.mistral.ai/capabilities/reasoning. Non-reasoning models (`mistral-code-latest`, `mistral-large-latest`) never receive the field. `MISTRAL_MODEL` is accepted. Provider aliases: `mistral-ai`, `mistralai`, `la-plateforme`. |
| `xai` | `[providers.xai]` | `XAI_API_KEY`, Codewhale-owned device OAuth, or explicit read-only Grok CLI consent | `XAI_BASE_URL`; default `https://api.x.ai/v1` | `grok-4.5` (default), `grok-4.3`, `grok-build`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning` | xAI/Grok OpenAI-compatible Chat Completions route. **API-key** (default): Bearer token from console.x.ai via `XAI_API_KEY` / keyring / `api_key`. **OAuth**: `codewhale auth xai-device` uses SSH-friendly device login and Codewhale-owned storage, which may refresh itself. Existing Grok CLI credentials require `codewhale auth external-consent --provider xai --mode read-only`; the granted external file is never refreshed or rewritten. OAuth may return HTTP 403 on some SuperGrok tiers — keep API-key as the reliable fallback. `XAI_MODEL` is accepted. Provider aliases: `x-ai`, `x_ai`, `grok`. |
| `modelstudio-token-plan` | `[providers.modelstudio_token_plan]` | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` | `MODELSTUDIO_TOKEN_PLAN_BASE_URL`; default `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` | `qwen3.8-max` (default), `qwen3.8-max-preview`, `qwen3.7-plus`, `qwen3.7-max`, `qwen3.6-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-0731`, `glm-5.2` | Alibaba Cloud Model Studio Token Plan OpenAI-compatible Chat Completions route. Token Plan Personal and Team share this endpoint. All listed models are reasoning-capable text/coding models. DeepSeek and GLM entries are provider-scoped and do not collide with first-party routes. `MODELSTUDIO_TOKEN_PLAN_MODEL` is accepted. Provider aliases: `modelstudio-token-plan`, `alibaba-token-plan`, `dashscope-token-plan`. |
| `modelstudio-token-plan-anthropic` | `[providers.modelstudio_token_plan_anthropic]` | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` | default `https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic` | Same model catalog as `modelstudio-token-plan` | Token Plan Anthropic-compatible Messages route (`/apps/anthropic`). Same API key as the OpenAI dialect. Provider aliases: `modelstudio-token-plan-anthropic`, `alibaba-token-plan-anthropic`. |
@@ -607,6 +610,7 @@ endpoint when the endpoint supports model listing.
| `opencode-go` | `deepseek-v4-pro`, `grok-4.5`, `glm-5.2`, `glm-5.1`, `kimi-k3`, `kimi-k2.7-code`, `kimi-k2.6`, `deepseek-v4-flash`, `mimo-v2.5`, `mimo-v2.5-pro` | yes | yes |
| `meta` | `muse-spark-1.2` | yes | yes |
| `xai` | `grok-4.5`, `grok-4.3`, `grok-build`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning` | yes | yes for `grok-4.5`, `grok-4.3`, `grok-build`, and `grok-4.20-0309-reasoning` |
| `mistral` | `mistral-code-latest`, `mistral-medium-latest`, `mistral-small-latest`, `magistral-small-latest`, `mistral-large-latest` | yes | yes for `mistral-medium-latest`, `mistral-small-latest`, and `magistral-small-latest` (reasoning_effort `none` or `high` only); no for `mistral-code-latest` and `mistral-large-latest` (400 code 3051 on unsupported models) |
| `modelstudio-token-plan`, `modelstudio-coding-plan` | `qwen3.8-max`, `qwen3.8-max-preview`, `qwen3.7-plus`, `qwen3.7-max`, `qwen3.6-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-0731`, `glm-5.2` | yes | yes |
AtlasCloud keeps the same default model as the config layer and adds