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)

Open
#5,545 10 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

pending
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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.