router-for-me / router-for-me/CLIProxyAPI
Codex Responses SSE stream dies after ~30s of upstream silence on deep-reasoning models (terra/astra: 20% / 15% drop rate)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 52.5k
- Forks
- 7.9k
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 60
Description
Summary
On v7.2.125, Codex Responses streams for deep-reasoning models die without a terminal event whenever the upstream stays silent for ~30s. Clients surface it as stream disconnected before completion: stream closed before response.completed.
The trigger is upstream silence, not total request duration. It is heavily model-dependent.
Production data (24h, ~40k requests)
Classified from usage events as failed=1 && latency_ms >= 15000. Note that PublishFailure in codex_executor_stream.go carries no usage detail, so output_tokens is always 0 for these — it cannot be used to tell "failed before first byte" from "died mid-stream". Latency is the usable signal.
| model | requests | mid-stream drops | rate |
|---|---|---|---|
| gpt-5.6-terra | 1026 | 209 | 20.4% |
| gpt-6-astra | 150 | 22 | 14.7% |
| gpt-5.6-sol | 38153 | 27 | 0.07% |
| gpt-5.6-luna | 239 | 1 | 0.42% |
| grok-4.6 | 51 | 1 | 1.96% |
Drop latencies cluster hard on 30s (131), 60s (52), 90s (28) — i.e. multiples of the upstream keepalive period — with almost nothing in between. Meanwhile 57 terra requests succeeded past 30s (longest 172s), so this is not a hard deadline on the request.
Why terra/astra and not sol
Same prompt, same effort, downstream SSE:
sol : 565s, 2942 events, response.completed ✅
continuous response.reasoning_summary_* ; max inter-event gap 9.9s
terra: 31.6s, 3 events, no completed ❌
response.created → response.in_progress → (30.0s silence) → keepalive → dead
terra emits no intermediate events at all while reasoning, so the stream idles past the window. sol keeps emitting reasoning summaries and never idles.
Tried and did not help on terra (9/9 still dropped): reasoning.summary = auto / detailed / concise (terra produces no reasoning summary regardless), include: ["reasoning.encrypted_content"], copying the full official Codex CLI parameter set, and lowering effort to medium / low.
Not a proxy-side timeout
Ruled out layer by layer: nginx proxy_read_timeout 3600s; our Go reverse proxy sets no WriteTimeout; CPA's own NewUtlsHTTPClient(ctx, cfg, auth, 0) passes 0 (no timeout); host tcp_keepalive_time 7200, nf_conntrack_tcp_timeout_established 432000.
Also not a connection-level idle timeout: with 3 concurrent sol streams saturating the shared HTTP/2 connection (max inter-event gap 2.2–2.4s, all completed), a concurrent terra stream still died at 61.1s. The connection was busy; only that one stream was reset.
WebSocket transport helps but does not fix it
Since the official model catalog marks these models "prefer_websockets": true, I enabled "websockets": true on credentials and drove /v1/responses over the downstream WebSocket handler. Same prompt, 7 runs:
| outcome | runs |
|---|---|
| completed (195s / 228s / 87s) | 3 |
| died at 30s silence (31s / 32s) | 2 |
| connection dropped immediately (0.5s) | 1 |
| survived past 30s, killed by my own client ping | 1 |
Much better than SSE (0/9), because over WS the upstream often pushes output_item events during reasoning (observed max gap 13.9–22.4s). But run 3 showed the exact SSE failure shape — in_progress then 32.2s of silence then dead — so WS is not immune, it just idles less often.
Note on a workaround that did not pan out
I patched a local build to allow an SSE downstream to reach the upstream over WebSocket (relaxing the DownstreamWebsocket(ctx) && codexWebsocketsEnabled(auth) condition in CodexAutoExecutor, gated by a per-model config list). The upstream WS connection was established correctly (codex websockets: upstream connected session= with an empty session id, i.e. the sess == nil path), but the run still died at 31s and no events reached the downstream client at all — worse than the SSE baseline. I rolled it back and am not proposing it as a fix; mentioning it only so nobody repeats the experiment blind.
Ask
Would it make sense for the Codex executor to treat "SSE stream ended with no terminal event, after N seconds of upstream silence" as retryable when nothing has been forwarded downstream yet? Today retry.go-style bootstrap retries only cover pre-first-byte failures, and #3055 notes keepalive-seconds does not address this class either.
Environment: v7.2.125, Codex OAuth credentials, downstream OpenAI-compatible SSE.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading codex_executor_stream.go, CodexAutoExecutor, and retry.go, then trace how upstream silence and missing terminal events are handled for SSE and WebSocket transports. Reproduce the ~30-second deep-reasoning failure described for terra/astra and determine the retry boundary; done means the failure mode is reliably handled without retrying after downstream output has begun.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100