Skip to content

fix(executors): rotate to the next account on network throws, not just 429 - #10402

Merged
diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.50from
maxmad64bis:fix/opencode-rotate-network-throw
Aug 16, 2026
Merged

fix(executors): rotate to the next account on network throws, not just 429#10402
diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.50from
maxmad64bis:fix/opencode-rotate-network-throw

Conversation

@maxmad64bis

@maxmad64bis maxmad64bis commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

If a request to a free "OpenCode"/"MiMoCode" account hit a network error (timeout, connection
reset) instead of a rate limit, the whole request failed immediately — even when the user had
other accounts configured that could have handled it. Only HTTP 429 triggered account rotation;
a network error did not.

Now a network error rotates to the next account too, same as a 429 does. The one nuance: if the
failing account has no proxy of its own, the next few accounts likely share the same network path
and would fail the same way, so we don't hammer through all of them — we cool down and move
straight to an account that has its own proxy (if one exists) instead of retrying doomed accounts.
This is on by default and can be turned off (NETWORK_ROTATION_SHARED_EGRESS_GUARD flag) to get
the old immediate-failure behavior back.

Also fixes the same bug in MimoCodeExecutor, which had it too but silently — no log at all, so
operators couldn't see why a request failed.

Related Issues

Validation

  • Change type: provider
  • Focused tests: check:provider-consistency, check:provider-assets,
    provider-translate-path-golden.test.ts, plus the executor/rotation/feature-flag test suites
    below — all passing
  • npm run lint clean on touched files
  • Rebased on the current release/v3.8.50 tip, focused checks rerun after
  • New/updated tests included in this PR
  • SonarQube — not yet run, awaiting CI

Tests Added Or Updated

  • tests/unit/account-rotation.test.ts (new) — shared rotation logic used by both executors
  • tests/unit/opencode-proxy-rotation-4954.test.ts / tests/unit/mimocode-executor.test.ts
    network-error rotation, mixed proxy/no-proxy accounts, and the guard-disabled fallback
  • tests/unit/feature-flags-settings.test.ts — the new flag's definition

Reviewer Notes

  • Both executors previously duplicated the same account-rotation code; it's now shared in
    open-sse/executors/accountRotation.ts.
  • No secrets or local config involved.

@maxmad64bis
maxmad64bis force-pushed the fix/opencode-rotate-network-throw branch from a676182 to ee58be1 Compare August 14, 2026 17:28
@maxmad64bis
maxmad64bis marked this pull request as draft August 14, 2026 17:38
… account has a dedicated proxy

OpencodeExecutor and MimocodeExecutor rotated to the next account only on
HTTP 429. A network exception (timeout, connection refused/reset) on one
account instead propagated out of execute() and failed the whole request,
even when other accounts remained available.

Both executors now rotate on a network exception only when the failed
account has its own dedicated proxy (account.proxy !== null) — a dead
proxy is genuinely account-scoped, so rotating away from it is safe.
Accounts sharing the default egress (no proxy configured) trigger the
same cooldown and are skipped for the rest of the request once the shared
egress is known down, but a later account with its own dedicated proxy is
still tried normally — a throw on a proxy-less account no longer strands
a proxied account further in the rotation. This behavior is gated behind
NETWORK_ROTATION_SHARED_EGRESS_GUARD (Feature Flag, default on); disabled,
it reproduces the immediate-propagation behavior this fix started from.

The shared rotation mechanics (pickAccount/markCooldown/markSuccess) are
extracted into executors/accountRotation.ts, used by both executors —
they had independently implemented the same round-robin+cooldown
skeleton. This also fixes an identical, pre-existing bug in
MimocodeExecutor that predates this PR: its catch block called
markCooldown unconditionally on any throw, with no proxy check and no
warn log (a silent exception swallow on a path that influences the
result).

The cooldown formula for both the proxy and shared-egress cases reuses
the repo's already-established "transient, not clearly attributable"
constants (errorConfig.ts TRANSIENT_COOLDOWN_MS/COOLDOWN_MS.transientMax,
already used by accountFallback.ts for network-error classification)
instead of introducing a separate value.

