mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
fix(executors): inject MiMoCode anti-abuse marker so free endpoint stops 403ing (#3728)
Integrated into release/v3.8.23
This commit is contained in:
@@ -28,6 +28,42 @@ const COOLDOWN_MAX_MS = 60_000;
|
||||
|
||||
const MIMO_SOURCE = "mimocode-cli-free";
|
||||
|
||||
/**
|
||||
* Anti-abuse gate marker required by the Xiaomi free endpoint.
|
||||
*
|
||||
* `/api/free-ai/openai/chat` returns `403 "Illegal access"` unless the request body
|
||||
* contains a recognized MiMoCode prompt signature as a substring inside a `system`-role
|
||||
* message (verified empirically — headers, fingerprint, and JWT are not what is checked).
|
||||
* This is the canonical MiMoCode agent opener the official CLI sends, and it is on the
|
||||
* upstream allowlist. We inject it as a leading system message so user requests pass the
|
||||
* gate. The string MUST stay byte-for-byte identical — the check is case-sensitive and
|
||||
* truncations are rejected.
|
||||
*/
|
||||
export const MIMO_SYSTEM_MARKER =
|
||||
"You are MiMoCode, an interactive CLI tool that helps users with software engineering tasks.";
|
||||
|
||||
/**
|
||||
* Ensure the outgoing body carries the MiMoCode anti-abuse marker in a system message.
|
||||
* Idempotent: if any system message already contains the marker, the body is returned
|
||||
* unchanged. Bodies without a `messages` array are left untouched.
|
||||
*/
|
||||
function injectSystemMarker(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const messages = body.messages;
|
||||
if (!Array.isArray(messages)) return body;
|
||||
|
||||
const hasMarker = messages.some(
|
||||
(m) =>
|
||||
m != null &&
|
||||
typeof m === "object" &&
|
||||
(m as { role?: unknown }).role === "system" &&
|
||||
typeof (m as { content?: unknown }).content === "string" &&
|
||||
(m as { content: string }).content.includes(MIMO_SYSTEM_MARKER)
|
||||
);
|
||||
if (hasMarker) return body;
|
||||
|
||||
return { ...body, messages: [{ role: "system", content: MIMO_SYSTEM_MARKER }, ...messages] };
|
||||
}
|
||||
|
||||
const USER_AGENTS = [
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
@@ -67,7 +103,9 @@ function getCpuModel(): string {
|
||||
try {
|
||||
const cpus = os.cpus();
|
||||
if (cpus.length > 0 && cpus[0].model) return cpus[0].model.trim();
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return "unknown-cpu";
|
||||
}
|
||||
|
||||
@@ -80,8 +118,13 @@ export function generateFingerprint(seed?: string): string {
|
||||
let username = "unknown-user";
|
||||
try {
|
||||
username = os.userInfo().username;
|
||||
} catch { /* ignore */ }
|
||||
return crypto.createHash("sha256").update(`${hostname}|${platform}|${arch}|${cpu}|${username}`).digest("hex");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(`${hostname}|${platform}|${arch}|${cpu}|${username}`)
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
// ── Bootstrap ──────────────────────────────────────────────────────────────
|
||||
@@ -91,7 +134,7 @@ const bootstrapInflight = new Map<string, Promise<{ jwt: string; expiresAt: numb
|
||||
async function bootstrapJwt(
|
||||
baseUrl: string,
|
||||
fingerprint: string,
|
||||
signal?: AbortSignal | null,
|
||||
signal?: AbortSignal | null
|
||||
): Promise<{ jwt: string; expiresAt: number }> {
|
||||
const existing = bootstrapInflight.get(fingerprint);
|
||||
if (existing) return existing;
|
||||
@@ -146,7 +189,13 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("mimocode", { format: "openai" });
|
||||
this.baseUrl = this.getBaseUrls()[0] || "https://api.xiaomimimo.com";
|
||||
this.accounts.push({ fingerprint: generateFingerprint(), jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 });
|
||||
this.accounts.push({
|
||||
fingerprint: generateFingerprint(),
|
||||
jwt: "",
|
||||
expiresAt: 0,
|
||||
cooldownUntil: 0,
|
||||
consecutiveFails: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private syncAccountsFromCredentials(credentials: ProviderCredentials): void {
|
||||
@@ -155,13 +204,22 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
const existing = new Set(this.accounts.map((a) => a.fingerprint));
|
||||
for (const fp of fingerprints) {
|
||||
if (typeof fp === "string" && !existing.has(fp)) {
|
||||
this.accounts.push({ fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0 });
|
||||
this.accounts.push({
|
||||
fingerprint: fp,
|
||||
jwt: "",
|
||||
expiresAt: 0,
|
||||
cooldownUntil: 0,
|
||||
consecutiveFails: 0,
|
||||
});
|
||||
existing.add(fp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getJwtForAccount(account: AccountState, signal?: AbortSignal | null): Promise<string> {
|
||||
private async getJwtForAccount(
|
||||
account: AccountState,
|
||||
signal?: AbortSignal | null
|
||||
): Promise<string> {
|
||||
if (isAccountReady(account)) return account.jwt;
|
||||
const result = await bootstrapJwt(this.baseUrl, account.fingerprint, signal);
|
||||
account.jwt = result.jwt;
|
||||
@@ -185,7 +243,10 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
|
||||
private markCooldown(account: AccountState): void {
|
||||
account.consecutiveFails++;
|
||||
const backoff = Math.min(COOLDOWN_BASE_MS * Math.pow(2, account.consecutiveFails - 1), COOLDOWN_MAX_MS);
|
||||
const backoff = Math.min(
|
||||
COOLDOWN_BASE_MS * Math.pow(2, account.consecutiveFails - 1),
|
||||
COOLDOWN_MAX_MS
|
||||
);
|
||||
account.cooldownUntil = Date.now() + backoff + Math.random() * 1000;
|
||||
}
|
||||
|
||||
@@ -193,7 +254,12 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
account.consecutiveFails = 0;
|
||||
}
|
||||
|
||||
buildUrl(_model: string, _stream: boolean, _urlIndex = 0, _credentials?: ProviderCredentials | null): string {
|
||||
buildUrl(
|
||||
_model: string,
|
||||
_stream: boolean,
|
||||
_urlIndex = 0,
|
||||
_credentials?: ProviderCredentials | null
|
||||
): string {
|
||||
return `${this.baseUrl.replace(/\/$/, "")}${CHAT_PATH}`;
|
||||
}
|
||||
|
||||
@@ -201,7 +267,7 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
_credentials: ProviderCredentials,
|
||||
stream = true,
|
||||
_clientHeaders?: Record<string, string> | null,
|
||||
_model?: string,
|
||||
_model?: string
|
||||
): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -212,9 +278,15 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
return headers;
|
||||
}
|
||||
|
||||
transformRequest(model: string, body: unknown, _stream: boolean, _credentials?: ProviderCredentials | null): unknown {
|
||||
transformRequest(
|
||||
model: string,
|
||||
body: unknown,
|
||||
_stream: boolean,
|
||||
_credentials?: ProviderCredentials | null
|
||||
): unknown {
|
||||
if (typeof body === "object" && body !== null) {
|
||||
return { ...(body as Record<string, unknown>), model: rewriteModelName(model) };
|
||||
const withModel = { ...(body as Record<string, unknown>), model: rewriteModelName(model) };
|
||||
return injectSystemMarker(withModel);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
@@ -222,15 +294,25 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
async testConnection(
|
||||
_credentials: ProviderCredentials,
|
||||
_signal?: AbortSignal | null,
|
||||
log?: ExecuteInput["log"],
|
||||
log?: ExecuteInput["log"]
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const account = this.accounts[0];
|
||||
const jwt = await this.getJwtForAccount(account, _signal);
|
||||
const resp = await fetch(this.buildUrl("mimo-auto", false), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}`, "X-Mimo-Source": MIMO_SOURCE },
|
||||
body: JSON.stringify({ model: "mimo-auto", messages: [{ role: "user", content: "ping" }], stream: false }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
"X-Mimo-Source": MIMO_SOURCE,
|
||||
},
|
||||
body: JSON.stringify(
|
||||
injectSystemMarker({
|
||||
model: "mimo-auto",
|
||||
messages: [{ role: "user", content: "ping" }],
|
||||
stream: false,
|
||||
})
|
||||
),
|
||||
signal: _signal ?? undefined,
|
||||
});
|
||||
return resp.status === 200;
|
||||
@@ -251,9 +333,14 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
|
||||
if (signal?.aborted) {
|
||||
return {
|
||||
response: new Response(encoder.encode(JSON.stringify({
|
||||
error: { message: "Request aborted", type: "abort", code: "ABORTED" },
|
||||
})), { status: 499, headers: { "Content-Type": "application/json" } }),
|
||||
response: new Response(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
error: { message: "Request aborted", type: "abort", code: "ABORTED" },
|
||||
})
|
||||
),
|
||||
{ status: 499, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url: this.buildUrl(model, stream),
|
||||
headers: this.buildHeaders(input.credentials, stream),
|
||||
transformedBody: body,
|
||||
@@ -282,45 +369,81 @@ export class MimocodeExecutor extends BaseExecutor {
|
||||
|
||||
// On auth failure, re-bootstrap this account and retry once
|
||||
if (resp.status === 401 || resp.status === 403) {
|
||||
log?.warn?.("MIMOCODE", `Auth failed (${resp.status}) on account ${account.fingerprint.slice(0, 8)}…`);
|
||||
log?.warn?.(
|
||||
"MIMOCODE",
|
||||
`Auth failed (${resp.status}) on account ${account.fingerprint.slice(0, 8)}…`
|
||||
);
|
||||
account.jwt = "";
|
||||
account.expiresAt = 0;
|
||||
account.consecutiveFails = 0;
|
||||
const freshJwt = await this.getJwtForAccount(account, signal);
|
||||
headers["Authorization"] = `Bearer ${freshJwt}`;
|
||||
resp = await fetch(url, { method: "POST", headers, body: JSON.stringify(reqBody), signal: signal ?? undefined });
|
||||
resp = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(reqBody),
|
||||
signal: signal ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (resp.status === 429) {
|
||||
this.markCooldown(account);
|
||||
log?.warn?.("MIMOCODE", `Rate limited on account ${account.fingerprint.slice(0, 8)}, trying next…`);
|
||||
log?.warn?.(
|
||||
"MIMOCODE",
|
||||
`Rate limited on account ${account.fingerprint.slice(0, 8)}, trying next…`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.markSuccess(account);
|
||||
const respHeaders: Record<string, string> = {};
|
||||
resp.headers.forEach((v, k) => { respHeaders[k] = v; });
|
||||
return { response: resp as unknown as Response, url, headers: respHeaders, transformedBody: reqBody };
|
||||
resp.headers.forEach((v, k) => {
|
||||
respHeaders[k] = v;
|
||||
});
|
||||
return {
|
||||
response: resp as unknown as Response,
|
||||
url,
|
||||
headers: respHeaders,
|
||||
transformedBody: reqBody,
|
||||
};
|
||||
} catch (err) {
|
||||
this.markCooldown(account);
|
||||
if (attempt === this.accounts.length - 1) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
log?.error?.("MIMOCODE", `Executor error: ${msg}`);
|
||||
return {
|
||||
response: new Response(encoder.encode(JSON.stringify({
|
||||
error: { message: msg, type: "upstream_error", code: "EXECUTOR_ERROR" },
|
||||
})), { status: 502, headers: { "Content-Type": "application/json" } }),
|
||||
url, headers: this.buildHeaders(input.credentials, stream), transformedBody: body,
|
||||
response: new Response(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
error: { message: msg, type: "upstream_error", code: "EXECUTOR_ERROR" },
|
||||
})
|
||||
),
|
||||
{ status: 502, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url,
|
||||
headers: this.buildHeaders(input.credentials, stream),
|
||||
transformedBody: body,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
response: new Response(encoder.encode(JSON.stringify({
|
||||
error: { message: "All accounts exhausted", type: "upstream_error", code: "NO_ACCOUNTS" },
|
||||
})), { status: 502, headers: { "Content-Type": "application/json" } }),
|
||||
url, headers: this.buildHeaders(input.credentials, stream), transformedBody: body,
|
||||
response: new Response(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "All accounts exhausted",
|
||||
type: "upstream_error",
|
||||
code: "NO_ACCOUNTS",
|
||||
},
|
||||
})
|
||||
),
|
||||
{ status: 502, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url,
|
||||
headers: this.buildHeaders(input.credentials, stream),
|
||||
transformedBody: body,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { MimocodeExecutor, generateFingerprint } from "../../open-sse/executors/mimocode.ts";
|
||||
import {
|
||||
MimocodeExecutor,
|
||||
generateFingerprint,
|
||||
MIMO_SYSTEM_MARKER,
|
||||
} from "../../open-sse/executors/mimocode.ts";
|
||||
|
||||
const executor = new MimocodeExecutor();
|
||||
|
||||
@@ -48,7 +52,7 @@ describe("MimocodeExecutor", () => {
|
||||
const result = (executor as any).transformRequest(
|
||||
"mcode/mimo-auto",
|
||||
{ model: "mcode/mimo-auto", messages: [{ role: "user", content: "hi" }] },
|
||||
false,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(result.model, "mimo-auto");
|
||||
});
|
||||
@@ -57,11 +61,91 @@ describe("MimocodeExecutor", () => {
|
||||
const result = (executor as any).transformRequest(
|
||||
"mimo-auto",
|
||||
{ model: "mimo-auto", messages: [{ role: "user", content: "hi" }] },
|
||||
false,
|
||||
false
|
||||
);
|
||||
assert.strictEqual(result.model, "mimo-auto");
|
||||
});
|
||||
|
||||
// The Xiaomi free endpoint rejects requests with `403 "Illegal access"` unless the
|
||||
// body contains a recognized MiMoCode prompt signature inside a `system`-role message.
|
||||
// The executor must inject that marker so user requests pass the upstream anti-abuse gate.
|
||||
it("transformRequest injects a MiMoCode system marker when none is present", () => {
|
||||
const result = (executor as any).transformRequest(
|
||||
"mcode/mimo-auto",
|
||||
{ model: "mcode/mimo-auto", messages: [{ role: "user", content: "write a haiku" }] },
|
||||
true
|
||||
);
|
||||
assert.ok(Array.isArray(result.messages));
|
||||
const first = result.messages[0];
|
||||
assert.strictEqual(first.role, "system");
|
||||
assert.ok(
|
||||
typeof first.content === "string" && first.content.includes(MIMO_SYSTEM_MARKER),
|
||||
"first message must be a system message containing the MiMoCode marker"
|
||||
);
|
||||
});
|
||||
|
||||
it("transformRequest preserves the original user message after injection", () => {
|
||||
const result = (executor as any).transformRequest(
|
||||
"mcode/mimo-auto",
|
||||
{ model: "mcode/mimo-auto", messages: [{ role: "user", content: "write a haiku" }] },
|
||||
true
|
||||
);
|
||||
const userMsg = result.messages.find((m: any) => m.role === "user");
|
||||
assert.ok(userMsg);
|
||||
assert.strictEqual(userMsg.content, "write a haiku");
|
||||
});
|
||||
|
||||
it("transformRequest preserves a caller-provided system prompt alongside the marker", () => {
|
||||
const result = (executor as any).transformRequest(
|
||||
"mcode/mimo-auto",
|
||||
{
|
||||
model: "mcode/mimo-auto",
|
||||
messages: [
|
||||
{ role: "system", content: "You are a pirate." },
|
||||
{ role: "user", content: "hi" },
|
||||
],
|
||||
},
|
||||
true
|
||||
);
|
||||
const systemContents = result.messages
|
||||
.filter((m: any) => m.role === "system")
|
||||
.map((m: any) => m.content)
|
||||
.join("\n");
|
||||
assert.ok(systemContents.includes(MIMO_SYSTEM_MARKER), "marker present");
|
||||
assert.ok(systemContents.includes("You are a pirate."), "caller system prompt preserved");
|
||||
});
|
||||
|
||||
it("transformRequest does not duplicate the marker when already present", () => {
|
||||
const result = (executor as any).transformRequest(
|
||||
"mcode/mimo-auto",
|
||||
{
|
||||
model: "mcode/mimo-auto",
|
||||
messages: [
|
||||
{ role: "system", content: `${MIMO_SYSTEM_MARKER}\nExtra context.` },
|
||||
{ role: "user", content: "hi" },
|
||||
],
|
||||
},
|
||||
true
|
||||
);
|
||||
const count = result.messages.filter(
|
||||
(m: any) =>
|
||||
m.role === "system" &&
|
||||
typeof m.content === "string" &&
|
||||
m.content.includes(MIMO_SYSTEM_MARKER)
|
||||
).length;
|
||||
assert.strictEqual(count, 1, "marker should not be duplicated");
|
||||
});
|
||||
|
||||
it("transformRequest leaves a body without a messages array untouched", () => {
|
||||
const result = (executor as any).transformRequest(
|
||||
"mcode/mimo-auto",
|
||||
{ model: "mcode/mimo-auto", prompt: "legacy" },
|
||||
true
|
||||
);
|
||||
assert.strictEqual((result as any).messages, undefined);
|
||||
assert.strictEqual((result as any).model, "mimo-auto");
|
||||
});
|
||||
|
||||
it("returns 499 on pre-aborted signal", async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort(new Error("cancelled"));
|
||||
|
||||
Reference in New Issue
Block a user