diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 278593bac2..d410681e73 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -343,6 +343,9 @@ const CHAT_OPENAI_COMPAT_MODELS: Record = { { id: "mimo-v2-omni", name: "MiMo-V2-Omni", contextLength: 262144, maxOutputTokens: 131072 }, { id: "mimo-v2-flash", name: "MiMo-V2-Flash", contextLength: 262144, maxOutputTokens: 65536 }, ], + mimocode: [ + { id: "mimo-auto", name: "MiMo Auto", contextLength: 1000000, maxOutputTokens: 128000 }, + ], gitlawb: [ { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", contextLength: 1048576, maxOutputTokens: 131072 }, { id: "mimo-v2.5", name: "MiMo-V2.5", contextLength: 1048576, maxOutputTokens: 131072 }, @@ -4067,6 +4070,17 @@ const _REGISTRY_EAGER: Record = { models: CHAT_OPENAI_COMPAT_MODELS["xiaomi-mimo"], }, + mimocode: { + id: "mimocode", + alias: "mcode", + format: "openai", + executor: "mimocode", + baseUrl: "https://api.xiaomimimo.com", + chatPath: "/api/free-ai/openai/chat", + authType: "none", + models: CHAT_OPENAI_COMPAT_MODELS["mimocode"], + }, + gitlawb: { id: "gitlawb", alias: "glb", diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 2379eb6e22..62f833f6ab 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -51,6 +51,7 @@ import { KimiExecutor } from "./kimi.ts" import { TheOldLlmExecutor } from "./theoldllm.ts"; import { ChipotleExecutor } from "./chipotle.ts"; import { LMArenaExecutor } from "./lmarena.ts"; +import { MimocodeExecutor } from "./mimocode.ts"; const executors = { antigravity: new AntigravityExecutor(), @@ -146,6 +147,8 @@ const executors = { pepper: new ChipotleExecutor(), // Alias lmarena: new LMArenaExecutor(), lma: new LMArenaExecutor(), // Alias + mimocode: new MimocodeExecutor(), + mcode: new MimocodeExecutor(), // Alias }; const defaultCache = new Map(); @@ -205,3 +208,4 @@ export { QwenWebExecutor } from "./qwen-web.ts"; export { TheOldLlmExecutor } from "./theoldllm.ts"; export { ChipotleExecutor } from "./chipotle.ts"; export { LMArenaExecutor } from "./lmarena.ts"; +export { MimocodeExecutor } from "./mimocode.ts"; diff --git a/open-sse/executors/mimocode.ts b/open-sse/executors/mimocode.ts new file mode 100644 index 0000000000..d75d24174f --- /dev/null +++ b/open-sse/executors/mimocode.ts @@ -0,0 +1,328 @@ +/** + * MiMoCode Executor — Free-tier Xiaomi MiMo models via bootstrap JWT auth. + * + * Implements the auth flow from the official MiMo-Code repository: + * https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/plugin/mimo-free.ts + * + * 1. Generate device fingerprint from hostname + OS + arch + CPU + username + * 2. POST /api/free-ai/bootstrap with fingerprint → JWT + * 3. Use JWT as Bearer token for chat requests + * 4. Custom endpoint: /api/free-ai/openai/chat (not /v1/chat/completions) + * 5. Custom header: X-Mimo-Source: mimocode-cli-free + * + * Only the "mimo-auto" model is supported (1M context, 128K output). + * Supports multiple accounts: N fingerprints → N JWTs → round-robin with cooldown. + * On 429, account enters cooldown (exponential backoff). On 401/403, JWT is re-bootstrapped. + */ + +import * as crypto from "node:crypto"; +import * as os from "node:os"; +import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts"; + +const BOOTSTRAP_PATH = "/api/free-ai/bootstrap"; +const CHAT_PATH = "/api/free-ai/openai/chat"; +const JWT_REFRESH_BUFFER_MS = 5 * 60 * 1000; +const BOOTSTRAP_TIMEOUT_MS = 15_000; +const COOLDOWN_BASE_MS = 5_000; +const COOLDOWN_MAX_MS = 60_000; + +const MIMO_SOURCE = "mimocode-cli-free"; + +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", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", +]; + +// ── Account State ────────────────────────────────────────────────────────── + +interface AccountState { + fingerprint: string; + jwt: string; + expiresAt: number; + cooldownUntil: number; + consecutiveFails: number; +} + +function parseJwtExp(jwt: string): number { + try { + const parts = jwt.split("."); + if (parts.length < 2) return Date.now() + 50 * 60 * 1000; + const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString()); + return (payload.exp ?? Math.floor(Date.now() / 1000) + 3000) * 1000; + } catch { + return Date.now() + 50 * 60 * 1000; + } +} + +function isAccountReady(account: AccountState): boolean { + if (account.cooldownUntil > Date.now()) return false; + if (account.jwt && account.expiresAt - Date.now() > JWT_REFRESH_BUFFER_MS) return true; + return false; +} + +// ── Fingerprint Generation ───────────────────────────────────────────────── + +function getCpuModel(): string { + try { + const cpus = os.cpus(); + if (cpus.length > 0 && cpus[0].model) return cpus[0].model.trim(); + } catch { /* ignore */ } + return "unknown-cpu"; +} + +export function generateFingerprint(seed?: string): string { + if (seed) return crypto.createHash("sha256").update(seed).digest("hex"); + const hostname = os.hostname(); + const platform = os.platform(); + const arch = os.arch(); + const cpu = getCpuModel(); + let username = "unknown-user"; + try { + username = os.userInfo().username; + } catch { /* ignore */ } + return crypto.createHash("sha256").update(`${hostname}|${platform}|${arch}|${cpu}|${username}`).digest("hex"); +} + +// ── Bootstrap ────────────────────────────────────────────────────────────── + +const bootstrapInflight = new Map>(); + +async function bootstrapJwt( + baseUrl: string, + fingerprint: string, + signal?: AbortSignal | null, +): Promise<{ jwt: string; expiresAt: number }> { + const existing = bootstrapInflight.get(fingerprint); + if (existing) return existing; + + const url = `${baseUrl}${BOOTSTRAP_PATH}`; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), BOOTSTRAP_TIMEOUT_MS); + const onSignal = signal ? () => controller.abort(signal.reason) : null; + if (signal && onSignal) signal.addEventListener("abort", onSignal, { once: true }); + + const promise = (async () => { + try { + const resp = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ client: fingerprint }), + signal: controller.signal, + }); + if (!resp.ok) { + const body = await resp.text().catch(() => ""); + throw new Error(`Bootstrap failed: ${resp.status} ${body.slice(0, 200)}`); + } + const data = (await resp.json()) as { jwt?: string }; + if (!data.jwt) throw new Error("Bootstrap response missing jwt field"); + return { jwt: data.jwt, expiresAt: parseJwtExp(data.jwt) }; + } finally { + clearTimeout(timer); + if (signal && onSignal) signal.removeEventListener("abort", onSignal); + bootstrapInflight.delete(fingerprint); + } + })(); + + bootstrapInflight.set(fingerprint, promise); + return promise; +} + +// ── Model Rewriting ──────────────────────────────────────────────────────── + +function rewriteModelName(model: string): string { + const idx = model.lastIndexOf("/"); + return idx >= 0 ? model.slice(idx + 1) : model; +} + +// ── Executor ─────────────────────────────────────────────────────────────── + +export class MimocodeExecutor extends BaseExecutor { + private accounts: AccountState[] = []; + private nextAccountIdx = 0; + private baseUrl: string; + private static encoder = new TextEncoder(); + + 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 }); + } + + private syncAccountsFromCredentials(credentials: ProviderCredentials): void { + const fingerprints = credentials?.providerSpecificData?.fingerprints; + if (!Array.isArray(fingerprints)) return; + 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 }); + existing.add(fp); + } + } + } + + private async getJwtForAccount(account: AccountState, signal?: AbortSignal | null): Promise { + if (isAccountReady(account)) return account.jwt; + const result = await bootstrapJwt(this.baseUrl, account.fingerprint, signal); + account.jwt = result.jwt; + account.expiresAt = result.expiresAt; + return account.jwt; + } + + private pickAccount(): AccountState { + for (let i = 0; i < this.accounts.length; i++) { + const idx = (this.nextAccountIdx + i) % this.accounts.length; + const acct = this.accounts[idx]; + if (isAccountReady(acct)) { + this.nextAccountIdx = (idx + 1) % this.accounts.length; + return acct; + } + } + const fallbackIdx = this.nextAccountIdx % this.accounts.length; + this.nextAccountIdx = (this.nextAccountIdx + 1) % this.accounts.length; + return this.accounts[fallbackIdx]; + } + + private markCooldown(account: AccountState): void { + account.consecutiveFails++; + const backoff = Math.min(COOLDOWN_BASE_MS * Math.pow(2, account.consecutiveFails - 1), COOLDOWN_MAX_MS); + account.cooldownUntil = Date.now() + backoff + Math.random() * 1000; + } + + private markSuccess(account: AccountState): void { + account.consecutiveFails = 0; + } + + buildUrl(_model: string, _stream: boolean, _urlIndex = 0, _credentials?: ProviderCredentials | null): string { + return `${this.baseUrl.replace(/\/$/, "")}${CHAT_PATH}`; + } + + buildHeaders( + _credentials: ProviderCredentials, + stream = true, + _clientHeaders?: Record | null, + _model?: string, + ): Record { + const headers: Record = { + "Content-Type": "application/json", + "X-Mimo-Source": MIMO_SOURCE, + "User-Agent": USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)], + }; + if (stream) headers["Accept"] = "text/event-stream, application/json"; + return headers; + } + + transformRequest(model: string, body: unknown, _stream: boolean, _credentials?: ProviderCredentials | null): unknown { + if (typeof body === "object" && body !== null) { + return { ...(body as Record), model: rewriteModelName(model) }; + } + return body; + } + + async testConnection( + _credentials: ProviderCredentials, + _signal?: AbortSignal | null, + log?: ExecuteInput["log"], + ): Promise { + 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 }), + signal: _signal ?? undefined, + }); + return resp.status === 200; + } catch { + log?.warn?.("MIMOCODE", "testConnection network error"); + return false; + } + } + + async execute(input: ExecuteInput): Promise<{ + response: Response; + url: string; + headers: Record; + transformedBody: unknown; + }> { + const { model, stream, body, signal, log } = input; + const encoder = MimocodeExecutor.encoder; + + 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" } }), + url: this.buildUrl(model, stream), + headers: this.buildHeaders(input.credentials, stream), + transformedBody: body, + }; + } + + const url = this.buildUrl(model, stream); + const reqBody = this.transformRequest(model, body, stream, input.credentials); + + this.syncAccountsFromCredentials(input.credentials); + + // Try each account, skip cooldown ones + for (let attempt = 0; attempt < this.accounts.length; attempt++) { + const account = this.pickAccount(); + try { + const jwt = await this.getJwtForAccount(account, signal); + const headers = this.buildHeaders(input.credentials, stream); + headers["Authorization"] = `Bearer ${jwt}`; + + let resp = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(reqBody), + signal: signal ?? undefined, + }); + + // 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)}…`); + 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 }); + } + + if (resp.status === 429) { + this.markCooldown(account); + log?.warn?.("MIMOCODE", `Rate limited on account ${account.fingerprint.slice(0, 8)}, trying next…`); + continue; + } + + this.markSuccess(account); + const respHeaders: Record = {}; + 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, + }; + } + } + } + + 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, + }; + } +} + +export default MimocodeExecutor; diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx index 5745520805..cb06cc977e 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx @@ -33,6 +33,7 @@ import { Select, ProxyConfigModal, NoAuthProviderCard, + NoAuthAccountCard, } from "@/shared/components"; import { LOCAL_PROVIDERS, @@ -4133,7 +4134,24 @@ export default function ProviderDetailPageClient() { )} {/* Connections */} - {!isUpstreamProxyProvider && isFreeNoAuth && } + {!isUpstreamProxyProvider && isFreeNoAuth && providerId === "mimocode" && ( + crypto.randomUUID().replace(/-/g, "")} + /> + )} + {!isUpstreamProxyProvider && isFreeNoAuth && providerId === "opencode" && ( + crypto.randomUUID().replace(/-/g, "")} + /> + )} + {!isUpstreamProxyProvider && + isFreeNoAuth && + providerId !== "mimocode" && + providerId !== "opencode" && } {!isUpstreamProxyProvider && !isFreeNoAuth && (
diff --git a/src/shared/components/NoAuthAccountCard.tsx b/src/shared/components/NoAuthAccountCard.tsx new file mode 100644 index 0000000000..9f03a5f80b --- /dev/null +++ b/src/shared/components/NoAuthAccountCard.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Card from "./Card"; +import Button from "./Button"; + +interface NoAuthAccountCardProps { + providerId: string; + /** Display name for the provider (e.g. "MiMoCode", "OpenCode") */ + providerName: string; + /** Generates a unique account identifier (fingerprint, session token, etc.) */ + generateAccountId: () => string; + /** Key in providerSpecificData where account IDs are stored (default: "fingerprints") */ + dataKey?: string; + /** Custom description text */ + description?: string; + /** Custom "add" button label */ + addLabel?: string; +} + +interface Connection { + id: string; + provider: string; + apiKey?: string; + providerSpecificData?: Record; + isActive?: boolean; +} + +export default function NoAuthAccountCard({ + providerId, + providerName, + generateAccountId, + dataKey = "fingerprints", + description = "Ready to use — no signup needed. Add accounts for rate-limit rotation.", + addLabel = "Add Account", +}: NoAuthAccountCardProps) { + const [connections, setConnections] = useState([]); + const [loading, setLoading] = useState(true); + const [adding, setAdding] = useState(false); + + const fetchConnections = useCallback(async () => { + try { + const res = await fetch("/api/providers"); + if (res.ok) { + const data = await res.json(); + const filtered = (data.connections || []).filter( + (c: Connection) => c.provider === providerId + ); + setConnections(filtered); + } + } catch (err) { + console.error("Failed to fetch connections:", err); + } finally { + setLoading(false); + } + }, [providerId]); + + useEffect(() => { + void fetchConnections(); + }, [fetchConnections]); + + const allAccountIds = connections.flatMap( + (c) => c.providerSpecificData?.[dataKey] || [] + ); + + const handleAddAccount = async () => { + setAdding(true); + try { + const accountId = generateAccountId(); + if (connections.length === 0) { + const res = await fetch("/api/providers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + provider: providerId, + name: `${providerName} Account 1`, + providerSpecificData: { [dataKey]: [accountId] }, + }), + }); + if (!res.ok) throw new Error("Failed to create connection"); + } else { + const conn = connections[0]; + const updated = [...allAccountIds, accountId]; + const res = await fetch(`/api/providers/${conn.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + providerSpecificData: { [dataKey]: updated }, + }), + }); + if (!res.ok) throw new Error("Failed to update connection"); + } + await fetchConnections(); + } catch (err) { + console.error("Failed to add account:", err); + } finally { + setAdding(false); + } + }; + + const handleRemoveAccount = async (accountId: string) => { + if (connections.length === 0) return; + const conn = connections[0]; + const updated = allAccountIds.filter((id) => id !== accountId); + try { + const res = await fetch(`/api/providers/${conn.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + providerSpecificData: { [dataKey]: updated }, + }), + }); + if (res.ok) await fetchConnections(); + } catch (err) { + console.error("Failed to remove account:", err); + } + }; + + return ( + +
+
+ lock_open +
+
+

No authentication required

+

{description}

+
+
+ +
+
+ + Accounts ({loading ? "..." : allAccountIds.length}) + + +
+ + {!loading && allAccountIds.length === 0 && ( +

+ Using auto-generated account. Click "{addLabel}" for rate-limit rotation. +

+ )} + + {!loading && allAccountIds.length > 0 && ( +
+ {allAccountIds.map((id, i) => ( +
+ + Account {i + 1}: {id.slice(0, 12)}... + + +
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/shared/components/index.tsx b/src/shared/components/index.tsx index 2c6b5e822f..9d47f02fdd 100644 --- a/src/shared/components/index.tsx +++ b/src/shared/components/index.tsx @@ -35,6 +35,7 @@ export { default as FilterBar } from "./FilterBar"; export { default as ColumnToggle } from "./ColumnToggle"; export { default as DataTable } from "./DataTable"; export { default as NoAuthProviderCard } from "./NoAuthProviderCard"; +export { default as NoAuthAccountCard } from "./NoAuthAccountCard"; export { default as CollapsibleSection } from "./CollapsibleSection"; export { default as InfoTooltip } from "./InfoTooltip"; export { default as PresetSlider } from "./PresetSlider"; diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index ec4bedc8fa..38e8d672c8 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -106,9 +106,28 @@ export const NOAUTH_PROVIDERS = { freeNote: "Free video generation — VEO 3.1, Seedance. 6 requests/hour.", authHint: "No auth required. Rate limited to 6 requests/hour per IP.", }, + mimocode: { + id: "mimocode", + alias: "mcode", + name: "MiMoCode (Free)", + icon: "devices", + color: "#FF6B35", + textIcon: "MC", + website: "https://mimo.mi.com", + noAuth: true, + hasFree: true, + serviceKinds: ["llm"], + freeNote: + "Free — Xiaomi MiMo models via bootstrap JWT auth. No API key required. Supports streaming.", + authHint: + "No API key required. The executor auto-generates JWT tokens via device fingerprint bootstrap.", + notice: { + text: "MiMoCode uses Xiaomi's public free AI endpoint with bootstrap-based JWT authentication. No signup needed. Rate limits apply.", + }, + }, }; -export const FREE_APIKEY_PROVIDER_IDS = new Set(["qoder"]); +export const FREE_APIKEY_PROVIDER_IDS = new Set(["qoder", "mimocode", "opencode"]); export function supportsApiKeyOnFreeProvider(providerId: unknown): boolean { return typeof providerId === "string" && FREE_APIKEY_PROVIDER_IDS.has(providerId); @@ -2853,6 +2872,8 @@ export function providerAllowsOptionalApiKey(providerId: unknown): boolean { providerId === "huggingchat" || providerId === "gitlawb" || providerId === "gitlawb-gmi" || + providerId === "mimocode" || + providerId === "opencode" || isLocalProvider(providerId) || isSelfHostedChatProvider(providerId) || isOpenAICompatibleProvider(providerId) || diff --git a/tests/unit/mimocode-executor.test.ts b/tests/unit/mimocode-executor.test.ts new file mode 100644 index 0000000000..ed89ca81ca --- /dev/null +++ b/tests/unit/mimocode-executor.test.ts @@ -0,0 +1,169 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { MimocodeExecutor, generateFingerprint } from "../../open-sse/executors/mimocode.ts"; + +const executor = new MimocodeExecutor(); + +describe("MimocodeExecutor", () => { + it("generateFingerprint returns a 64-char hex string", () => { + const fp = generateFingerprint(); + assert.match(fp, /^[0-9a-f]{64}$/); + }); + + it("generateFingerprint is deterministic", () => { + assert.strictEqual(generateFingerprint(), generateFingerprint()); + }); + + it("generateFingerprint with seed is deterministic", () => { + assert.strictEqual(generateFingerprint("seed-a"), generateFingerprint("seed-a")); + }); + + it("generateFingerprint with different seeds differs", () => { + assert.notStrictEqual(generateFingerprint("seed-a"), generateFingerprint("seed-b")); + }); + + it("buildUrl returns the free-ai chat endpoint", () => { + const url = executor.buildUrl("mimo-auto", false); + assert.ok(url.includes("/api/free-ai/openai/chat")); + assert.ok(url.startsWith("https://")); + }); + + it("buildHeaders includes X-Mimo-Source and Content-Type", () => { + const headers = (executor as any).buildHeaders({}, true); + assert.strictEqual(headers["Content-Type"], "application/json"); + assert.strictEqual(headers["X-Mimo-Source"], "mimocode-cli-free"); + }); + + it("buildHeaders includes Accept for streaming", () => { + const headers = (executor as any).buildHeaders({}, true); + assert.ok(headers["Accept"]?.includes("text/event-stream")); + }); + + it("buildHeaders omits Accept for non-streaming", () => { + const headers = (executor as any).buildHeaders({}, false); + assert.ok(!headers["Accept"]?.includes("text/event-stream")); + }); + + it("transformRequest strips model prefix", () => { + const result = (executor as any).transformRequest( + "mcode/mimo-auto", + { model: "mcode/mimo-auto", messages: [{ role: "user", content: "hi" }] }, + false, + ); + assert.strictEqual(result.model, "mimo-auto"); + }); + + it("transformRequest passes model through when no prefix", () => { + const result = (executor as any).transformRequest( + "mimo-auto", + { model: "mimo-auto", messages: [{ role: "user", content: "hi" }] }, + false, + ); + assert.strictEqual(result.model, "mimo-auto"); + }); + + it("returns 499 on pre-aborted signal", async () => { + const controller = new AbortController(); + controller.abort(new Error("cancelled")); + + const result = await executor.execute({ + model: "mimo-auto", + body: { messages: [{ role: "user", content: "hi" }], stream: false }, + stream: false, + signal: controller.signal, + credentials: {}, + log: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }, + }); + + assert.strictEqual((result as any).response.status, 499); + }); + + it("is registered in executor index", async () => { + const { getExecutor } = await import("../../open-sse/executors/index.ts"); + const exec = getExecutor("mimocode"); + assert.ok(exec instanceof MimocodeExecutor); + }); + + it("mcode alias works", async () => { + const { getExecutor } = await import("../../open-sse/executors/index.ts"); + const exec = getExecutor("mcode"); + assert.ok(exec instanceof MimocodeExecutor); + }); +}); + +describe("mimocode multi-account", () => { + it("executor has at least one account", () => { + const accounts = (executor as any).accounts; + assert.ok(Array.isArray(accounts)); + assert.ok(accounts.length >= 1); + }); + + it("each account has required fields", () => { + const accounts = (executor as any).accounts; + for (const acct of accounts) { + assert.ok(typeof acct.fingerprint === "string"); + assert.ok(typeof acct.jwt === "string"); + assert.ok(typeof acct.expiresAt === "number"); + assert.ok(typeof acct.cooldownUntil === "number"); + assert.ok(typeof acct.consecutiveFails === "number"); + } + }); + + it("pickAccount returns an account", () => { + const acct = (executor as any).pickAccount(); + assert.ok(acct); + assert.ok(typeof acct.fingerprint === "string"); + }); + + it("markCooldown increases consecutiveFails and sets cooldownUntil", () => { + const acct = (executor as any).accounts[0]; + const before = acct.consecutiveFails; + (executor as any).markCooldown(acct); + assert.strictEqual(acct.consecutiveFails, before + 1); + assert.ok(acct.cooldownUntil > Date.now()); + }); + + it("markSuccess resets consecutiveFails", () => { + const acct = (executor as any).accounts[0]; + acct.consecutiveFails = 5; + (executor as any).markSuccess(acct); + assert.strictEqual(acct.consecutiveFails, 0); + }); +}); + +describe("mimocode provider registration", () => { + it("provider is registered in NOAUTH_PROVIDERS", async () => { + const { NOAUTH_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); + const provider = (NOAUTH_PROVIDERS as Record)["mimocode"]; + assert.ok(provider); + assert.strictEqual(provider.id, "mimocode"); + assert.strictEqual(provider.alias, "mcode"); + assert.strictEqual(provider.noAuth, true); + assert.strictEqual(provider.hasFree, true); + }); + + it("provider has correct service kinds", async () => { + const { NOAUTH_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); + const provider = (NOAUTH_PROVIDERS as Record)["mimocode"]; + assert.ok(provider.serviceKinds?.includes("llm")); + }); +}); + +describe("mimocode providerRegistry entry", () => { + it("registry entry exists with correct executor", async () => { + const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); + const entry = getRegistryEntry("mimocode"); + assert.ok(entry); + assert.strictEqual(entry.executor, "mimocode"); + assert.strictEqual(entry.format, "openai"); + assert.strictEqual(entry.authType, "none"); + }); + + it("registry entry has mimo-auto model", async () => { + const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); + const entry = getRegistryEntry("mimocode"); + const models = entry.models as Array<{ id: string }>; + const mimoAuto = models.find((m) => m.id === "mimo-auto"); + assert.ok(mimoAuto); + }); +});