Repository navigation
fix(mimocode): route per-account traffic through SOCKS5 proxy dispatchers - #5317
pizzav-xyz wants to merge 1 commit into
Conversation
…hers The mimocode executor stored per-fingerprint proxy assignments in providerSpecificData.accountProxies but routed all traffic through the global proxy via plain fetch(). Xiaomi rate-limits by source IP, so all 10 fingerprints sharing one Mullvad exit IP hit the same bucket. Changes: - Replace runWithProxyContext + plain fetch() with direct undici fetch() using per-fingerprint Dispatcher instances from createProxyDispatcher() - Build fingerprint→proxyUrl map in syncAccountsFromCredentials() with support for authenticated proxies (username:password in URL) - Add fetchWithProxy() helper that uses the dispatcher when available and falls back to plain fetch() for unproxied accounts - Add getProxyDispatcher() that lazily creates/caches dispatchers per fingerprint via the shared proxyDispatcher cache - Fix testConnection() to call syncAccountsFromCredentials() first so proxyUrlMap is populated before use - Restore AccountProxyConfig export (needed by tests and consumers) - Update test suite: 13 proxy tests covering URL construction, auth credentials, default ports, dispatcher creation, and fallback behavior
There was a problem hiding this comment.
Code Review
This pull request introduces the MimocodeExecutor to support free-tier Xiaomi MiMo models via bootstrap JWT authentication, including multi-account round-robin rotation, proxy dispatching, and anti-abuse system marker injection, along with corresponding unit tests. The review feedback provides valuable improvements to make the executor more robust: cleaning up stale or default accounts in syncAccountsFromCredentials to prevent IP leaks, checking for pre-aborted signals immediately in bootstrapJwt, continuing to the next account on persistent authentication failures (401/403), and adding a unit test to verify account synchronization.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of syncAccountsFromCredentials only appends new fingerprints to this.accounts and never removes old or default accounts. This has two issues:
- The default unproxied account created in the constructor is always kept in the pool, which can leak the server's real IP and cause rate-limiting.
- If the user updates their credentials to a different set of fingerprints, the old fingerprints are never removed and will continue to be used.
We should filter this.accounts to only keep accounts that are present in the new fingerprints list, and discard the default/stale ones.
if (!Array.isArray(fingerprints)) return;
const targetFingerprints = new Set(
fingerprints.filter((fp): fp is string => typeof fp === "string" && fp.trim() !== "")
);
if (targetFingerprints.size === 0) return;
this.accounts = this.accounts.filter((a) => targetFingerprints.has(a.fingerprint));
const existingFingerprints = new Set(this.accounts.map((a) => a.fingerprint));
for (const fp of targetFingerprints) {
if (!existingFingerprints.has(fp)) {
this.accounts.push({
fingerprint: fp,
jwt: "",
expiresAt: 0,
cooldownUntil: 0,
consecutiveFails: 0,
});
}
}| const onSignal = signal ? () => controller.abort(signal.reason) : null; | ||
| if (signal && onSignal) signal.addEventListener("abort", onSignal, { once: true }); |
There was a problem hiding this comment.
If the signal passed to bootstrapJwt is already aborted before the function is called, the abort event listener will never fire. This results in the request being sent to the server despite the client signal already being in an aborted state. We should check signal.aborted immediately and abort the controller if so.
const onSignal = signal ? () => controller.abort(signal.reason) : null;
if (signal && onSignal) {
if (signal.aborted) {
controller.abort(signal.reason);
} else {
signal.addEventListener("abort", onSignal, { once: true });
}
}| 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 this.fetchWithProxy( | ||
| url, | ||
| { | ||
| method: "POST", | ||
| headers, | ||
| body: JSON.stringify(reqBody), | ||
| signal: signal ?? undefined, | ||
| }, | ||
| account.fingerprint | ||
| ); | ||
| } |
There was a problem hiding this comment.
If an account persistently fails with 401 or 403 even after re-bootstrapping, the executor currently treats it as a success and returns the error response directly to the user. Instead, we should mark this specific account as failed/cooldown and continue to the next available account in the pool to ensure maximum resilience.
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 this.fetchWithProxy(
url,
{
method: "POST",
headers,
body: JSON.stringify(reqBody),
signal: signal ?? undefined,
},
account.fingerprint
);
}
if (resp.status === 401 || resp.status === 403) {
this.markCooldown(account);
log?.warn?.(
"MIMOCODE",
`Persistent auth failure (${resp.status}) on account ${account.fingerprint.slice(0, 8)}, trying next…`
);
continue;
}| }); | ||
| }); |
There was a problem hiding this comment.
Let's add a unit test to verify that syncAccountsFromCredentials correctly updates the accounts list and discards stale or default accounts.
| }); | |
| }); | |
| }); | |
| it("syncAccountsFromCredentials updates accounts list and discards stale/default accounts", () => { | |
| const testExec = new MimocodeExecutor(); | |
| const defaultFp = (testExec as any).accounts[0].fingerprint; | |
| (testExec as any).syncAccountsFromCredentials({ | |
| providerSpecificData: { | |
| fingerprints: ["fp-1", "fp-2"], | |
| }, | |
| }); | |
| const accounts = (testExec as any).accounts; | |
| assert.strictEqual(accounts.length, 2); | |
| assert.strictEqual(accounts[0].fingerprint, "fp-1"); | |
| assert.strictEqual(accounts[1].fingerprint, "fp-2"); | |
| assert.ok(!accounts.some((a: any) => a.fingerprint === defaultFp)); | |
| }); | |
| }); |
|
Thanks @pizzav-xyz! Per-account SOCKS5 dispatch is a reasonable need, but this conflicts with the current codebase and has a security blocker:
Your test coverage is solid (35+ cases) 👍. Leaving open for the rebase + Rule #12 fix. |
|
Thanks for digging into the Xiaomi per-IP rate-limiting, @pizzav-xyz — that diagnosis is spot-on. Heads up on an overlap, though: Since this PR was opened, per-account proxy routing already landed in v3.8.40 via #3837 ( const proxy = account.proxy; // per-fingerprint, from providerSpecificData.accountProxies
const resp = await runWithProxyContext(proxy, () => fetch(url, { ... }));(see Because of that, this branch now conflicts heavily with #3837 (15 semantic blocks in Could you:
If #3837 covers your case, this PR may be safe to close as superseded — your call. Really appreciate the contribution either way. 🙏 |
|
Thanks for the thorough review and the heads-up about #3837! I've checked So each fingerprint does egress through its own Mullvad exit IP as of v3.8.40. This PR is superseded — closing. |
Summary
The mimocode executor stored per-fingerprint proxy assignments in
providerSpecificData.accountProxiesbut routed all traffic through the global proxy via plainfetch(). Xiaomi rate-limits by source IP, so all 10 fingerprints sharing one Mullvad exit IP hit the same rate-limit bucket.Changes
runWithProxyContext+ plainfetch()with directundici fetch()using per-fingerprintDispatcherinstances fromcreateProxyDispatcher()syncAccountsFromCredentials()with support for authenticated proxies (username:password in URL)fetchWithProxy()helper that uses the dispatcher when available and falls back to plainfetch()for unproxied accountsgetProxyDispatcher()that lazily creates/caches dispatchers per fingerprint via the sharedproxyDispatchercachetestConnection()to callsyncAccountsFromCredentials()first soproxyUrlMapis populated before useAccountProxyConfigexport (needed by tests and consumers)Background
The mimocode executor (Xiaomi MiMo free-tier via bootstrap JWT auth) uses 10 device fingerprints, each assigned a dedicated Mullvad SOCKS5 proxy in
providerSpecificData.accountProxies. However, the executor was using plainfetch()for all requests — meaning all 10 fingerprints shared a single Mullvad exit IP. Xiaomi tightened anti-abuse detection after June 22, causing 100% failure rate (400 errors: "Detected high-frequency non-compliant requests") because all fingerprints appeared to come from the same source.This fix routes each fingerprint's bootstrap JWT and chat requests through its assigned SOCKS5 proxy, giving each fingerprint a unique exit IP.