fix(streaming): recover transient Codex streams before commitment - #4643
fix(streaming): recover transient Codex streams before commitment#4643jroth1111 wants to merge 6 commits into
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce85c9cc3f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0405648949
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 663adca221
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var terminal bool | ||
| var buffered []cliproxyexecutor.StreamChunk | ||
| var remaining <-chan cliproxyexecutor.StreamChunk | ||
| buffered, remaining, terminal, failOpenReason, errStream = collectRecoveryAttempt(ctx, streamResult, state.policy.MaxBufferBytes) |
There was a problem hiding this comment.
Start keep-alives while recovery buffers the stream
When recovery.attempts > 0 and the response fits within max-buffer-bytes, this synchronous collection does not return until a terminal event. The fresh evidence beyond the earlier default-mode report is that full recovery still blocks the OpenAI, Claude, and Gemini handlers before they receive their first chunk and start the configured keep-alive forwarder, so a long generation sends no downstream bytes despite streaming.keepalive-seconds and can be terminated by an ingress idle timeout. Provide a downstream heartbeat path while buffering without timing out the established upstream connection.
AGENTS.md reference: AGENTS.md:L58-L58
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Agreed. The existing #4459 implements the shared pre-first-chunk heartbeat path across Claude, Gemini, OpenAI chat/completions, and Responses, including the status-code trade-off after the first heartbeat. I do not want to duplicate or silently supersede that active contribution in this PR. I have marked #4643 draft and documented the dependency while requesting maintainer direction on whether to stack/rebase #4643 onto #4459 or wait for #4459 to land. Until reconciled, #4643 should not merge for deployments relying on streaming.keepalive-seconds during the full-buffer window.
|
We reproduced the incomplete-EOF case that this PR is designed to recover. In the affected client session, the original requests were large (about 200k-232k input tokens), but the failure continued after a successful compact reduced the request to 24,987 input tokens (about 96 KiB). Four consecutive attempts returned HTTP 200 with zero usage and then closed before response.completed. Other sessions continued to succeed, so context pressure and a global proxy/account outage are not sufficient explanations. The important recovery boundary for us is: response.queued / response.created / response.in_progress The narrower #4580 bootstrap fix cannot recover once a complete non-empty Responses data frame has committed. #4643 full-stream buffering, same-route retry, losing-attempt discard, and request-scoped incomplete-stream handling match this production case much more closely. We are carrying a temporary local extension to #4580 for provisional lifecycle-only failures, with the intent to drop it once #4643 is reconciled and merged. Please keep regression coverage for both provisional-only EOF and EOF after semantic output; the latter is the key reason the full-stream design is needed. |
|
Additional production case from the same session family: a streaming Responses request can fail synchronously in This path returns before #4580's bootstrap retry loop in Please include regression coverage in #4643 for:
We are adding this as a temporary local extension alongside our #4580-based patch and intend to remove it once #4643 covers the case. |
|
Added The new manager-level regression test exercises direct
A separate exhaustion test verifies that the exact final structured error and HTTP status are returned when the recovery budget is spent, rather than synthesizing an incomplete-stream error. Validation passed:
|
|
Added duration-bounded recovery in When Codex is reachable but transiently returns 502/503/504 or terminates a stream before completion, CPA can otherwise cool the credential and make immediate client retries fail locally with It is explicitly opt-in: Validation: full |
…transient Codex/OpenAI response failures Reconciled with upstream's newer handler-side bootstrap machinery: - keep the handler-side bootstrap retry loop (committer, responses prefix buffering) and gate it off when conductor-level recovery is enabled - drop the PR's conductor-side legacy bootstrap retry loop (superseded by the handler loop); bootstrap eligibility filtering is upstream's bootstrapEligible - emit records terminal failures before the cancellation drop, preserving request-scoped Claude OAuth cancellation accounting - restore the upstream dropped-payload retry test semantics Includes review fixes: legacy bootstrap eligibility, retry-start deadline from first attempt, attempt budget consumed only by real retries, dead branch removal, rewriter fragment/continuation newline handling.
Summary
Fixes #4642.
Why this matters
When Codex is reachable but transiently returns 502/503/504 or ends a stream before completion, normal cooldown accounting can make immediate client retries fail locally with
503 auth_unavailable. Duration-bounded recovery keeps the original request inside CPA, retries the same route, and exposes only the winning response until the configured deadline or client cancellation. Rate limits, authentication failures, and invalid requests remain excluded.Root cause
Codex
response.createdis translated into Claudemessage_start, and the existing bootstrap layers treated the first non-empty translated chunk as irreversible commitment. A later HTTP-200 terminal SSEserver_errorwas therefore surfaced as 502 instead of being recoverable inside the proxy. Normal transient failure accounting could then place the only credential/model into a 60-second cooldown, causing immediate Claude Code retries to return503 auth_unavailablewhen the configured retry wait ceiling was shorter.Behavior
When recovery is enabled explicitly or with a positive attempt cap:
server_error, incomplete EOF, and temporary transport failures may retry within one request-scoped budget;No timeout is added to an established upstream connection.
max-retry-window-secondsonly limits whether another attempt may begin.Configuration
Recovery is disabled by default:
All numeric settings are normalized to explicit safe upper bounds before
time.Durationor atomic-counter conversion.Safety properties
StreamCommitmentUnknown.Tests
Added coverage for:
error/response.failedserver errors and stream commitments;}data:/}event:markers followed by real boundaries, and scanner limits;BaseAPIHandler+CodexExecutor+httptest.Servertwo-attempt Claude recovery, proving one downstreammessage_start, no losing response ID, no error event, and a complete winning stream.Validation performed on current
upstream/dev(c9417c8a):Related work
streaming.keepalive-secondsenabled until the two are reconciled.Retry-After; this PR does not depend on it.🤖 Generated with Claude Code