Skip to content

fix: stream responses transform through chat completions#154

Merged
ymkiux merged 1 commit into
mainfrom
fix/builtin-transform-stream-retry
May 9, 2026
Merged

fix: stream responses transform through chat completions#154
ymkiux merged 1 commit into
mainfrom
fix/builtin-transform-stream-retry

Conversation

@awsl233777
Copy link
Copy Markdown
Collaborator

@awsl233777 awsl233777 commented May 9, 2026

Summary

  • route streaming /v1/responses bridge requests directly through upstream /chat/completions streaming instead of blocking on upstream /responses
  • translate chat-completions SSE chunks into Responses SSE events for Codex-compatible clients
  • report failed Responses SSE when upstream chat streams error, abort, or end before [DONE]
  • add regression coverage for avoiding a hanging /responses probe and for premature upstream stream termination

Validation

  • NODE_PATH=/home/moltbot/clawd-wechat/codexmate/node_modules node --test tests/unit/openai-bridge-upstream-responses.test.mjs tests/unit/builtin-proxy-responses-shim.test.mjs
  • NODE_PATH=/home/moltbot/clawd-wechat/codexmate/node_modules npm run lint
  • NODE_PATH=/home/moltbot/clawd-wechat/codexmate/node_modules npm run test:unit — 527 tests passed
  • NODE_PATH=/home/moltbot/clawd-wechat/codexmate/node_modules npm run test:e2e
  • git diff --check

Note: this worktree reuses the existing dependency install from /home/moltbot/clawd-wechat/codexmate/node_modules via NODE_PATH because the clean worktree does not have its own node_modules directory.

Summary by CodeRabbit

  • New Features
    • Added streaming support for responses via real-time event delivery with automatic format conversion.
    • Improved error handling for stream failures with descriptive error messages and proper event emission.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The OpenAI bridge now routes SSE-accepting /responses streaming requests to upstream /chat/completions instead of probing /responses, translates chat-completion chunks into Responses SSE format, and handles stream completion and early-termination errors with appropriate event emission.

Changes

Responses SSE Streaming Conversion

Layer / File(s) Summary
Stream Response Safety
cli/openai-bridge.js
writeSse now guards against writes to missing, ended, or destroyed responses; formatUpstreamStreamError normalizes error shapes into strings.
Stream Event Translation
cli/openai-bridge.js
appendChatStreamToolCall accumulates tool-call fragments by id/type/function/args; writeChatCompletionChunkAsResponsesSse converts chat-completion delta chunks to Responses SSE output items and text deltas.
Stream Completion and Failure
cli/openai-bridge.js
finishChatStreamResponsesSse emits completion events (text/item done, response completed, [DONE]); failChatStreamResponsesSse emits failure and [DONE].
Stream Orchestration
cli/openai-bridge.js
streamChatCompletionsAsResponsesSse proxies upstream streaming /chat/completions, reads and translates chunks, validates sizes, detects [DONE], and handles parse/timeout/early-end errors.
HTTP Handler Integration
cli/openai-bridge.js
HTTP handler detects streamRequested && wantsSse, converts Responses→chat/completions, validates, invokes streaming orchestration, and returns JSON or SSE errors on failure.
Tests and Validation
tests/unit/openai-bridge-upstream-responses.test.mjs
New streaming test verifies upstream /chat/completions called with stream: true and bridge returns Responses SSE. Error test validates response.failed emission when upstream stream ends early without [DONE].

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • SakuraByteCore/codexmate#90: Introduces the initial OpenAI bridge and Responses→chat/completions conversion with SSE support; this PR adds dedicated SSE streaming conversion logic.
  • SakuraByteCore/codexmate#151: Modifies Responses→ChatCompletions field normalization in the non-streaming path; this PR extends the bridge with the streaming conversion path.

Suggested reviewers

  • ymkiux

Poem

🐰 A stream of responses flows so bright,
Chat completions shimmer in the night,
Tool calls dance and deltas gleam,
SSE events paint the dream,
From upstream clouds, pure data streams!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: implementing streaming transformation of /v1/responses requests through /chat/completions upstream, which is the core objective of the PR.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/builtin-transform-stream-retry

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
tests/unit/openai-bridge-upstream-responses.test.mjs (1)

91-170: 💤 Low value

Optional: Add a hang-guard for the /responses-probe regression test.

The upstream /v1/responses handler at lines 95-99 intentionally never responds. If a future change causes the bridge to start probing /responses again, the bridge request will hang indefinitely instead of producing a clear assertion failure, since requestText has no timeout. Consider adding a short upstream timeout (e.g., res.socket?.setTimeout) or a wall-clock guard around the request to fail fast with a useful message.

