Repository navigation
Description
Description
A failover combo whose first target is the native Codex route does not move to its next target when the upstream declines with server_is_overloaded (503) before any output. The combo returns the 503 to the client. Codex then reports "Selected model is at capacity. Please try a different model." and ends the turn.
This is the combo and main-account counterpart of #6969. The pre-output refusal hold from #6969 makes such a decline replayable, but it is enabled only for non-combo requests on pool / main-pool auth.
Steps to reproduce
- Run OpenCodex 2.95.0 with the
openaiprovider incodexAccountMode: direct,authMode: forward(the main ChatGPT account, no pool). - Define a failover combo
gpt-6.1-solwith targets[openai/gpt-6.1-sol, deepseek/deepseek-flash](any second provider shows the same result). - Send streaming Responses requests (Codex CLI and other Responses clients) to
gpt-6.1-solwhile the ChatGPT backend is at capacity.
Expected
A decline before any output is safe to replay. The combo should try its next target, because comboFailureDecision() already returns hop for 5xx and for server_is_overloaded.
Actual
Each failed request has one attempt and the combo stops:
- request:
provider: "combo",status: 503,firstOutputMs: null - attempt:
provider: "openai",status: 503,errorCode: "server_is_overloaded",sendCount: 1,recoveryKinds: [],failureStage: "protocol-prelude",failureCause: "upstream-declined" codexWsStage:sent: true,relayedEvents: 4,terminalEvent: "error"routeDecision.candidates[1](the second target):exclusions: [{ code: "not-selected" }]
On 2026-11-02, 116 of 488 combo requests to the native route failed this way. Before 2026-11-01 the rate was below 1%.
As we read the code:
poolPreOutputRefusalHold()(src/server/responses/core-codex-account.ts:174) returnsundefinedwhencomboAttemptis true. It also requiresusesCodexForwardPoolAuth(), which is true only forpoolandmain-poolauth contexts, notmain.- Without the hold, the empty lifecycle frames are relayed before the refusal, so the failure is a post-send WebSocket failure and is non-replayable.
- In
src/server/responses/core-combo.ts(around line 1381),failure.nonReplayablemakes the decisionstopbeforecomboFailureDecision()is asked.
The same code is on main at 684ad75.
Suggested change
Enable the #6969 pre-output refusal hold for combo attempts and for the main auth context. Then a structured 5xx that arrives before any output settles as a replayable status, and the combo moves on. A refusal after any output should still stop, as it does now.
Environment
- OpenCodex 2.95.0 (Bun 1.4.3), Linux,
ocx start --port 10100behind a TLS reverse proxy - Clients: Codex CLI and a Responses client, both streaming
openaiprovider:codexAccountMode: direct,authMode: forward; failover combos
Impact
For agent workloads every transient ChatGPT overload becomes a hard turn failure, although the combo has a healthy next target. Codex CLI review runs failed 61 times on 2026-11-02 for this reason.
Activity
github-actions commentedon Nov 2, 2026
Automated translation bookkeeping — detected language: English.
coderabbitai commentedon Nov 2, 2026
🔗 Related PRs
#7482 - feat(fallback): OCX-11 fallback off per scope, with home_unreachable on Children [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
Ingwannu commentedon Nov 4, 2026
Thanks for the detailed attempt record. I traced this against current
dev(72e80d280d). The relevant code is unchanged since 2.95.0, and the stop happens for a different reason than the one in the report.core-codex-account.ts:163-177). Even with the hold enabled, an unrecovered projection is restored to the original 200 stream inpassthrough-dispatch.ts:1711before the combo sees it. So enabling the hold for combo attempts would not change the result.response.createdand the overloaderrorframe is relayed in-band. That body then goes through the combo stream preflight (core-combo.ts:1255). For a zero-output 503server_is_overloaded, the preflight would normally returnfailed, without the non-replayable marker, andcomboFailureDecisionwouldhop.codex.rate_limits/codex.response.metadatacontrol frames into the same SSE body (codex-ws-exchange.ts:626-716,codex-ws-metadata.ts:242). The combo preflight only treatsresponse.created/in_progress/queued/heartbeatas pre-output control events (combo-stream-preflight.ts:24). Every other type commits the child (comboStreamPayloadCommitsOutput,:148-162,:336). So a rate-limit frame ahead ofresponse.createdcommits the native target before the overload arrives, and the combo never reaches its next target. That matches your one attempt andcandidates[1]: not-selected.Proposed fix: treat the two Codex metadata frame types as non-committing control events in the combo preflight. Add a unit test and a fake-WebSocket combo failover regression test (
rate_limits → created → in_progress → error 503). Making the hold apply to the plainmaincontext outside combos is a separate change and is not needed for the combo case.One thing to confirm: your stage record shows
relayedEvents: 4, one more than thecreated → in_progress → errorsequence we test with. If you can share only the orderedtypevalues of the frames before theerror(no payloads), we can check whether a new Responses event type also needs to be treated as pre-output.