feat(config): add Xiaomi MiMo token plan mode

Harvested from PR #2627 by @xyuai.

Refs #2621 reported by @springeye.
This commit is contained in:
xyuai
2026-06-05 09:04:45 -07:00
committed by Hunter B
parent 15c506b777
commit be37dbd34e
9 changed files with 580 additions and 31 deletions
+4
View File
@@ -65,6 +65,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`SOFYA_API_KEY` fallback, while keeping Sofya scoped to web search rather
than model-provider routing (#2790). Thanks @yusufgurdogan for the
implementation.
- Added Xiaomi MiMo `mode` / `XIAOMI_MIMO_MODE` / `MIMO_MODE` selection for
Token Plan region endpoints and pay-as-you-go routing, plus dedicated Token
Plan env keys for `tp-*` subscriptions (#2621, #2627). Thanks @springeye for
the request and @xyuai for the implementation.
### Changed
+4 -2
View File
@@ -253,6 +253,8 @@ codewhale --provider arcee --model trinity-large-thinking
codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_KEY"
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
codewhale --provider xiaomi-mimo speech "Hello from MiMo" --model tts -o hello.wav
XIAOMI_MIMO_TOKEN_PLAN_API_KEY="tp-..." XIAOMI_MIMO_MODE="token-plan-sgp" \
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
codewhale auth set --provider openai --api-key "YOUR_OPENAI_COMPATIBLE_API_KEY"
OPENAI_BASE_URL="https://openai-compatible.example/v4" \
@@ -503,13 +505,13 @@ Key environment variables:
| `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` / `VOLCENGINE_API_KEY` / `VOLCENGINE_ARK_API_KEY` / `ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `XIAOMI_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SILICONFLOW_API_KEY` / `ARCEE_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` / `HUGGINGFACE_API_KEY` / `HF_TOKEN` | Provider auth |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `VOLCENGINE_API_KEY` / `VOLCENGINE_ARK_API_KEY` / `ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_TOKEN_PLAN_API_KEY` / `MIMO_TOKEN_PLAN_API_KEY` / `XIAOMI_MIMO_API_KEY` / `XIAOMI_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SILICONFLOW_API_KEY` / `ARCEE_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` / `HUGGINGFACE_API_KEY` / `HF_TOKEN` | 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 |
| `VOLCENGINE_BASE_URL` / `VOLCENGINE_ARK_BASE_URL` / `ARK_BASE_URL` / `VOLCENGINE_MODEL` / `VOLCENGINE_ARK_MODEL` | Volcengine 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; Token Plan default is `https://token-plan-sgp.xiaomimimo.com/v1` |
| `XIAOMI_MIMO_BASE_URL` / `MIMO_BASE_URL` / `XIAOMI_MIMO_MODEL` / `MIMO_MODEL` / `XIAOMI_MIMO_MODE` / `MIMO_MODE` | Xiaomi MiMo endpoint, model, and Token Plan mode override; Token Plan default is `https://token-plan-sgp.xiaomimimo.com/v1` |
| `NOVITA_BASE_URL` | Novita endpoint override |
| `FIREWORKS_BASE_URL` | Fireworks endpoint override |
| `SILICONFLOW_BASE_URL` / `SILICONFLOW_MODEL` | SiliconFlow endpoint and model override |
+4 -2
View File
@@ -272,6 +272,8 @@ codewhale --provider openrouter --model qwen/qwen3.7-max
codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_MIMO_API_KEY"
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
codewhale --provider xiaomi-mimo speech "???MiMo" --model tts -o hello.wav
XIAOMI_MIMO_TOKEN_PLAN_API_KEY="tp-..." XIAOMI_MIMO_MODE="token-plan-sgp" \
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
# Novita
codewhale auth set --provider novita --api-key "YOUR_NOVITA_API_KEY"
@@ -427,13 +429,13 @@ DeepSeek 可作为自定义 Agent Client Protocol 服务器运行,供 Zed 等
| `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` / `VOLCENGINE_API_KEY` / `ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SILICONFLOW_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` / `HUGGINGFACE_API_KEY` / `HF_TOKEN` | 提供商认证 |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `VOLCENGINE_API_KEY` / `ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_TOKEN_PLAN_API_KEY` / `MIMO_TOKEN_PLAN_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SILICONFLOW_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` / `HUGGINGFACE_API_KEY` / `HF_TOKEN` | 提供商认证 |
| `OPENAI_BASE_URL` / `OPENAI_MODEL` | 通用 OpenAI 兼容端点和模型 ID |
| `ATLASCLOUD_BASE_URL` / `ATLASCLOUD_MODEL` | AtlasCloud 端点和模型覆盖 |
| `WANJIE_ARK_BASE_URL` / `WANJIE_ARK_MODEL` | Wanjie Ark 端点和模型覆盖 |
| `VOLCENGINE_BASE_URL` / `ARK_BASE_URL` / `VOLCENGINE_MODEL` / `ARK_MODEL` | Volcengine Ark 端点和模型覆盖 |
| `OPENROUTER_BASE_URL` | OpenRouter 端点覆盖 |
| `XIAOMI_MIMO_BASE_URL` / `MIMO_BASE_URL` / `XIAOMI_MIMO_MODEL` / `MIMO_MODEL` | Xiaomi MiMo 端点模型覆盖 |
| `XIAOMI_MIMO_BASE_URL` / `MIMO_BASE_URL` / `XIAOMI_MIMO_MODEL` / `MIMO_MODEL` / `XIAOMI_MIMO_MODE` / `MIMO_MODE` | Xiaomi MiMo 端点模型和 Token Plan 模式覆盖 |
| `NOVITA_BASE_URL` | Novita 端点覆盖 |
| `FIREWORKS_BASE_URL` | Fireworks 端点覆盖 |
| `SILICONFLOW_BASE_URL` / `SILICONFLOW_MODEL` | SiliconFlow 端点和模型覆盖 |
+6 -1
View File
@@ -239,7 +239,7 @@ max_subagents = 10 # optional (1-20)
# Volcengine Ark: VOLCENGINE_API_KEY (or VOLCENGINE_ARK_API_KEY / ARK_API_KEY), VOLCENGINE_BASE_URL, VOLCENGINE_MODEL
# OpenRouter: OPENROUTER_API_KEY, OPENROUTER_BASE_URL, OPENROUTER_MODEL
# Xiaomi MiMo: XIAOMI_MIMO_API_KEY (or XIAOMI_API_KEY / MIMO_API_KEY), XIAOMI_MIMO_BASE_URL, XIAOMI_MIMO_MODEL
# Token Plan keys (`tp-...`) default to https://token-plan-sgp.xiaomimimo.com/v1.
# Token Plan: XIAOMI_MIMO_TOKEN_PLAN_API_KEY (or MIMO_TOKEN_PLAN_API_KEY), XIAOMI_MIMO_MODE/MIMO_MODE
# Novita: NOVITA_API_KEY, NOVITA_BASE_URL, NOVITA_MODEL
# Fireworks: FIREWORKS_API_KEY, FIREWORKS_BASE_URL
# SiliconFlow: SILICONFLOW_API_KEY, SILICONFLOW_BASE_URL, SILICONFLOW_MODEL
@@ -329,6 +329,11 @@ max_subagents = 10 # optional (1-20)
# # base_url = "https://api.xiaomimimo.com/v1" # Pay-as-you-go / sk- keys
# model = "mimo-v2.5-pro" # chat/reasoning
# Chat model IDs: mimo-v2.5-pro, mimo-v2.5
# Token Plan subscriptions use separate tp-* API keys plus api-key auth.
# mode = "token-plan-sgp" # default Token Plan endpoint
# mode = "token-plan-cn" # China cluster
# mode = "token-plan-ams" # Europe cluster
# mode = "pay-as-you-go" # standard API / sk- keys
# TTS aliases are also accepted by `codewhale speech`: tts, voice-design, voice-clone
# TTS model IDs: mimo-v2.5-tts, mimo-v2.5-tts-voicedesign, mimo-v2.5-tts-voiceclone, mimo-v2-tts
+267 -6
View File
@@ -70,6 +70,9 @@ const DEFAULT_SGLANG_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
const DEFAULT_OPENROUTER_BASE_URL: &str = "https://openrouter.ai/api/v1";
const XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
const DEFAULT_XIAOMI_MIMO_BASE_URL: &str = "https://token-plan-sgp.xiaomimimo.com/v1";
const XIAOMI_MIMO_TOKEN_PLAN_CN_BASE_URL: &str = "https://token-plan-cn.xiaomimimo.com/v1";
const XIAOMI_MIMO_TOKEN_PLAN_SGP_BASE_URL: &str = DEFAULT_XIAOMI_MIMO_BASE_URL;
const XIAOMI_MIMO_TOKEN_PLAN_AMS_BASE_URL: &str = "https://token-plan-ams.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_SILICONFLOW_BASE_URL: &str = "https://api.siliconflow.com/v1";
@@ -196,6 +199,7 @@ pub struct ProviderConfigToml {
pub api_key: Option<String>,
pub base_url: Option<String>,
pub model: Option<String>,
pub mode: Option<String>,
pub auth_mode: Option<String>,
#[serde(default)]
pub http_headers: BTreeMap<String, String>,
@@ -828,6 +832,7 @@ impl ConfigToml {
"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.mode" => self.providers.xiaomi_mimo.mode.clone(),
"providers.xiaomi_mimo.http_headers" => {
serialize_http_headers(&self.providers.xiaomi_mimo.http_headers)
}
@@ -1020,6 +1025,9 @@ impl ConfigToml {
"providers.xiaomi_mimo.model" => {
self.providers.xiaomi_mimo.model = Some(value.to_string());
}
"providers.xiaomi_mimo.mode" => {
self.providers.xiaomi_mimo.mode = Some(value.to_string());
}
"providers.xiaomi_mimo.http_headers" => {
self.providers.xiaomi_mimo.http_headers = parse_http_headers(value)?;
}
@@ -1208,6 +1216,7 @@ impl ConfigToml {
"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.mode" => self.providers.xiaomi_mimo.mode = None,
"providers.xiaomi_mimo.http_headers" => {
self.providers.xiaomi_mimo.http_headers.clear();
}
@@ -1403,6 +1412,9 @@ impl ConfigToml {
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) = self.providers.xiaomi_mimo.mode.as_ref() {
out.insert("providers.xiaomi_mimo.mode".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);
}
@@ -1573,15 +1585,38 @@ impl ConfigToml {
.or_else(|| provider_cfg.auth_mode.clone())
.or_else(|| self.auth_mode.clone());
let from_file = provider_cfg.api_key.clone().or(root_deepseek_api_key);
let explicit_api_key_for_endpoint = cli.api_key.as_deref().or(from_file.as_deref());
let configured_base_url = cli
.base_url
.clone()
.or_else(|| env.base_url_for(provider))
.or_else(|| provider_cfg.base_url.clone())
.or(root_deepseek_base_url);
let xiaomi_mimo_mode = if provider == ProviderKind::XiaomiMimo {
env.xiaomi_mimo_mode
.clone()
.or_else(|| provider_cfg.mode.clone())
} else {
None
};
let xiaomi_mimo_env_api_key = if provider == ProviderKind::XiaomiMimo {
xiaomi_mimo_env_api_key_for_runtime(
xiaomi_mimo_mode.as_deref(),
configured_base_url.as_deref(),
)
} else {
None
};
let explicit_api_key_for_endpoint = cli
.api_key
.as_deref()
.or(from_file.as_deref())
.or(xiaomi_mimo_env_api_key.as_deref());
let base_url = if provider == ProviderKind::XiaomiMimo {
resolve_xiaomi_mimo_base_url(configured_base_url, explicit_api_key_for_endpoint)
resolve_xiaomi_mimo_base_url(
configured_base_url,
explicit_api_key_for_endpoint,
xiaomi_mimo_mode.as_deref(),
)
} else {
configured_base_url.unwrap_or_else(|| match provider {
ProviderKind::Deepseek => DEFAULT_DEEPSEEK_BASE_URL.to_string(),
@@ -1623,6 +1658,8 @@ impl ConfigToml {
(None, None)
} else if let Some(value) = from_file.clone().filter(|v| !v.trim().is_empty()) {
(Some(value), Some(RuntimeApiKeySource::ConfigFile))
} else if let Some(value) = xiaomi_mimo_env_api_key.filter(|v| !v.trim().is_empty()) {
(Some(value), Some(RuntimeApiKeySource::Env))
} else if should_skip_secret_store_for_provider(provider, &base_url, auth_mode.as_deref()) {
match codewhale_secrets::env_for(provider.as_str()) {
Some(value) => (Some(value), Some(RuntimeApiKeySource::Env)),
@@ -2051,15 +2088,124 @@ fn moonshot_base_url_uses_kimi_code(base_url: &str) -> bool {
|| normalized.starts_with("https://api.kimi.com/coding/")
}
fn resolve_xiaomi_mimo_base_url(configured: Option<String>, api_key: Option<&str>) -> String {
fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
let normalized = mode.trim().to_ascii_lowercase().replace(['_', ' '], "-");
if normalized.is_empty() || xiaomi_mimo_mode_uses_standard_endpoint(&normalized) {
return None;
}
Some(match normalized.as_str() {
"token-plan" | "tokenplan" | "subscription" | "subscribed" | "plan" => {
DEFAULT_XIAOMI_MIMO_BASE_URL
}
"token-plan-cn"
| "token-plan-china"
| "token-plan-mainland"
| "token-plan-mainland-china"
| "cn"
| "china" => XIAOMI_MIMO_TOKEN_PLAN_CN_BASE_URL,
"token-plan-sgp"
| "token-plan-sg"
| "token-plan-singapore"
| "sgp"
| "sg"
| "singapore" => XIAOMI_MIMO_TOKEN_PLAN_SGP_BASE_URL,
"token-plan-ams"
| "token-plan-eu"
| "token-plan-europe"
| "token-plan-amsterdam"
| "ams"
| "eu"
| "europe"
| "amsterdam" => XIAOMI_MIMO_TOKEN_PLAN_AMS_BASE_URL,
_ => DEFAULT_XIAOMI_MIMO_BASE_URL,
})
}
fn xiaomi_mimo_mode_uses_standard_endpoint(normalized_mode: &str) -> bool {
matches!(
normalized_mode,
"standard" | "default" | "payg" | "paygo" | "pay-as-you-go" | "pay-as-go"
)
}
fn xiaomi_mimo_base_url_uses_token_plan(base_url: &str) -> bool {
let normalized = base_url.trim_end_matches('/').to_ascii_lowercase();
normalized == XIAOMI_MIMO_TOKEN_PLAN_CN_BASE_URL
|| normalized == XIAOMI_MIMO_TOKEN_PLAN_SGP_BASE_URL
|| normalized == XIAOMI_MIMO_TOKEN_PLAN_AMS_BASE_URL
}
fn xiaomi_mimo_env_var(candidates: &[&str]) -> Option<String> {
candidates.iter().find_map(|name| {
std::env::var(name)
.ok()
.filter(|value| !value.trim().is_empty())
})
}
fn xiaomi_mimo_env_api_key_for_runtime(
mode: Option<&str>,
base_url: Option<&str>,
) -> Option<String> {
const TOKEN_PLAN_ENV_VARS: &[&str] =
&["XIAOMI_MIMO_TOKEN_PLAN_API_KEY", "MIMO_TOKEN_PLAN_API_KEY"];
const STANDARD_ENV_VARS: &[&str] = &["XIAOMI_MIMO_API_KEY", "XIAOMI_API_KEY", "MIMO_API_KEY"];
let normalized_mode =
mode.map(|value| value.trim().to_ascii_lowercase().replace(['_', ' '], "-"));
let standard_selected = normalized_mode
.as_deref()
.is_some_and(xiaomi_mimo_mode_uses_standard_endpoint)
|| base_url.is_some_and(xiaomi_mimo_base_url_is_pay_as_you_go);
if standard_selected {
return xiaomi_mimo_env_var(STANDARD_ENV_VARS);
}
let token_plan_selected = normalized_mode
.as_deref()
.and_then(xiaomi_mimo_base_url_for_mode)
.is_some()
|| base_url.is_some_and(xiaomi_mimo_base_url_uses_token_plan);
if token_plan_selected {
return xiaomi_mimo_env_var(TOKEN_PLAN_ENV_VARS);
}
xiaomi_mimo_env_var(TOKEN_PLAN_ENV_VARS).or_else(|| xiaomi_mimo_env_var(STANDARD_ENV_VARS))
}
fn resolve_xiaomi_mimo_base_url(
configured: Option<String>,
api_key: Option<&str>,
mode: Option<&str>,
) -> String {
let normalized_mode =
mode.map(|value| value.trim().to_ascii_lowercase().replace(['_', ' '], "-"));
let uses_standard_mode = normalized_mode
.as_deref()
.is_some_and(xiaomi_mimo_mode_uses_standard_endpoint);
let mode_base_url = normalized_mode
.as_deref()
.and_then(xiaomi_mimo_base_url_for_mode);
let uses_token_plan = xiaomi_mimo_api_key_uses_token_plan(api_key);
match configured {
Some(base_url) if uses_standard_mode => base_url,
Some(base_url) if uses_token_plan && xiaomi_mimo_base_url_is_pay_as_you_go(&base_url) => {
DEFAULT_XIAOMI_MIMO_BASE_URL.to_string()
mode_base_url
.unwrap_or(DEFAULT_XIAOMI_MIMO_BASE_URL)
.to_string()
}
Some(base_url) => base_url,
None if uses_token_plan || api_key.is_none() => DEFAULT_XIAOMI_MIMO_BASE_URL.to_string(),
None => XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL.to_string(),
None => {
if let Some(base_url) = mode_base_url {
base_url.to_string()
} else if uses_standard_mode {
XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL.to_string()
} else if uses_token_plan || api_key.is_none() {
DEFAULT_XIAOMI_MIMO_BASE_URL.to_string()
} else {
XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL.to_string()
}
}
}
}
@@ -2078,6 +2224,12 @@ fn base_url_is_custom_for_provider(provider: ProviderKind, base_url: &str) -> bo
if provider.is_siliconflow() && siliconflow_base_url_is_official(base_url) {
return false;
}
if provider == ProviderKind::XiaomiMimo
&& (xiaomi_mimo_base_url_uses_token_plan(base_url)
|| xiaomi_mimo_base_url_is_pay_as_you_go(base_url))
{
return false;
}
let actual = base_url.trim_end_matches('/');
let default = default_base_url_for_provider(provider).trim_end_matches('/');
actual != default
@@ -2633,6 +2785,7 @@ struct EnvRuntimeOverrides {
openrouter_model: Option<String>,
moonshot_model: Option<String>,
xiaomi_mimo_model: Option<String>,
xiaomi_mimo_mode: Option<String>,
novita_model: Option<String>,
fireworks_model: Option<String>,
arcee_model: Option<String>,
@@ -2698,6 +2851,10 @@ impl EnvRuntimeOverrides {
.or_else(|_| std::env::var("MIMO_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
xiaomi_mimo_mode: std::env::var("XIAOMI_MIMO_MODE")
.or_else(|_| std::env::var("MIMO_MODE"))
.ok()
.filter(|v| !v.trim().is_empty()),
novita_model: std::env::var("NOVITA_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
@@ -3011,6 +3168,8 @@ mod tests {
openrouter_api_key: Option<OsString>,
openrouter_base_url: Option<OsString>,
openrouter_model: Option<OsString>,
xiaomi_mimo_token_plan_api_key: Option<OsString>,
mimo_token_plan_api_key: Option<OsString>,
xiaomi_mimo_api_key: Option<OsString>,
xiaomi_api_key: Option<OsString>,
mimo_api_key: Option<OsString>,
@@ -3018,6 +3177,8 @@ mod tests {
mimo_base_url: Option<OsString>,
xiaomi_mimo_model: Option<OsString>,
mimo_model: Option<OsString>,
xiaomi_mimo_mode: Option<OsString>,
mimo_mode: Option<OsString>,
wanjie_ark_api_key: Option<OsString>,
volcengine_api_key: Option<OsString>,
volcengine_ark_api_key: Option<OsString>,
@@ -3084,6 +3245,8 @@ mod tests {
openrouter_api_key: env::var_os("OPENROUTER_API_KEY"),
openrouter_base_url: env::var_os("OPENROUTER_BASE_URL"),
openrouter_model: env::var_os("OPENROUTER_MODEL"),
xiaomi_mimo_token_plan_api_key: env::var_os("XIAOMI_MIMO_TOKEN_PLAN_API_KEY"),
mimo_token_plan_api_key: env::var_os("MIMO_TOKEN_PLAN_API_KEY"),
xiaomi_mimo_api_key: env::var_os("XIAOMI_MIMO_API_KEY"),
xiaomi_api_key: env::var_os("XIAOMI_API_KEY"),
mimo_api_key: env::var_os("MIMO_API_KEY"),
@@ -3091,6 +3254,8 @@ mod tests {
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"),
xiaomi_mimo_mode: env::var_os("XIAOMI_MIMO_MODE"),
mimo_mode: env::var_os("MIMO_MODE"),
wanjie_ark_api_key: env::var_os("WANJIE_ARK_API_KEY"),
volcengine_api_key: env::var_os("VOLCENGINE_API_KEY"),
volcengine_ark_api_key: env::var_os("VOLCENGINE_ARK_API_KEY"),
@@ -3152,6 +3317,8 @@ mod tests {
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENROUTER_BASE_URL");
env::remove_var("OPENROUTER_MODEL");
env::remove_var("XIAOMI_MIMO_TOKEN_PLAN_API_KEY");
env::remove_var("MIMO_TOKEN_PLAN_API_KEY");
env::remove_var("XIAOMI_MIMO_API_KEY");
env::remove_var("XIAOMI_API_KEY");
env::remove_var("MIMO_API_KEY");
@@ -3159,6 +3326,8 @@ mod tests {
env::remove_var("MIMO_BASE_URL");
env::remove_var("XIAOMI_MIMO_MODEL");
env::remove_var("MIMO_MODEL");
env::remove_var("XIAOMI_MIMO_MODE");
env::remove_var("MIMO_MODE");
env::remove_var("WANJIE_ARK_API_KEY");
env::remove_var("VOLCENGINE_API_KEY");
env::remove_var("VOLCENGINE_ARK_API_KEY");
@@ -3237,6 +3406,14 @@ mod tests {
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("OPENROUTER_MODEL", self.openrouter_model.take());
Self::restore_var(
"XIAOMI_MIMO_TOKEN_PLAN_API_KEY",
self.xiaomi_mimo_token_plan_api_key.take(),
);
Self::restore_var(
"MIMO_TOKEN_PLAN_API_KEY",
self.mimo_token_plan_api_key.take(),
);
Self::restore_var("XIAOMI_MIMO_API_KEY", self.xiaomi_mimo_api_key.take());
Self::restore_var("XIAOMI_API_KEY", self.xiaomi_api_key.take());
Self::restore_var("MIMO_API_KEY", self.mimo_api_key.take());
@@ -3244,6 +3421,8 @@ mod tests {
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("XIAOMI_MIMO_MODE", self.xiaomi_mimo_mode.take());
Self::restore_var("MIMO_MODE", self.mimo_mode.take());
Self::restore_var("WANJIE_ARK_API_KEY", self.wanjie_ark_api_key.take());
Self::restore_var("VOLCENGINE_API_KEY", self.volcengine_api_key.take());
Self::restore_var("VOLCENGINE_ARK_API_KEY", self.volcengine_ark_api_key.take());
@@ -4305,6 +4484,46 @@ model = "mimo-v2.5-pro"
assert_eq!(resolved.model, DEFAULT_XIAOMI_MIMO_MODEL);
}
#[test]
fn xiaomi_mimo_token_plan_mode_accepts_region_aliases() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let config: ConfigToml = toml::from_str(
r#"
provider = "mimo"
[providers.mimo]
mode = "token-plan-ams"
"#,
)
.expect("xiaomi token-plan region config");
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::XiaomiMimo);
assert_eq!(resolved.base_url, XIAOMI_MIMO_TOKEN_PLAN_AMS_BASE_URL);
}
#[test]
fn xiaomi_mimo_unknown_mode_stays_on_token_plan_endpoint() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let config: ConfigToml = toml::from_str(
r#"
provider = "mimo"
[providers.mimo]
mode = "token-plan-usa"
"#,
)
.expect("xiaomi token-plan unknown mode config");
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::XiaomiMimo);
assert_eq!(resolved.base_url, DEFAULT_XIAOMI_MIMO_BASE_URL);
}
#[test]
fn xiaomi_mimo_aliases_resolve_to_canonical_models() {
assert_eq!(
@@ -4772,6 +4991,48 @@ model = "mimo-v2.5-pro"
assert_eq!(resolved.model, "mimo-v2.5");
}
#[test]
fn xiaomi_mimo_env_token_plan_mode_uses_token_plan_key_and_endpoint() {
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("XIAOMI_MIMO_MODE", "token-plan-cn");
env::set_var("XIAOMI_MIMO_TOKEN_PLAN_API_KEY", "tp-env-key");
env::set_var("XIAOMI_MIMO_API_KEY", "sk-env-key");
}
let resolved =
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::XiaomiMimo);
assert_eq!(resolved.api_key.as_deref(), Some("tp-env-key"));
assert_eq!(resolved.api_key_source, Some(RuntimeApiKeySource::Env));
assert_eq!(resolved.base_url, XIAOMI_MIMO_TOKEN_PLAN_CN_BASE_URL);
}
#[test]
fn xiaomi_mimo_env_pay_as_you_go_mode_prefers_standard_key() {
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("XIAOMI_MIMO_MODE", "pay-as-you-go");
env::set_var("XIAOMI_MIMO_TOKEN_PLAN_API_KEY", "tp-env-key");
env::set_var("XIAOMI_MIMO_API_KEY", "sk-env-key");
}
let resolved =
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::XiaomiMimo);
assert_eq!(resolved.api_key.as_deref(), Some("sk-env-key"));
assert_eq!(resolved.api_key_source, Some(RuntimeApiKeySource::Env));
assert_eq!(resolved.base_url, XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL);
}
#[test]
fn novita_env_overrides_key_and_model_when_config_missing() {
let _lock = env_lock();
+4
View File
@@ -65,6 +65,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`SOFYA_API_KEY` fallback, while keeping Sofya scoped to web search rather
than model-provider routing (#2790). Thanks @yusufgurdogan for the
implementation.
- Added Xiaomi MiMo `mode` / `XIAOMI_MIMO_MODE` / `MIMO_MODE` selection for
Token Plan region endpoints and pay-as-you-go routing, plus dedicated Token
Plan env keys for `tp-*` subscriptions (#2621, #2627). Thanks @springeye for
the request and @xyuai for the implementation.
### Changed
+285 -17
View File
@@ -99,6 +99,9 @@ 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 XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
pub const DEFAULT_XIAOMI_MIMO_BASE_URL: &str = "https://token-plan-sgp.xiaomimimo.com/v1";
pub const XIAOMI_MIMO_TOKEN_PLAN_CN_BASE_URL: &str = "https://token-plan-cn.xiaomimimo.com/v1";
pub const XIAOMI_MIMO_TOKEN_PLAN_SGP_BASE_URL: &str = DEFAULT_XIAOMI_MIMO_BASE_URL;
pub const XIAOMI_MIMO_TOKEN_PLAN_AMS_BASE_URL: &str = "https://token-plan-ams.xiaomimimo.com/v1";
pub const XIAOMI_MIMO_V2_5_OMNI_MODEL: &str = "mimo-v2.5";
pub const XIAOMI_MIMO_ASR_MODEL: &str = "mimo-v2.5-asr";
pub const XIAOMI_MIMO_TTS_MODEL: &str = "mimo-v2.5-tts";
@@ -1883,6 +1886,7 @@ pub struct ProviderConfig {
pub api_key: Option<String>,
pub base_url: Option<String>,
pub model: Option<String>,
pub mode: Option<String>,
pub auth_mode: Option<String>,
pub http_headers: Option<HashMap<String, String>>,
pub path_suffix: Option<String>,
@@ -2405,12 +2409,16 @@ impl Config {
};
let configured_base_url = provider_base.or(root_base);
let base = if provider == ApiProvider::XiaomiMimo {
let env_api_key = xiaomi_mimo_env_api_key_for_base_url();
let config_api_key = self
.provider_config_for(provider)
.and_then(|provider| provider.api_key.as_deref());
let mode = self
.provider_config_for(provider)
.and_then(|provider| provider.mode.as_deref());
let env_api_key =
xiaomi_mimo_env_api_key_for_runtime(mode, configured_base_url.as_deref());
let api_key = config_api_key.or(env_api_key.as_deref());
resolve_xiaomi_mimo_base_url(configured_base_url, api_key)
resolve_xiaomi_mimo_base_url(configured_base_url, api_key, mode)
} else {
configured_base_url.unwrap_or_else(|| {
match provider {
@@ -2522,6 +2530,17 @@ impl Config {
// 2. Environment variables. Do not query platform credential stores
// here; routine startup and doctor checks must stay prompt-free.
if provider == ApiProvider::XiaomiMimo {
let mode = self
.provider_config_for(provider)
.and_then(|provider| provider.mode.as_deref());
if let Some(value) =
xiaomi_mimo_env_api_key_for_runtime(mode, Some(&self.deepseek_base_url()))
&& !value.trim().is_empty()
{
return Ok(value);
}
}
if let Some(value) = codewhale_secrets::env_for(slot)
&& !value.trim().is_empty()
{
@@ -3459,6 +3478,16 @@ fn apply_env_overrides(config: &mut Config) {
.xiaomi_mimo
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::XiaomiMimo)
&& let Ok(value) = std::env::var("XIAOMI_MIMO_MODE").or_else(|_| std::env::var("MIMO_MODE"))
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.xiaomi_mimo
.mode = 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"))
@@ -4117,24 +4146,125 @@ fn default_base_url_for_provider(provider: ApiProvider) -> &'static str {
}
}
fn resolve_xiaomi_mimo_base_url(configured: Option<String>, api_key: Option<&str>) -> String {
let uses_token_plan = xiaomi_mimo_api_key_uses_token_plan(api_key);
match configured {
Some(base_url) if uses_token_plan && xiaomi_mimo_base_url_is_pay_as_you_go(&base_url) => {
DEFAULT_XIAOMI_MIMO_BASE_URL.to_string()
}
Some(base_url) => base_url,
None if uses_token_plan || api_key.is_none() => DEFAULT_XIAOMI_MIMO_BASE_URL.to_string(),
None => XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL.to_string(),
fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
let normalized = mode.trim().to_ascii_lowercase().replace(['_', ' '], "-");
if normalized.is_empty() || xiaomi_mimo_mode_uses_standard_endpoint(&normalized) {
return None;
}
Some(match normalized.as_str() {
"token-plan" | "tokenplan" | "subscription" | "subscribed" | "plan" => {
DEFAULT_XIAOMI_MIMO_BASE_URL
}
"token-plan-cn"
| "token-plan-china"
| "token-plan-mainland"
| "token-plan-mainland-china"
| "cn"
| "china" => XIAOMI_MIMO_TOKEN_PLAN_CN_BASE_URL,
"token-plan-sgp"
| "token-plan-sg"
| "token-plan-singapore"
| "sgp"
| "sg"
| "singapore" => XIAOMI_MIMO_TOKEN_PLAN_SGP_BASE_URL,
"token-plan-ams"
| "token-plan-eu"
| "token-plan-europe"
| "token-plan-amsterdam"
| "ams"
| "eu"
| "europe"
| "amsterdam" => XIAOMI_MIMO_TOKEN_PLAN_AMS_BASE_URL,
_ => DEFAULT_XIAOMI_MIMO_BASE_URL,
})
}
fn xiaomi_mimo_env_api_key_for_base_url() -> Option<String> {
std::env::var("XIAOMI_MIMO_API_KEY")
.or_else(|_| std::env::var("XIAOMI_API_KEY"))
.or_else(|_| std::env::var("MIMO_API_KEY"))
.ok()
.filter(|key| !key.trim().is_empty())
fn xiaomi_mimo_mode_uses_standard_endpoint(normalized_mode: &str) -> bool {
matches!(
normalized_mode,
"standard" | "default" | "payg" | "paygo" | "pay-as-you-go" | "pay-as-go"
)
}
fn xiaomi_mimo_base_url_uses_token_plan(base_url: &str) -> bool {
let normalized = normalize_base_url(base_url).to_ascii_lowercase();
normalized == XIAOMI_MIMO_TOKEN_PLAN_CN_BASE_URL
|| normalized == XIAOMI_MIMO_TOKEN_PLAN_SGP_BASE_URL
|| normalized == XIAOMI_MIMO_TOKEN_PLAN_AMS_BASE_URL
}
fn xiaomi_mimo_env_var(candidates: &[&str]) -> Option<String> {
candidates.iter().find_map(|name| {
std::env::var(name)
.ok()
.filter(|value| !value.trim().is_empty())
})
}
fn xiaomi_mimo_env_api_key_for_runtime(
mode: Option<&str>,
base_url: Option<&str>,
) -> Option<String> {
const TOKEN_PLAN_ENV_VARS: &[&str] =
&["XIAOMI_MIMO_TOKEN_PLAN_API_KEY", "MIMO_TOKEN_PLAN_API_KEY"];
const STANDARD_ENV_VARS: &[&str] = &["XIAOMI_MIMO_API_KEY", "XIAOMI_API_KEY", "MIMO_API_KEY"];
let normalized_mode =
mode.map(|value| value.trim().to_ascii_lowercase().replace(['_', ' '], "-"));
let standard_selected = normalized_mode
.as_deref()
.is_some_and(xiaomi_mimo_mode_uses_standard_endpoint)
|| base_url.is_some_and(xiaomi_mimo_base_url_is_pay_as_you_go);
if standard_selected {
return xiaomi_mimo_env_var(STANDARD_ENV_VARS);
}
let token_plan_selected = normalized_mode
.as_deref()
.and_then(xiaomi_mimo_base_url_for_mode)
.is_some()
|| base_url.is_some_and(xiaomi_mimo_base_url_uses_token_plan);
if token_plan_selected {
return xiaomi_mimo_env_var(TOKEN_PLAN_ENV_VARS);
}
xiaomi_mimo_env_var(TOKEN_PLAN_ENV_VARS).or_else(|| xiaomi_mimo_env_var(STANDARD_ENV_VARS))
}
fn resolve_xiaomi_mimo_base_url(
configured: Option<String>,
api_key: Option<&str>,
mode: Option<&str>,
) -> String {
let normalized_mode =
mode.map(|value| value.trim().to_ascii_lowercase().replace(['_', ' '], "-"));
let uses_standard_mode = normalized_mode
.as_deref()
.is_some_and(xiaomi_mimo_mode_uses_standard_endpoint);
let mode_base_url = normalized_mode
.as_deref()
.and_then(xiaomi_mimo_base_url_for_mode);
let uses_token_plan = xiaomi_mimo_api_key_uses_token_plan(api_key);
match configured {
Some(base_url) if uses_standard_mode => base_url,
Some(base_url) if uses_token_plan && xiaomi_mimo_base_url_is_pay_as_you_go(&base_url) => {
mode_base_url
.unwrap_or(DEFAULT_XIAOMI_MIMO_BASE_URL)
.to_string()
}
Some(base_url) => base_url,
None => {
if let Some(base_url) = mode_base_url {
base_url.to_string()
} else if uses_standard_mode {
XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL.to_string()
} else if uses_token_plan || api_key.is_none() {
DEFAULT_XIAOMI_MIMO_BASE_URL.to_string()
} else {
XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL.to_string()
}
}
}
}
fn xiaomi_mimo_api_key_uses_token_plan(api_key: Option<&str>) -> bool {
@@ -4154,6 +4284,12 @@ fn base_url_is_custom_for_provider(provider: ApiProvider, base_url: &str) -> boo
{
return false;
}
if provider == ApiProvider::XiaomiMimo
&& (xiaomi_mimo_base_url_uses_token_plan(base_url)
|| xiaomi_mimo_base_url_is_pay_as_you_go(base_url))
{
return false;
}
normalize_base_url(base_url) != normalize_base_url(default_base_url_for_provider(provider))
}
@@ -4389,6 +4525,7 @@ fn merge_provider_config(base: ProviderConfig, override_cfg: ProviderConfig) ->
api_key: override_cfg.api_key.or(base.api_key),
base_url: override_cfg.base_url.or(base.base_url),
model: override_cfg.model.or(base.model),
mode: override_cfg.mode.or(base.mode),
auth_mode: override_cfg.auth_mode.or(base.auth_mode),
http_headers: override_cfg.http_headers.or(base.http_headers),
path_suffix: override_cfg.path_suffix.or(base.path_suffix),
@@ -5891,6 +6028,8 @@ mod tests {
ark_base_url: Option<OsString>,
volcengine_model: Option<OsString>,
volcengine_ark_model: Option<OsString>,
xiaomi_mimo_token_plan_api_key: Option<OsString>,
mimo_token_plan_api_key: Option<OsString>,
xiaomi_mimo_api_key: Option<OsString>,
xiaomi_api_key: Option<OsString>,
mimo_api_key: Option<OsString>,
@@ -5898,6 +6037,8 @@ mod tests {
mimo_base_url: Option<OsString>,
xiaomi_mimo_model: Option<OsString>,
mimo_model: Option<OsString>,
xiaomi_mimo_mode: Option<OsString>,
mimo_mode: Option<OsString>,
novita_api_key: Option<OsString>,
novita_base_url: Option<OsString>,
novita_model: Option<OsString>,
@@ -5990,6 +6131,8 @@ mod tests {
let ark_base_url_prev = env::var_os("ARK_BASE_URL");
let volcengine_model_prev = env::var_os("VOLCENGINE_MODEL");
let volcengine_ark_model_prev = env::var_os("VOLCENGINE_ARK_MODEL");
let xiaomi_mimo_token_plan_api_key_prev = env::var_os("XIAOMI_MIMO_TOKEN_PLAN_API_KEY");
let mimo_token_plan_api_key_prev = env::var_os("MIMO_TOKEN_PLAN_API_KEY");
let xiaomi_mimo_api_key_prev = env::var_os("XIAOMI_MIMO_API_KEY");
let xiaomi_api_key_prev = env::var_os("XIAOMI_API_KEY");
let mimo_api_key_prev = env::var_os("MIMO_API_KEY");
@@ -5997,6 +6140,8 @@ mod tests {
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 xiaomi_mimo_mode_prev = env::var_os("XIAOMI_MIMO_MODE");
let mimo_mode_prev = env::var_os("MIMO_MODE");
let novita_api_key_prev = env::var_os("NOVITA_API_KEY");
let novita_base_url_prev = env::var_os("NOVITA_BASE_URL");
let novita_model_prev = env::var_os("NOVITA_MODEL");
@@ -6084,6 +6229,8 @@ mod tests {
env::remove_var("ARK_BASE_URL");
env::remove_var("VOLCENGINE_MODEL");
env::remove_var("VOLCENGINE_ARK_MODEL");
env::remove_var("XIAOMI_MIMO_TOKEN_PLAN_API_KEY");
env::remove_var("MIMO_TOKEN_PLAN_API_KEY");
env::remove_var("XIAOMI_MIMO_API_KEY");
env::remove_var("XIAOMI_API_KEY");
env::remove_var("MIMO_API_KEY");
@@ -6091,6 +6238,8 @@ mod tests {
env::remove_var("MIMO_BASE_URL");
env::remove_var("XIAOMI_MIMO_MODEL");
env::remove_var("MIMO_MODEL");
env::remove_var("XIAOMI_MIMO_MODE");
env::remove_var("MIMO_MODE");
env::remove_var("NOVITA_API_KEY");
env::remove_var("NOVITA_BASE_URL");
env::remove_var("NOVITA_MODEL");
@@ -6178,6 +6327,8 @@ mod tests {
ark_base_url: ark_base_url_prev,
volcengine_model: volcengine_model_prev,
volcengine_ark_model: volcengine_ark_model_prev,
xiaomi_mimo_token_plan_api_key: xiaomi_mimo_token_plan_api_key_prev,
mimo_token_plan_api_key: mimo_token_plan_api_key_prev,
xiaomi_mimo_api_key: xiaomi_mimo_api_key_prev,
xiaomi_api_key: xiaomi_api_key_prev,
mimo_api_key: mimo_api_key_prev,
@@ -6185,6 +6336,8 @@ mod tests {
mimo_base_url: mimo_base_url_prev,
xiaomi_mimo_model: xiaomi_mimo_model_prev,
mimo_model: mimo_model_prev,
xiaomi_mimo_mode: xiaomi_mimo_mode_prev,
mimo_mode: mimo_mode_prev,
novita_api_key: novita_api_key_prev,
novita_base_url: novita_base_url_prev,
novita_model: novita_model_prev,
@@ -6290,6 +6443,14 @@ mod tests {
Self::restore_var("ARK_BASE_URL", self.ark_base_url.take());
Self::restore_var("VOLCENGINE_MODEL", self.volcengine_model.take());
Self::restore_var("VOLCENGINE_ARK_MODEL", self.volcengine_ark_model.take());
Self::restore_var(
"XIAOMI_MIMO_TOKEN_PLAN_API_KEY",
self.xiaomi_mimo_token_plan_api_key.take(),
);
Self::restore_var(
"MIMO_TOKEN_PLAN_API_KEY",
self.mimo_token_plan_api_key.take(),
);
Self::restore_var("XIAOMI_MIMO_API_KEY", self.xiaomi_mimo_api_key.take());
Self::restore_var("XIAOMI_API_KEY", self.xiaomi_api_key.take());
Self::restore_var("MIMO_API_KEY", self.mimo_api_key.take());
@@ -6297,6 +6458,8 @@ mod tests {
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("XIAOMI_MIMO_MODE", self.xiaomi_mimo_mode.take());
Self::restore_var("MIMO_MODE", self.mimo_mode.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("NOVITA_MODEL", self.novita_model.take());
@@ -8190,6 +8353,43 @@ model = "mimo-v2.5-pro"
Ok(())
}
#[test]
fn xiaomi_mimo_token_plan_mode_accepts_region_aliases() -> Result<()> {
let config: Config = toml::from_str(
r#"
provider = "mimo"
[providers.mimo]
mode = "token-plan-ams"
"#,
)?;
config.validate()?;
assert_eq!(config.api_provider(), ApiProvider::XiaomiMimo);
assert_eq!(
config.deepseek_base_url(),
XIAOMI_MIMO_TOKEN_PLAN_AMS_BASE_URL
);
Ok(())
}
#[test]
fn xiaomi_mimo_unknown_mode_stays_on_token_plan_endpoint() -> Result<()> {
let config: Config = toml::from_str(
r#"
provider = "mimo"
[providers.mimo]
mode = "token-plan-usa"
"#,
)?;
config.validate()?;
assert_eq!(config.api_provider(), ApiProvider::XiaomiMimo);
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();
@@ -8224,6 +8424,74 @@ model = "mimo-v2.5-pro"
Ok(())
}
#[test]
fn xiaomi_mimo_env_token_plan_mode_uses_token_plan_key_and_endpoint() -> 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-token-plan-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", "xiaomi-mimo");
env::set_var("XIAOMI_MIMO_MODE", "token-plan-cn");
env::set_var("XIAOMI_MIMO_TOKEN_PLAN_API_KEY", "tp-env-key");
env::set_var("XIAOMI_MIMO_API_KEY", "sk-env-key");
env::set_var("XIAOMI_MIMO_MODEL", "voiceclone");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::XiaomiMimo);
assert_eq!(config.deepseek_api_key()?, "tp-env-key");
assert_eq!(
config.deepseek_base_url(),
XIAOMI_MIMO_TOKEN_PLAN_CN_BASE_URL
);
assert_eq!(config.default_model(), "voiceclone");
Ok(())
}
#[test]
fn xiaomi_mimo_env_pay_as_you_go_mode_prefers_standard_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-payg-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", "xiaomi-mimo");
env::set_var("XIAOMI_MIMO_MODE", "pay-as-you-go");
env::set_var("XIAOMI_MIMO_TOKEN_PLAN_API_KEY", "tp-env-key");
env::set_var("XIAOMI_MIMO_API_KEY", "sk-env-key");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::XiaomiMimo);
assert_eq!(config.deepseek_api_key()?, "sk-env-key");
assert_eq!(
config.deepseek_base_url(),
XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL
);
Ok(())
}
#[test]
fn atlascloud_provider_uses_documented_defaults() -> Result<()> {
let config = Config {
+2 -1
View File
@@ -434,9 +434,10 @@ Remaining variables:
- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`
- `OPENROUTER_API_KEY`
- `OPENROUTER_BASE_URL`
- `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`
- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`
- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`
- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`
- `XIAOMI_MIMO_MODE` or `MIMO_MODE`
- `NOVITA_API_KEY`
- `NOVITA_BASE_URL`
- `FIREWORKS_API_KEY`
+4 -2
View File
@@ -118,7 +118,7 @@ endpoint.
| `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. |
| `volcengine` | `[providers.volcengine]` | `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, `ARK_API_KEY` | `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, `ARK_BASE_URL`; default `https://ark.cn-beijing.volces.com/api/coding/v3` | `DeepSeek-V4-Pro`, `DeepSeek-V4-Flash` | Volcengine/Volcano Engine Ark OpenAI-compatible coding endpoint. `VOLCENGINE_MODEL` and `VOLCENGINE_ARK_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`; recent large IDs include `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `google/gemma-4-31b-it`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6` | 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`, `XIAOMI_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`; default `https://token-plan-sgp.xiaomimimo.com/v1` | Chat: `mimo-v2.5-pro`, `mimo-v2.5`; speech/TTS: `mimo-v2.5-tts`, `mimo-v2.5-tts-voicedesign`, `mimo-v2.5-tts-voiceclone`, `mimo-v2-tts` | Xiaomi MiMo OpenAI-compatible chat completions route. Token Plan keys (`tp-...`) use the token-plan endpoint by default; pay-as-you-go keys can set `base_url = "https://api.xiaomimimo.com/v1"`. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. `codewhale speech` / `tts` uses the TTS models. |
| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`, `XIAOMI_MIMO_MODE`, `MIMO_MODE`; default `https://token-plan-sgp.xiaomimimo.com/v1` | Chat: `mimo-v2.5-pro`, `mimo-v2.5`; speech/TTS: `mimo-v2.5-tts`, `mimo-v2.5-tts-voicedesign`, `mimo-v2.5-tts-voiceclone`, `mimo-v2-tts` | Xiaomi MiMo OpenAI-compatible chat completions route. Token Plan keys (`tp-...`) use `api-key` auth and the token-plan endpoint by default; pay-as-you-go mode uses standard API keys (`sk-...`) and `https://api.xiaomimimo.com/v1`. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. `codewhale speech` / `tts` uses the TTS models. |
| `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. |
| `siliconflow` | `[providers.siliconflow]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.com/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | OpenAI-compatible hosted route. Official docs use the `.com` endpoint. `SILICONFLOW_MODEL` is accepted. Reasoning aliases `deepseek-reasoner` and `deepseek-r1` map to Pro; `deepseek-chat` and `deepseek-v3` map to Flash. |
@@ -141,7 +141,9 @@ Agent/YOLO mode.
Token Plan keys default to the Singapore endpoint
`https://token-plan-sgp.xiaomimimo.com/v1`. If your MiMo account is provisioned
for the China region, set `base_url = "https://token-plan-cn.xiaomimimo.com/v1"`
explicitly in `[providers.xiaomi_mimo]`.
explicitly in `[providers.xiaomi_mimo]` or set `mode = "token-plan-cn"`. Europe
Token Plan accounts can use `mode = "token-plan-ams"`; `mode = "pay-as-you-go"`
selects the standard API endpoint and standard MiMo key family.
Voice-design and voice-clone shorthands map to `mimo-v2.5-tts-voicedesign` and
`mimo-v2.5-tts-voiceclone`. Xiaomi's current