Skip to content

fix(responses): reassemble split SSE event/data frames before streaming#2492

Merged
luispater merged 2 commits intorouter-for-me:mainfrom
davidwushi1145:main
Apr 2, 2026
Merged

fix(responses): reassemble split SSE event/data frames before streaming#2492
luispater merged 2 commits intorouter-for-me:mainfrom
davidwushi1145:main

Conversation

@davidwushi1145
Copy link
Copy Markdown
Contributor

Summary

Reassemble split event: + data: SSE fragments in the /v1/responses handler before flushing them downstream, so line-oriented upstream executors no longer emit malformed Responses stream events.

Problem

In the current Responses streaming path, some upstream executors can emit SSE line-by-line rather than as complete framed events. When event: and data: arrive as separate chunks, the handler was incorrectly closing each chunk as its own SSE event, producing malformed output like:

event: response.created

data: {...}

Downstream clients such as OpenClaw then fail to parse the stream and report JSON errors like:

Could not parse message into JSON
Unexpected end of JSON input

This was not limited to a single executor implementation. Any line-oriented openai-response stream routed through the Responses handler could hit the same framing bug.

Fix

Added a small stateful responsesSSEFramer in the Responses handler layer.

It now:

  • buffers standalone event: lines until the matching data: line arrives
  • preserves already complete SSE events unchanged
  • preserves valid data-only SSE events
  • drops delimiter-only leftovers safely on flush
  • handles both \n\n and \r\n\r\n framing
  • inserts the missing line break when adjacent SSE fields arrive in separate chunks without a trailing newline

Also added regression coverage for:

  • split event: + data: chunks in Responses stream forwarding
  • valid full-event chunk passthrough
  • terminal error streaming path compatibility
  • bootstrap / ExecuteStreamWithAuthManager acceptance of split openai-response SSE event lines from non-Codex executors

Testing

  • Verified locally with Go 1.26.1:
    • /tmp/go1.26.1/go/bin/go test ./sdk/api/handlers/openai -count=1
    • /tmp/go1.26.1/go/bin/go test ./sdk/api/handlers -count=1
    • /tmp/go1.26.1/go/bin/go test ./sdk/api/handlers/... -count=1
    • /tmp/go1.26.1/go/bin/go vet ./sdk/api/handlers/...
  • Verified with a temporary patched CLIProxyAPI instance on 127.0.0.1:8317:
    • /v1/models returned 200
    • /v1/responses non-stream returned 200
    • /v1/responses stream returned valid combined event: + data: frames
    • confirmed malformed event: response.created\n\ndata: output no longer appeared

Fixes #2484

Line-oriented upstream executors can emit `event:` and `data:` as
separate chunks, but the Responses handler had started terminating
each incoming chunk as a full SSE event. That split `response.created`
into an empty event plus a later data block, which broke downstream
clients like OpenClaw.

This keeps the fix in the handler layer: a small stateful framer now
buffers standalone `event:` lines until the matching `data:` arrives,
preserves already-framed events, and ignores delimiter-only leftovers.
The regression suite now covers split event/data framing, full-event
passthrough, terminal errors, and the bootstrap path that forwards
line-oriented openai-response streams from non-Codex executors too.

Constraint: Keep the fix localized to Responses handler framing instead of patching every executor
Rejected: Revert to v6.9.7 chunk writing | would reintroduce data-only framing regressions
Rejected: Patch each line-oriented executor separately | duplicates fragile SSE assembly logic
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Do not assume incoming Responses stream chunks are already complete SSE events; preserve handler-layer reassembly for split `event:`/`data:` inputs
Tested: /tmp/go1.26.1/go/bin/go test ./sdk/api/handlers/openai -count=1
Tested: /tmp/go1.26.1/go/bin/go test ./sdk/api/handlers -count=1
Tested: /tmp/go1.26.1/go test ./sdk/api/handlers/... -count=1
Tested: /tmp/go1.26.1/go/bin/go vet ./sdk/api/handlers/...
Tested: Temporary patched server on 127.0.0.1:18317 -> /v1/models 200, /v1/responses non-stream 200, /v1/responses stream emitted combined `event:` + `data:` frames
Not-tested: Full repository test suite outside sdk/api/handlers packages
Copy link
Copy Markdown
Contributor

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

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 a responsesSSEFramer to correctly reassemble split Server-Sent Events (SSE) in the OpenAI responses handler, ensuring that partial chunks are buffered until a complete frame is formed. It also updates line-ending handling and adds unit tests for split-event scenarios. The review feedback highlights potential issues with eager flushing of incomplete frames, redundant newline injections when chunks already contain line breaks, and performance optimizations for line-prefix checks to avoid unnecessary allocations.

Comment thread sdk/api/handlers/openai/openai_responses_handlers.go Outdated
Comment thread sdk/api/handlers/openai/openai_responses_handlers.go
Comment thread sdk/api/handlers/openai/openai_responses_handlers.go
Follow-up review found two real framing hazards in the handler-layer
framer: it could flush a partial `data:` payload before the JSON was
complete, and it could inject an extra newline before chunks that
already began with `\n`/`\r\n`. This commit tightens the framer so it
only emits undelimited events when the buffered `data:` payload is
already valid JSON (or `[DONE]`), skips newline injection for chunks
that already start with a line break, and avoids the heavier
`bytes.Split` path while scanning SSE fields.

The regression suite now covers split `data:` payload chunks,
newline-prefixed chunks, and dropping incomplete trailing data on
flush, so the original Responses fix remains intact while the review
concerns are explicitly locked down.

Constraint: Keep the follow-up limited to handler-layer framing and tests
Rejected: Ignore the review and rely on current executor chunk shapes | leaves partial data payload corruption possible
Rejected: Build a fully generic SSE parser | wider change than needed for the identified risks
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Do not emit undelimited Responses SSE events unless buffered `data:` content is already complete and valid
Tested: /tmp/go1.26.1/go/bin/go test ./sdk/api/handlers/openai -count=1
Tested: /tmp/go1.26.1/go/bin/go test ./sdk/api/handlers -count=1
Tested: /tmp/go1.26.1/go/bin/go vet ./sdk/api/handlers/...
Not-tested: Full repository test suite outside sdk/api/handlers packages
@luispater luispater merged commit fcba912 into router-for-me:main Apr 2, 2026
2 checks passed
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.

OpenClaw使用Response报错

2 participants