MimocodeExecutor's network-error 502 body also now goes through
buildErrorBody()/sanitizeErrorMessage() instead of embedding the raw
caught error message directly (Hard Rule diegosouzapw#12), matching the sanitization
already used on its diegosouzapw#2101 malformed-request path.

Validated by TDD (Hard Rule diegosouzapw#18): tests/unit/account-rotation.test.ts
covers the shared module directly; opencode-proxy-rotation-4954.test.ts
and mimocode-executor.test.ts cover the proxy-configured rotation path,
the mixed-fleet case, the shared-egress single-network-call case, and the
NETWORK_ROTATION_SHARED_EGRESS_GUARD-disabled legacy path, for each
executor. tsc, lint, and the provider golden-path gates
(check:provider-consistency, check:provider-assets,
provider-translate-path-golden.test.ts) are clean on all touched files.
@maxmad64bis
maxmad64bis force-pushed the fix/opencode-rotate-network-throw branch from ee58be1 to 0358c0e Compare August 14, 2026 18:17
@maxmad64bis maxmad64bis changed the title fix(executors): rotate to the next account on network throws when the account has a dedicated proxy fix(executors): rotate to the next account on network throws, not just 429 Aug 14, 2026
@maxmad64bis
maxmad64bis marked this pull request as ready for review August 14, 2026 18:27
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks for this — nice catch, and a clean fix. I confirmed the underlying bug directly in the diff: the previous opencode.ts rotation loop called runWithProxyContext(...) with no surrounding try/catch, so a network throw (timeout, ECONNRESET) escaped the whole loop and killed the request instead of falling through to the next account, while a 429 correctly rotated. Wrapping that call and gating rotation on isNetworkErrorRotatable (only rotate when the failing account has its own proxy, otherwise apply the shared-egress guard) is the right call, and extracting accountRotation.ts to de-duplicate the two executors is a good side effect.

I ran the new/updated test files locally (account-rotation.test.ts, opencode-proxy-rotation-4954.test.ts, mimocode-executor.test.ts, feature-flags-settings.test.ts) and 116/117 passed; the one local failure traced back to a stale/corrupted sqlite in my own dev environment (unrelated to your code — a DB-read error made the feature flag default to "enabled" ahead of my env override), and your own CI run shows all 4 unit-test shards green, so I'm treating that as a non-issue.

Two very minor, non-blocking notes, no action needed from you:

  • The resolveFeatureFlag() DB-error path defaults to "enabled" even when an operator explicitly sets the env var to false — but that's an existing pattern shared by every other flag wrapper in the file, not something new here.
  • The new flag's descriptionI18nKey doesn't have a locale-file entry yet in what I could see, but CI's docs/i18n gates are green, so it's likely picked up by a separate sync pass.

The Build (advisory) red on your run is the known Turbopack-OOM issue that's currently failing on every PR — unrelated to your change.

Overall this looks merge-ready to me. Nice work, especially on the shared-egress guard nuance and the parity fix for MimocodeExecutor's error-body sanitization along the way.

@diegosouzapw
diegosouzapw merged commit 94cf4c4 into diegosouzapw:release/v3.8.50 Aug 16, 2026
23 of 24 checks passed
@diegosouzapw

Copy link
Copy Markdown
Owner

Validated in local merge-train .claude/worktrees/merge-train-20260816-000002-suite.log on devbox-vm-06-dev002 @ 8be62df65f61cc4cf29cdc3e6bc38173ffafa4e3 (FAST gates green: static + changed tests + vitest; daily full-suite run still required). Merged under the batch merge-train protocol (merge-gates §7). ⚠️ base-red inherited: #10523 (ServiceSupervisor order-dependent test, not touched by this batch).

@maxmad64bis
maxmad64bis deleted the fix/opencode-rotate-network-throw branch August 17, 2026 23:51
diegosouzapw added a commit to Tushar49/OmniRoute that referenced this pull request Aug 18, 2026
…ocode-sunset-provider

Resolves conflicts from the mimocode provider sunset removal overlapping
with the account-rotation refactor (diegosouzapw#10402) and provider-count drift
across docs/i18n mirrors, README, AGENTS.md, package.json and the
generated PROVIDER_REFERENCE.md/diagram SVGs.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
maxmad64bis pushed a commit to maxmad64bis/OmniRoute that referenced this pull request Aug 22, 2026
OpencodeExecutor treated a malformed upstream 400 (completion envelope with no
error field, empty content, null finish_reason) as a successful response and
propagated it fatally — killing client/subagent sessions on free-tier models.

- Add isEmptyUpstreamRejection / extractChatcmplId predicates in accountRotation.ts
  (strict signature: status 400, no error field, no real content/tool_calls,
  null finish_reason; conservative on other content shapes).
- Loop now retries/rotates on an empty 400 (bounded +1 only for a single account;
  multi-account rotation through the fleet is the retry). Body is read only for a
  400 (never a 200/streaming) so the success path is never buffered.
- Fast path (single direct account, no proxies) retries exactly once on an empty 400.
- 400s carrying a real error field still propagate immediately, untouched.
- Clean up the now-obsolete Mimocode mention in the accountRotation header (diegosouzapw#10130).

Tests: accountRotation predicate suite + OpencodeExecutor wiring suite
(rotation, bounded retry, fast path, anti-bufferisation, error-400 passthrough)
against the existing proxy-rotation-4954 fetch-stub harness.

Refs upstream precedent diegosouzapw#10402 (rotate beyond 429), diegosouzapw#10460 (classify 400 by
signature before rotating).
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