feat: add Xiaomi MiMo provider

Adds native xiaomi-mimo provider configuration, auth/env aliases, model registry entries, TUI request handling, tests, and docs. Keeps credentials in existing provider-scoped config/env/keyring paths and uses placeholders only in docs.
This commit is contained in:
AdityaG
2026-05-26 19:01:33 -04:00
committed by Hunter Bown
parent 37d4ec963b
commit 3f4c4496f2
19 changed files with 645 additions and 48 deletions
+7 -2
View File
@@ -216,6 +216,10 @@ codewhale --provider wanjie-ark --model deepseek-reasoner
codewhale auth set --provider openrouter --api-key "YOUR_OPENROUTER_API_KEY"
codewhale --provider openrouter --model deepseek/deepseek-v4-pro
# Xiaomi MiMo
codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_MIMO_API_KEY"
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
# Novita
codewhale auth set --provider novita --api-key "YOUR_NOVITA_API_KEY"
codewhale --provider novita --model deepseek/deepseek-v4-pro
@@ -321,15 +325,16 @@ codewhale update # バイナリ更新の確認
| `DEEPSEEK_HTTP_HEADERS` | 任意のモデルリクエストヘッダー |
| `DEEPSEEK_MODEL` | デフォルトモデル |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | ストリームのアイドルタイムアウト秒数 |
| `DEEPSEEK_PROVIDER` | `codewhale`(デフォルト)、`nvidia-nim``openai``atlascloud``wanjie-ark``openrouter``novita``fireworks``sglang``vllm``ollama` |
| `DEEPSEEK_PROVIDER` | `codewhale`(デフォルト)、`nvidia-nim``openai``atlascloud``wanjie-ark``openrouter``xiaomi-mimo``novita``fireworks``sglang``vllm``ollama` |
| `DEEPSEEK_PROFILE` | 設定プロファイル名 |
| `DEEPSEEK_MEMORY` | `on` に設定するとユーザーメモリを有効化 |
| `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | 信頼できるネットワークで非ローカル `http://` API ベース URL を許可 |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | プロバイダー認証 |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | プロバイダー認証 |
| `OPENAI_BASE_URL` / `OPENAI_MODEL` | 汎用 OpenAI 互換エンドポイントとモデル ID |
| `ATLASCLOUD_BASE_URL` / `ATLASCLOUD_MODEL` | AtlasCloud エンドポイントとモデル上書き |
| `WANJIE_ARK_BASE_URL` / `WANJIE_ARK_MODEL` | Wanjie Ark エンドポイントとモデル上書き |
| `OPENROUTER_BASE_URL` | OpenRouter エンドポイント上書き |
| `XIAOMI_MIMO_BASE_URL` / `MIMO_BASE_URL` / `XIAOMI_MIMO_MODEL` / `MIMO_MODEL` | Xiaomi MiMo エンドポイントとモデル上書き |
| `NOVITA_BASE_URL` | Novita エンドポイント上書き |
| `FIREWORKS_BASE_URL` | Fireworks エンドポイント上書き |
| `SGLANG_BASE_URL` | セルフホスト SGLang のエンドポイント |
+7 -2
View File
@@ -313,6 +313,10 @@ codewhale --provider wanjie-ark --model deepseek-reasoner
codewhale auth set --provider openrouter --api-key "YOUR_OPENROUTER_API_KEY"
codewhale --provider openrouter --model deepseek/deepseek-v4-pro
# Xiaomi MiMo
codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_MIMO_API_KEY"
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
# Novita
codewhale auth set --provider novita --api-key "YOUR_NOVITA_API_KEY"
codewhale --provider novita --model deepseek/deepseek-v4-pro
@@ -490,15 +494,16 @@ Key environment variables:
| `DEEPSEEK_HTTP_HEADERS` | Optional custom model request headers, e.g. `X-Model-Provider-Id=your-model-provider` |
| `DEEPSEEK_MODEL` | Default model |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | Stream idle timeout in seconds, default `300`, clamped to `1..=3600` |
| `CODEWHALE_PROVIDER` / `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, `ollama` |
| `CODEWHALE_PROVIDER` / `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, `ollama` |
| `DEEPSEEK_PROFILE` | Config profile name |
| `DEEPSEEK_MEMORY` | Set to `on` to enable user memory |
| `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | Allow non-local `http://` API base URLs on trusted networks |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
| `OPENAI_BASE_URL` / `OPENAI_MODEL` | Generic OpenAI-compatible endpoint and model ID |
| `ATLASCLOUD_BASE_URL` / `ATLASCLOUD_MODEL` | AtlasCloud endpoint and model override |
| `WANJIE_ARK_BASE_URL` / `WANJIE_ARK_MODEL` | Wanjie Ark endpoint and model override |
| `OPENROUTER_BASE_URL` | OpenRouter endpoint override |
| `XIAOMI_MIMO_BASE_URL` / `MIMO_BASE_URL` / `XIAOMI_MIMO_MODEL` / `MIMO_MODEL` | Xiaomi MiMo endpoint and model override |
| `NOVITA_BASE_URL` | Novita endpoint override |
| `FIREWORKS_BASE_URL` | Fireworks endpoint override |
| `SGLANG_BASE_URL` | Self-hosted SGLang endpoint |
+7 -2
View File
@@ -261,6 +261,10 @@ codewhale --provider wanjie-ark --model deepseek-reasoner
codewhale auth set --provider openrouter --api-key "YOUR_OPENROUTER_API_KEY"
codewhale --provider openrouter --model deepseek/deepseek-v4-pro
# Xiaomi MiMo
codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_MIMO_API_KEY"
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
# Novita
codewhale auth set --provider novita --api-key "YOUR_NOVITA_API_KEY"
codewhale --provider novita --model deepseek/deepseek-v4-pro
@@ -402,15 +406,16 @@ DeepSeek 可作为自定义 Agent Client Protocol 服务器运行,供 Zed 等
| `DEEPSEEK_HTTP_HEADERS` | 可选模型请求头,例如 `X-Model-Provider-Id=your-model-provider` |
| `DEEPSEEK_MODEL` | 默认模型 |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | 流式响应空闲超时秒数,默认 `300`,限制在 `1..=3600` |
| `DEEPSEEK_PROVIDER` | `codewhale`(默认)、`nvidia-nim``openai``atlascloud``wanjie-ark``openrouter``novita``fireworks``sglang``vllm``ollama` |
| `DEEPSEEK_PROVIDER` | `codewhale`(默认)、`nvidia-nim``openai``atlascloud``wanjie-ark``openrouter``xiaomi-mimo``novita``fireworks``sglang``vllm``ollama` |
| `DEEPSEEK_PROFILE` | 配置 profile 名称 |
| `DEEPSEEK_MEMORY` | 设为 `on` 启用用户记忆 |
| `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | 在可信网络上允许非本机 `http://` API base URL |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | 提供商认证 |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | 提供商认证 |
| `OPENAI_BASE_URL` / `OPENAI_MODEL` | 通用 OpenAI 兼容端点和模型 ID |
| `ATLASCLOUD_BASE_URL` / `ATLASCLOUD_MODEL` | AtlasCloud 端点和模型覆盖 |
| `WANJIE_ARK_BASE_URL` / `WANJIE_ARK_MODEL` | Wanjie Ark 端点和模型覆盖 |
| `OPENROUTER_BASE_URL` | OpenRouter 端点覆盖 |
| `XIAOMI_MIMO_BASE_URL` / `MIMO_BASE_URL` / `XIAOMI_MIMO_MODEL` / `MIMO_MODEL` | Xiaomi MiMo 端点和模型覆盖 |
| `NOVITA_BASE_URL` | Novita 端点覆盖 |
| `FIREWORKS_BASE_URL` | Fireworks 端点覆盖 |
| `SGLANG_BASE_URL` | 自托管 SGLang 端点 |
+17 -3
View File
@@ -13,11 +13,12 @@
# `[providers.*]` sections near the bottom of
# this file — keeping both stored at once means `/provider deepseek` and
# `/provider nvidia-nim` (or `--provider openai`, `--provider wanjie-ark`,
# `--provider fireworks`, `/provider sglang`, `/provider vllm`, `/provider ollama`)
# toggle without having to re-enter keys. Top-level `api_key` / `base_url` are
# `--provider xiaomi-mimo`, `--provider fireworks`, `/provider sglang`,
# `/provider vllm`, `/provider ollama`) toggle without having to re-enter keys.
# Top-level `api_key` / `base_url` are
# still read as DeepSeek defaults when `[providers.deepseek]` is absent
# (backward compatibility).
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | openrouter | novita | fireworks | sglang | vllm | ollama
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | openrouter | xiaomi-mimo | novita | fireworks | sglang | vllm | ollama
api_key = "YOUR_DEEPSEEK_API_KEY" # must be non-empty
base_url = "https://api.deepseek.com/beta"
# provider = "deepseek-cn" # legacy alias (official host is still https://api.deepseek.com)
@@ -37,6 +38,7 @@ base_url = "https://api.deepseek.com/beta"
# gpt-4.1 — default generic OpenAI-compatible model ID
# deepseek-ai/deepseek-v4-flash — default AtlasCloud model ID
# deepseek-reasoner — default Wanjie Ark model ID
# mimo-v2.5-pro — default Xiaomi MiMo model ID
# accounts/fireworks/models/deepseek-v4-pro — Fireworks AI Pro model ID
# deepseek-ai/DeepSeek-V4-Pro — SGLang self-hosted Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SGLang self-hosted Flash model ID
@@ -186,6 +188,7 @@ max_subagents = 10 # optional (1-20)
# OpenAI-compatible: OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL
# Wanjie Ark: WANJIE_ARK_API_KEY (or WANJIE_API_KEY), WANJIE_ARK_BASE_URL, WANJIE_ARK_MODEL
# OpenRouter: OPENROUTER_API_KEY, OPENROUTER_BASE_URL, OPENROUTER_MODEL
# Xiaomi MiMo: XIAOMI_MIMO_API_KEY (or MIMO_API_KEY), XIAOMI_MIMO_BASE_URL, XIAOMI_MIMO_MODEL
# Novita: NOVITA_API_KEY, NOVITA_BASE_URL, NOVITA_MODEL
# Fireworks: FIREWORKS_API_KEY, FIREWORKS_BASE_URL
# SGLang: SGLANG_BASE_URL, SGLANG_MODEL, optional SGLANG_API_KEY
@@ -244,6 +247,12 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://openrouter.ai/api/v1"
# model = "deepseek/deepseek-v4-pro" # or deepseek/deepseek-v4-flash
# Xiaomi MiMo OpenAI-compatible endpoint (https://platform.xiaomimimo.com)
[providers.xiaomi_mimo]
# api_key = "YOUR_XIAOMI_MIMO_API_KEY"
# base_url = "https://api.xiaomimimo.com/v1"
# model = "mimo-v2.5-pro"
# Novita AI-hosted inference (https://novita.ai)
[providers.novita]
# api_key = "YOUR_NOVITA_API_KEY"
@@ -380,6 +389,11 @@ exec_policy = true
# model = "gemini-3.1-flash-lite-preview" # Required: vision-capable model ID
# api_key = "YOUR_API_KEY" # Optional: defaults to main api_key
# base_url = "https://generativelanguage.googleapis.com/v1beta/openai/" # Optional
#
# Xiaomi MiMo image understanding can be configured through the same tool:
# model = "mimo-v2.5"
# api_key = "YOUR_XIAOMI_MIMO_API_KEY"
# base_url = "https://api.xiaomimimo.com/v1"
# ─────────────────────────────────────────────────────────────────────────────────
# Retry Configuration
+24
View File
@@ -119,6 +119,20 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "mimo-v2.5-pro".to_string(),
provider: ProviderKind::XiaomiMimo,
aliases: vec!["mimo".to_string()],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "mimo-v2.5".to_string(),
provider: ProviderKind::XiaomiMimo,
aliases: vec!["xiaomi-mimo-v2.5".to_string()],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "deepseek/deepseek-v4-pro".to_string(),
provider: ProviderKind::Novita,
@@ -382,6 +396,16 @@ mod tests {
assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
}
#[test]
fn xiaomi_mimo_default_uses_canonical_model_id() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
assert!(resolved.resolved.supports_reasoning);
}
#[test]
fn wanjie_ark_default_uses_reasoner_model_id() {
let registry = ModelRegistry::default();
+13 -2
View File
@@ -29,6 +29,7 @@ enum ProviderArg {
Atlascloud,
WanjieArk,
Openrouter,
XiaomiMimo,
Novita,
Fireworks,
Moonshot,
@@ -46,6 +47,7 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::Atlascloud => ProviderKind::Atlascloud,
ProviderArg::WanjieArk => ProviderKind::WanjieArk,
ProviderArg::Openrouter => ProviderKind::Openrouter,
ProviderArg::XiaomiMimo => ProviderKind::XiaomiMimo,
ProviderArg::Novita => ProviderKind::Novita,
ProviderArg::Fireworks => ProviderKind::Fireworks,
ProviderArg::Moonshot => ProviderKind::Moonshot,
@@ -721,6 +723,7 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
ProviderKind::Atlascloud => "atlascloud",
ProviderKind::WanjieArk => "wanjie-ark",
ProviderKind::Openrouter => "openrouter",
ProviderKind::XiaomiMimo => "xiaomi-mimo",
ProviderKind::Novita => "novita",
ProviderKind::Fireworks => "fireworks",
ProviderKind::Moonshot => "moonshot",
@@ -731,13 +734,14 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
}
/// Provider order used by the `auth list` and `auth status` outputs.
const PROVIDER_LIST: [ProviderKind; 12] = [
const PROVIDER_LIST: [ProviderKind; 13] = [
ProviderKind::Deepseek,
ProviderKind::NvidiaNim,
ProviderKind::Openai,
ProviderKind::Atlascloud,
ProviderKind::WanjieArk,
ProviderKind::Openrouter,
ProviderKind::XiaomiMimo,
ProviderKind::Novita,
ProviderKind::Fireworks,
ProviderKind::Moonshot,
@@ -792,6 +796,7 @@ fn provider_env_vars(provider: ProviderKind) -> &'static [&'static str] {
match provider {
ProviderKind::Deepseek => &["DEEPSEEK_API_KEY"],
ProviderKind::Openrouter => &["OPENROUTER_API_KEY"],
ProviderKind::XiaomiMimo => &["XIAOMI_MIMO_API_KEY", "MIMO_API_KEY"],
ProviderKind::Novita => &["NOVITA_API_KEY"],
ProviderKind::NvidiaNim => &["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
ProviderKind::Fireworks => &["FIREWORKS_API_KEY"],
@@ -1476,6 +1481,7 @@ fn build_tui_command(
| ProviderKind::Atlascloud
| ProviderKind::WanjieArk
| ProviderKind::Openrouter
| ProviderKind::XiaomiMimo
| ProviderKind::Novita
| ProviderKind::Fireworks
| ProviderKind::Moonshot
@@ -1484,7 +1490,7 @@ fn build_tui_command(
| ProviderKind::Ollama
) {
bail!(
"The interactive TUI supports DeepSeek, NVIDIA NIM, OpenAI-compatible, AtlasCloud, Wanjie Ark, OpenRouter, Novita, Fireworks, Moonshot/Kimi, SGLang, vLLM, and Ollama providers. Remove --provider {} or use `codewhale model ...` for provider registry inspection.",
"The interactive TUI supports DeepSeek, NVIDIA NIM, OpenAI-compatible, AtlasCloud, Wanjie Ark, OpenRouter, Xiaomi MiMo, Novita, Fireworks, Moonshot/Kimi, SGLang, vLLM, and Ollama providers. Remove --provider {} or use `codewhale model ...` for provider registry inspection.",
resolved_runtime.provider.as_str()
);
}
@@ -2897,6 +2903,11 @@ mod tests {
"openrouter",
&["OPENROUTER_API_KEY"],
),
(
ProviderKind::XiaomiMimo,
"xiaomi-mimo",
&["XIAOMI_MIMO_API_KEY", "MIMO_API_KEY"],
),
(ProviderKind::Novita, "novita", &["NOVITA_API_KEY"]),
(
ProviderKind::NvidiaNim,
+143 -1
View File
@@ -27,6 +27,7 @@ const DEFAULT_WANJIE_ARK_MODEL: &str = "deepseek-reasoner";
const DEFAULT_WANJIE_ARK_BASE_URL: &str = "https://maas-openapi.wanjiedata.com/api/v1";
const DEFAULT_OPENROUTER_MODEL: &str = "deepseek/deepseek-v4-pro";
const DEFAULT_OPENROUTER_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
const DEFAULT_XIAOMI_MIMO_MODEL: &str = "mimo-v2.5-pro";
const DEFAULT_NOVITA_MODEL: &str = "deepseek/deepseek-v4-pro";
const DEFAULT_NOVITA_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
const DEFAULT_FIREWORKS_MODEL: &str = "accounts/fireworks/models/deepseek-v4-pro";
@@ -37,6 +38,7 @@ const DEFAULT_KIMI_CODE_BASE_URL: &str = "https://api.kimi.com/coding/v1";
const DEFAULT_SGLANG_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
const DEFAULT_SGLANG_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
const DEFAULT_OPENROUTER_BASE_URL: &str = "https://openrouter.ai/api/v1";
const DEFAULT_XIAOMI_MIMO_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
const DEFAULT_NOVITA_BASE_URL: &str = "https://api.novita.ai/v1";
const DEFAULT_FIREWORKS_BASE_URL: &str = "https://api.fireworks.ai/inference/v1";
const DEFAULT_SGLANG_BASE_URL: &str = "http://localhost:30000/v1";
@@ -71,6 +73,8 @@ pub enum ProviderKind {
)]
WanjieArk,
Openrouter,
#[serde(alias = "mimo", alias = "xiaomi", alias = "xiaomi_mimo")]
XiaomiMimo,
Novita,
Fireworks,
Moonshot,
@@ -89,6 +93,7 @@ impl ProviderKind {
Self::Atlascloud => "atlascloud",
Self::WanjieArk => "wanjie-ark",
Self::Openrouter => "openrouter",
Self::XiaomiMimo => "xiaomi-mimo",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
Self::Moonshot => "moonshot",
@@ -109,6 +114,9 @@ impl ProviderKind {
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => Some(Self::WanjieArk),
"openrouter" | "open_router" => Some(Self::Openrouter),
"xiaomi-mimo" | "xiaomi_mimo" | "xiaomimimo" | "mimo" | "xiaomi" => {
Some(Self::XiaomiMimo)
}
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => Some(Self::Moonshot),
@@ -145,6 +153,8 @@ pub struct ProvidersToml {
#[serde(default)]
pub openrouter: ProviderConfigToml,
#[serde(default)]
pub xiaomi_mimo: ProviderConfigToml,
#[serde(default)]
pub novita: ProviderConfigToml,
#[serde(default)]
pub fireworks: ProviderConfigToml,
@@ -168,6 +178,7 @@ impl ProvidersToml {
ProviderKind::Atlascloud => &self.atlascloud,
ProviderKind::WanjieArk => &self.wanjie_ark,
ProviderKind::Openrouter => &self.openrouter,
ProviderKind::XiaomiMimo => &self.xiaomi_mimo,
ProviderKind::Novita => &self.novita,
ProviderKind::Fireworks => &self.fireworks,
ProviderKind::Moonshot => &self.moonshot,
@@ -185,6 +196,7 @@ impl ProvidersToml {
ProviderKind::Atlascloud => &mut self.atlascloud,
ProviderKind::WanjieArk => &mut self.wanjie_ark,
ProviderKind::Openrouter => &mut self.openrouter,
ProviderKind::XiaomiMimo => &mut self.xiaomi_mimo,
ProviderKind::Novita => &mut self.novita,
ProviderKind::Fireworks => &mut self.fireworks,
ProviderKind::Moonshot => &mut self.moonshot,
@@ -405,6 +417,10 @@ impl ConfigToml {
&mut self.providers.openrouter,
&project.providers.openrouter,
);
merge_project_provider_config(
&mut self.providers.xiaomi_mimo,
&project.providers.xiaomi_mimo,
);
merge_project_provider_config(&mut self.providers.novita, &project.providers.novita);
merge_project_provider_config(&mut self.providers.fireworks, &project.providers.fireworks);
merge_project_provider_config(&mut self.providers.sglang, &project.providers.sglang);
@@ -464,6 +480,12 @@ impl ConfigToml {
"providers.openrouter.http_headers" => {
serialize_http_headers(&self.providers.openrouter.http_headers)
}
"providers.xiaomi_mimo.api_key" => self.providers.xiaomi_mimo.api_key.clone(),
"providers.xiaomi_mimo.base_url" => self.providers.xiaomi_mimo.base_url.clone(),
"providers.xiaomi_mimo.model" => self.providers.xiaomi_mimo.model.clone(),
"providers.xiaomi_mimo.http_headers" => {
serialize_http_headers(&self.providers.xiaomi_mimo.http_headers)
}
"providers.novita.api_key" => self.providers.novita.api_key.clone(),
"providers.novita.base_url" => self.providers.novita.base_url.clone(),
"providers.novita.model" => self.providers.novita.model.clone(),
@@ -609,6 +631,18 @@ impl ConfigToml {
"providers.openrouter.http_headers" => {
self.providers.openrouter.http_headers = parse_http_headers(value)?;
}
"providers.xiaomi_mimo.api_key" => {
self.providers.xiaomi_mimo.api_key = Some(value.to_string());
}
"providers.xiaomi_mimo.base_url" => {
self.providers.xiaomi_mimo.base_url = Some(value.to_string());
}
"providers.xiaomi_mimo.model" => {
self.providers.xiaomi_mimo.model = Some(value.to_string());
}
"providers.xiaomi_mimo.http_headers" => {
self.providers.xiaomi_mimo.http_headers = parse_http_headers(value)?;
}
"providers.novita.api_key" => {
self.providers.novita.api_key = Some(value.to_string());
}
@@ -744,6 +778,12 @@ impl ConfigToml {
"providers.openrouter.base_url" => self.providers.openrouter.base_url = None,
"providers.openrouter.model" => self.providers.openrouter.model = None,
"providers.openrouter.http_headers" => self.providers.openrouter.http_headers.clear(),
"providers.xiaomi_mimo.api_key" => self.providers.xiaomi_mimo.api_key = None,
"providers.xiaomi_mimo.base_url" => self.providers.xiaomi_mimo.base_url = None,
"providers.xiaomi_mimo.model" => self.providers.xiaomi_mimo.model = None,
"providers.xiaomi_mimo.http_headers" => {
self.providers.xiaomi_mimo.http_headers.clear();
}
"providers.novita.api_key" => self.providers.novita.api_key = None,
"providers.novita.base_url" => self.providers.novita.base_url = None,
"providers.novita.model" => self.providers.novita.model = None,
@@ -886,6 +926,21 @@ impl ConfigToml {
if let Some(v) = serialize_http_headers(&self.providers.openrouter.http_headers) {
out.insert("providers.openrouter.http_headers".to_string(), v);
}
if let Some(v) = self.providers.xiaomi_mimo.api_key.as_ref() {
out.insert(
"providers.xiaomi_mimo.api_key".to_string(),
redact_secret(v),
);
}
if let Some(v) = self.providers.xiaomi_mimo.base_url.as_ref() {
out.insert("providers.xiaomi_mimo.base_url".to_string(), v.clone());
}
if let Some(v) = self.providers.xiaomi_mimo.model.as_ref() {
out.insert("providers.xiaomi_mimo.model".to_string(), v.clone());
}
if let Some(v) = serialize_http_headers(&self.providers.xiaomi_mimo.http_headers) {
out.insert("providers.xiaomi_mimo.http_headers".to_string(), v);
}
if let Some(v) = self.providers.novita.api_key.as_ref() {
out.insert("providers.novita.api_key".to_string(), redact_secret(v));
}
@@ -1023,6 +1078,7 @@ impl ConfigToml {
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL.to_string(),
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL.to_string(),
ProviderKind::Openrouter => DEFAULT_OPENROUTER_BASE_URL.to_string(),
ProviderKind::XiaomiMimo => DEFAULT_XIAOMI_MIMO_BASE_URL.to_string(),
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL.to_string(),
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL.to_string(),
ProviderKind::Moonshot => {
@@ -1225,7 +1281,10 @@ pub fn load_project_config(workspace: &Path) -> Option<ConfigToml> {
fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
if matches!(
provider,
ProviderKind::Atlascloud | ProviderKind::WanjieArk | ProviderKind::Ollama
ProviderKind::Atlascloud
| ProviderKind::WanjieArk
| ProviderKind::XiaomiMimo
| ProviderKind::Ollama
) {
return model.to_string();
}
@@ -1288,6 +1347,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_MODEL,
ProviderKind::Openrouter => DEFAULT_OPENROUTER_MODEL,
ProviderKind::XiaomiMimo => DEFAULT_XIAOMI_MIMO_MODEL,
ProviderKind::Novita => DEFAULT_NOVITA_MODEL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_MODEL,
ProviderKind::Moonshot => DEFAULT_MOONSHOT_MODEL,
@@ -1305,6 +1365,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL,
ProviderKind::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ProviderKind::XiaomiMimo => DEFAULT_XIAOMI_MIMO_BASE_URL,
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ProviderKind::Moonshot => DEFAULT_MOONSHOT_BASE_URL,
@@ -1800,6 +1861,7 @@ struct EnvRuntimeOverrides {
model: Option<String>,
wanjie_ark_model: Option<String>,
moonshot_model: Option<String>,
xiaomi_mimo_model: Option<String>,
output_mode: Option<String>,
auth_mode: Option<String>,
log_level: Option<String>,
@@ -1814,6 +1876,7 @@ struct EnvRuntimeOverrides {
atlascloud_base_url: Option<String>,
wanjie_ark_base_url: Option<String>,
openrouter_base_url: Option<String>,
xiaomi_mimo_base_url: Option<String>,
novita_base_url: Option<String>,
fireworks_base_url: Option<String>,
moonshot_base_url: Option<String>,
@@ -1844,6 +1907,10 @@ impl EnvRuntimeOverrides {
.or_else(|_| std::env::var("KIMI_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
xiaomi_mimo_model: std::env::var("XIAOMI_MIMO_MODEL")
.or_else(|_| std::env::var("MIMO_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
output_mode: std::env::var("DEEPSEEK_OUTPUT_MODE").ok(),
auth_mode: std::env::var("DEEPSEEK_AUTH_MODE").ok(),
log_level: std::env::var("DEEPSEEK_LOG_LEVEL").ok(),
@@ -1882,6 +1949,10 @@ impl EnvRuntimeOverrides {
openrouter_base_url: std::env::var("OPENROUTER_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
xiaomi_mimo_base_url: std::env::var("XIAOMI_MIMO_BASE_URL")
.or_else(|_| std::env::var("MIMO_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
novita_base_url: std::env::var("NOVITA_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
@@ -1914,6 +1985,7 @@ impl EnvRuntimeOverrides {
ProviderKind::Atlascloud => self.atlascloud_base_url.clone(),
ProviderKind::WanjieArk => self.wanjie_ark_base_url.clone(),
ProviderKind::Openrouter => self.openrouter_base_url.clone(),
ProviderKind::XiaomiMimo => self.xiaomi_mimo_base_url.clone(),
ProviderKind::Novita => self.novita_base_url.clone(),
ProviderKind::Fireworks => self.fireworks_base_url.clone(),
ProviderKind::Moonshot => self.moonshot_base_url.clone(),
@@ -1927,6 +1999,7 @@ impl EnvRuntimeOverrides {
match provider {
ProviderKind::WanjieArk => self.wanjie_ark_model.clone(),
ProviderKind::Moonshot => self.moonshot_model.clone(),
ProviderKind::XiaomiMimo => self.xiaomi_mimo_model.clone(),
_ => None,
}
}
@@ -1975,6 +2048,12 @@ mod tests {
nvidia_nim_base_url: Option<OsString>,
openrouter_api_key: Option<OsString>,
openrouter_base_url: Option<OsString>,
xiaomi_mimo_api_key: Option<OsString>,
mimo_api_key: Option<OsString>,
xiaomi_mimo_base_url: Option<OsString>,
mimo_base_url: Option<OsString>,
xiaomi_mimo_model: Option<OsString>,
mimo_model: Option<OsString>,
wanjie_ark_api_key: Option<OsString>,
wanjie_ark_base_url: Option<OsString>,
wanjie_base_url: Option<OsString>,
@@ -2024,6 +2103,12 @@ mod tests {
nvidia_nim_base_url: env::var_os("NVIDIA_NIM_BASE_URL"),
openrouter_api_key: env::var_os("OPENROUTER_API_KEY"),
openrouter_base_url: env::var_os("OPENROUTER_BASE_URL"),
xiaomi_mimo_api_key: env::var_os("XIAOMI_MIMO_API_KEY"),
mimo_api_key: env::var_os("MIMO_API_KEY"),
xiaomi_mimo_base_url: env::var_os("XIAOMI_MIMO_BASE_URL"),
mimo_base_url: env::var_os("MIMO_BASE_URL"),
xiaomi_mimo_model: env::var_os("XIAOMI_MIMO_MODEL"),
mimo_model: env::var_os("MIMO_MODEL"),
wanjie_ark_api_key: env::var_os("WANJIE_ARK_API_KEY"),
wanjie_ark_base_url: env::var_os("WANJIE_ARK_BASE_URL"),
wanjie_base_url: env::var_os("WANJIE_BASE_URL"),
@@ -2068,6 +2153,12 @@ mod tests {
env::remove_var("NVIDIA_NIM_BASE_URL");
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENROUTER_BASE_URL");
env::remove_var("XIAOMI_MIMO_API_KEY");
env::remove_var("MIMO_API_KEY");
env::remove_var("XIAOMI_MIMO_BASE_URL");
env::remove_var("MIMO_BASE_URL");
env::remove_var("XIAOMI_MIMO_MODEL");
env::remove_var("MIMO_MODEL");
env::remove_var("WANJIE_ARK_API_KEY");
env::remove_var("WANJIE_ARK_BASE_URL");
env::remove_var("WANJIE_BASE_URL");
@@ -2129,6 +2220,12 @@ mod tests {
Self::restore_var("NVIDIA_NIM_BASE_URL", self.nvidia_nim_base_url.take());
Self::restore_var("OPENROUTER_API_KEY", self.openrouter_api_key.take());
Self::restore_var("OPENROUTER_BASE_URL", self.openrouter_base_url.take());
Self::restore_var("XIAOMI_MIMO_API_KEY", self.xiaomi_mimo_api_key.take());
Self::restore_var("MIMO_API_KEY", self.mimo_api_key.take());
Self::restore_var("XIAOMI_MIMO_BASE_URL", self.xiaomi_mimo_base_url.take());
Self::restore_var("MIMO_BASE_URL", self.mimo_base_url.take());
Self::restore_var("XIAOMI_MIMO_MODEL", self.xiaomi_mimo_model.take());
Self::restore_var("MIMO_MODEL", self.mimo_model.take());
Self::restore_var("WANJIE_ARK_API_KEY", self.wanjie_ark_api_key.take());
Self::restore_var("WANJIE_ARK_BASE_URL", self.wanjie_ark_base_url.take());
Self::restore_var("WANJIE_BASE_URL", self.wanjie_base_url.take());
@@ -2712,6 +2809,14 @@ mod tests {
ProviderKind::parse("OPEN_ROUTER"),
Some(ProviderKind::Openrouter)
);
assert_eq!(
ProviderKind::parse("xiaomi-mimo"),
Some(ProviderKind::XiaomiMimo)
);
assert_eq!(
ProviderKind::parse("xiaomi"),
Some(ProviderKind::XiaomiMimo)
);
assert_eq!(ProviderKind::parse("novita"), Some(ProviderKind::Novita));
assert_eq!(ProviderKind::parse("Novita"), Some(ProviderKind::Novita));
assert_eq!(
@@ -2777,6 +2882,22 @@ mod tests {
assert_eq!(resolved.model, DEFAULT_OPENROUTER_MODEL);
}
#[test]
fn xiaomi_mimo_provider_defaults_to_canonical_endpoint_and_model() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let config = ConfigToml {
provider: ProviderKind::XiaomiMimo,
..ConfigToml::default()
};
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::XiaomiMimo);
assert_eq!(resolved.base_url, DEFAULT_XIAOMI_MIMO_BASE_URL);
assert_eq!(resolved.model, DEFAULT_XIAOMI_MIMO_MODEL);
}
#[test]
fn novita_provider_defaults_to_canonical_endpoint_and_model() {
let _lock = env_lock();
@@ -3181,6 +3302,27 @@ mod tests {
assert_eq!(resolved.base_url, DEFAULT_OPENROUTER_BASE_URL);
}
#[test]
fn xiaomi_mimo_env_overrides_provider_key_base_url_and_model() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Safety: test-only environment mutation guarded by a module mutex.
unsafe {
env::set_var("DEEPSEEK_PROVIDER", "xiaomi-mimo");
env::set_var("MIMO_API_KEY", "mimo-env-key");
env::set_var("MIMO_BASE_URL", "https://mimo-gateway.example/v1");
env::set_var("MIMO_MODEL", "mimo-v2.5");
}
let resolved =
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::XiaomiMimo);
assert_eq!(resolved.api_key.as_deref(), Some("mimo-env-key"));
assert_eq!(resolved.base_url, "https://mimo-gateway.example/v1");
assert_eq!(resolved.model, "mimo-v2.5");
}
#[test]
fn novita_env_api_key_falls_back_when_config_missing() {
let _lock = env_lock();
+19
View File
@@ -525,6 +525,9 @@ pub fn env_for(name: &str) -> Option<String> {
let candidates: &[&str] = match name.to_ascii_lowercase().as_str() {
"deepseek" => &["DEEPSEEK_API_KEY"],
"openrouter" => &["OPENROUTER_API_KEY"],
"xiaomi-mimo" | "xiaomi_mimo" | "xiaomimimo" | "mimo" | "xiaomi" => {
&["XIAOMI_MIMO_API_KEY", "MIMO_API_KEY"]
}
"novita" => &["NOVITA_API_KEY"],
// NVIDIA NIM falls back to `DEEPSEEK_API_KEY` last because the
// catalog endpoint accepts the same DeepSeek-issued key when no
@@ -587,6 +590,8 @@ mod tests {
"WANJIE_ARK_API_KEY",
"WANJIE_API_KEY",
"WANJIE_MAAS_API_KEY",
"XIAOMI_MIMO_API_KEY",
"MIMO_API_KEY",
SECRET_BACKEND_ENV,
] {
// Safety: tests serialise on env_lock(); the broader
@@ -764,6 +769,20 @@ mod tests {
clear_known_envs();
}
#[test]
fn xiaomi_mimo_env_aliases_resolve() {
let _guard = env_lock();
clear_known_envs();
unsafe { std::env::set_var("MIMO_API_KEY", "mimo-key") };
assert_eq!(env_for("xiaomi-mimo").as_deref(), Some("mimo-key"));
assert_eq!(env_for("xiaomimimo").as_deref(), Some("mimo-key"));
assert_eq!(env_for("mimo").as_deref(), Some("mimo-key"));
assert_eq!(env_for("xiaomi").as_deref(), Some("mimo-key"));
clear_known_envs();
}
#[test]
fn fireworks_env_aliases_resolve() {
let _lock = env_lock();
+30
View File
@@ -882,6 +882,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Deepseek
| ApiProvider::DeepseekCN
| ApiProvider::Openrouter
| ApiProvider::XiaomiMimo
| ApiProvider::Novita
| ApiProvider::Sglang => {
body["thinking"] = json!({ "type": "disabled" });
@@ -930,6 +931,9 @@ pub(super) fn apply_reasoning_effort(
body["reasoning_effort"] = json!(value);
body["thinking"] = json!({ "type": "enabled" });
}
ApiProvider::XiaomiMimo => {
body["thinking"] = json!({ "type": "enabled" });
}
ApiProvider::Fireworks => {
body["reasoning_effort"] = json!("high");
}
@@ -967,6 +971,9 @@ pub(super) fn apply_reasoning_effort(
body["reasoning_effort"] = json!("xhigh");
body["thinking"] = json!({ "type": "enabled" });
}
ApiProvider::XiaomiMimo => {
body["thinking"] = json!({ "type": "enabled" });
}
ApiProvider::Fireworks => {
body["reasoning_effort"] = json!("max");
}
@@ -2044,6 +2051,29 @@ mod tests {
}
}
#[test]
fn reasoning_effort_uses_xiaomi_mimo_thinking_parameter_only() {
for input in ["low", "medium", "max", "xhigh"] {
let mut body = json!({});
apply_reasoning_effort(&mut body, Some(input), ApiProvider::XiaomiMimo);
assert_eq!(
body.pointer("/thinking/type").and_then(Value::as_str),
Some("enabled"),
"MiMo thinking mapping for {input}"
);
assert!(body.get("reasoning_effort").is_none());
}
let mut body = json!({});
apply_reasoning_effort(&mut body, Some("off"), ApiProvider::XiaomiMimo);
assert_eq!(
body.pointer("/thinking/type").and_then(Value::as_str),
Some("disabled")
);
assert!(body.get("reasoning_effort").is_none());
}
#[test]
fn chat_parser_accepts_nvidia_nim_reasoning_field() -> Result<()> {
let response = parse_chat_message(&json!({
+37 -3
View File
@@ -71,6 +71,17 @@ use super::{
release_stream_buffer, system_to_instructions, to_api_tool_name,
};
fn apply_provider_token_limit(body: &mut Value, provider: ApiProvider, max_tokens: u32) {
if provider != ApiProvider::XiaomiMimo {
return;
}
if let Some(object) = body.as_object_mut() {
object.remove("max_tokens");
}
body["max_completion_tokens"] = json!(max_tokens);
}
impl DeepSeekClient {
pub(super) async fn create_message_chat(
&self,
@@ -82,6 +93,7 @@ impl DeepSeekClient {
"messages": messages,
"max_tokens": request.max_tokens,
});
apply_provider_token_limit(&mut body, self.api_provider, request.max_tokens);
if let Some(temperature) = request.temperature {
body["temperature"] = json!(temperature);
@@ -156,6 +168,7 @@ impl DeepSeekClient {
"include_usage": true
},
});
apply_provider_token_limit(&mut body, self.api_provider, request.max_tokens);
if let Some(temperature) = request.temperature {
body["temperature"] = json!(temperature);
@@ -1729,6 +1742,7 @@ fn provider_accepts_reasoning_content(provider: ApiProvider) -> bool {
| ApiProvider::DeepseekCN
| ApiProvider::NvidiaNim
| ApiProvider::Openrouter
| ApiProvider::XiaomiMimo
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Sglang
@@ -3092,11 +3106,12 @@ mod alias_thinking_detection_tests {
//! turn. See upstream API docs:
//! https://api-docs.deepseek.com/guides/thinking_mode
use super::{
is_reasoning_model_for_stream, provider_accepts_reasoning_content,
requires_reasoning_content, should_replay_reasoning_content,
should_replay_reasoning_content_for_provider,
apply_provider_token_limit, is_reasoning_model_for_stream,
provider_accepts_reasoning_content, requires_reasoning_content,
should_replay_reasoning_content, should_replay_reasoning_content_for_provider,
};
use crate::config::ApiProvider;
use serde_json::json;
#[test]
fn aliases_routed_to_v4_require_reasoning_content() {
@@ -3162,6 +3177,25 @@ mod alias_thinking_detection_tests {
assert!(!provider_accepts_reasoning_content(ApiProvider::Openai));
assert!(provider_accepts_reasoning_content(ApiProvider::Deepseek));
assert!(provider_accepts_reasoning_content(ApiProvider::NvidiaNim));
assert!(provider_accepts_reasoning_content(ApiProvider::XiaomiMimo));
}
#[test]
fn xiaomi_mimo_uses_max_completion_tokens_payload_key() {
let mut body = json!({
"model": "mimo-v2.5-pro",
"messages": [],
"max_tokens": 8192,
});
apply_provider_token_limit(&mut body, ApiProvider::XiaomiMimo, 8192);
assert!(body.get("max_tokens").is_none());
assert_eq!(
body.get("max_completion_tokens")
.and_then(serde_json::Value::as_u64),
Some(8192)
);
}
#[test]
+15 -1
View File
@@ -27,7 +27,7 @@ pub fn provider(app: &mut App, args: Option<&str>) -> CommandResult {
let Some(target) = ApiProvider::parse(name) else {
return CommandResult::error(format!(
"Unknown provider '{name}'. Expected: deepseek, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, novita, fireworks, sglang, vllm, or ollama."
"Unknown provider '{name}'. Expected: deepseek, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, xiaomi-mimo, novita, fireworks, sglang, vllm, or ollama."
));
};
@@ -112,6 +112,7 @@ mod tests {
let msg = result.message.expect("expected error message");
assert!(msg.contains("Unknown provider"));
assert!(msg.contains("openrouter"));
assert!(msg.contains("xiaomi-mimo"));
assert!(msg.contains("novita"));
assert!(result.action.is_none());
}
@@ -129,6 +130,19 @@ mod tests {
}
}
#[test]
fn switch_to_xiaomi_mimo_emits_action() {
let mut app = create_test_app();
let result = provider(&mut app, Some("xiaomi-mimo"));
match result.action {
Some(AppAction::SwitchProvider { provider, model }) => {
assert_eq!(provider, ApiProvider::XiaomiMimo);
assert_eq!(model, None);
}
other => panic!("expected SwitchProvider, got {other:?}"),
}
}
#[test]
fn switch_to_atlascloud_emits_action() {
let mut app = create_test_app();
+188 -1
View File
@@ -46,6 +46,8 @@ pub const DEFAULT_WANJIE_ARK_BASE_URL: &str = "https://maas-openapi.wanjiedata.c
pub const DEFAULT_OPENROUTER_MODEL: &str = "deepseek/deepseek-v4-pro";
pub const DEFAULT_OPENROUTER_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
pub const DEFAULT_OPENROUTER_BASE_URL: &str = "https://openrouter.ai/api/v1";
pub const DEFAULT_XIAOMI_MIMO_MODEL: &str = "mimo-v2.5-pro";
pub const DEFAULT_XIAOMI_MIMO_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
pub const DEFAULT_NOVITA_MODEL: &str = "deepseek/deepseek-v4-pro";
pub const DEFAULT_NOVITA_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
pub const DEFAULT_NOVITA_BASE_URL: &str = "https://api.novita.ai/v1";
@@ -91,6 +93,7 @@ pub enum ApiProvider {
Atlascloud,
WanjieArk,
Openrouter,
XiaomiMimo,
Novita,
Fireworks,
Moonshot,
@@ -113,6 +116,9 @@ impl ApiProvider {
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => Some(Self::WanjieArk),
"openrouter" | "open_router" => Some(Self::Openrouter),
"xiaomi-mimo" | "xiaomi_mimo" | "xiaomimimo" | "mimo" | "xiaomi" => {
Some(Self::XiaomiMimo)
}
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => Some(Self::Moonshot),
@@ -133,6 +139,7 @@ impl ApiProvider {
Self::Atlascloud => "atlascloud",
Self::WanjieArk => "wanjie-ark",
Self::Openrouter => "openrouter",
Self::XiaomiMimo => "xiaomi-mimo",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
Self::Moonshot => "moonshot",
@@ -153,6 +160,7 @@ impl ApiProvider {
Self::Atlascloud => "AtlasCloud",
Self::WanjieArk => "Wanjie Ark",
Self::Openrouter => "OpenRouter",
Self::XiaomiMimo => "Xiaomi MiMo",
Self::Novita => "Novita AI",
Self::Fireworks => "Fireworks AI",
Self::Moonshot => "Moonshot/Kimi",
@@ -172,6 +180,7 @@ impl ApiProvider {
Self::Atlascloud,
Self::WanjieArk,
Self::Openrouter,
Self::XiaomiMimo,
Self::Novita,
Self::Fireworks,
Self::Moonshot,
@@ -259,6 +268,19 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi
};
}
if matches!(provider, ApiProvider::XiaomiMimo) {
return ProviderCapability {
provider,
resolved_model: resolved_model.to_string(),
context_window: 1_000_000,
max_output: 128_000,
thinking_supported: true,
cache_telemetry_supported: false,
request_payload_mode: RequestPayloadMode::ChatCompletions,
alias_deprecation: None,
};
}
if matches!(provider, ApiProvider::Ollama) {
return ProviderCapability {
provider,
@@ -443,6 +465,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
ApiProvider::Deepseek | ApiProvider::DeepseekCN => OFFICIAL_DEEPSEEK_MODELS.to_vec(),
ApiProvider::NvidiaNim => vec![DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_NVIDIA_NIM_FLASH_MODEL],
ApiProvider::Openrouter => vec![DEFAULT_OPENROUTER_MODEL, DEFAULT_OPENROUTER_FLASH_MODEL],
ApiProvider::XiaomiMimo => vec![DEFAULT_XIAOMI_MIMO_MODEL, "mimo-v2.5"],
ApiProvider::Novita => vec![DEFAULT_NOVITA_MODEL, DEFAULT_NOVITA_FLASH_MODEL],
ApiProvider::Fireworks => vec![DEFAULT_FIREWORKS_MODEL],
ApiProvider::Moonshot => vec![DEFAULT_MOONSHOT_MODEL],
@@ -1342,6 +1365,8 @@ pub struct ProvidersConfig {
#[serde(default)]
pub openrouter: ProviderConfig,
#[serde(default)]
pub xiaomi_mimo: ProviderConfig,
#[serde(default)]
pub novita: ProviderConfig,
#[serde(default)]
pub fireworks: ProviderConfig,
@@ -1501,6 +1526,7 @@ impl Config {
ApiProvider::Atlascloud => "providers.atlascloud",
ApiProvider::WanjieArk => "providers.wanjie_ark",
ApiProvider::Openrouter => "providers.openrouter",
ApiProvider::XiaomiMimo => "providers.xiaomi_mimo",
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
ApiProvider::Moonshot => "providers.moonshot",
@@ -1523,7 +1549,7 @@ impl Config {
&& ApiProvider::parse(provider).is_none()
{
anyhow::bail!(
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, novita, fireworks, sglang, vllm, or ollama."
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, xiaomi-mimo, novita, fireworks, sglang, vllm, or ollama."
);
}
if let Some(ref key) = self.api_key
@@ -1643,6 +1669,7 @@ impl Config {
ApiProvider::Atlascloud => &providers.atlascloud,
ApiProvider::WanjieArk => &providers.wanjie_ark,
ApiProvider::Openrouter => &providers.openrouter,
ApiProvider::XiaomiMimo => &providers.xiaomi_mimo,
ApiProvider::Novita => &providers.novita,
ApiProvider::Fireworks => &providers.fireworks,
ApiProvider::Moonshot => &providers.moonshot,
@@ -1733,6 +1760,7 @@ impl Config {
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
ApiProvider::WanjieArk => DEFAULT_WANJIE_ARK_MODEL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_MODEL,
ApiProvider::XiaomiMimo => DEFAULT_XIAOMI_MIMO_MODEL,
ApiProvider::Novita => DEFAULT_NOVITA_MODEL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_MODEL,
ApiProvider::Moonshot => DEFAULT_MOONSHOT_MODEL,
@@ -1765,6 +1793,7 @@ impl Config {
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Openrouter
| ApiProvider::XiaomiMimo
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Moonshot
@@ -1781,6 +1810,7 @@ impl Config {
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ApiProvider::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ApiProvider::XiaomiMimo => DEFAULT_XIAOMI_MIMO_BASE_URL,
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ApiProvider::Moonshot => {
@@ -1824,6 +1854,7 @@ impl Config {
ApiProvider::Atlascloud => "atlascloud",
ApiProvider::WanjieArk => "wanjie-ark",
ApiProvider::Openrouter => "openrouter",
ApiProvider::XiaomiMimo => "xiaomi-mimo",
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
ApiProvider::Moonshot => "moonshot",
@@ -1909,6 +1940,10 @@ impl Config {
"OpenRouter API key not found. Run 'codewhale auth set --provider openrouter', \
set OPENROUTER_API_KEY, or add [providers.openrouter] api_key in ~/.deepseek/config.toml."
),
ApiProvider::XiaomiMimo => anyhow::bail!(
"Xiaomi MiMo API key not found. Run 'codewhale auth set --provider xiaomi-mimo', \
set XIAOMI_MIMO_API_KEY/MIMO_API_KEY, or add [providers.xiaomi_mimo] api_key in ~/.deepseek/config.toml."
),
ApiProvider::Novita => anyhow::bail!(
"Novita API key not found. Run 'codewhale auth set --provider novita', \
set NOVITA_API_KEY, or add [providers.novita] api_key in ~/.deepseek/config.toml."
@@ -2497,6 +2532,13 @@ fn apply_env_overrides(config: &mut Config) {
.openrouter
.base_url = Some(value);
}
ApiProvider::XiaomiMimo => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.xiaomi_mimo
.base_url = Some(value);
}
ApiProvider::WanjieArk => {
config
.providers
@@ -2599,6 +2641,17 @@ fn apply_env_overrides(config: &mut Config) {
.openrouter
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::XiaomiMimo)
&& let Ok(value) =
std::env::var("XIAOMI_MIMO_BASE_URL").or_else(|_| std::env::var("MIMO_BASE_URL"))
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.xiaomi_mimo
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::WanjieArk)
&& let Ok(value) = std::env::var("WANJIE_ARK_BASE_URL")
.or_else(|_| std::env::var("WANJIE_BASE_URL"))
@@ -2682,6 +2735,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::WanjieArk => &mut providers.wanjie_ark,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Moonshot => &mut providers.moonshot,
@@ -2727,6 +2781,16 @@ fn apply_env_overrides(config: &mut Config) {
.openai
.model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::XiaomiMimo)
&& let Ok(value) =
std::env::var("XIAOMI_MIMO_MODEL").or_else(|_| std::env::var("MIMO_MODEL"))
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.xiaomi_mimo
.model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Atlascloud)
&& let Ok(value) = std::env::var("ATLASCLOUD_MODEL")
{
@@ -2786,6 +2850,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::WanjieArk => &mut providers.wanjie_ark,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Moonshot => &mut providers.moonshot,
@@ -3050,6 +3115,7 @@ pub(crate) fn provider_passes_model_through(provider: ApiProvider) -> bool {
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::XiaomiMimo
| ApiProvider::Moonshot
| ApiProvider::Ollama
)
@@ -3071,6 +3137,7 @@ fn default_base_url_for_provider(provider: ApiProvider) -> &'static str {
ApiProvider::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ApiProvider::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL,
ApiProvider::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ApiProvider::XiaomiMimo => DEFAULT_XIAOMI_MIMO_BASE_URL,
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ApiProvider::Moonshot => DEFAULT_MOONSHOT_BASE_URL,
@@ -3328,6 +3395,7 @@ fn merge_providers(
atlascloud: merge_provider_config(base.atlascloud, override_cfg.atlascloud),
wanjie_ark: merge_provider_config(base.wanjie_ark, override_cfg.wanjie_ark),
openrouter: merge_provider_config(base.openrouter, override_cfg.openrouter),
xiaomi_mimo: merge_provider_config(base.xiaomi_mimo, override_cfg.xiaomi_mimo),
novita: merge_provider_config(base.novita, override_cfg.novita),
fireworks: merge_provider_config(base.fireworks, override_cfg.fireworks),
moonshot: merge_provider_config(base.moonshot, override_cfg.moonshot),
@@ -3748,6 +3816,10 @@ pub fn active_provider_has_env_api_key(config: &Config) -> bool {
ApiProvider::Openrouter => {
std::env::var("OPENROUTER_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::XiaomiMimo => {
std::env::var("XIAOMI_MIMO_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("MIMO_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::Novita => std::env::var("NOVITA_API_KEY").is_ok_and(|k| !k.trim().is_empty()),
ApiProvider::Fireworks => {
std::env::var("FIREWORKS_API_KEY").is_ok_and(|k| !k.trim().is_empty())
@@ -3779,6 +3851,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY",
@@ -3800,6 +3873,11 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
{
return true;
}
if matches!(provider, ApiProvider::XiaomiMimo)
&& std::env::var("MIMO_API_KEY").is_ok_and(|k| !k.trim().is_empty())
{
return true;
}
if matches!(provider, ApiProvider::Moonshot)
&& std::env::var("KIMI_API_KEY").is_ok_and(|k| !k.trim().is_empty())
{
@@ -3873,6 +3951,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::Atlascloud => "providers.atlascloud",
ApiProvider::WanjieArk => "providers.wanjie_ark",
ApiProvider::Openrouter => "providers.openrouter",
ApiProvider::XiaomiMimo => "providers.xiaomi_mimo",
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
ApiProvider::Moonshot => "providers.moonshot",
@@ -3910,6 +3989,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::Atlascloud => "atlascloud",
ApiProvider::WanjieArk => "wanjie_ark",
ApiProvider::Openrouter => "openrouter",
ApiProvider::XiaomiMimo => "xiaomi_mimo",
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
ApiProvider::Moonshot => "moonshot",
@@ -3999,6 +4079,7 @@ fn provider_config_key(provider: ApiProvider) -> Result<&'static str> {
ApiProvider::Atlascloud => Ok("atlascloud"),
ApiProvider::WanjieArk => Ok("wanjie_ark"),
ApiProvider::Openrouter => Ok("openrouter"),
ApiProvider::XiaomiMimo => Ok("xiaomi_mimo"),
ApiProvider::Novita => Ok("novita"),
ApiProvider::Fireworks => Ok("fireworks"),
ApiProvider::Moonshot => Ok("moonshot"),
@@ -4391,6 +4472,12 @@ mod tests {
wanjie_maas_model: Option<OsString>,
openrouter_api_key: Option<OsString>,
openrouter_base_url: Option<OsString>,
xiaomi_mimo_api_key: Option<OsString>,
mimo_api_key: Option<OsString>,
xiaomi_mimo_base_url: Option<OsString>,
mimo_base_url: Option<OsString>,
xiaomi_mimo_model: Option<OsString>,
mimo_model: Option<OsString>,
novita_api_key: Option<OsString>,
novita_base_url: Option<OsString>,
fireworks_api_key: Option<OsString>,
@@ -4456,6 +4543,12 @@ mod tests {
let wanjie_maas_model_prev = env::var_os("WANJIE_MAAS_MODEL");
let openrouter_api_key_prev = env::var_os("OPENROUTER_API_KEY");
let openrouter_base_url_prev = env::var_os("OPENROUTER_BASE_URL");
let xiaomi_mimo_api_key_prev = env::var_os("XIAOMI_MIMO_API_KEY");
let mimo_api_key_prev = env::var_os("MIMO_API_KEY");
let xiaomi_mimo_base_url_prev = env::var_os("XIAOMI_MIMO_BASE_URL");
let mimo_base_url_prev = env::var_os("MIMO_BASE_URL");
let xiaomi_mimo_model_prev = env::var_os("XIAOMI_MIMO_MODEL");
let mimo_model_prev = env::var_os("MIMO_MODEL");
let novita_api_key_prev = env::var_os("NOVITA_API_KEY");
let novita_base_url_prev = env::var_os("NOVITA_BASE_URL");
let fireworks_api_key_prev = env::var_os("FIREWORKS_API_KEY");
@@ -4516,6 +4609,12 @@ mod tests {
env::remove_var("WANJIE_MAAS_MODEL");
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENROUTER_BASE_URL");
env::remove_var("XIAOMI_MIMO_API_KEY");
env::remove_var("MIMO_API_KEY");
env::remove_var("XIAOMI_MIMO_BASE_URL");
env::remove_var("MIMO_BASE_URL");
env::remove_var("XIAOMI_MIMO_MODEL");
env::remove_var("MIMO_MODEL");
env::remove_var("NOVITA_API_KEY");
env::remove_var("NOVITA_BASE_URL");
env::remove_var("FIREWORKS_API_KEY");
@@ -4576,6 +4675,12 @@ mod tests {
wanjie_maas_model: wanjie_maas_model_prev,
openrouter_api_key: openrouter_api_key_prev,
openrouter_base_url: openrouter_base_url_prev,
xiaomi_mimo_api_key: xiaomi_mimo_api_key_prev,
mimo_api_key: mimo_api_key_prev,
xiaomi_mimo_base_url: xiaomi_mimo_base_url_prev,
mimo_base_url: mimo_base_url_prev,
xiaomi_mimo_model: xiaomi_mimo_model_prev,
mimo_model: mimo_model_prev,
novita_api_key: novita_api_key_prev,
novita_base_url: novita_base_url_prev,
fireworks_api_key: fireworks_api_key_prev,
@@ -4645,6 +4750,12 @@ mod tests {
Self::restore_var("WANJIE_MAAS_MODEL", self.wanjie_maas_model.take());
Self::restore_var("OPENROUTER_API_KEY", self.openrouter_api_key.take());
Self::restore_var("OPENROUTER_BASE_URL", self.openrouter_base_url.take());
Self::restore_var("XIAOMI_MIMO_API_KEY", self.xiaomi_mimo_api_key.take());
Self::restore_var("MIMO_API_KEY", self.mimo_api_key.take());
Self::restore_var("XIAOMI_MIMO_BASE_URL", self.xiaomi_mimo_base_url.take());
Self::restore_var("MIMO_BASE_URL", self.mimo_base_url.take());
Self::restore_var("XIAOMI_MIMO_MODEL", self.xiaomi_mimo_model.take());
Self::restore_var("MIMO_MODEL", self.mimo_model.take());
Self::restore_var("NOVITA_API_KEY", self.novita_api_key.take());
Self::restore_var("NOVITA_BASE_URL", self.novita_base_url.take());
Self::restore_var("FIREWORKS_API_KEY", self.fireworks_api_key.take());
@@ -5987,6 +6098,54 @@ http_headers = { "X-Model-Provider-Id" = "from-file" }
Ok(())
}
#[test]
fn xiaomi_mimo_provider_uses_documented_defaults() -> Result<()> {
let config = Config {
provider: Some("xiaomi-mimo".to_string()),
..Default::default()
};
config.validate()?;
assert_eq!(config.api_provider(), ApiProvider::XiaomiMimo);
assert_eq!(config.default_model(), DEFAULT_XIAOMI_MIMO_MODEL);
assert_eq!(config.deepseek_base_url(), DEFAULT_XIAOMI_MIMO_BASE_URL);
Ok(())
}
#[test]
fn xiaomi_mimo_env_overrides_provider_base_url_model_and_key() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-xiaomi-mimo-env-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var("DEEPSEEK_PROVIDER", "mimo");
env::set_var("MIMO_API_KEY", "mimo-env-key");
env::set_var("MIMO_BASE_URL", "https://mimo-gateway.example/v1");
env::set_var("MIMO_MODEL", "mimo-v2.5");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::XiaomiMimo);
assert_eq!(config.deepseek_api_key()?, "mimo-env-key");
assert_eq!(
config.deepseek_base_url(),
"https://mimo-gateway.example/v1"
);
assert_eq!(config.default_model(), "mimo-v2.5");
Ok(())
}
#[test]
fn atlascloud_provider_uses_documented_defaults() -> Result<()> {
let config = Config {
@@ -7034,6 +7193,7 @@ api_key = "moonshot-platform-key"
assert!(!has_api_key_for(&config, ApiProvider::Openai));
assert!(!has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(!has_api_key_for(&config, ApiProvider::Openrouter));
assert!(!has_api_key_for(&config, ApiProvider::XiaomiMimo));
assert!(
has_api_key_for(&config, ApiProvider::Sglang),
"SGLang is self-hosted and does not require a key by default"
@@ -7048,10 +7208,12 @@ api_key = "moonshot-platform-key"
env::set_var("OPENROUTER_API_KEY", "or-env");
env::set_var("OPENAI_API_KEY", "openai-env");
env::set_var("WANJIE_API_KEY", "wanjie-env");
env::set_var("MIMO_API_KEY", "mimo-env");
}
assert!(has_api_key_for(&config, ApiProvider::Openai));
assert!(has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(has_api_key_for(&config, ApiProvider::Openrouter));
assert!(has_api_key_for(&config, ApiProvider::XiaomiMimo));
assert!(!has_api_key_for(&config, ApiProvider::Novita));
// Safety: test-only environment mutation guarded by a global mutex.
@@ -7059,14 +7221,17 @@ api_key = "moonshot-platform-key"
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENAI_API_KEY");
env::remove_var("WANJIE_API_KEY");
env::remove_var("MIMO_API_KEY");
}
let mut providers = ProvidersConfig::default();
providers.openai.api_key = Some("file-openai".to_string());
providers.wanjie_ark.api_key = Some("file-wanjie".to_string());
providers.xiaomi_mimo.api_key = Some("file-mimo".to_string());
providers.novita.api_key = Some("file-novita".to_string());
config.providers = Some(providers);
assert!(has_api_key_for(&config, ApiProvider::Openai));
assert!(has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(has_api_key_for(&config, ApiProvider::XiaomiMimo));
assert!(has_api_key_for(&config, ApiProvider::Novita));
assert!(!has_api_key_for(&config, ApiProvider::Openrouter));
Ok(())
@@ -7159,6 +7324,7 @@ api_key = "moonshot-platform-key"
save_api_key_for(ApiProvider::Openai, "openai-saved-key")?;
save_api_key_for(ApiProvider::WanjieArk, "wanjie-saved-key")?;
save_api_key_for(ApiProvider::Fireworks, "fireworks-saved-key")?;
save_api_key_for(ApiProvider::XiaomiMimo, "mimo-saved-key")?;
save_api_key_for(ApiProvider::Sglang, "sglang-saved-key")?;
let contents = fs::read_to_string(&path)?;
let parsed: toml::Value = toml::from_str(&contents)?;
@@ -7186,6 +7352,14 @@ api_key = "moonshot-platform-key"
.and_then(toml::Value::as_str),
Some("fireworks-saved-key")
);
assert_eq!(
parsed
.get("providers")
.and_then(|p| p.get("xiaomi_mimo"))
.and_then(|t| t.get("api_key"))
.and_then(toml::Value::as_str),
Some("mimo-saved-key")
);
assert_eq!(
parsed
.get("providers")
@@ -7421,6 +7595,19 @@ model = "deepseek-ai/deepseek-v4-pro"
);
}
#[test]
fn provider_capability_xiaomi_mimo_has_thinking_no_cache() {
let cap = provider_capability(ApiProvider::XiaomiMimo, DEFAULT_XIAOMI_MIMO_MODEL);
assert_eq!(cap.context_window, 1_000_000);
assert_eq!(cap.max_output, 128_000);
assert!(cap.thinking_supported);
assert!(!cap.cache_telemetry_supported);
assert_eq!(
cap.request_payload_mode,
RequestPayloadMode::ChatCompletions
);
}
#[test]
fn provider_capability_novita_v4_pro_has_thinking_no_cache() {
let cap = provider_capability(ApiProvider::Novita, DEFAULT_NOVITA_MODEL);
+1
View File
@@ -395,6 +395,7 @@ impl Engine {
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY/WANJIE_API_KEY/WANJIE_MAAS_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY/MIMO_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY/KIMI_API_KEY",
+10
View File
@@ -1876,6 +1876,10 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
"OPENROUTER_API_KEY",
"codewhale auth set --provider openrouter --api-key \"...\"",
),
crate::config::ApiProvider::XiaomiMimo => (
"XIAOMI_MIMO_API_KEY/MIMO_API_KEY",
"codewhale auth set --provider xiaomi-mimo --api-key \"...\"",
),
crate::config::ApiProvider::Novita => (
"NOVITA_API_KEY",
"codewhale auth set --provider novita --api-key \"...\"",
@@ -1912,6 +1916,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
crate::config::ApiProvider::Atlascloud => "atlascloud",
crate::config::ApiProvider::WanjieArk => "wanjie_ark",
crate::config::ApiProvider::Openrouter => "openrouter",
crate::config::ApiProvider::XiaomiMimo => "xiaomi_mimo",
crate::config::ApiProvider::Novita => "novita",
crate::config::ApiProvider::Fireworks => "fireworks",
crate::config::ApiProvider::Moonshot => "moonshot",
@@ -2218,6 +2223,11 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
"openrouter",
&["OPENROUTER_API_KEY"][..],
),
(
crate::config::ApiProvider::XiaomiMimo,
"xiaomi-mimo",
&["XIAOMI_MIMO_API_KEY", "MIMO_API_KEY"][..],
),
(
crate::config::ApiProvider::Novita,
"novita",
+2
View File
@@ -102,6 +102,7 @@ impl ProviderPickerView {
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY / MIMO_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY / KIMI_API_KEY",
@@ -473,6 +474,7 @@ mod tests {
"AtlasCloud",
"Wanjie Ark",
"OpenRouter",
"Xiaomi MiMo",
"Novita AI",
"Fireworks AI",
"Moonshot/Kimi",
+3
View File
@@ -5932,6 +5932,7 @@ fn render(f: &mut Frame, app: &mut App) {
crate::config::ApiProvider::Atlascloud => Some("Atlas"),
crate::config::ApiProvider::WanjieArk => Some("Wanjie"),
crate::config::ApiProvider::Openrouter => Some("OR"),
crate::config::ApiProvider::XiaomiMimo => Some("MiMo"),
crate::config::ApiProvider::Novita => Some("Novita"),
crate::config::ApiProvider::Fireworks => Some("Fireworks"),
crate::config::ApiProvider::Moonshot => Some("Kimi"),
@@ -6849,6 +6850,7 @@ async fn apply_provider_picker_api_key(
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::WanjieArk => &mut providers.wanjie_ark,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Moonshot => &mut providers.moonshot,
@@ -6901,6 +6903,7 @@ fn set_provider_auth_mode_in_memory(config: &mut Config, provider: ApiProvider,
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::WanjieArk => &mut providers.wanjie_ark,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Moonshot => &mut providers.moonshot,
+74 -19
View File
@@ -13,6 +13,8 @@ use crate::tools::spec::{
ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec, required_str,
};
const DEFAULT_VISION_MAX_OUTPUT_TOKENS: u32 = 4096;
pub struct ImageAnalyzeTool {
config: VisionModelConfig,
client: reqwest::Client,
@@ -67,6 +69,48 @@ impl ImageAnalyzeTool {
fn api_key(&self) -> String {
self.config.api_key.clone().unwrap_or_default()
}
fn uses_max_completion_tokens(base_url: &str) -> bool {
let Ok(url) = reqwest::Url::parse(base_url) else {
return false;
};
let Some(domain) = url.domain() else {
return false;
};
domain.eq_ignore_ascii_case("xiaomimimo.com")
|| domain.to_ascii_lowercase().ends_with(".xiaomimimo.com")
}
fn request_payload(&self, prompt: &str, image_data: &str, mime_type: &str) -> Value {
let mut payload = json!({
"model": self.config.model,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": format!("data:{};base64,{}", mime_type, image_data)
}
}
]
}
],
"temperature": 0.7
});
let token_limit_field = if Self::uses_max_completion_tokens(&self.base_url()) {
"max_completion_tokens"
} else {
"max_tokens"
};
payload[token_limit_field] = json!(DEFAULT_VISION_MAX_OUTPUT_TOKENS);
payload
}
}
#[async_trait]
@@ -122,25 +166,7 @@ impl ToolSpec for ImageAnalyzeTool {
let resolved_path = context.workspace.join(image_path_buf);
let (image_data, mime_type) = Self::read_image_file(&resolved_path).await?;
let payload = json!({
"model": self.config.model,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": format!("data:{};base64,{}", mime_type, image_data)
}
}
]
}
],
"max_tokens": 4096,
"temperature": 0.7
});
let payload = self.request_payload(prompt, &image_data, &mime_type);
let url = format!("{}/chat/completions", self.base_url());
let api_key = self.api_key();
@@ -262,6 +288,35 @@ mod tests {
assert!(err.to_string().contains("Unsupported image format"));
}
#[test]
fn generic_vision_payload_uses_max_tokens() {
let tool = ImageAnalyzeTool::new(fake_config());
let payload = tool.request_payload("describe", "abc123", "image/png");
assert_eq!(
payload.get("max_tokens").and_then(Value::as_u64),
Some(u64::from(DEFAULT_VISION_MAX_OUTPUT_TOKENS))
);
assert!(payload.get("max_completion_tokens").is_none());
}
#[test]
fn xiaomi_mimo_vision_payload_uses_max_completion_tokens() {
let mut config = fake_config();
config.model = "mimo-v2.5".to_string();
config.base_url = Some("https://api.xiaomimimo.com/v1".to_string());
let tool = ImageAnalyzeTool::new(config);
let payload = tool.request_payload("describe", "abc123", "image/png");
assert_eq!(
payload.get("max_completion_tokens").and_then(Value::as_u64),
Some(u64::from(DEFAULT_VISION_MAX_OUTPUT_TOKENS))
);
assert!(payload.get("max_tokens").is_none());
}
#[tokio::test]
async fn execute_rejects_absolute_path() {
// Trust-boundary pin: image_path must stay inside the workspace
+29 -6
View File
@@ -63,8 +63,8 @@ provider's keyring entry.
For hosted, generic OpenAI-compatible, or self-hosted providers, set
`provider = "nvidia-nim"`, `"openai"`, `"atlascloud"`, `"wanjie-ark"`,
`"openrouter"`, `"novita"`, `"fireworks"`, `"moonshot"`, `"sglang"`,
`"vllm"`, or `"ollama"` or pass `codewhale --provider <name>`.
`"openrouter"`, `"xiaomi-mimo"`, `"novita"`, `"fireworks"`, `"moonshot"`,
`"sglang"`, `"vllm"`, or `"ollama"` or pass `codewhale --provider <name>`.
For the provider-by-provider registry, including auth variables, default base
URLs, model IDs, and capability metadata, see [PROVIDERS.md](PROVIDERS.md).
The facade saves provider credentials to the shared user config and forwards
@@ -73,6 +73,7 @@ the resolved key, base URL, provider, and model to the TUI process. Use
`codewhale auth set --provider openai --api-key "YOUR_OPENAI_COMPATIBLE_API_KEY"` or
`codewhale auth set --provider atlascloud --api-key "YOUR_ATLASCLOUD_API_KEY"` or
`codewhale auth set --provider wanjie-ark --api-key "YOUR_WANJIE_API_KEY"` or
`codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_MIMO_API_KEY"` or
`codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"`
to save provider keys through the facade. The generic `openai` provider defaults
to `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to
@@ -129,6 +130,25 @@ environment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated
and `Content-Type` are managed by the client and are not overridden by this
setting.
### Vision Model
CodeWhale's chat provider and `image_analyze` tool are configured separately.
The main chat path remains the selected text/tool provider; image analysis runs
through `[vision_model]` when the `vision_model` feature is enabled.
Xiaomi's current image-understanding docs include `mimo-v2.5` for image input.
To use MiMo for `image_analyze`, configure the vision model explicitly:
```toml
[features]
vision_model = true
[vision_model]
model = "mimo-v2.5"
api_key = "YOUR_XIAOMI_MIMO_API_KEY"
base_url = "https://api.xiaomimimo.com/v1"
```
To bootstrap MCP and skills directories at their resolved paths, run `codewhale-tui setup`.
To only scaffold MCP, run `codewhale-tui mcp init`.
@@ -207,7 +227,7 @@ aliases. When both forms are set the `CODEWHALE_*` value wins; the
`DEEPSEEK_*` form is kept for older shells:
- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —
`deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|openrouter|novita|fireworks|moonshot|sglang|vllm|ollama`
`deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|openrouter|xiaomi-mimo|novita|fireworks|moonshot|sglang|vllm|ollama`
- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider
- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider
@@ -232,6 +252,9 @@ Remaining variables:
- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`
- `OPENROUTER_API_KEY`
- `OPENROUTER_BASE_URL`
- `XIAOMI_MIMO_API_KEY` or `MIMO_API_KEY`
- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`
- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`
- `NOVITA_API_KEY`
- `NOVITA_BASE_URL`
- `FIREWORKS_API_KEY`
@@ -441,10 +464,10 @@ If you are upgrading from older releases:
### Core keys (used by the TUI/engine)
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, or `ollama`. 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. `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`; `openrouter` targets `https://openrouter.ai/api/v1`; `novita` targets `https://api.novita.ai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `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`.
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, or `ollama`. 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. `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`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint at `https://api.xiaomimimo.com/v1`; `novita` targets `https://api.novita.ai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `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`.
- `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://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://openrouter.ai/api/v1` for `openrouter`, `https://api.novita.ai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.moonshot.ai/v1` for `moonshot`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `kimi-k2.6` for Moonshot, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai`, `atlascloud`, `wanjie-ark`, and Ollama model IDs are passed through unchanged. OpenRouter provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.
- `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://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://openrouter.ai/api/v1` for `openrouter`, `https://api.xiaomimimo.com/v1` for `xiaomi-mimo`, `https://api.novita.ai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.moonshot.ai/v1` for `moonshot`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.5-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `kimi-k2.6` for Moonshot, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, and Ollama model IDs are passed through unchanged. OpenRouter provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.
- `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.
- `allow_shell` (bool, optional): defaults to `true` (sandboxed).
- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.
+19 -6
View File
@@ -6,11 +6,11 @@ limited to provider IDs, config keys, auth paths, base URLs, model resolution,
and capability metadata that the code already knows about.
DeepSeek remains the first-class default provider. NVIDIA NIM, OpenRouter,
Novita, Fireworks, generic OpenAI-compatible endpoints, self-hosted runtimes,
and Moonshot/Kimi are additive routes for running the same terminal harness
against other hosted or local model endpoints. Hugging Face Inference Providers
are a planned additive open-model routing layer; they are not a native provider
in this checkout yet.
Xiaomi MiMo, Novita, Fireworks, generic OpenAI-compatible endpoints,
self-hosted runtimes, and Moonshot/Kimi are additive routes for running the
same terminal harness against other hosted or local model endpoints. Hugging
Face Inference Providers are a planned additive open-model routing layer; they
are not a native provider in this checkout yet.
Sources to keep in sync:
@@ -30,7 +30,8 @@ Sources to keep in sync:
The canonical provider IDs are:
`deepseek`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`,
`novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, and `ollama`.
`xiaomi-mimo`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, and
`ollama`.
Use any of these surfaces to select a provider:
@@ -116,6 +117,7 @@ endpoint.
| `atlascloud` | `[providers.atlascloud]` | `ATLASCLOUD_API_KEY` | `ATLASCLOUD_BASE_URL`; default `https://api.atlascloud.ai/v1` | Default config model `deepseek-ai/deepseek-v4-flash` | OpenAI-compatible hosted route. `ATLASCLOUD_MODEL` is accepted by the TUI config path. The static `ModelRegistry` does not currently list AtlasCloud rows. |
| `wanjie-ark` | `[providers.wanjie_ark]` | `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, `WANJIE_MAAS_API_KEY` | `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, `WANJIE_MAAS_BASE_URL`; default `https://maas-openapi.wanjiedata.com/api/v1` | `deepseek-reasoner` | OpenAI-compatible hosted route. `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, and `WANJIE_MAAS_MODEL` are accepted. |
| `openrouter` | `[providers.openrouter]` | `OPENROUTER_API_KEY` | `OPENROUTER_BASE_URL`; default `https://openrouter.ai/api/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | Additive open-model routing layer. It does not replace DeepSeek; it lets users route supported model IDs through OpenRouter when they choose it. |
| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | `XIAOMI_MIMO_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`; default `https://api.xiaomimimo.com/v1` | `mimo-v2.5-pro`, `mimo-v2.5` | Xiaomi MiMo OpenAI-compatible chat completions route. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. |
| `novita` | `[providers.novita]` | `NOVITA_API_KEY` | `NOVITA_BASE_URL`; default `https://api.novita.ai/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | OpenAI-compatible hosted route for DeepSeek model IDs. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |
| `fireworks` | `[providers.fireworks]` | `FIREWORKS_API_KEY` | `FIREWORKS_BASE_URL`; default `https://api.fireworks.ai/inference/v1` | `accounts/fireworks/models/deepseek-v4-pro` | OpenAI-compatible hosted route. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |
| `moonshot` | `[providers.moonshot]` | `MOONSHOT_API_KEY`, `KIMI_API_KEY` | `MOONSHOT_BASE_URL`, `KIMI_BASE_URL`; default `https://api.moonshot.ai/v1` | `kimi-k2.6`; Kimi Code path uses `kimi-for-coding` at `https://api.kimi.com/coding/v1` | Moonshot/Kimi route. `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, and `KIMI_MODEL` are accepted. `[providers.moonshot] auth_mode = "kimi_oauth"` reads Kimi CLI OAuth credentials when present. |
@@ -123,6 +125,15 @@ endpoint.
| `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. |
### Xiaomi MiMo Notes
`xiaomi-mimo` defaults to `mimo-v2.5-pro` for long-context reasoning and coding
work, while the static registry also exposes `mimo-v2.5`. Xiaomi's current
[image-understanding guide](https://platform.xiaomimimo.com/docs/en-US/usage-guide/multimodal-understanding/image-understanding)
includes `mimo-v2.5` for image input. CodeWhale exposes image analysis through the
separate `[vision_model]` / `image_analyze` path; set that model to
`mimo-v2.5` when using MiMo for vision.
## Static Model Registry
`codewhale model list` and `codewhale model resolve` use the static registry in
@@ -137,6 +148,7 @@ endpoint when the endpoint supports model listing.
| `openai` | `deepseek-v4-pro`, `deepseek-v4-flash` | yes | yes |
| `wanjie-ark` | `deepseek-reasoner` | yes | yes |
| `openrouter` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | yes | yes |
| `xiaomi-mimo` | `mimo-v2.5-pro`, `mimo-v2.5` | yes | yes |
| `novita` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | yes | yes |
| `fireworks` | `accounts/fireworks/models/deepseek-v4-pro` | yes | yes |
| `moonshot` | `kimi-k2.6` | yes | yes |
@@ -164,6 +176,7 @@ All shipped providers use the Chat Completions request payload mode today.
| DeepSeek compatibility aliases (`deepseek-chat`, `deepseek-reasoner`) | 1,000,000 | 384,000 | yes | yes | DeepSeek beta only |
| NVIDIA NIM V4 registry models | 1,000,000 | 384,000 | yes | yes | not documented in code |
| OpenRouter, Novita, Fireworks, SGLang, and vLLM V4 model IDs | 1,000,000 | 384,000 | yes | no | not documented in code |
| Xiaomi MiMo models | 1,000,000 | 128,000 | yes | no | not documented in code |
| Wanjie Ark `reasoner` / `r1` model IDs | 128,000 | 4,096 | yes | no | not documented in code |
| Generic `openai`, AtlasCloud, and Moonshot/Kimi | 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 |