Merge origin/master (v0.5.75) into gitea/new_feature

Resolve conflicts:
- package.json / cli/package.json: take 0.5.75
- .gitignore: union both sides (upstream 9router-*/temp files + local state dirs)
- CHANGELOG.md: keep both blocks, v0.5.75 above v0.5.70
- nonStreamingHandler.js: merge imports (unwrapClineEnvelope +
  tokensForDetail/shouldPersistRequestDetail); drop dead appendRequestLog
- providers/[id]/page.js: union useState blocks (compatible-model states
  + importingClineModels)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
2026-09-17 14:14:43 +07:00
113 changed files with 6634 additions and 482 deletions

View File

@@ -5,7 +5,7 @@ import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, AG_DEFAULT_TOOLS, AG_TOOL_SUFFIX,
import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { resolveSessionId, toNumericSessionId } from "../utils/sessionManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { cleanJSONSchemaForAntigravity } from "../translator/formats/gemini.js";
import { cleanJSONSchemaForAntigravity, normalizeGeminiContents } from "../translator/formats/gemini.js";
import { DEFAULT_THINKING_AG_SIGNATURE } from "../config/defaultThinkingSignature.js";
import { getGeminiThoughtSignatureSync } from "../services/thoughtSignatureStore.js";
@@ -193,7 +193,7 @@ export class AntigravityExecutor extends BaseExecutor {
// ─── Standard (non-image) request ───
// Fix contents for Claude models via Antigravity
const contents = body.request?.contents?.map(c => {
const rawContents = (body.request?.contents || []).map(c => {
let role = c.role;
// functionResponse must be role "user" for Claude models
if (c.parts?.some(p => p.functionResponse)) {
@@ -226,15 +226,13 @@ export class AntigravityExecutor extends BaseExecutor {
return p;
});
const partsChanged = parts?.length !== c.parts?.length || modifiedParts?.some((p, idx) => p !== c.parts[idx]);
if (role !== c.role || partsChanged) {
return {
...c, role,
parts: modifiedParts || parts,
};
}
return c;
return {
...c,
role,
parts: modifiedParts || parts || [],
};
});
const contents = normalizeGeminiContents(rawContents);
// Sanitize tool schemas and function names before sending to Antigravity.
let tools = body.request?.tools;

View File

@@ -12,6 +12,7 @@ import { getThinkingLevels } from "../providers/thinkingLevels.js";
import { DEFAULT_RETRY_CONFIG, HTTP_STATUS, resolveRetryEntry } from "../config/runtimeConfig.js";
import { dbg } from "../utils/debugLog.js";
import { resolveSessionId } from "../utils/sessionManager.js";
import { stripCodexUnsupportedPatterns } from "../utils/codexToolSchema.js";
// SSE error patterns inside 200-OK bodies. Some retry same account first; capacity rotates accounts.
const CODEX_SSE_RETRY_PATTERNS = ["server_is_overloaded", "service_unavailable_error"];
@@ -72,6 +73,9 @@ function stripStoredItemReferences(body) {
function normalizeCodexTools(body) {
if (!Array.isArray(body.tools)) return;
const validNames = new Set();
// Codex's schema validator has no Unicode property escapes; a `pattern`
// carrying `\p{...}` 400s the whole request on every account (#3922).
const patternStats = { removed: 0 };
body.tools = body.tools.filter((tool) => {
if (!tool || typeof tool !== "object" || Array.isArray(tool)) return false;
const type = typeof tool.type === "string" ? tool.type : "";
@@ -80,6 +84,9 @@ function normalizeCodexTools(body) {
for (const st of tool.tools) {
const n = typeof st?.name === "string" ? st.name.trim().slice(0, 128) : "";
if (n) validNames.add(n);
if (st?.parameters && typeof st.parameters === "object") {
st.parameters = stripCodexUnsupportedPatterns(st.parameters, patternStats);
}
}
}
return true;
@@ -101,10 +108,13 @@ function normalizeCodexTools(body) {
tool.type = "function";
tool.name = name.slice(0, 128);
if (description) tool.description = description;
tool.parameters = parameters;
tool.parameters = stripCodexUnsupportedPatterns(parameters, patternStats);
validNames.add(name);
return true;
});
if (patternStats.removed > 0) {
dbg("CODEX", `stripped ${patternStats.removed} unsupported tool schema pattern(s)`);
}
// Drop tool_choice if it references an unknown function name
if (body.tool_choice && typeof body.tool_choice === "object" && !Array.isArray(body.tool_choice)) {
if (body.tool_choice.type === "function") {

View File

@@ -17,6 +17,7 @@ import { PerplexityWebExecutor } from "./perplexity-web.js";
import { OllamaLocalExecutor } from "./ollama-local.js";
import { CommandCodeExecutor } from "./commandcode.js";
import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
import { XiaomiMimoExecutor } from "./xiaomi-mimo.js";
import { MimoFreeExecutor } from "./mimo-free.js";
import { CodeBuddyExecutor } from "./codebuddy-cn.js";
import { CodeBuddyIntlExecutor } from "./codebuddy-intl.js";
@@ -50,6 +51,7 @@ const executors = {
"ollama-local": new OllamaLocalExecutor(),
commandcode: new CommandCodeExecutor(),
"xiaomi-tokenplan": new XiaomiTokenplanExecutor(),
"xiaomi-mimo": new XiaomiMimoExecutor(),
"mimo-free": new MimoFreeExecutor(),
mmf: new MimoFreeExecutor(), // Alias for mimo-free
"codebuddy-cn": new CodeBuddyExecutor(),
@@ -93,6 +95,7 @@ export { PerplexityWebExecutor } from "./perplexity-web.js";
export { OllamaLocalExecutor } from "./ollama-local.js";
export { CommandCodeExecutor } from "./commandcode.js";
export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
export { XiaomiMimoExecutor } from "./xiaomi-mimo.js";
export { MimoFreeExecutor } from "./mimo-free.js";
export { CodeBuddyExecutor } from "./codebuddy-cn.js";
export { CodeBuddyIntlExecutor } from "./codebuddy-intl.js";

View File

@@ -127,12 +127,18 @@ async function readResponsePrefix(response, signal, maxBytes, timeoutMs) {
return decoder.decode(concatChunks(chunks, totalBytes));
}
// The instruction goes into the current user turn, never into a top-level
// `systemPrompt`: kiro.dev answers any body carrying that field with
// 400 REQUEST_BODY_INVALID, so writing it here turned every repair retry into
// a hard failure.
function appendRepairInstruction(body, kind) {
const repaired = structuredClone(body || {});
const instruction = REPAIR_INSTRUCTIONS[kind] || "Retry the previous incomplete Kiro response.";
repaired.systemPrompt = repaired.systemPrompt
? `${repaired.systemPrompt}\n\n${instruction}`
: instruction;
const msg = repaired?.conversationState?.currentMessage?.userInputMessage;
if (msg) {
const content = typeof msg.content === "string" ? msg.content : "";
msg.content = content ? `${content}\n\n${instruction}` : instruction;
}
return repaired;
}
@@ -259,6 +265,19 @@ export class KiroExecutor extends BaseExecutor {
}
}
// CLIRO parity for the Amazon surfaces: the Kiro runtime accepts the
// SSO bearer header + agent-mode marker. Without these the deprecated
// path gateway answers REQUEST_BODY_INVALID for modern payloads.
if (credentials?.accessToken) {
headers["x-amz-sso-bearer"] = credentials.accessToken;
}
headers["x-amzn-kiro-agent-mode"] = "spec";
headers["x-amzn-codewhisperer-machine-id"] = "kiro-desktop";
const profileArn = credentials?.providerSpecificData?.profileArn;
if (profileArn) {
headers["x-amzn-codewhisperer-profile-arn"] = profileArn;
}
return headers;
}
@@ -285,9 +304,13 @@ export class KiroExecutor extends BaseExecutor {
// 403 "bearer token invalid", so they must hit the CodeWhisperer
// *.amazonaws.com surface, and in the region the token was minted in
// (the baseUrls are hardcoded us-east-1).
const isCodeWhispererSurface =
authMethod === "api_key" || authMethod === "external_idp" || authMethod === "idc";
if (!isCodeWhispererSurface) return baseUrls;
// Kiro deprecated the legacy path-style GenerateAssistantResponse on
// runtime.*.kiro.dev (IDE 1.0.228+ moved to POST / + x-amz-target). The
// path gateway now answers valid modern payloads with 400
// REQUEST_BODY_INVALID, and 400 is terminal in BaseExecutor, so kiro.dev
// must never be the first surface for any auth method. Amazon surfaces
// reject foreign tokens with 401/403, which DO fall through, so trying
// q/codewhisperer first is safe for every auth method (CLIRO parity).
const region = (credentials?.providerSpecificData?.region || "us-east-1").trim();
const regionalize = (u) =>
@@ -297,20 +320,17 @@ export class KiroExecutor extends BaseExecutor {
const amazon = baseUrls.filter((u) => u.includes("amazonaws.com")).map(regionalize);
const others = baseUrls.filter((u) => !u.includes("amazonaws.com"));
if (authMethod === "api_key") {
const q = amazon.filter((u) => u.includes("://q."));
const remaining = amazon.filter((u) => !u.includes("://q."));
return q.length > 0
? [...q, ...remaining, ...others]
: [...amazon, ...others];
}
return amazon.length > 0 ? [...amazon, ...others] : baseUrls;
const q = amazon.filter((u) => u.includes("://q."));
const remaining = amazon.filter((u) => !u.includes("://q."));
return q.length > 0
? [...q, ...remaining, ...others]
: [...amazon, ...others];
}
buildUrl(model, stream, urlIndex = 0, credentials = null) {
const baseUrls = this.getOrderedBaseUrls(credentials);
return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl;
const url = baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl;
return url;
}
// Retry only endpoint/auth-surface failures. Payload-invalid HTTP 400 must be

View File

@@ -32,14 +32,16 @@ import { SSE_DONE } from "../utils/sseConstants.js";
import { FETCH_CONNECT_TIMEOUT_MS } from "../config/runtimeConfig.js";
import { resolveProviderTimeoutMs } from "../services/providerTimeout.js";
import {
QODER_CHAT_URL_ENCODED,
QODER_CHAT_BASE_ALT,
QODER_CHAT_SIG_PATH,
QODER_MODEL_MAP,
QODER_CONTEXT_TIER_ENV,
qoderInferenceBase,
} from "../shared/qoder/constants.js";
import { getQoderModelConfig, resolveQoderModels, isQoderPat, resolveQoderCredentials } from "../services/qoderModels.js";
import { OPENAI_BLOCK, CLAUDE_BLOCK } from "../translator/schema/blocks.js";
import { encodeDataUri } from "../translator/concerns/image.js";
import { createQoderSseCoalescer } from "../shared/qoder/sse.js";
import { rewriteQoderMessageAttachments } from "../shared/qoder/attachments.js";
import { resolveQoderContextTier, applyQoderContextTier } from "../shared/qoder/contextTier.js";
/**
* Hoist role:"system" messages out of the messages array (Qoder rejects
@@ -71,15 +73,16 @@ function normalizeMessages(messages) {
*
* Text-only content is flattened to a plain string (Qoder's historical
* shape). When images are present the content stays an array and image
* blocks are kept as OpenAI-style `image_url` parts — verified against the
* upstream: it accepts both http(s) URLs and inline base64 data: URIs
* directly, no pre-upload to the /image/upload OSS flow required (that is
* a qodercli client-side choice, not a protocol requirement). The legacy
* blocks are kept as OpenAI-style `image_url` parts. Native qodercli
* uploads inlined bytes to `/api/v2/image/upload` first and then sends
* the OSS URL — `buildQoderRequestBody` does that rewrite before this
* runs. Tiny leftover data URIs are still accepted. The legacy
* top-level `image_urls` / `chat_context.imageUrls` slots stay null —
* qodercli leaves them null too.
*
* Claude-style `{type:"image", source:{...}}` blocks are converted to
* `image_url` so claude-format clients also round-trip.
* `image_url`. File/document blocks that survived rewrite become short
* stubs so 30MB PDFs never land in agent_chat_generation.
*/
function normalizeContent(content) {
if (typeof content === "string") return content;
@@ -89,10 +92,24 @@ function normalizeContent(content) {
const blocks = [];
const textParts = [];
let hasImage = false;
const pushText = (text) => {
if (!text) return;
if (hasImage || blocks.length) blocks.push({ type: OPENAI_BLOCK.TEXT, text });
else textParts.push(text);
};
const imageUrlOf = (item) => {
if (typeof item.image_url === "string" && item.image_url) return item.image_url;
if (typeof item.image_url?.url === "string" && item.image_url.url) return item.image_url.url;
return null;
};
for (const item of content) {
if (!item || typeof item !== "object") continue;
if (item.type === OPENAI_BLOCK.IMAGE_URL && typeof item.image_url?.url === "string" && item.image_url.url) {
blocks.push({ type: OPENAI_BLOCK.IMAGE_URL, image_url: { url: item.image_url.url } });
const imageUrl = item.type === OPENAI_BLOCK.IMAGE_URL ? imageUrlOf(item) : null;
if (imageUrl) {
blocks.push({ type: OPENAI_BLOCK.IMAGE_URL, image_url: { url: imageUrl } });
hasImage = true;
} else if (item.type === CLAUDE_BLOCK.IMAGE && item.source) {
// Claude base64/url image → OpenAI image_url equivalent.
@@ -104,13 +121,14 @@ function normalizeContent(content) {
blocks.push({ type: OPENAI_BLOCK.IMAGE_URL, image_url: { url } });
hasImage = true;
}
} else if (item.type === OPENAI_BLOCK.FILE) {
const name = item.file?.filename || item.file?.name || "file";
pushText(`[file omitted: ${name} — Qoder reads documents via its file API, not inlined bytes]`);
} else if (item.type === CLAUDE_BLOCK.DOCUMENT) {
const name = item.title || "document";
pushText(`[file omitted: ${name} — Qoder reads documents via its file API, not inlined bytes]`);
} else if (typeof item.text === "string" && item.text) {
if (hasImage || blocks.length) {
// Keep ordering faithful once images are in play.
blocks.push({ type: OPENAI_BLOCK.TEXT, text: item.text });
} else {
textParts.push(item.text);
}
pushText(item.text);
}
}
@@ -190,7 +208,7 @@ function truncate(s, n) {
/**
* Map the OpenAI-style request body into the exact shape Qoder expects.
*/
async function buildQoderRequestBody({ model, body, credentials, log, proxyOptions, signal }) {
async function buildQoderRequestBody({ model, body, credentials, log, proxyOptions, signal, uploadFn = null }) {
const qoderKey = String(model || "").replace(/^qoder\//, "");
// Fetch model config from dynamic API instead of relying on static QODER_MODEL_MAP.
@@ -209,7 +227,30 @@ async function buildQoderRequestBody({ model, body, credentials, log, proxyOptio
modelConfig = { ...retried, key: qoderKey };
}
const { messages, systemText } = normalizeMessages(body.messages || []);
const incoming = Array.isArray(body.messages)
? body.messages.map((m) => {
if (!m || typeof m !== "object") return m;
return {
...m,
content: Array.isArray(m.content)
? m.content.map((b) => (b && typeof b === "object" ? { ...b } : b))
: m.content,
};
})
: [];
try {
await rewriteQoderMessageAttachments(incoming, {
credentials,
log,
proxyOptions,
signal,
uploadFn,
});
} catch (err) {
log?.warn?.("QODER", `attachment rewrite failed: ${err.message}`);
}
const { messages, systemText } = normalizeMessages(incoming);
const tools = body.tools;
const isReasoning = !!modelConfig.is_reasoning;
const maxOutputTokens = Number(modelConfig.max_output_tokens) || 0;
@@ -228,7 +269,21 @@ async function buildQoderRequestBody({ model, body, credentials, log, proxyOptio
const sessionId = stableHash("qoder-session", psd.userId, qoderKey);
const recordId = stableChatRecordId(qoderKey, messages, tools, maxTokens);
return {
// Context-window tier (200K/400K/1M): the IDE picks one from model_config.context_config;
// qodercli-style requests default to the smallest. Escalate when the prompt no longer fits.
const tierChoice = resolveQoderContextTier(
modelConfig,
{ system: systemText, messages, tools },
{ preference: process.env[QODER_CONTEXT_TIER_ENV] },
);
if (tierChoice) {
log?.info?.(
"QODER",
`context tier ${tierChoice.tier.name} (${tierChoice.tier.tokenCount} tokens, ${tierChoice.reason}) for ~${tierChoice.estimatedTokens} prompt tokens`,
);
}
const built = {
qoderKey,
payload: {
request_id: uuidv4(),
@@ -276,6 +331,8 @@ async function buildQoderRequestBody({ model, body, credentials, log, proxyOptio
},
modelConfig,
};
if (tierChoice) applyQoderContextTier(built.payload, tierChoice.tier);
return built;
}
/**
@@ -339,6 +396,11 @@ async function peekFirstQoderFrame(reader, decoder) {
* response.text() which hangs until the socket closes — so on terminal
* events we cancel the upstream reader and close our stream immediately.
*
* Usage: Qoder puts finish_reason on `delta` and sends token counts on a
* later `choices: []` frame. Downstream OpenAI/Claude clients only read
* usage from the finish chunk, so we coalesce those two frames (see
* createQoderSseCoalescer) before forwarding.
*
* NEW: Peek first frame to detect billing blocks (code 112/10605/pricingUrl).
* If detected, return 403 response so chatCore marks connection unavailable
* and triggers combo fallback instead of leaking error text into chat.
@@ -365,6 +427,11 @@ async function wrapQoderSSE(response, model) {
const upstreamDrained = peek.upstreamDone === true;
const encoder = new TextEncoder();
let doneEmitted = false;
const coalescer = createQoderSseCoalescer({ model, encoder, sseDone: SSE_DONE });
const syncDone = () => {
if (coalescer.doneEmitted) doneEmitted = true;
};
// Process one already-extracted SSE line (no trailing newline).
const processLine = (line, controller) => {
@@ -375,15 +442,17 @@ async function wrapQoderSSE(response, model) {
const data = trimmed.slice(5).trimStart();
if (data === "[DONE]") {
controller.enqueue(encoder.encode(SSE_DONE));
doneEmitted = true;
coalescer.flush(controller);
syncDone();
return;
}
let envelope;
try { envelope = JSON.parse(data); } catch { return; }
const statusVal = typeof envelope.statusCodeValue === "number" ? envelope.statusCodeValue : 200;
const inner = typeof envelope.body === "string" ? envelope.body : "";
const inner = typeof envelope.body === "string"
? envelope.body
: envelope.body != null ? JSON.stringify(envelope.body) : "";
if (statusVal !== 200) {
const msg = inner || `upstream status ${statusVal}`;
const errChunk = JSON.stringify({
@@ -399,14 +468,8 @@ async function wrapQoderSSE(response, model) {
return;
}
if (!inner) return;
if (inner === "[DONE]") {
controller.enqueue(encoder.encode(SSE_DONE));
doneEmitted = true;
return;
}
// Strip embedded newlines so the SSE frame stays a single event.
const sanitized = inner.replace(/\r?\n/g, "");
controller.enqueue(encoder.encode(`data: ${sanitized}\n\n`));
coalescer.handleInner(inner, controller);
syncDone();
};
const stream = new ReadableStream({
@@ -465,7 +528,7 @@ async function wrapQoderSSE(response, model) {
} finally {
if (!doneEmitted) {
try {
controller.enqueue(encoder.encode(SSE_DONE));
coalescer.flush(controller);
doneEmitted = true;
} catch { /* already closed */ }
}
@@ -494,13 +557,7 @@ export class QoderExecutor extends BaseExecutor {
}
buildUrl(credentials) {
// Job-token (jt-...) traffic must hit api2.qoder.sh — api3 rejects jt-
// with "Login expired" (403). Device tokens (dt-...) stay on api3.
const raw = credentials?.apiKey || credentials?.accessToken;
if (typeof raw === "string" && !raw.startsWith("pt-") && (raw.startsWith("jt-") || (credentials?.accessToken || "").startsWith("jt-"))) {
return `${QODER_CHAT_BASE_ALT}/algo${QODER_CHAT_SIG_PATH}?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1`;
}
return QODER_CHAT_URL_ENCODED;
return `${qoderInferenceBase(credentials)}/algo${QODER_CHAT_SIG_PATH}?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1`;
}
// Override execute entirely — Qoder needs:

View File

@@ -0,0 +1,99 @@
import { DefaultExecutor } from "./default.js";
import { getMimoAccountCookie, invalidateMimoAccountCookieCache, MIMO_API_BASE, MIMO_API_UA } from "../shared/mimoAccount.js";
// Desktop-exclusive Preview models. These are served by the account service's
// /api/route proxy, authorized by the Xiaomi account session (NOT the sk- key).
// See shared/mimoAccount.js for the session handshake.
const PREVIEW_MODELS = new Set(["mimo-x-pro-preview", "mimo-x-flash-preview"]);
// Session cookie resolved in execute() (async) and read back by buildHeaders()
// (sync — BaseExecutor.execute does not await it). Carried on the per-request
// credentials object, same as runtimeTransport.
const COOKIE_KEY = "__mimoAccountCookie";
// Upstream calls may hand us either the bare id or a `provider/model` ref.
function bareModel(model) {
const s = String(model || "");
const i = s.indexOf("/");
return i >= 0 ? s.slice(i + 1) : s;
}
export class XiaomiMimoExecutor extends DefaultExecutor {
constructor() {
super("xiaomi-mimo");
}
static isPreviewModel(model) {
return PREVIEW_MODELS.has(bareModel(model));
}
buildUrl(model, stream, urlIndex = 0, credentials = null) {
// Preview models live on the account-service route, which is not one of the
// declared transports — resolve it before the default runtimeTransport path.
if (XiaomiMimoExecutor.isPreviewModel(model)) {
return `${MIMO_API_BASE}/api/route/chat/completions`;
}
// Cloud API models keep default handling, so a Claude-format client reaches
// the /anthropic/v1/messages transport.
return super.buildUrl(model, stream, urlIndex, credentials);
}
buildHeaders(credentials, stream = true, url, model) {
if (XiaomiMimoExecutor.isPreviewModel(model) && credentials?.[COOKIE_KEY]) {
// Preview models authenticate with the account-session cookie, not the key.
return {
"Content-Type": "application/json",
Accept: stream ? "text/event-stream" : "application/json",
"User-Agent": MIMO_API_UA,
Cookie: credentials[COOKIE_KEY],
};
}
return super.buildHeaders(credentials, stream, url, model);
}
transformRequest(model, body, stream, credentials) {
// super runs stripUnsupportedParams, which flattens Preview content-part
// arrays (see the xiaomi-mimo rule in translator/concerns/paramSupport.js).
const out = super.transformRequest(model, body, stream, credentials);
// Preview models: thinking/params get defaults only — never override what the
// caller set explicitly. (body.model is already `xiaomi/<id>` via upstreamModelId.)
if (XiaomiMimoExecutor.isPreviewModel(model)) {
if (out.thinking == null) out.thinking = { type: "enabled" };
if (out.temperature == null) out.temperature = 1.0;
if (out.top_p == null) out.top_p = 0.95;
if (!out.max_tokens) out.max_tokens = 4096;
}
return out;
}
async execute(args) {
const { model, credentials, proxyOptions = null } = args;
if (!XiaomiMimoExecutor.isPreviewModel(model)) return super.execute(args);
const cookie = await getMimoAccountCookie(credentials?.providerSpecificData, proxyOptions);
if (!cookie) {
throw new Error(
"Xiaomi MiMo account session unavailable. Sign in to MiMo Desktop once so its passToken is present, then retry.",
);
}
credentials[COOKIE_KEY] = cookie;
const result = await super.execute(args);
// A cached session can expire early — drop it and retry once with a fresh one.
if (result.response.status === 401) {
invalidateMimoAccountCookieCache();
const fresh = await getMimoAccountCookie(credentials?.providerSpecificData, proxyOptions).catch(() => null);
if (fresh) {
credentials[COOKIE_KEY] = fresh;
return super.execute(args);
}
}
return result;
}
}
export const __test__ = { PREVIEW_MODELS, bareModel, COOKIE_KEY };
export default XiaomiMimoExecutor;