mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(mimocode): per-account proxy support for multi-account round-robin (#3837)
Integrated into release/v3.8.25 — feat(mimocode): per-account proxy for multi-account round-robin (runWithProxyContext per account, keyed by fingerprint). Orphan test relocated to a collected vitest path (14/14 green).
This commit is contained in:
150
tests/integration/mimocode-proxy.integration.test.ts
Normal file
150
tests/integration/mimocode-proxy.integration.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { describe, it, before } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { MimocodeExecutor, generateFingerprint } from "../../open-sse/executors/mimocode.ts";
|
||||
|
||||
const PROXY_URL = process.env.MIMOCODE_SOCKS5_PROXY;
|
||||
|
||||
function parseProxyUrl(url: string): { type: string; host: string; port: number } | null {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return { type: parsed.protocol.replace(":", ""), host: parsed.hostname, port: parsed.port ? Number(parsed.port) : 1080 };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function requireProxy() {
|
||||
if (!PROXY_URL) {
|
||||
return false;
|
||||
}
|
||||
const parsed = parseProxyUrl(PROXY_URL);
|
||||
return parsed !== null;
|
||||
}
|
||||
|
||||
const proxyConfig = PROXY_URL ? parseProxyUrl(PROXY_URL) : null;
|
||||
|
||||
describe("mimocode per-account proxy — SOCKS5 integration", { timeout: 30_000 }, () => {
|
||||
before(() => {
|
||||
if (!PROXY_URL) {
|
||||
console.log("# MIMOCODE_SOCKS5_PROXY not set, skipping live proxy tests");
|
||||
}
|
||||
});
|
||||
|
||||
it("bootstrap returns JWT through configured proxy", { skip: !requireProxy() ? "MIMOCODE_SOCKS5_PROXY not set" : false }, async () => {
|
||||
process.env.ENABLE_SOCKS5_PROXY = "true";
|
||||
const { Socks5ProxyAgent } = await import("undici");
|
||||
const agent = new Socks5ProxyAgent(PROXY_URL!);
|
||||
|
||||
const fp = generateFingerprint("integration-bootstrap-" + Date.now());
|
||||
const resp = await fetch("https://api.xiaomimimo.com/api/free-ai/bootstrap", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ client: fp }),
|
||||
// @ts-expect-error — undici dispatcher
|
||||
dispatcher: agent,
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
assert.strictEqual(resp.status, 200, `Bootstrap through proxy: expected 200, got ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
assert.ok(data.jwt, "Response should contain JWT");
|
||||
assert.ok(typeof data.jwt === "string" && data.jwt.length > 10, "JWT should be a non-trivial string");
|
||||
});
|
||||
|
||||
it("chat request succeeds through configured proxy", { skip: !requireProxy() ? "MIMOCODE_SOCKS5_PROXY not set" : false }, async () => {
|
||||
process.env.ENABLE_SOCKS5_PROXY = "true";
|
||||
const { Socks5ProxyAgent } = await import("undici");
|
||||
const agent = new Socks5ProxyAgent(PROXY_URL!);
|
||||
|
||||
const fp = generateFingerprint("integration-chat-" + Date.now());
|
||||
const bootstrapResp = await fetch("https://api.xiaomimimo.com/api/free-ai/bootstrap", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ client: fp }),
|
||||
// @ts-expect-error — undici dispatcher
|
||||
dispatcher: agent,
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
assert.strictEqual(bootstrapResp.status, 200);
|
||||
const { jwt } = await bootstrapResp.json();
|
||||
|
||||
const chatResp = await fetch("https://api.xiaomimimo.com/api/free-ai/openai/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${jwt}`,
|
||||
"X-Mimo-Source": "mimocode-cli-free",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "mimo-auto",
|
||||
messages: [
|
||||
{ role: "system", content: "You are MiMoCode, an interactive CLI tool that helps users with software engineering tasks." },
|
||||
{ role: "user", content: "Say exactly: proxy-integration-ok" },
|
||||
],
|
||||
stream: false,
|
||||
}),
|
||||
// @ts-expect-error — undici dispatcher
|
||||
dispatcher: agent,
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
});
|
||||
assert.ok(chatResp.status === 200 || chatResp.status === 429,
|
||||
`Chat through proxy: expected 200/429, got ${chatResp.status}`);
|
||||
});
|
||||
|
||||
it("accounts carry proxy config after sync", () => {
|
||||
const exec = new MimocodeExecutor();
|
||||
const fp = "integration-fp-1";
|
||||
const cfg = proxyConfig || { type: "socks5", host: "127.0.0.1", port: 1080 };
|
||||
(exec as any).accounts = [
|
||||
{ fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null },
|
||||
];
|
||||
(exec as any).nextAccountIdx = 0;
|
||||
|
||||
(exec as any).syncAccountsFromCredentials({
|
||||
providerSpecificData: {
|
||||
accountProxies: [{ fingerprint: fp, proxy: cfg }],
|
||||
},
|
||||
});
|
||||
|
||||
const acct = (exec as any).accounts.find((a: any) => a.fingerprint === fp);
|
||||
assert.ok(acct, "Account should exist");
|
||||
assert.deepStrictEqual(acct.proxy, cfg);
|
||||
});
|
||||
|
||||
it("two accounts with different proxies tracked independently", () => {
|
||||
const exec = new MimocodeExecutor();
|
||||
const fp1 = "integration-fp-a";
|
||||
const fp2 = "integration-fp-b";
|
||||
const proxy1 = { type: "http" as const, host: "proxy-a.example.com", port: 8080 };
|
||||
const proxy2 = { type: "socks5" as const, host: "proxy-b.example.com", port: 1080 };
|
||||
|
||||
(exec as any).accounts = [
|
||||
{ fingerprint: fp1, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null },
|
||||
{ fingerprint: fp2, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null },
|
||||
];
|
||||
(exec as any).nextAccountIdx = 0;
|
||||
|
||||
(exec as any).syncAccountsFromCredentials({
|
||||
providerSpecificData: {
|
||||
accountProxies: [
|
||||
{ fingerprint: fp1, proxy: proxy1 },
|
||||
{ fingerprint: fp2, proxy: proxy2 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const a1 = (exec as any).accounts.find((a: any) => a.fingerprint === fp1);
|
||||
const a2 = (exec as any).accounts.find((a: any) => a.fingerprint === fp2);
|
||||
assert.deepStrictEqual(a1.proxy, proxy1, "Account 1 should have proxy1");
|
||||
assert.deepStrictEqual(a2.proxy, proxy2, "Account 2 should have proxy2");
|
||||
assert.notDeepStrictEqual(a1.proxy, a2.proxy, "Proxies should differ");
|
||||
});
|
||||
|
||||
it("no accountProxies keeps all proxies null (backward compat)", () => {
|
||||
const exec = new MimocodeExecutor();
|
||||
const accounts = (exec as any).accounts;
|
||||
assert.ok(accounts.length >= 1);
|
||||
for (const acct of accounts) {
|
||||
assert.strictEqual(acct.proxy, null, "Default account proxy should be null");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
MimocodeExecutor,
|
||||
generateFingerprint,
|
||||
MIMO_SYSTEM_MARKER,
|
||||
type AccountProxyConfig,
|
||||
} from "../../open-sse/executors/mimocode.ts";
|
||||
|
||||
const executor = new MimocodeExecutor();
|
||||
@@ -251,3 +252,136 @@ describe("mimocode providerRegistry entry", () => {
|
||||
assert.ok(mimoAuto);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mimocode per-account proxy", () => {
|
||||
const exec = new MimocodeExecutor();
|
||||
|
||||
it("AccountProxyConfig type has required fields", () => {
|
||||
const config: AccountProxyConfig = {
|
||||
fingerprint: "abc123",
|
||||
proxy: { type: "http", host: "proxy.example.com", port: 8080 },
|
||||
};
|
||||
assert.strictEqual(config.fingerprint, "abc123");
|
||||
assert.strictEqual(config.proxy?.host, "proxy.example.com");
|
||||
});
|
||||
|
||||
it("default account has null proxy", () => {
|
||||
const accounts = (exec as any).accounts;
|
||||
assert.ok(Array.isArray(accounts));
|
||||
assert.ok(accounts.length >= 1);
|
||||
assert.strictEqual(accounts[0].proxy, null);
|
||||
});
|
||||
|
||||
it("syncAccountsFromCredentials reads accountProxies", () => {
|
||||
const testExec = new MimocodeExecutor();
|
||||
const fp1 = "fingerprint-1";
|
||||
const fp2 = "fingerprint-2";
|
||||
(testExec as any).accounts = [
|
||||
{ fingerprint: fp1, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null },
|
||||
{ fingerprint: fp2, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null },
|
||||
];
|
||||
(testExec as any).nextAccountIdx = 0;
|
||||
|
||||
const credentials = {
|
||||
providerSpecificData: {
|
||||
accountProxies: [
|
||||
{ fingerprint: fp1, proxy: { type: "http", host: "p1.example.com", port: 1080 } },
|
||||
{ fingerprint: fp2, proxy: null },
|
||||
],
|
||||
},
|
||||
};
|
||||
(testExec as any).syncAccountsFromCredentials(credentials);
|
||||
|
||||
const accounts = (testExec as any).accounts;
|
||||
const acct1 = accounts.find((a: any) => a.fingerprint === fp1);
|
||||
const acct2 = accounts.find((a: any) => a.fingerprint === fp2);
|
||||
assert.deepStrictEqual(acct1.proxy, { type: "http", host: "p1.example.com", port: 1080 });
|
||||
assert.strictEqual(acct2.proxy, null);
|
||||
});
|
||||
|
||||
it("syncAccountsFromCredentials skips when accountProxies absent", () => {
|
||||
const testExec = new MimocodeExecutor();
|
||||
const before = JSON.parse(JSON.stringify((testExec as any).accounts));
|
||||
(testExec as any).syncAccountsFromCredentials({ providerSpecificData: {} });
|
||||
const after = (testExec as any).accounts;
|
||||
assert.strictEqual(after.length, before.length);
|
||||
assert.strictEqual(after[0].proxy, null);
|
||||
});
|
||||
|
||||
it("syncAccountsFromCredentials skips unknown fingerprints", () => {
|
||||
const testExec = new MimocodeExecutor();
|
||||
const existingFp = (testExec as any).accounts[0].fingerprint;
|
||||
(testExec as any).syncAccountsFromCredentials({
|
||||
providerSpecificData: {
|
||||
accountProxies: [
|
||||
{ fingerprint: "nonexistent-fingerprint", proxy: { type: "socks5", host: "s5.example.com", port: 1080 } },
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.strictEqual((testExec as any).accounts[0].proxy, null);
|
||||
});
|
||||
|
||||
it("accounts with different proxies are tracked independently", () => {
|
||||
const testExec = new MimocodeExecutor();
|
||||
const fp1 = "fp-a";
|
||||
const fp2 = "fp-b";
|
||||
(testExec as any).accounts = [
|
||||
{ fingerprint: fp1, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null },
|
||||
{ fingerprint: fp2, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null },
|
||||
];
|
||||
(testExec as any).syncAccountsFromCredentials({
|
||||
providerSpecificData: {
|
||||
accountProxies: [
|
||||
{ fingerprint: fp1, proxy: { type: "http", host: "a.com", port: 8080 } },
|
||||
{ fingerprint: fp2, proxy: { type: "socks5", host: "b.com", port: 1080 } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const accounts = (testExec as any).accounts;
|
||||
const a1 = accounts.find((a: any) => a.fingerprint === fp1);
|
||||
const a2 = accounts.find((a: any) => a.fingerprint === fp2);
|
||||
assert.notDeepStrictEqual(a1.proxy, a2.proxy);
|
||||
assert.strictEqual(a1.proxy?.host, "a.com");
|
||||
assert.strictEqual(a2.proxy?.host, "b.com");
|
||||
});
|
||||
|
||||
it("getJwtForAccount reads proxy from account", async () => {
|
||||
const testExec = new MimocodeExecutor();
|
||||
const fp = "test-fp-proxy";
|
||||
const proxyConfig = { type: "http", host: "proxy.test", port: 3128 };
|
||||
(testExec as any).accounts = [
|
||||
{ fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: proxyConfig },
|
||||
];
|
||||
(testExec as any).nextAccountIdx = 0;
|
||||
|
||||
const acct = (testExec as any).accounts[0];
|
||||
assert.deepStrictEqual(acct.proxy, proxyConfig);
|
||||
assert.strictEqual(acct.jwt, "");
|
||||
});
|
||||
|
||||
it("proxy field persists on account after sync", () => {
|
||||
const testExec = new MimocodeExecutor();
|
||||
const fp = "test-fp-persist";
|
||||
(testExec as any).accounts = [
|
||||
{ fingerprint: fp, jwt: "", expiresAt: 0, cooldownUntil: 0, consecutiveFails: 0, proxy: null },
|
||||
];
|
||||
(testExec as any).nextAccountIdx = 0;
|
||||
|
||||
const proxy1 = { type: "http", host: "first.proxy", port: 8080 };
|
||||
(testExec as any).syncAccountsFromCredentials({
|
||||
providerSpecificData: {
|
||||
accountProxies: [{ fingerprint: fp, proxy: proxy1 }],
|
||||
},
|
||||
});
|
||||
assert.deepStrictEqual((testExec as any).accounts[0].proxy, proxy1);
|
||||
|
||||
const proxy2 = { type: "socks5", host: "second.proxy", port: 1080 };
|
||||
(testExec as any).syncAccountsFromCredentials({
|
||||
providerSpecificData: {
|
||||
accountProxies: [{ fingerprint: fp, proxy: proxy2 }],
|
||||
},
|
||||
});
|
||||
assert.deepStrictEqual((testExec as any).accounts[0].proxy, proxy2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user