Skip to content

fix: 499 completed Codex responses streams#1248

Closed
miraserver wants to merge 1 commit into
ding113:devfrom
miraserver:499-codex-fix
Closed

fix: 499 completed Codex responses streams#1248
miraserver wants to merge 1 commit into
ding113:devfrom
miraserver:499-codex-fix

Conversation

@miraserver

@miraserver miraserver commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Problem

Codex CLI closes the downstream HTTP connection immediately after receiving the terminal SSE event (response.completed). CCH observed the client abort before the internal reader finished, so successful requests were logged as 499 CLIENT_ABORTED and counted as provider failures.

Root cause: The internal reader that builds allContent breaks early on clientAbort. With truncated content, the deferred finalization had no terminal evidence, so it fell through to the client-abort branch.

Related: #1083, #1242, #985

Solution

Track Codex terminal state, usage metrics, service tier, and prompt_cache_key incrementally in a TransformStream before tee() — the data is captured regardless of when the client disconnects.

The tracker is the primary source of truth for Codex deferred streams; allContent is the fallback for non-Codex paths.

Terminal state semantics

Terminal event Result
response.completed, response.incomplete 200 success (even if client aborted after receiving it)
response.failed, response.error, error 502 + recordFailure (circuit breaker)
No terminal event seen 499 CLIENT_ABORTED / 502 (original behavior)
Non-Codex stream Entirely unaffected

Changes

  • response-handler.tscreateCodexResponsesTerminalStateTracker: incremental SSE parser with finalize() for trailing data without a boundary newline (last-event-wins semantics); tracker-first priority for usage/service_tier/prompt_cache_key in deferred finalization; providerType guard uses deferred meta, not current session provider (correct under failover).
  • stream-finalization.ts — add optional providerType to DeferredStreamingFinalization; normalize at set-time from meta or session provider.
  • Tests — 30 cases covering the full matrix: completed/failed/incomplete/error/response.error terminal states, abort timing, provider-mismatch, multiline SSE data, trailing-\n\n-less event, non-Codex unaffected, prompt_cache_key preservation.

Testing

  • vitest: 30/30 pass
  • typecheck: exit 0
  • biome check: clean

Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

本 PR 为代理响应流最终化引入 Codex SSE 终止态解析与转发端增量追踪,扩展 DeferredStreamingFinalization 以携带 providerType,并调整 finalize 判定、状态映射、会话清理与熔断落库逻辑;同时补充大量针对 /v1/responses SSE 的单元测试覆盖。

Changes

Codex SSE 完成状态与客户端中止语义处理

Layer / File(s) Summary
延迟元数据的提供商类型扩展
src/app/v1/_lib/proxy/stream-finalization.ts
DeferredStreamingFinalization 新增可选 providerType 字段;setDeferredStreamingFinalization 在写入前优先使用 meta.providerType,否则从 session.provider?.providerType 推导并合并到写入的元数据。
Codex SSE 终止态解析契约与增量 tracker
src/app/v1/_lib/proxy/response-handler.ts
新增 CodexResponsesTerminalState 与从 SSE 文本推导终止态的解析函数,以及基于事件边界的增量缓冲 tracker,支持按流式 chunk 累积并缓存与终止态关联的 usage 指标;导出测试工厂 __codexResponsesTerminalStateTrackerForTests
流式转发侧的终止态追踪集成
src/app/v1/_lib/proxy/response-handler.ts
在 SSE 转发的 pipeThrough 中基于 deferred meta/providerType 创建 forwardedCodexTerminalTracker,在 TransformStream 的 transform/flush 解码并推入 tracker,并在 finalize 时将 tracker.getTerminalState() 传入最终判定;当主 usage 缺失且 provider 为 codex 时回退使用 tracker.getUsageMetrics()/getServiceTier()
流完成判定与假 200 检测逻辑
src/app/v1/_lib/proxy/response-handler.ts
finalizeDeferredStreamingFinalizationIfNeeded 增加 forwardedCodexTerminalState 入参并结合 allContent 推导 codexTerminalStatecodexTerminalNonSuccessstreamCompletedForFinalization;将假 200 检测条件改为仅在 streamCompletedForFinalization === true && upstreamStatusCode === 200 时触发。
客户端中止时的状态码与错误信息映射
src/app/v1/_lib/proxy/response-handler.ts
重写 effectiveStatusCodeerrorMessage 的映射:当 Codex terminal 为非成功态时优先映射为 502 并使用 Codex 错误码;其它非完成场景在客户端中断时映射为 499 且 CLIENT_ABORTED,否则映射为 502 并使用 abortReason/STREAM_ABORTED
会话清理与熔断器分支条件更新
src/app/v1/_lib/proxy/response-handler.ts
将会话清理与熔断写入的门控从依赖 streamEndedNormally 调整为依赖 codexTerminalNonSuccess!streamCompletedForFinalization;在 Codex terminal 非成功态分支中直接清理会话绑定、记录告警,并在允许熔断且 terminalState 非 incomplete 时写入 circuit-breaker failure 后以 retry_failed 将失败落入 provider chain。
测试基础设施与 Codex SSE 场景覆盖
tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
扩展测试工厂以支持 overrides(动态 pathname/providerType)、新增 setDeferredMeta 等辅助函数;新增大量用例覆盖 Codex /v1/responses SSE 在不同事件与不同 client/upstream abort 时刻的状态映射、会话清理和熔断记录断言,并验证终态跟踪器的 usage/service-tier 语义。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ding113/claude-code-hub#759: 两者均修改 finalizeDeferredStreamingFinalizationIfNeeded 的完成/中止语义与状态映射逻辑,存在代码层面重叠。
  • ding113/claude-code-hub#1113: 与本 PR 在客户端 abort 监听器绑定与清理相关的测试与实现区域有直接关系。
  • ding113/claude-code-hub#762: 修改了流最终化/usage 导出路径,与本 PR 在 usage/terminal 状态推导上存在相似改动。