🤖 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/openai-bridge-upstream-responses.test.mjs` around lines 91 - 170,
The /v1/responses probe in the test intentionally never responds, which can
cause the test to hang indefinitely; add a short hang-guard to fail fast —
either set a socket timeout on the incoming upstream request inside the upstream
handler for '/v1/responses' (e.g., use req.socket.setTimeout(...) and in its
callback end the response with a 504 or similar) or wrap the requestText call in
the test with a wall-clock timeout promise that rejects after a few seconds and
fails the test; update the test named "openai-bridge streams chat/completions
directly when Responses client requests SSE" referencing the upstream handler
and requestText so the test will abort quickly if /v1/responses is ever probed
again.
cli/openai-bridge.js (2)

910-925: 💤 Low value

Detach the close listener after upstream finishes.

res.once('close', abortUpstream) is registered for the lifetime of the response. After successful completion the listener still fires once close is emitted; upstreamReq.destroy() on an already-finished request is a safe no-op, so this is not a bug — but the state and upstreamReq closures stay reachable from the res event listener until the socket is GC'd. Removing the listener (and pairing it with res.off('close', abortUpstream) once finish resolves) keeps the lifetime tighter and avoids a small retention window when many concurrent streams are in flight.

🤖 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 `@cli/openai-bridge.js` around lines 910 - 925, The close listener attached
with res.once('close', abortUpstream) should be removed when the upstream work
finishes to avoid retaining the finish/upstreamReq/settled closures; update code
so that finish (or the promise resolution path) calls res.off('close',
abortUpstream) (guarding that typeof res.off === 'function') after it marks
settled and resolves, and also ensure any other early-return paths call the same
cleanup so abortUpstream is detached whenever the upstream completes.

788-794: ⚖️ Poor tradeoff

Streaming tool-call argument deltas are not emitted.

writeChatCompletionChunkAsResponsesSse accumulates delta.tool_calls into state.toolCalls without emitting SSE events during the stream. Tool calls are only announced in finishChatStreamResponsesSse on completion with response.output_item.added immediately followed by response.output_item.done. This differs from text content handling, which emits response.output_text.delta events during streaming.

Clients relying on response.function_call_arguments.delta to render progressive tool-call arguments or begin execution before all arguments arrive will instead receive complete tool calls all-at-once. While this defers alignment with OpenAI Responses API streaming events, it is reasonable to defer given effort vs. immediate impact.

Also applies to: 818-841

🤖 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 `@cli/openai-bridge.js` around lines 788 - 794, The function
writeChatCompletionChunkAsResponsesSse currently accumulates streaming tool-call
fragments into state.toolCalls via appendChatStreamToolCall but never emits
progressive SSE events; change it so that whenever appendChatStreamToolCall
appends or updates a tool call fragment inside
writeChatCompletionChunkAsResponsesSse you also emit a SSE event named
response.function_call_arguments.delta (matching the text delta flow) carrying
the incremental arguments/fragment and identifying which toolCall (e.g., by
index or id) is being updated; ensure the same logic is added to the analogous
block around lines 818-841 and keep finishChatStreamResponsesSse behavior
(response.output_item.added + response.output_item.done) for completion only.
🤖 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 `@cli/openai-bridge.js`:
- Around line 910-925: The close listener attached with res.once('close',
abortUpstream) should be removed when the upstream work finishes to avoid
retaining the finish/upstreamReq/settled closures; update code so that finish
(or the promise resolution path) calls res.off('close', abortUpstream) (guarding
that typeof res.off === 'function') after it marks settled and resolves, and
also ensure any other early-return paths call the same cleanup so abortUpstream
is detached whenever the upstream completes.
- Around line 788-794: The function writeChatCompletionChunkAsResponsesSse
currently accumulates streaming tool-call fragments into state.toolCalls via
appendChatStreamToolCall but never emits progressive SSE events; change it so
that whenever appendChatStreamToolCall appends or updates a tool call fragment
inside writeChatCompletionChunkAsResponsesSse you also emit a SSE event named
response.function_call_arguments.delta (matching the text delta flow) carrying
the incremental arguments/fragment and identifying which toolCall (e.g., by
index or id) is being updated; ensure the same logic is added to the analogous
block around lines 818-841 and keep finishChatStreamResponsesSse behavior
(response.output_item.added + response.output_item.done) for completion only.

In `@tests/unit/openai-bridge-upstream-responses.test.mjs`:
- Around line 91-170: The /v1/responses probe in the test intentionally never
responds, which can cause the test to hang indefinitely; add a short hang-guard
to fail fast — either set a socket timeout on the incoming upstream request
inside the upstream handler for '/v1/responses' (e.g., use
req.socket.setTimeout(...) and in its callback end the response with a 504 or
similar) or wrap the requestText call in the test with a wall-clock timeout
promise that rejects after a few seconds and fails the test; update the test
named "openai-bridge streams chat/completions directly when Responses client
requests SSE" referencing the upstream handler and requestText so the test will
abort quickly if /v1/responses is ever probed again.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c94139fb-5d8f-473b-9152-71cbf59399d1

📥 Commits

Reviewing files that changed from the base of the PR and between 3605cae and 7651e45.

📒 Files selected for processing (2)
  • cli/openai-bridge.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
📜 Review details
🔇 Additional comments (1)
cli/openai-bridge.js (1)

858-877: ⚡ Quick win

Move error details into response object per OpenAI Responses API shape — but verify official spec first.

The current failChatStreamResponsesSse places error at the top level: { type, response: {...}, error: string }. However, this conflicts with other error handlers in the same file (lines ~909, ~943) which also use top-level error. Before restructuring, verify against the official OpenAI Responses API schema whether error should live inside response as a nested object. Official documentation is incomplete; confirm with OpenAI's TypeScript SDK types or API reference whether consumers expect response.error.message or top-level error, and whether all error handlers should be consistent.

@ymkiux ymkiux merged commit 3e1deb0 into main May 9, 2026
7 checks passed
@ymkiux ymkiux deleted the fix/builtin-transform-stream-retry branch May 9, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants