Stream Codex HTTP responses incrementally#51
Open
mulfyx wants to merge 1 commit into
Open
Conversation
Contributor
Author
|
Moving this back to draft while the implementation is being finished. The branch still needs to be rebased onto the latest main, and the HTTP streaming retry boundary needs additional work so retryable upstream failures are not exposed after an early progress or heartbeat frame. The PR will be updated once the fix and regression coverage are ready. |
Decode fragmented Responses SSE frames as they arrive and keep Claude Code alive with Anthropic heartbeats during silent upstream gaps. Preserve the bounded HTTP retry window across control-only events, but close it after semantic output to avoid duplicate text or tool calls. Propagate downstream cancellation into active reads and retry backoff. Add integration coverage for incremental delivery, overload, rate limiting, transient failures, early EOF, retry exhaustion, and cancellation.
6f8deb0 to
7b77990
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stream Codex HTTP responses incrementally
Summary
pingevents during startup and silent reasoning gapsresponse.createdandresponse.in_progressRoot cause
We found the original streaming bug while diagnosing long-running Claude Code
background agents through the proxy. The HTTP transport waited for
response.bytes()to collect the complete Codex response before parsing its SSEevents. Codex was streaming correctly, but Claude Code received the translated
body only after the model finished, so an active agent appeared stuck and then
delivered its output in one burst.
The initial incremental implementation exposed a second boundary bug. A Codex
attempt can emit control events and later fail inside an HTTP 200 SSE stream.
Once
response.createdhad produced an Anthropicmessage_startor heartbeat,the proxy treated the downstream response as committed. A later retryable
response.failedwas therefore translated into an Anthropicapi_errorinstead of using the existing bounded retry policy.
We reproduced this with a real upstream overload response:
The buffered HTTP implementation did not have this regression because it could
inspect the complete upstream response before opening the downstream body.
Implementation
The HTTP transport now owns a restartable incremental SSE reader.
closing the retry window.
output or completes successfully.
5xx, body timeout, connection failure,malformed/truncated SSE, and pre-terminal EOF can restart the upstream
attempt while no semantic output has been exposed.
Retry-After, auth refresh, and request bodyare reused.
emit duplicate
message_startormessage_stopevents.retry is disabled to avoid duplicate model output or tool execution.
pending retry delay.
Comparison with Codex CLI
This was implemented independently, but the current Codex CLI source confirms
the same broad separation between SSE parsing and turn-level recovery:
response.failedand extracts aserver-requested rate-limit delay:
https://github.com/openai/codex/blob/5d325ba2234ead1a6c03313de5d6ecf8df015f1a/codex-rs/codex-api/src/sse/responses.rs#L386-L415
response.completedbecomes astream error:
https://github.com/openai/codex/blob/5d325ba2234ead1a6c03313de5d6ecf8df015f1a/codex-rs/codex-api/src/sse/responses.rs#L491-L526
errors:
https://github.com/openai/codex/blob/5d325ba2234ead1a6c03313de5d6ecf8df015f1a/codex-rs/core/src/session/turn.rs#L1141-L1208
response.completedis retried as a new request:https://github.com/openai/codex/blob/5d325ba2234ead1a6c03313de5d6ecf8df015f1a/codex-rs/core/tests/suite/stream_no_completed.rs#L23-L101
There is one relevant current difference. Codex
mainstill excludesServerOverloadedfrom its generic retry set:https://github.com/openai/codex/blob/5d325ba2234ead1a6c03313de5d6ecf8df015f1a/codex-rs/protocol/src/error.rs#L176-L204
However, openai/codex#31058 proposes dedicated model-capacity recovery with a
separate bounded retry budget, long backoff, and cancellation-aware waits:
https://github.com/openai/codex/blob/49b5b721c12dc1ae674abe47a347baf7f28e82d1/codex-rs/core/src/responses_retry.rs#L16-L102
https://github.com/openai/codex/blob/49b5b721c12dc1ae674abe47a347baf7f28e82d1/codex-rs/core/src/session/turn.rs#L1160-L1235
https://github.com/openai/codex/blob/49b5b721c12dc1ae674abe47a347baf7f28e82d1/codex-rs/core/tests/suite/client.rs#L3528-L3612
CCP cannot copy Codex's boundary exactly because CCP has already opened an
Anthropic-compatible downstream stream. Its additional semantic-output guard is
what makes restarting safe: control frames do not close the retry window, but
real model output does.
Independent prior art
After diagnosing the original buffering problem, we checked other proxy
implementations and found the same incremental-forwarding and heartbeat pattern:
codex-proxyforwards translated chunks immediately and emits heartbeats onlyduring inactive gaps:
CLIProxyAPIselects between upstream chunks and a keepalive ticker, flushingafter every chunk or heartbeat:
https://github.com/router-for-me/CLIProxyAPI/blob/06a4e46f693b4abbd0031dc7e042df3c51771feb/sdk/api/handlers/stream_forwarder.go#L64-L119
auth2apiparses partial SSE input and writes transformed chunks withoutcollecting the complete body first:
https://github.com/AmazingAng/auth2api/blob/a34c011f9fda1013ff3f9299160694c2ab62e4db/src/upstream/streaming.ts#L125-L229
symptom:
[BUG] claude code 接入 cliproxyapi 使用时,模型的输出没有呈现流式,而是一下子蹦出来回答结果 router-for-me/CLIProxyAPI#1620
These projects support the incremental-streaming mechanics; they were not used
as evidence that semantic-aware retry was already implemented elsewhere. No
source code was copied from them.
Verification
cargo test --lib: 466 passedcargo test --test smoke_cutover: 25 passedrustfmt --edition 2024 --checkpassed for the modified Rust filesgit diff --checkpassed503failures retry successfullymessage_startis emitted across retriesoverload-to-success retry through
curlrunning healthy on
127.0.0.1:18765Temporary diagnostic logging used to isolate the bug is intentionally not part
of the PR.