Suggested reviewers

  • ding113
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题清晰准确地总结了主要修复:解决 Codex 应答流在接收到终态事件后客户端断开连接时被错误记录为 499 的问题。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed 拉取请求描述清晰地说明了问题、解决方案和实现细节,与变更集高度相关。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added bug Something isn't working area:core labels Jun 4, 2026
@miraserver miraserver changed the title fix: preserve completed Codex responses streams fix: 499 completed Codex responses streams Jun 4, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces logic to handle client aborts gracefully for Codex streams by parsing SSE data to determine if a terminal response.completed event was received before the abort occurred. If completed, the stream is treated as successful, preventing unnecessary error reporting, session clearing, or circuit breaker triggers. Extensive unit tests have been added to cover these scenarios. The feedback suggests removing the clientAborted restriction when detecting the Codex terminal state to also handle other non-normal stream terminations (like timeouts or connection drops) after a successful completion.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9bb85df4b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated
@github-actions github-actions Bot added the size/L Large PR (< 1000 lines) label Jun 4, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR correctly addresses the 499-misclassification for Codex Responses SSE streams. The terminal-state detection logic in getCodexResponsesTerminalState is sound — it iterates all parsed SSE events and tracks the last terminal state, correctly handling edge cases like response.failed after response.completed. The providerType capture in deferred streaming metadata prevents stale session state from influencing the finalization decision. Test coverage is thorough with 10 new cases covering all specified scenarios.

PR Size: L

  • Lines changed: 670 (652 additions, 18 deletions)
  • Files changed: 3

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

No significant issues identified. Key validation points:

  • Operator precedence in the codexTerminalState ternary is correct — && binds tighter than ?:, so all five conditions must hold before getCodexResponsesTerminalState is invoked.
  • Last-wins semantics in the switch statement ensures that a terminal failure after response.completed is not misclassified as success (verified by the "terminal failure follows response.completed" test).
  • shouldDetectFake200 now gates on streamCompletedForFinalization instead of streamEndedNormally, which is correct — when Codex has emitted response.completed, the protocol evidence supports running fake-200 detection.
  • Comment accuracy: All inline comments were updated to match the new semantics (e.g., "流自然结束" → "流已完成", added Codex bullet point).
  • parseSSEData safety: JSON parse errors within it are caught, and partial/incomplete SSE events gracefully degrade — getCodexResponsesTerminalState correctly skips non-object data.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Thorough (10 new tests)
  • Code clarity - Good

Automated review by Claude AI

Comment thread src/app/v1/_lib/proxy/response-handler.ts
Comment thread tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
@coderabbitai coderabbitai Bot requested a review from ding113 June 4, 2026 21:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts (1)

