diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45b527d16..fdea69d70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ Status: beta.
### Changes
- Gateway: prevent crash on transient fetch/network failures (#3815)
- Providers: add Venice AI integration; update Moonshot Kimi references to kimi-k2.5; update MiniMax API endpoint/format. (#2762, #3064)
+- Providers: add Xiaomi MiMo (mimo-v2-flash) support and onboarding flow. (#3454) Thanks @WqyJh.
- Telegram: quote replies, edit-message action, silent sends, sticker support + vision caching, linkPreview toggle, plugin sendPayload support. (#2900, #2394, #2382, #2548, #1700, #1917)
- Discord: configurable privileged gateway intents for presences/members. (#2266) Thanks @kentaro.
- Browser: route browser control via gateway/node; fallback URL matching for relay targets. (#1999)
@@ -46,6 +47,7 @@ Status: beta.
- Config: auto-migrate legacy state/config paths; honor state dir overrides.
- Packaging: include missing dist/shared and dist/link-understanding outputs in npm tarball installs.
- Telegram: avoid silent empty replies, improve polling/network recovery, handle video notes, keep DM thread sessions, ignore non-forum message_thread_id, centralize API error logging, include AccountId in native command context. (#3796, #3013, #2905, #2731, #2492, #2942)
+- Telegram: preserve reasoning tags inside code blocks. (#3952) Thanks @vinaygit18.
- Discord: restore username resolution, resolve outbound usernames to IDs, honor threadId replies, guard forum thread access. (#3131, #2649)
- BlueBubbles: coalesce URL link previews, improve reaction handling, preserve reply-tag GUIDs. (#1981, #1641)
- Voice Call: prevent TTS overlap, validate env-var config, return TwiML for conversation calls. (#1713, #1634)
diff --git a/README.md b/README.md
index 70ca70157..ec970bb5b 100644
--- a/README.md
+++ b/README.md
@@ -479,38 +479,38 @@ Thanks to all clawtributors:
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/docs.json b/docs/docs.json
index a463479aa..389adbe51 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -69,6 +69,14 @@
"source": "/minimax/",
"destination": "/providers/minimax"
},
+ {
+ "source": "/xiaomi",
+ "destination": "/providers/xiaomi"
+ },
+ {
+ "source": "/xiaomi/",
+ "destination": "/providers/xiaomi"
+ },
{
"source": "/openai",
"destination": "/providers/openai"
diff --git a/docs/providers/index.md b/docs/providers/index.md
index c18ad70fb..a63a642cc 100644
--- a/docs/providers/index.md
+++ b/docs/providers/index.md
@@ -42,6 +42,7 @@ See [Venice AI](/providers/venice).
- [OpenCode Zen](/providers/opencode)
- [Amazon Bedrock](/bedrock)
- [Z.AI](/providers/zai)
+- [Xiaomi](/providers/xiaomi)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
- [Venius (Venice AI, privacy-focused)](/providers/venice)
diff --git a/docs/providers/xiaomi.md b/docs/providers/xiaomi.md
new file mode 100644
index 000000000..008c42105
--- /dev/null
+++ b/docs/providers/xiaomi.md
@@ -0,0 +1,62 @@
+---
+summary: "Use Xiaomi MiMo (mimo-v2-flash) with Moltbot"
+read_when:
+ - You want Xiaomi MiMo models in Moltbot
+ - You need XIAOMI_API_KEY setup
+---
+# Xiaomi MiMo
+
+Xiaomi MiMo is the API platform for **MiMo** models. It provides REST APIs compatible with
+OpenAI and Anthropic formats and uses API keys for authentication. Create your API key in
+the [Xiaomi MiMo console](https://platform.xiaomimimo.com/#/console/api-keys). Moltbot uses
+the `xiaomi` provider with a Xiaomi MiMo API key.
+
+## Model overview
+
+- **mimo-v2-flash**: 262144-token context window, Anthropic Messages API compatible.
+- Base URL: `https://api.xiaomimimo.com/anthropic`
+- Authorization: `Bearer $XIAOMI_API_KEY`
+
+## CLI setup
+
+```bash
+moltbot onboard --auth-choice xiaomi-api-key
+# or non-interactive
+moltbot onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY"
+```
+
+## Config snippet
+
+```json5
+{
+ env: { XIAOMI_API_KEY: "your-key" },
+ agents: { defaults: { model: { primary: "xiaomi/mimo-v2-flash" } } },
+ models: {
+ mode: "merge",
+ providers: {
+ xiaomi: {
+ baseUrl: "https://api.xiaomimimo.com/anthropic",
+ api: "anthropic-messages",
+ apiKey: "XIAOMI_API_KEY",
+ models: [
+ {
+ id: "mimo-v2-flash",
+ name: "Xiaomi MiMo V2 Flash",
+ reasoning: false,
+ input: ["text"],
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 262144,
+ maxTokens: 8192
+ }
+ ]
+ }
+ }
+ }
+}
+```
+
+## Notes
+
+- Model ref: `xiaomi/mimo-v2-flash`.
+- The provider is injected automatically when `XIAOMI_API_KEY` is set (or an auth profile exists).
+- See [/concepts/model-providers](/concepts/model-providers) for provider rules.
diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts
index 96e4e4ae6..5d1c095d2 100644
--- a/src/agents/model-auth.ts
+++ b/src/agents/model-auth.ts
@@ -281,6 +281,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
moonshot: "MOONSHOT_API_KEY",
"kimi-code": "KIMICODE_API_KEY",
minimax: "MINIMAX_API_KEY",
+ xiaomi: "XIAOMI_API_KEY",
synthetic: "SYNTHETIC_API_KEY",
venice: "VENICE_API_KEY",
mistral: "MISTRAL_API_KEY",
diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts
index a176dac8a..f38ad46c7 100644
--- a/src/agents/models-config.providers.ts
+++ b/src/agents/models-config.providers.ts
@@ -30,6 +30,17 @@ const MINIMAX_API_COST = {
cacheWrite: 10,
};
+const XIAOMI_BASE_URL = "https://api.xiaomimimo.com/anthropic";
+export const XIAOMI_DEFAULT_MODEL_ID = "mimo-v2-flash";
+const XIAOMI_DEFAULT_CONTEXT_WINDOW = 262144;
+const XIAOMI_DEFAULT_MAX_TOKENS = 8192;
+const XIAOMI_DEFAULT_COST = {
+ input: 0,
+ output: 0,
+ cacheRead: 0,
+ cacheWrite: 0,
+};
+
const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2.5";
const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
@@ -341,6 +352,24 @@ function buildSyntheticProvider(): ProviderConfig {
};
}
+export function buildXiaomiProvider(): ProviderConfig {
+ return {
+ baseUrl: XIAOMI_BASE_URL,
+ api: "anthropic-messages",
+ models: [
+ {
+ id: XIAOMI_DEFAULT_MODEL_ID,
+ name: "Xiaomi MiMo V2 Flash",
+ reasoning: false,
+ input: ["text"],
+ cost: XIAOMI_DEFAULT_COST,
+ contextWindow: XIAOMI_DEFAULT_CONTEXT_WINDOW,
+ maxTokens: XIAOMI_DEFAULT_MAX_TOKENS,
+ },
+ ],
+ };
+}
+
async function buildVeniceProvider(): Promise {
const models = await discoverVeniceModels();
return {
@@ -410,6 +439,13 @@ export async function resolveImplicitProviders(params: {
};
}
+ const xiaomiKey =
+ resolveEnvApiKeyVarName("xiaomi") ??
+ resolveApiKeyFromProfiles({ provider: "xiaomi", store: authStore });
+ if (xiaomiKey) {
+ providers.xiaomi = { ...buildXiaomiProvider(), apiKey: xiaomiKey };
+ }
+
// Ollama provider - only add if explicitly configured
const ollamaKey =
resolveEnvApiKeyVarName("ollama") ??
diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts
index fef8fa6a4..08a66469f 100644
--- a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts
+++ b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts
@@ -53,6 +53,7 @@ describe("models-config", () => {
const previousMoonshot = process.env.MOONSHOT_API_KEY;
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
const previousVenice = process.env.VENICE_API_KEY;
+ const previousXiaomi = process.env.XIAOMI_API_KEY;
delete process.env.COPILOT_GITHUB_TOKEN;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
@@ -61,6 +62,7 @@ describe("models-config", () => {
delete process.env.MOONSHOT_API_KEY;
delete process.env.SYNTHETIC_API_KEY;
delete process.env.VENICE_API_KEY;
+ delete process.env.XIAOMI_API_KEY;
try {
vi.resetModules();
@@ -93,6 +95,8 @@ describe("models-config", () => {
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
if (previousVenice === undefined) delete process.env.VENICE_API_KEY;
else process.env.VENICE_API_KEY = previousVenice;
+ if (previousXiaomi === undefined) delete process.env.XIAOMI_API_KEY;
+ else process.env.XIAOMI_API_KEY = previousXiaomi;
}
});
});
diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts
index 8f31635f0..de7080103 100644
--- a/src/cli/program/register.onboard.ts
+++ b/src/cli/program/register.onboard.ts
@@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
.option("--mode ", "Wizard mode: local|remote")
.option(
"--auth-choice ",
- "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
+ "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
)
.option(
"--token-provider ",
@@ -72,6 +72,7 @@ export function registerOnboardCommand(program: Command) {
.option("--kimi-code-api-key ", "Kimi Code API key")
.option("--gemini-api-key ", "Gemini API key")
.option("--zai-api-key ", "Z.AI API key")
+ .option("--xiaomi-api-key ", "Xiaomi API key")
.option("--minimax-api-key ", "MiniMax API key")
.option("--synthetic-api-key ", "Synthetic API key")
.option("--venice-api-key ", "Venice API key")
@@ -122,6 +123,7 @@ export function registerOnboardCommand(program: Command) {
kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined,
geminiApiKey: opts.geminiApiKey as string | undefined,
zaiApiKey: opts.zaiApiKey as string | undefined,
+ xiaomiApiKey: opts.xiaomiApiKey as string | undefined,
minimaxApiKey: opts.minimaxApiKey as string | undefined,
syntheticApiKey: opts.syntheticApiKey as string | undefined,
veniceApiKey: opts.veniceApiKey as string | undefined,
diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts
index 7bf917a27..c85cc0b4d 100644
--- a/src/commands/auth-choice-options.test.ts
+++ b/src/commands/auth-choice-options.test.ts
@@ -33,6 +33,16 @@ describe("buildAuthChoiceOptions", () => {
expect(options.some((opt) => opt.value === "zai-api-key")).toBe(true);
});
+ it("includes Xiaomi auth choice", () => {
+ const store: AuthProfileStore = { version: 1, profiles: {} };
+ const options = buildAuthChoiceOptions({
+ store,
+ includeSkip: false,
+ });
+
+ expect(options.some((opt) => opt.value === "xiaomi-api-key")).toBe(true);
+ });
+
it("includes MiniMax auth choice", () => {
const store: AuthProfileStore = { version: 1, profiles: {} };
const options = buildAuthChoiceOptions({
diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts
index 6b49ff17b..5acddf4e3 100644
--- a/src/commands/auth-choice-options.ts
+++ b/src/commands/auth-choice-options.ts
@@ -16,6 +16,7 @@ export type AuthChoiceGroupId =
| "ai-gateway"
| "moonshot"
| "zai"
+ | "xiaomi"
| "opencode-zen"
| "minimax"
| "synthetic"
@@ -107,6 +108,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "API key",
choices: ["zai-api-key"],
},
+ {
+ value: "xiaomi",
+ label: "Xiaomi",
+ hint: "API key",
+ choices: ["xiaomi-api-key"],
+ },
{
value: "opencode-zen",
label: "OpenCode Zen",
@@ -164,6 +171,10 @@ export function buildAuthChoiceOptions(params: {
hint: "Uses the bundled Gemini CLI auth plugin",
});
options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" });
+ options.push({
+ value: "xiaomi-api-key",
+ label: "Xiaomi API key",
+ });
options.push({ value: "qwen-portal", label: "Qwen OAuth" });
options.push({
value: "copilot-proxy",
diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts
index 8be02008b..fa4fc77e7 100644
--- a/src/commands/auth-choice.apply.api-providers.ts
+++ b/src/commands/auth-choice.apply.api-providers.ts
@@ -27,6 +27,8 @@ import {
applyVeniceProviderConfig,
applyVercelAiGatewayConfig,
applyVercelAiGatewayProviderConfig,
+ applyXiaomiConfig,
+ applyXiaomiProviderConfig,
applyZaiConfig,
KIMI_CODE_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
@@ -34,6 +36,7 @@ import {
SYNTHETIC_DEFAULT_MODEL_REF,
VENICE_DEFAULT_MODEL_REF,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
+ XIAOMI_DEFAULT_MODEL_REF,
setGeminiApiKey,
setKimiCodeApiKey,
setMoonshotApiKey,
@@ -42,6 +45,7 @@ import {
setSyntheticApiKey,
setVeniceApiKey,
setVercelAiGatewayApiKey,
+ setXiaomiApiKey,
setZaiApiKey,
ZAI_DEFAULT_MODEL_REF,
} from "./onboard-auth.js";
@@ -79,6 +83,8 @@ export async function applyAuthChoiceApiProviders(
authChoice = "gemini-api-key";
} else if (params.opts.tokenProvider === "zai") {
authChoice = "zai-api-key";
+ } else if (params.opts.tokenProvider === "xiaomi") {
+ authChoice = "xiaomi-api-key";
} else if (params.opts.tokenProvider === "synthetic") {
authChoice = "synthetic-api-key";
} else if (params.opts.tokenProvider === "venice") {
@@ -431,6 +437,54 @@ export async function applyAuthChoiceApiProviders(
return { config: nextConfig, agentModelOverride };
}
+ if (authChoice === "xiaomi-api-key") {
+ let hasCredential = false;
+
+ if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "xiaomi") {
+ await setXiaomiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
+ hasCredential = true;
+ }
+
+ const envKey = resolveEnvApiKey("xiaomi");
+ if (envKey) {
+ const useExisting = await params.prompter.confirm({
+ message: `Use existing XIAOMI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
+ initialValue: true,
+ });
+ if (useExisting) {
+ await setXiaomiApiKey(envKey.apiKey, params.agentDir);
+ hasCredential = true;
+ }
+ }
+ if (!hasCredential) {
+ const key = await params.prompter.text({
+ message: "Enter Xiaomi API key",
+ validate: validateApiKeyInput,
+ });
+ await setXiaomiApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
+ }
+ nextConfig = applyAuthProfileConfig(nextConfig, {
+ profileId: "xiaomi:default",
+ provider: "xiaomi",
+ mode: "api_key",
+ });
+ {
+ const applied = await applyDefaultModelChoice({
+ config: nextConfig,
+ setDefaultModel: params.setDefaultModel,
+ defaultModel: XIAOMI_DEFAULT_MODEL_REF,
+ applyDefaultConfig: applyXiaomiConfig,
+ applyProviderConfig: applyXiaomiProviderConfig,
+ noteDefault: XIAOMI_DEFAULT_MODEL_REF,
+ noteAgentModel,
+ prompter: params.prompter,
+ });
+ nextConfig = applied.config;
+ agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
+ }
+ return { config: nextConfig, agentModelOverride };
+ }
+
if (authChoice === "synthetic-api-key") {
if (params.opts?.token && params.opts?.tokenProvider === "synthetic") {
await setSyntheticApiKey(String(params.opts.token).trim(), params.agentDir);
diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts
index 6fe26b59a..a4d831c92 100644
--- a/src/commands/auth-choice.preferred-provider.ts
+++ b/src/commands/auth-choice.preferred-provider.ts
@@ -18,6 +18,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = {
"google-antigravity": "google-antigravity",
"google-gemini-cli": "google-gemini-cli",
"zai-api-key": "zai",
+ "xiaomi-api-key": "xiaomi",
"synthetic-api-key": "synthetic",
"venice-api-key": "venice",
"github-copilot": "github-copilot",
diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts
index 921ee01d1..222f0a5c6 100644
--- a/src/commands/onboard-auth.config-core.ts
+++ b/src/commands/onboard-auth.config-core.ts
@@ -1,3 +1,4 @@
+import { buildXiaomiProvider, XIAOMI_DEFAULT_MODEL_ID } from "../agents/models-config.providers.js";
import {
buildSyntheticModelDefinition,
SYNTHETIC_BASE_URL,
@@ -14,6 +15,7 @@ import type { MoltbotConfig } from "../config/config.js";
import {
OPENROUTER_DEFAULT_MODEL_REF,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
+ XIAOMI_DEFAULT_MODEL_REF,
ZAI_DEFAULT_MODEL_REF,
} from "./onboard-auth.credentials.js";
import {
@@ -336,6 +338,77 @@ export function applySyntheticConfig(cfg: MoltbotConfig): MoltbotConfig {
};
}
+export function applyXiaomiProviderConfig(cfg: MoltbotConfig): MoltbotConfig {
+ const models = { ...cfg.agents?.defaults?.models };
+ models[XIAOMI_DEFAULT_MODEL_REF] = {
+ ...models[XIAOMI_DEFAULT_MODEL_REF],
+ alias: models[XIAOMI_DEFAULT_MODEL_REF]?.alias ?? "Xiaomi",
+ };
+
+ const providers = { ...cfg.models?.providers };
+ const existingProvider = providers.xiaomi;
+ const defaultProvider = buildXiaomiProvider();
+ const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
+ const defaultModels = defaultProvider.models ?? [];
+ const hasDefaultModel = existingModels.some((model) => model.id === XIAOMI_DEFAULT_MODEL_ID);
+ const mergedModels =
+ existingModels.length > 0
+ ? hasDefaultModel
+ ? existingModels
+ : [...existingModels, ...defaultModels]
+ : defaultModels;
+ const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
+ string,
+ unknown
+ > as { apiKey?: string };
+ const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
+ const normalizedApiKey = resolvedApiKey?.trim();
+ providers.xiaomi = {
+ ...existingProviderRest,
+ baseUrl: defaultProvider.baseUrl,
+ api: defaultProvider.api,
+ ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
+ models: mergedModels.length > 0 ? mergedModels : defaultProvider.models,
+ };
+
+ return {
+ ...cfg,
+ agents: {
+ ...cfg.agents,
+ defaults: {
+ ...cfg.agents?.defaults,
+ models,
+ },
+ },
+ models: {
+ mode: cfg.models?.mode ?? "merge",
+ providers,
+ },
+ };
+}
+
+export function applyXiaomiConfig(cfg: MoltbotConfig): MoltbotConfig {
+ const next = applyXiaomiProviderConfig(cfg);
+ const existingModel = next.agents?.defaults?.model;
+ return {
+ ...next,
+ agents: {
+ ...next.agents,
+ defaults: {
+ ...next.agents?.defaults,
+ model: {
+ ...(existingModel && "fallbacks" in (existingModel as Record)
+ ? {
+ fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
+ }
+ : undefined),
+ primary: XIAOMI_DEFAULT_MODEL_REF,
+ },
+ },
+ },
+ };
+}
+
/**
* Apply Venice provider configuration without changing the default model.
* Registers Venice models and sets up the provider, but preserves existing model selection.
diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts
index b2fb58542..053026162 100644
--- a/src/commands/onboard-auth.credentials.ts
+++ b/src/commands/onboard-auth.credentials.ts
@@ -113,6 +113,7 @@ export async function setVeniceApiKey(key: string, agentDir?: string) {
}
export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7";
+export const XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash";
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5";
@@ -129,6 +130,18 @@ export async function setZaiApiKey(key: string, agentDir?: string) {
});
}
+export async function setXiaomiApiKey(key: string, agentDir?: string) {
+ upsertAuthProfile({
+ profileId: "xiaomi:default",
+ credential: {
+ type: "api_key",
+ provider: "xiaomi",
+ key,
+ },
+ agentDir: resolveAuthAgentDir(agentDir),
+ });
+}
+
export async function setOpenrouterApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "openrouter:default",
diff --git a/src/commands/onboard-auth.test.ts b/src/commands/onboard-auth.test.ts
index 0a2c67f94..80d71852c 100644
--- a/src/commands/onboard-auth.test.ts
+++ b/src/commands/onboard-auth.test.ts
@@ -15,6 +15,8 @@ import {
applyOpenrouterProviderConfig,
applySyntheticConfig,
applySyntheticProviderConfig,
+ applyXiaomiConfig,
+ applyXiaomiProviderConfig,
OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_ID,
SYNTHETIC_DEFAULT_MODEL_REF,
@@ -343,6 +345,50 @@ describe("applySyntheticConfig", () => {
});
});
+describe("applyXiaomiConfig", () => {
+ it("adds Xiaomi provider with correct settings", () => {
+ const cfg = applyXiaomiConfig({});
+ expect(cfg.models?.providers?.xiaomi).toMatchObject({
+ baseUrl: "https://api.xiaomimimo.com/anthropic",
+ api: "anthropic-messages",
+ });
+ expect(cfg.agents?.defaults?.model?.primary).toBe("xiaomi/mimo-v2-flash");
+ });
+
+ it("merges Xiaomi models and keeps existing provider overrides", () => {
+ const cfg = applyXiaomiProviderConfig({
+ models: {
+ providers: {
+ xiaomi: {
+ baseUrl: "https://old.example.com",
+ apiKey: "old-key",
+ api: "openai-completions",
+ models: [
+ {
+ id: "custom-model",
+ name: "Custom",
+ reasoning: false,
+ input: ["text"],
+ cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 1000,
+ maxTokens: 100,
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ expect(cfg.models?.providers?.xiaomi?.baseUrl).toBe("https://api.xiaomimimo.com/anthropic");
+ expect(cfg.models?.providers?.xiaomi?.api).toBe("anthropic-messages");
+ expect(cfg.models?.providers?.xiaomi?.apiKey).toBe("old-key");
+ expect(cfg.models?.providers?.xiaomi?.models.map((m) => m.id)).toEqual([
+ "custom-model",
+ "mimo-v2-flash",
+ ]);
+ });
+});
+
describe("applyOpencodeZenProviderConfig", () => {
it("adds allowlist entry for the default model", () => {
const cfg = applyOpencodeZenProviderConfig({});
diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts
index b122d89cf..612b24865 100644
--- a/src/commands/onboard-auth.ts
+++ b/src/commands/onboard-auth.ts
@@ -17,6 +17,8 @@ export {
applyVeniceProviderConfig,
applyVercelAiGatewayConfig,
applyVercelAiGatewayProviderConfig,
+ applyXiaomiConfig,
+ applyXiaomiProviderConfig,
applyZaiConfig,
} from "./onboard-auth.config-core.js";
export {
@@ -44,9 +46,11 @@ export {
setSyntheticApiKey,
setVeniceApiKey,
setVercelAiGatewayApiKey,
+ setXiaomiApiKey,
setZaiApiKey,
writeOAuthCredentials,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
+ XIAOMI_DEFAULT_MODEL_REF,
ZAI_DEFAULT_MODEL_REF,
} from "./onboard-auth.credentials.js";
export {
diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts
index 7d952730c..46085acb5 100644
--- a/src/commands/onboard-non-interactive/local/auth-choice.ts
+++ b/src/commands/onboard-non-interactive/local/auth-choice.ts
@@ -17,6 +17,7 @@ import {
applySyntheticConfig,
applyVeniceConfig,
applyVercelAiGatewayConfig,
+ applyXiaomiConfig,
applyZaiConfig,
setAnthropicApiKey,
setGeminiApiKey,
@@ -28,6 +29,7 @@ import {
setSyntheticApiKey,
setVeniceApiKey,
setVercelAiGatewayApiKey,
+ setXiaomiApiKey,
setZaiApiKey,
} from "../../onboard-auth.js";
import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
@@ -177,6 +179,25 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyZaiConfig(nextConfig);
}
+ if (authChoice === "xiaomi-api-key") {
+ const resolved = await resolveNonInteractiveApiKey({
+ provider: "xiaomi",
+ cfg: baseConfig,
+ flagValue: opts.xiaomiApiKey,
+ flagName: "--xiaomi-api-key",
+ envVar: "XIAOMI_API_KEY",
+ runtime,
+ });
+ if (!resolved) return null;
+ if (resolved.source !== "profile") await setXiaomiApiKey(resolved.key);
+ nextConfig = applyAuthProfileConfig(nextConfig, {
+ profileId: "xiaomi:default",
+ provider: "xiaomi",
+ mode: "api_key",
+ });
+ return applyXiaomiConfig(nextConfig);
+ }
+
if (authChoice === "openai-api-key") {
const resolved = await resolveNonInteractiveApiKey({
provider: "openai",
diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts
index aa1d9afe0..f4154bc6d 100644
--- a/src/commands/onboard-types.ts
+++ b/src/commands/onboard-types.ts
@@ -23,6 +23,7 @@ export type AuthChoice =
| "google-antigravity"
| "google-gemini-cli"
| "zai-api-key"
+ | "xiaomi-api-key"
| "minimax-cloud"
| "minimax"
| "minimax-api"
@@ -67,6 +68,7 @@ export type OnboardOptions = {
kimiCodeApiKey?: string;
geminiApiKey?: string;
zaiApiKey?: string;
+ xiaomiApiKey?: string;
minimaxApiKey?: string;
syntheticApiKey?: string;
veniceApiKey?: string;
diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts
index 90d73bb59..e0d9a6ef9 100644
--- a/src/infra/provider-usage.auth.ts
+++ b/src/infra/provider-usage.auth.ts
@@ -96,6 +96,33 @@ function resolveMinimaxApiKey(): string | undefined {
return undefined;
}
+function resolveXiaomiApiKey(): string | undefined {
+ const envDirect = process.env.XIAOMI_API_KEY?.trim();
+ if (envDirect) return envDirect;
+
+ const envResolved = resolveEnvApiKey("xiaomi");
+ if (envResolved?.apiKey) return envResolved.apiKey;
+
+ const cfg = loadConfig();
+ const key = getCustomProviderApiKey(cfg, "xiaomi");
+ if (key) return key;
+
+ const store = ensureAuthProfileStore();
+ const apiProfile = listProfilesForProvider(store, "xiaomi").find((id) => {
+ const cred = store.profiles[id];
+ return cred?.type === "api_key" || cred?.type === "token";
+ });
+ if (!apiProfile) return undefined;
+ const cred = store.profiles[apiProfile];
+ if (cred?.type === "api_key" && cred.key?.trim()) {
+ return cred.key.trim();
+ }
+ if (cred?.type === "token" && cred.token?.trim()) {
+ return cred.token.trim();
+ }
+ return undefined;
+}
+
async function resolveOAuthToken(params: {
provider: UsageProviderId;
agentDir?: string;
@@ -199,6 +226,11 @@ export async function resolveProviderAuths(params: {
if (apiKey) auths.push({ provider, token: apiKey });
continue;
}
+ if (provider === "xiaomi") {
+ const apiKey = resolveXiaomiApiKey();
+ if (apiKey) auths.push({ provider, token: apiKey });
+ continue;
+ }
if (!oauthProviders.includes(provider)) continue;
const auth = await resolveOAuthToken({
diff --git a/src/infra/provider-usage.load.ts b/src/infra/provider-usage.load.ts
index 39a97a86c..5eb101d85 100644
--- a/src/infra/provider-usage.load.ts
+++ b/src/infra/provider-usage.load.ts
@@ -66,6 +66,12 @@ export async function loadProviderUsageSummary(
return await fetchCodexUsage(auth.token, auth.accountId, timeoutMs, fetchFn);
case "minimax":
return await fetchMinimaxUsage(auth.token, timeoutMs, fetchFn);
+ case "xiaomi":
+ return {
+ provider: "xiaomi",
+ displayName: PROVIDER_LABELS.xiaomi,
+ windows: [],
+ };
case "zai":
return await fetchZaiUsage(auth.token, timeoutMs, fetchFn);
default:
diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts
index 6c8c1d9bb..55eca4757 100644
--- a/src/infra/provider-usage.shared.ts
+++ b/src/infra/provider-usage.shared.ts
@@ -10,6 +10,7 @@ export const PROVIDER_LABELS: Record = {
"google-antigravity": "Antigravity",
minimax: "MiniMax",
"openai-codex": "Codex",
+ xiaomi: "Xiaomi",
zai: "z.ai",
};
@@ -20,6 +21,7 @@ export const usageProviders: UsageProviderId[] = [
"google-antigravity",
"minimax",
"openai-codex",
+ "xiaomi",
"zai",
];
diff --git a/src/infra/provider-usage.types.ts b/src/infra/provider-usage.types.ts
index cef446ceb..0a4637a7d 100644
--- a/src/infra/provider-usage.types.ts
+++ b/src/infra/provider-usage.types.ts
@@ -24,4 +24,5 @@ export type UsageProviderId =
| "google-antigravity"
| "minimax"
| "openai-codex"
+ | "xiaomi"
| "zai";
diff --git a/src/shared/text/reasoning-tags.test.ts b/src/shared/text/reasoning-tags.test.ts
new file mode 100644
index 000000000..d72d0cde2
--- /dev/null
+++ b/src/shared/text/reasoning-tags.test.ts
@@ -0,0 +1,218 @@
+import { describe, expect, it } from "vitest";
+import { stripReasoningTagsFromText } from "./reasoning-tags.js";
+
+describe("stripReasoningTagsFromText", () => {
+ describe("basic functionality", () => {
+ it("returns text unchanged when no reasoning tags present", () => {
+ const input = "Hello, this is a normal message.";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("strips proper think tags", () => {
+ const input = "Hello internal reasoning world!";
+ expect(stripReasoningTagsFromText(input)).toBe("Hello world!");
+ });
+
+ it("strips thinking tags", () => {
+ const input = "Before some thought after";
+ expect(stripReasoningTagsFromText(input)).toBe("Before after");
+ });
+
+ it("strips thought tags", () => {
+ const input = "A hmm B";
+ expect(stripReasoningTagsFromText(input)).toBe("A B");
+ });
+
+ it("strips antthinking tags", () => {
+ const input = "X internal Y";
+ expect(stripReasoningTagsFromText(input)).toBe("X Y");
+ });
+
+ it("strips multiple reasoning blocks", () => {
+ const input = "firstAsecondB";
+ expect(stripReasoningTagsFromText(input)).toBe("AB");
+ });
+ });
+
+ describe("code block preservation (issue #3952)", () => {
+ it("preserves think tags inside fenced code blocks", () => {
+ const input = "Use the tag like this:\n```\nreasoning\n```\nThat's it!";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("preserves think tags inside inline code", () => {
+ const input =
+ "The `` tag is used for reasoning. Don't forget the closing `` tag.";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("preserves tags in fenced code blocks with language specifier", () => {
+ const input = "Example:\n```xml\n\n nested\n\n```\nDone!";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("handles mixed real tags and code tags", () => {
+ const input = "hiddenVisible text with `` example.";
+ expect(stripReasoningTagsFromText(input)).toBe("Visible text with `` example.");
+ });
+
+ it("preserves both opening and closing tags in backticks", () => {
+ const input = "Use `` to open and `` to close.";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("preserves think tags in code block at EOF without trailing newline", () => {
+ const input = "Example:\n```\nreasoning\n```";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("preserves final tags inside code blocks", () => {
+ const input = "Use `` for final answers in code: ```\n42\n```";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("handles code block followed by real tags", () => {
+ const input = "```\ncode\n```\nreal hiddenvisible";
+ expect(stripReasoningTagsFromText(input)).toBe("```\ncode\n```\nvisible");
+ });
+
+ it("handles multiple code blocks with tags", () => {
+ const input = "First `` then ```\nblock\n``` then ``";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+ });
+
+ describe("edge cases", () => {
+ it("preserves unclosed {
+ const input = "Here is how to use {
+ const input = "You can start with ";
+ expect(stripReasoningTagsFromText(input)).toBe(
+ "You can start with {
+ const input = "A < think >content< /think > B";
+ expect(stripReasoningTagsFromText(input)).toBe("A B");
+ });
+
+ it("handles empty input", () => {
+ expect(stripReasoningTagsFromText("")).toBe("");
+ });
+
+ it("handles null-ish input", () => {
+ expect(stripReasoningTagsFromText(null as unknown as string)).toBe(null);
+ });
+
+ it("preserves think tags inside tilde fenced code blocks", () => {
+ const input = "Example:\n~~~\nreasoning\n~~~\nDone!";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("preserves tags in tilde block at EOF without trailing newline", () => {
+ const input = "Example:\n~~~js\ncode\n~~~";
+ expect(stripReasoningTagsFromText(input)).toBe(input);
+ });
+
+ it("handles nested think patterns (first close ends block)", () => {
+ const input = "outer inner still outervisible";
+ expect(stripReasoningTagsFromText(input)).toBe("still outervisible");
+ });
+
+ it("strips final tag markup but preserves content (by design)", () => {
+ const input = "A1B2C";
+ expect(stripReasoningTagsFromText(input)).toBe("A1B2C");
+ });
+
+ it("preserves final tags in inline code (markup only stripped outside)", () => {
+ const input = "`` in code, visible outside";
+ expect(stripReasoningTagsFromText(input)).toBe("`` in code, visible outside");
+ });
+
+ it("handles double backtick inline code with tags", () => {
+ const input = "Use ``code`` with hidden text";
+ expect(stripReasoningTagsFromText(input)).toBe("Use ``code`` with text");
+ });
+
+ it("handles fenced code blocks with content", () => {
+ const input = "Before\n```\ncode\n```\nAfter with hidden";
+ expect(stripReasoningTagsFromText(input)).toBe("Before\n```\ncode\n```\nAfter with");
+ });
+
+ it("does not match mismatched fence types (``` vs ~~~)", () => {
+ const input = "```\nnot protected\n~~~\ntext";
+ const result = stripReasoningTagsFromText(input);
+ expect(result).toBe(input);
+ });
+
+ it("handles unicode content inside and around tags", () => {
+ const input = "你好 思考 🤔 世界";
+ expect(stripReasoningTagsFromText(input)).toBe("你好 世界");
+ });
+
+ it("handles very long content between tags efficiently", () => {
+ const longContent = "x".repeat(10000);
+ const input = `${longContent}visible`;
+ expect(stripReasoningTagsFromText(input)).toBe("visible");
+ });
+
+ it("handles tags with attributes", () => {
+ const input = "A hidden B";
+ expect(stripReasoningTagsFromText(input)).toBe("A B");
+ });
+
+ it("is case-insensitive for tag names", () => {
+ const input = "A hidden also hidden B";
+ expect(stripReasoningTagsFromText(input)).toBe("A B");
+ });
+
+ it("handles pathological nested backtick patterns without hanging", () => {
+ const input = "`".repeat(100) + "test" + "`".repeat(100);
+ const start = Date.now();
+ stripReasoningTagsFromText(input);
+ const elapsed = Date.now() - start;
+ expect(elapsed).toBeLessThan(1000);
+ });
+
+ it("handles unclosed inline code gracefully", () => {
+ const input = "Start `unclosed hidden end";
+ const result = stripReasoningTagsFromText(input);
+ expect(result).toBe("Start `unclosed end");
+ });
+ });
+
+ describe("strict vs preserve mode", () => {
+ it("strict mode truncates on unclosed tag", () => {
+ const input = "Before unclosed content after";
+ expect(stripReasoningTagsFromText(input, { mode: "strict" })).toBe("Before");
+ });
+
+ it("preserve mode keeps content after unclosed tag", () => {
+ const input = "Before unclosed content after";
+ expect(stripReasoningTagsFromText(input, { mode: "preserve" })).toBe(
+ "Before unclosed content after",
+ );
+ });
+ });
+
+ describe("trim options", () => {
+ it("trims both sides by default", () => {
+ const input = " x result y ";
+ expect(stripReasoningTagsFromText(input)).toBe("result");
+ });
+
+ it("trim=none preserves whitespace", () => {
+ const input = " x result ";
+ expect(stripReasoningTagsFromText(input, { trim: "none" })).toBe(" result ");
+ });
+
+ it("trim=start only trims start", () => {
+ const input = " x result ";
+ expect(stripReasoningTagsFromText(input, { trim: "start" })).toBe("result ");
+ });
+ });
+});
diff --git a/src/shared/text/reasoning-tags.ts b/src/shared/text/reasoning-tags.ts
index 822138e55..afb8f891f 100644
--- a/src/shared/text/reasoning-tags.ts
+++ b/src/shared/text/reasoning-tags.ts
@@ -2,8 +2,40 @@ export type ReasoningTagMode = "strict" | "preserve";
export type ReasoningTagTrim = "none" | "start" | "both";
const QUICK_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking|final)\b/i;
-const FINAL_TAG_RE = /<\s*\/?\s*final\b[^>]*>/gi;
-const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;
+const FINAL_TAG_RE = /<\s*\/?\s*final\b[^<>]*>/gi;
+const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
+
+interface CodeRegion {
+ start: number;
+ end: number;
+}
+
+function findCodeRegions(text: string): CodeRegion[] {
+ const regions: CodeRegion[] = [];
+
+ const fencedRe = /(^|\n)(```|~~~)[^\n]*\n[\s\S]*?(?:\n\2(?:\n|$)|$)/g;
+ for (const match of text.matchAll(fencedRe)) {
+ const start = (match.index ?? 0) + match[1].length;
+ regions.push({ start, end: start + match[0].length - match[1].length });
+ }
+
+ const inlineRe = /`+[^`]+`+/g;
+ for (const match of text.matchAll(inlineRe)) {
+ const start = match.index ?? 0;
+ const end = start + match[0].length;
+ const insideFenced = regions.some((r) => start >= r.start && end <= r.end);
+ if (!insideFenced) {
+ regions.push({ start, end });
+ }
+ }
+
+ regions.sort((a, b) => a.start - b.start);
+ return regions;
+}
+
+function isInsideCode(pos: number, regions: CodeRegion[]): boolean {
+ return regions.some((r) => pos >= r.start && pos < r.end);
+}
function applyTrim(value: string, mode: ReasoningTagTrim): string {
if (mode === "none") return value;
@@ -27,11 +59,29 @@ export function stripReasoningTagsFromText(
let cleaned = text;
if (FINAL_TAG_RE.test(cleaned)) {
FINAL_TAG_RE.lastIndex = 0;
- cleaned = cleaned.replace(FINAL_TAG_RE, "");
+ const finalMatches: Array<{ start: number; length: number; inCode: boolean }> = [];
+ const preCodeRegions = findCodeRegions(cleaned);
+ for (const match of cleaned.matchAll(FINAL_TAG_RE)) {
+ const start = match.index ?? 0;
+ finalMatches.push({
+ start,
+ length: match[0].length,
+ inCode: isInsideCode(start, preCodeRegions),
+ });
+ }
+
+ for (let i = finalMatches.length - 1; i >= 0; i--) {
+ const m = finalMatches[i];
+ if (!m.inCode) {
+ cleaned = cleaned.slice(0, m.start) + cleaned.slice(m.start + m.length);
+ }
+ }
} else {
FINAL_TAG_RE.lastIndex = 0;
}
+ const codeRegions = findCodeRegions(cleaned);
+
THINKING_TAG_RE.lastIndex = 0;
let result = "";
let lastIndex = 0;
@@ -41,6 +91,10 @@ export function stripReasoningTagsFromText(
const idx = match.index ?? 0;
const isClose = match[1] === "/";
+ if (isInsideCode(idx, codeRegions)) {
+ continue;
+ }
+
if (!inThinking) {
result += cleaned.slice(lastIndex, idx);
if (!isClose) {