diff --git a/README.ja-JP.md b/README.ja-JP.md index 667aafb5d..d3f04c513 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -214,6 +214,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 @@ -319,15 +323,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 のエンドポイント | diff --git a/README.md b/README.md index 3213ee154..beb843d75 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,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 @@ -477,15 +481,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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index f079cc800..22ddd3d81 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -259,6 +259,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 @@ -400,15 +404,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 端点 | diff --git a/config.example.toml b/config.example.toml index c8a7155bc..c9c7086dd 100644 --- a/config.example.toml +++ b/config.example.toml @@ -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 @@ -232,6 +235,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" @@ -368,6 +377,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 diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs index 349951ce8..4f10025fa 100644 --- a/crates/agent/src/lib.rs +++ b/crates/agent/src/lib.rs @@ -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(); diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 9f98eebff..38d86f61d 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -29,6 +29,7 @@ enum ProviderArg { Atlascloud, WanjieArk, Openrouter, + XiaomiMimo, Novita, Fireworks, Moonshot, @@ -46,6 +47,7 @@ impl From 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, @@ -718,6 +720,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", @@ -728,13 +731,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, @@ -789,6 +793,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"], @@ -1473,6 +1478,7 @@ fn build_tui_command( | ProviderKind::Atlascloud | ProviderKind::WanjieArk | ProviderKind::Openrouter + | ProviderKind::XiaomiMimo | ProviderKind::Novita | ProviderKind::Fireworks | ProviderKind::Moonshot @@ -1481,7 +1487,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() ); } @@ -2879,6 +2885,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, diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index bb2c339bb..253df4014 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -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 { 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, wanjie_ark_model: Option, moonshot_model: Option, + xiaomi_mimo_model: Option, output_mode: Option, auth_mode: Option, log_level: Option, @@ -1814,6 +1876,7 @@ struct EnvRuntimeOverrides { atlascloud_base_url: Option, wanjie_ark_base_url: Option, openrouter_base_url: Option, + xiaomi_mimo_base_url: Option, novita_base_url: Option, fireworks_base_url: Option, moonshot_base_url: Option, @@ -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, openrouter_api_key: Option, openrouter_base_url: Option, + xiaomi_mimo_api_key: Option, + mimo_api_key: Option, + xiaomi_mimo_base_url: Option, + mimo_base_url: Option, + xiaomi_mimo_model: Option, + mimo_model: Option, wanjie_ark_api_key: Option, wanjie_ark_base_url: Option, wanjie_base_url: Option, @@ -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(); diff --git a/crates/secrets/src/lib.rs b/crates/secrets/src/lib.rs index 69c3fc9a3..2e012fc40 100644 --- a/crates/secrets/src/lib.rs +++ b/crates/secrets/src/lib.rs @@ -525,6 +525,9 @@ pub fn env_for(name: &str) -> Option { 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(); diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index ad9ae2e0f..b7281b362 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -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!({ diff --git a/crates/tui/src/client/chat.rs b/crates/tui/src/client/chat.rs index 656330fc8..1c66079a7 100644 --- a/crates/tui/src/client/chat.rs +++ b/crates/tui/src/client/chat.rs @@ -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] diff --git a/crates/tui/src/commands/provider.rs b/crates/tui/src/commands/provider.rs index 915cce8c5..6caaacc96 100644 --- a/crates/tui/src/commands/provider.rs +++ b/crates/tui/src/commands/provider.rs @@ -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(); diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 282d2023e..53338d295 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -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], @@ -1341,6 +1364,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, @@ -1500,6 +1525,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", @@ -1522,7 +1548,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 @@ -1642,6 +1668,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, @@ -1732,6 +1759,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, @@ -1764,6 +1792,7 @@ impl Config { | ApiProvider::Atlascloud | ApiProvider::WanjieArk | ApiProvider::Openrouter + | ApiProvider::XiaomiMimo | ApiProvider::Novita | ApiProvider::Fireworks | ApiProvider::Moonshot @@ -1780,6 +1809,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 => { @@ -1823,6 +1853,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", @@ -1908,6 +1939,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." @@ -2496,6 +2531,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 @@ -2598,6 +2640,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")) @@ -2681,6 +2734,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, @@ -2726,6 +2780,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") { @@ -2785,6 +2849,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, @@ -3049,6 +3114,7 @@ pub(crate) fn provider_passes_model_through(provider: ApiProvider) -> bool { ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::WanjieArk + | ApiProvider::XiaomiMimo | ApiProvider::Moonshot | ApiProvider::Ollama ) @@ -3070,6 +3136,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, @@ -3327,6 +3394,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), @@ -3747,6 +3815,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()) @@ -3778,6 +3850,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", @@ -3799,6 +3872,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()) { @@ -3872,6 +3950,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result 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", @@ -3909,6 +3988,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result ApiProvider::Atlascloud => "atlascloud", ApiProvider::WanjieArk => "wanjie_ark", ApiProvider::Openrouter => "openrouter", + ApiProvider::XiaomiMimo => "xiaomi_mimo", ApiProvider::Novita => "novita", ApiProvider::Fireworks => "fireworks", ApiProvider::Moonshot => "moonshot", @@ -3998,6 +4078,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"), @@ -4390,6 +4471,12 @@ mod tests { wanjie_maas_model: Option, openrouter_api_key: Option, openrouter_base_url: Option, + xiaomi_mimo_api_key: Option, + mimo_api_key: Option, + xiaomi_mimo_base_url: Option, + mimo_base_url: Option, + xiaomi_mimo_model: Option, + mimo_model: Option, novita_api_key: Option, novita_base_url: Option, fireworks_api_key: Option, @@ -4455,6 +4542,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"); @@ -4515,6 +4608,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"); @@ -4575,6 +4674,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, @@ -4644,6 +4749,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()); @@ -5986,6 +6097,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 { @@ -7033,6 +7192,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" @@ -7047,10 +7207,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. @@ -7058,14 +7220,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(()) @@ -7158,6 +7323,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)?; @@ -7185,6 +7351,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") @@ -7420,6 +7594,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); diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 02737eb7a..c9caa1f1c 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -387,6 +387,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", diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 99579eac3..f12bcfa2e 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -1866,6 +1866,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 \"...\"", @@ -1902,6 +1906,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", @@ -2163,6 +2168,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", diff --git a/crates/tui/src/tui/provider_picker.rs b/crates/tui/src/tui/provider_picker.rs index b2ac79e6c..d7d176d6d 100644 --- a/crates/tui/src/tui/provider_picker.rs +++ b/crates/tui/src/tui/provider_picker.rs @@ -92,6 +92,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", @@ -406,6 +407,7 @@ mod tests { "AtlasCloud", "Wanjie Ark", "OpenRouter", + "Xiaomi MiMo", "Novita AI", "Fireworks AI", "Moonshot/Kimi", diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index fb89de619..62fd3da7a 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -5748,6 +5748,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"), @@ -6648,6 +6649,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, @@ -6700,6 +6702,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, diff --git a/crates/tui/src/vision/tools.rs b/crates/tui/src/vision/tools.rs index bfce551df..37e8507f3 100644 --- a/crates/tui/src/vision/tools.rs +++ b/crates/tui/src/vision/tools.rs @@ -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 diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f0b75de0b..56d69715a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 `. +`"openrouter"`, `"xiaomi-mimo"`, `"novita"`, `"fireworks"`, `"moonshot"`, +`"sglang"`, `"vllm"`, or `"ollama"` or pass `codewhale --provider `. 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. diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 38969415f..bcd1f6ddc 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -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: @@ -27,7 +27,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: @@ -71,6 +72,7 @@ self-hosted runtimes. | `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. | @@ -78,6 +80,15 @@ self-hosted runtimes. | `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 @@ -92,6 +103,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 | @@ -119,6 +131,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 |