268-270: 💤 Low value

可选:移除冗余的 mock 清理调用。

第 268 行的 vi.clearAllMocks() 已经清理了所有 mock(包括 testState.cancelTasktestState.cleanupTask),因此第 269-270 行的 mockClear() 调用是冗余的。

♻️ 建议的简化
  beforeEach(() => {
    testState.asyncTasks = [];
    vi.clearAllMocks();
-   testState.cancelTask.mockClear();
-   testState.cleanupTask.mockClear();
    vi.restoreAllMocks();
  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts` around
lines 268 - 270, 移除冗余的 mock 清理调用:在测试中已调用 vi.clearAllMocks()(第 268 行),它已清除所有
mocks,因此删除后续对 testState.cancelTask.mockClear() 和
testState.cleanupTask.mockClear() 的显式调用以简化代码并避免重复清理。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts`:
- Around line 268-270: 移除冗余的 mock 清理调用:在测试中已调用 vi.clearAllMocks()(第 268
行),它已清除所有 mocks,因此删除后续对 testState.cancelTask.mockClear() 和
testState.cleanupTask.mockClear() 的显式调用以简化代码并避免重复清理。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e17588bf-c17d-4bcd-85ae-a1771ce5cef8

📥 Commits

Reviewing files that changed from the base of the PR and between e9bb85d and 4e0283b.

📒 Files selected for processing (3)
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/stream-finalization.ts
  • tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/v1/_lib/proxy/stream-finalization.ts
  • src/app/v1/_lib/proxy/response-handler.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3062d6a4a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/app/v1/_lib/proxy/response-handler.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47f513f6b4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated
Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b3cd58c056

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc0b5aa3f9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/app/v1/_lib/proxy/response-handler.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1723378be8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const codexTerminalState = shouldInspectCodexTerminalState
? forwardedCodexTerminalState !== "none"
? forwardedCodexTerminalState
: getCodexResponsesTerminalState(allContent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require a complete SSE boundary before overriding aborts

When a Codex stream aborts after the stats reader has buffered event: response.completed\ndata: ... but before the terminating blank line, the forwarded tracker correctly stays at none, but this fallback parses allContent with parseSSEData, which flushes trailing data as an event even without an SSE boundary. That makes a non-normal/client-aborted partial terminal chunk get recorded as status 200 even though the terminal SSE event was never dispatchable to the client and the upstream stream did not end normally.

Useful? React with 👍 / 👎.

@miraserver miraserver marked this pull request as draft June 5, 2026 00:19
Codex CLI closes the connection immediately after receiving the terminal
SSE event (response.completed/incomplete). Previously CCH observed the
client abort before the upstream reader finished, so successful requests
were logged as 499 CLIENT_ABORTED.

Root cause: the internal reader that builds allContent breaks early on
clientAbort, so allContent is truncated and misses the terminal event.
Fix: track Codex terminal state, usage, service_tier and prompt_cache_key
incrementally in a TransformStream before tee(), so the data is captured
regardless of abort timing.

Changes:
- response-handler.ts: add createCodexResponsesTerminalStateTracker
  (incremental SSE parser, last-event-wins, finalize() for trailing
  data without boundary newline). tracker is the primary source of
  truth for Codex deferred streams; allContent is the fallback.
  codexTerminalNonSuccess (failed/error) -> 502 + recordFailure.
  codexTerminalFinished (completed/incomplete) + clientAbort -> 200.
  providerType captured in deferred meta at set-time for failover.
- stream-finalization.ts: add optional providerType to
  DeferredStreamingFinalization; normalize at set-time from meta or
  session provider.
- tests: 30 cases covering full terminal-state matrix
  (completed/failed/incomplete/error/response.error, abort timing,
  provider-mismatch, multiline SSE, trailing-\n\n-less event,
  non-Codex unaffected, prompt_cache_key preservation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@miraserver

Copy link
Copy Markdown
Contributor Author

Superseded by #1249: same fix squashed into a single clean commit. This thread accumulated bot-review noise from 8 intermediate iterations — closing to keep the review history readable.

@miraserver miraserver closed this Jun 5, 2026
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Jun 5, 2026
@miraserver miraserver deleted the 499-codex-fix branch June 5, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core bug Something isn't working size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant