Skip to content

fix(mimocode): route per-account traffic through SOCKS5 proxy dispatchers - #5317

Closed
pizzav-xyz wants to merge 1 commit into
diegosouzapw:release/v3.8.42from
pizzav-xyz:fix/mimocode-proxy-v4
Closed

pizzav-xyz wants to merge 1 commit into
diegosouzapw:release/v3.8.42from
pizzav-xyz:fix/mimocode-proxy-v4

Conversation

@pizzav-xyz

Copy link
Copy Markdown
Contributor

Summary

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 rate-limit 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

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 plain fetch() 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.

…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
@pizzav-xyz
pizzav-xyz requested a review from diegosouzapw as a code owner June 29, 2026 10:01

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +275 to +288
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);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of syncAccountsFromCredentials only appends new fingerprints to this.accounts and never removes old or default accounts. This has two issues:

  1. 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.
  2. 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,
        });
      }
    }

Comment on lines +160 to +161
const onSignal = signal ? () => controller.abort(signal.reason) : null;
if (signal && onSignal) signal.addEventListener("abort", onSignal, { once: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 });
    }
  }

Comment on lines +453 to +473
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
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
        }

Comment on lines +486 to +487
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Let's add a unit test to verify that syncAccountsFromCredentials correctly updates the accounts list and discards stale or default accounts.

Suggested change
});
});
});
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));
});
});

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.41 June 29, 2026 12:40
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @pizzav-xyz! Per-account SOCKS5 dispatch is a reasonable need, but this conflicts with the current codebase and has a security blocker:

  1. Conflict / full replacement — open-sse/executors/mimocode.ts already exists on release/v3.8.41 (~497 lines) and routes through runWithProxyContext from proxyFetch.ts. This PR re-adds the file with a different implementation (createProxyDispatcher). Please rebase and submit only the incremental per-account proxy additions on top of the existing executor, not a replacement.
  2. Hard Rule fix(ui): fix Select dropdown dark theme inconsistency #12 — the error path puts err.message raw into the HTTP response body. All error responses must go through buildErrorBody()/sanitizeErrorMessage() (open-sse/utils/error.ts). See docs/security/ERROR_SANITIZATION.md.
  3. Minor: proxy.host isn't range-validated (SSRF surface) — mitigated since it's operator-controlled, but worth an allowlist.

Your test coverage is solid (35+ cases) 👍. Leaving open for the rebase + Rule #12 fix.

@diegosouzapw

Copy link
Copy Markdown
Owner

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 (feat(mimocode): per-account proxy support for multi-account round-robin). On the current release/v3.8.41, the mimocode executor already routes each fingerprint through its own proxy at egress using OmniRoute's standard mechanism:

const proxy = account.proxy;            // per-fingerprint, from providerSpecificData.accountProxies
const resp = await runWithProxyContext(proxy, () => fetch(url, { ... }));

(see open-sse/executors/mimocode.ts lines ~258/340/403/425). So the "all 10 fingerprints share one exit IP" symptom should already be fixed — each account egresses through its assigned proxy.

Because of that, this branch now conflicts heavily with #3837 (15 semantic blocks in mimocode.ts), and the raw-undici dispatcher approach here would bypass runWithProxyContext — which means it would skip the ProxyEgress logging (#5217/#5351) and the TLS-fingerprint context that the rest of the pipeline relies on.

Could you:

  1. Pull the latest release/v3.8.41 and confirm whether feat(mimocode): per-account proxy support for multi-account round-robin #3837 already resolves your per-IP rate-limit issue (each fingerprint should now exit through its own proxy)?
  2. If you still observe all fingerprints sharing one exit IP, let us know exactly where — and if the per-fingerprint dispatcher model genuinely adds something feat(mimocode): per-account proxy support for multi-account round-robin #3837 lacks, we'd want it layered on top of runWithProxyContext (so ProxyEgress + TLS-fingerprint context keep working), not replacing it.

If #3837 covers your case, this PR may be safe to close as superseded — your call. Really appreciate the contribution either way. 🙏

@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.41 to release/v3.8.42 June 29, 2026 21:31
@pizzav-xyz

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review and the heads-up about #3837!

I've checked open-sse/executors/mimocode.ts on release/v3.8.41 and can confirm — #3837 already resolves the per-IP rate-limit issue. The executor now reads providerSpecificData.accountProxies, maps each fingerprint to its proxy, and routes every request (bootstrap + chat) through runWithProxyContext(account.proxy, ...), which creates a per-fingerprint dispatcher via createProxyDispatcher.

So each fingerprint does egress through its own Mullvad exit IP as of v3.8.40. This PR is superseded — closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants