feat: add Xiaomi MiMo Free API provider (mimo-auto)

- Add mimo-auth.ts: JWT bootstrap with device fingerprint, cached auto-refresh, 401/403 invalidation
- Add mimo-auto entry to MODEL_ROUTES with anti-abuse system message injection
- Inject JWT auth + x-session-affinity header before Mimo API fetch
- Retry once on 401/403 (invalidate JWT, re-bootstrap)
- Extend SSE passthrough for mimo-free provider
- Strip 'data:' prefix from Mimo non-streaming responses

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-17 13:22:07 +07:00
co-authored by Claude
parent 89f0ff0f32
commit e4406b586e
2 changed files with 205 additions and 3 deletions
+82 -3
View File
@@ -14,6 +14,7 @@
import type { ProxyPool, SessionProxyPool } from "./proxy-pool";
import { fetchWithRetry, fetchWithSessionRetry, SSELineBuffer, isDevMode, type FetchWithRetryResult } from "./fetch-utils";
import { getJwt, invalidateJwt } from "./mimo-auth";
// --- Types -------------------------------------------------------------------
@@ -179,6 +180,50 @@ export const MODEL_ROUTES: Record<string, BackendConfig> = {
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
}),
},
// -- Xiaomi MiMo Free (OpenAI-compatible, JWT bootstrap auth) -----------------
"mimo-auto": {
provider: "mimo-free",
url: "https://api.xiaomimimo.com/api/free-ai/openai/chat",
headers: {
"Content-Type": "application/json",
"X-Mimo-Source": "mimocode-cli-free",
Accept: "text/event-stream",
},
adaptRequest: (req) => {
// Inject the anti-abuse system-message marker if not present.
// Without it the Mimo API returns 403 Illegal access.
const messages = [...req.messages];
const hasMarker = messages.some(
(m) =>
m.role === "system" &&
m.content.includes("You are MiMoCode"),
);
if (!hasMarker) {
messages.unshift({
role: "system",
content:
"You are MiMoCode, an interactive CLI tool that helps users with software engineering tasks.",
});
}
return { model: req.model, messages, stream: req.stream };
},
adaptResponse: (raw: any) => ({
id: raw.id ?? `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: raw.created ?? Math.floor(Date.now() / 1000),
model: "mimo-auto",
choices: (raw.choices ?? []).map((c: any) => ({
index: c.index ?? 0,
message: {
role: c.message?.role ?? "assistant",
content: c.message?.content ?? "",
},
finish_reason: c.finish_reason ?? "stop",
})),
usage: raw.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
}),
},
};
// --- Helpers ------------------------------------------------------------------
@@ -233,9 +278,15 @@ function parseJSONResponse(
config: BackendConfig,
req: OpenAIRequest,
): unknown {
// Mimo Free always wraps with "data:" prefix (SSE-style), strip it.
let cleaned = text;
if (config.provider === "mimo-free" && text.startsWith("data:")) {
cleaned = text.slice(5).trim();
}
if (config.adaptResponse) {
try {
const raw = JSON.parse(text);
const raw = JSON.parse(cleaned);
return config.adaptResponse(raw, req);
} catch {
// fall through
@@ -358,12 +409,40 @@ export async function handleChatCompletion(
const wantsStream = req.stream === true;
const { url, init } = buildBackendRequest(req, config);
// -- Mimo Free: inject JWT authentication and session affinity --------------
if (config.provider === "mimo-free") {
const jwt = await getJwt();
init.headers = {
...init.headers,
Authorization: `Bearer ${jwt}`,
"x-session-affinity": `ses_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`,
};
}
// -- Execute with session-aware or standard retry --------------------------
const result: FetchWithRetryResult =
let result: FetchWithRetryResult =
sessionPool && sessionId
? await fetchWithSessionRetry(url, init, sessionPool, sessionId, `openai:${req.model}`)
: await fetchWithRetry(url, init, proxyPool, `openai:${req.model}`);
// -- Mimo Free: auth failure → invalidate JWT and retry once ---------------
if (
config.provider === "mimo-free" &&
result.response &&
(result.response.status === 401 || result.response.status === 403)
) {
invalidateJwt();
const jwt = await getJwt();
init.headers = {
...init.headers,
Authorization: `Bearer ${jwt}`,
};
result =
sessionPool && sessionId
? await fetchWithSessionRetry(url, init, sessionPool, sessionId, `openai:${req.model}`)
: await fetchWithRetry(url, init, proxyPool, `openai:${req.model}`);
}
if (result.errorClassification) {
return new Response(
JSON.stringify({
@@ -396,7 +475,7 @@ export async function handleChatCompletion(
const contentType = response.headers.get("content-type") ?? "";
const isNativeStream = contentType.includes("text/event-stream");
if (isNativeStream && config.provider === "opencode") {
if (isNativeStream && (config.provider === "opencode" || config.provider === "mimo-free")) {
// Passthrough for OpenAI-compatible SSE
const headers: Record<string, string> = {
"Content-Type": "text/event-stream",
+123
View File
@@ -0,0 +1,123 @@
/**
* Mimo Free API — JWT bootstrap & auth cache.
*
* Mimo Free does not use a static API key. Instead, authentication is done
* via a JWT obtained by sending a device fingerprint to the bootstrap endpoint.
* The JWT is cached in-memory and auto-refreshed before expiry.
*
* Flow:
* 1. generateDeviceFingerprint() → sha256(hostname|platform|arch|cpu|username)
* 2. bootstrapJwt() → POST /api/free-ai/bootstrap with {client: fingerprint}
* 3. getJwt() → return cached JWT, auto-refresh if near expiry (5 min buffer)
* 4. invalidateJwt() → clear cache (called on 401/403)
*/
import * as os from "node:os";
// --- Module-level cache ------------------------------------------------------
let cachedJwt: string | null = null;
let jwtExpiry = 0; // epoch ms
const MIMO_BOOTSTRAP_URL = "https://api.xiaomimimo.com/api/free-ai/bootstrap";
const EXPIRY_BUFFER_MS = 300_000; // 5 minutes
// --- Device fingerprint ------------------------------------------------------
/**
* Generate a SHA-256 device fingerprint from OS-level attributes.
*
* Format: `sha256(hostname|platform|arch|cpu|username)`
* This matches the 9Router reference implementation.
*/
function generateDeviceFingerprint(): string {
const hostname = os.hostname();
const platform = process.platform;
const arch = process.arch;
const cpus = os.cpus();
const cpuModel = cpus.length > 0 ? (cpus[0]?.model ?? "unknown") : "unknown";
const username = process.env.USER ?? process.env.USERNAME ?? "unknown";
const raw = `${hostname}|${platform}|${arch}|${cpuModel}|${username}`;
const hasher = new Bun.CryptoHasher("sha256");
hasher.update(raw);
return hasher.digest("hex") as string;
}
// --- JWT bootstrap -----------------------------------------------------------
/**
* Request a fresh JWT from the Mimo bootstrap endpoint.
*
* Sends the device fingerprint and stores the returned JWT along with its
* expiry time (parsed from the `exp` claim in the JWT payload).
*
* @throws If the bootstrap request fails or returns an unexpected response.
*/
async function bootstrapJwt(): Promise<string> {
const fingerprint = generateDeviceFingerprint();
const resp = await fetch(MIMO_BOOTSTRAP_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ client: fingerprint }),
});
if (!resp.ok) {
throw new Error(`Mimo bootstrap failed: ${resp.status} ${resp.statusText}`);
}
const data = (await resp.json()) as { jwt: string };
if (!data.jwt || typeof data.jwt !== "string") {
throw new Error("Mimo bootstrap response missing jwt field");
}
cachedJwt = data.jwt;
// Decode the JWT payload (second dot-separated segment) to extract `exp`.
try {
const payloadBase64 = data.jwt.split(".")[1]!;
const payloadJson = Buffer.from(payloadBase64, "base64url").toString("utf-8");
const payload = JSON.parse(payloadJson) as { exp?: number };
if (payload.exp && typeof payload.exp === "number") {
jwtExpiry = payload.exp * 1000; // JWT exp is in seconds
} else {
// No exp claim — set a conservative 10-minute TTL
jwtExpiry = Date.now() + 600_000;
}
} catch {
// Payload decode failed — set a conservative 10-minute TTL
jwtExpiry = Date.now() + 600_000;
}
return cachedJwt;
}
// --- Public API --------------------------------------------------------------
/**
* Get a valid JWT for Mimo API requests.
*
* Returns the cached JWT if it is still valid (expiry > now + 5 min buffer).
* Otherwise bootstraps a fresh JWT.
*
* @throws If bootstrap fails.
*/
export async function getJwt(): Promise<string> {
const now = Date.now();
if (cachedJwt && jwtExpiry > now + EXPIRY_BUFFER_MS) {
return cachedJwt;
}
return bootstrapJwt();
}
/**
* Invalidate the cached JWT.
*
* Call this after receiving a 401 or 403 from the Mimo API so the next
* request bootstraps a fresh token.
*/
export function invalidateJwt(): void {
cachedJwt = null;
jwtExpiry = 0;
}