fix: finalize Claude streams after upstream EOF - #6714
Conversation
WalkthroughThe Claude streaming path now finalizes responses when an OpenAI-compatible stream ends with ChangesClaude stream finalization
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant OaiStreamHandler
participant FinalizeStreamResponseOpenAI2Claude
participant ClaudeResponse
OaiStreamHandler->>FinalizeStreamResponseOpenAI2Claude: finalize after [DONE]
FinalizeStreamResponseOpenAI2Claude-->>OaiStreamHandler: return terminal Claude responses
OaiStreamHandler->>ClaudeResponse: emit finalized responses
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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.
Inline comments:
In `@relay/channel/openai/claude_stream_terminal_test.go`:
- Around line 37-57: Before invoking OaiStreamHandler in this test fixture,
explicitly initialize or reset the database, request context, user group,
settings, and cache state alongside the existing Gin request setup. Ensure the
shared fixture state is isolated from prior tests while preserving the current
recorder, request, response, and RelayInfo configuration.
- Around line 29-35: Extend the test fixture around the existing streaming body
to add a second case that omits the `[DONE]` event and terminates at EOF. Run
the same terminal-event assertions and count checks used for the current
`[DONE]` case, preserving the expected event sequence while covering EOF
termination.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a88ae15c-ea8d-41ac-acd5-b2148dfeff42
📒 Files selected for processing (3)
relay/channel/openai/claude_stream_terminal_test.gorelay/channel/openai/helper.gorelaykit/relayconvert/response_compat.go
| body := strings.Join([]string{ | ||
| `data: {"id":"chatcmpl_1","object":"chat.completion.chunk","created":1710000000,"model":"coding-deepseek","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}`, | ||
| `data: {"id":"chatcmpl_1","object":"chat.completion.chunk","created":1710000000,"model":"coding-deepseek","choices":[{"index":0,"delta":{"reasoning_content":"thinking"},"finish_reason":null}]}`, | ||
| `data: {"id":"chatcmpl_1","object":"chat.completion.chunk","created":1710000000,"model":"coding-deepseek","choices":[{"index":0,"delta":{"content":"XAG_OK"},"finish_reason":null}]}`, | ||
| `data: [DONE]`, | ||
| ``, | ||
| }, "\n") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add an EOF termination case.
This fixture always terminates with [DONE]. Add a deterministic case that omits Line 33 and ends at EOF. Assert the same terminal event sequence and counts. The EOF regression path is not protected by this test.
🤖 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 `@relay/channel/openai/claude_stream_terminal_test.go` around lines 29 - 35,
Extend the test fixture around the existing streaming body to add a second case
that omits the `[DONE]` event and terminates at EOF. Run the same terminal-event
assertions and count checks used for the current `[DONE]` case, preserving the
expected event sequence while covering EOF termination.
| recorder := httptest.NewRecorder() | ||
| c, _ := gin.CreateTestContext(recorder) | ||
| c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) | ||
| c.Set(common.RequestIdKey, "claude-terminal-test") | ||
|
|
||
| resp := &http.Response{ | ||
| StatusCode: http.StatusOK, | ||
| Body: io.NopCloser(strings.NewReader(body)), | ||
| Header: http.Header{"Content-Type": []string{"text/event-stream"}}, | ||
| } | ||
| info := &relaycommon.RelayInfo{ | ||
| ChannelMeta: &relaycommon.ChannelMeta{ | ||
| UpstreamModelName: "coding-deepseek", | ||
| }, | ||
| RelayMode: relayconstant.RelayModeChatCompletions, | ||
| RelayFormat: types.RelayFormatClaude, | ||
| DisablePing: true, | ||
| ClaudeConvertInfo: &convmeta.ClaudeConvertInfo{ | ||
| LastMessagesType: convmeta.LastMessageTypeNone, | ||
| }, | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Initialize all shared fixture state explicitly.
The test initializes the request context, but it does not explicitly initialize database, user group, settings, or cache state. Initialize or reset these dependencies before calling OaiStreamHandler so prior tests cannot affect this regression test.
As per coding guidelines, “Initialize database, request context, user group, settings, and cache state explicitly in test fixtures.”
🤖 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 `@relay/channel/openai/claude_stream_terminal_test.go` around lines 37 - 57,
Before invoking OaiStreamHandler in this test fixture, explicitly initialize or
reset the database, request context, user group, settings, and cache state
alongside the existing Gin request setup. Ensure the shared fixture state is
isolated from prior tests while preserving the current recorder, request,
response, and RelayInfo configuration.
Source: Coding guidelines
📝 变更描述 / Description
修复 OpenAI 兼容流在发送
[DONE]或直接 EOF、但没有可转换的终止 chunk 时,Claude Messages 流缺少结束事件的问题。此前
HandleFinalResponse只处理最后一个普通 chunk,随后直接将转换状态标记为完成。若上游没有提供finish_reason终止 chunk,当前 content block 不会关闭,message_delta和message_stop也不会发送,Claude Code 等严格等待 Anthropic SSE 结束事件的客户端会一直停留在生成状态。本次改动在处理最后一个 chunk 后调用现有的 Claude 流 finalizer,由转换状态统一补齐仍未发送的
content_block_stop、message_delta和message_stop。正常已完成的流会由Done状态阻止重复结束事件。同时增加端到端回归测试,覆盖 reasoning、text、
[DONE]的事件顺序及终止事件数量。与 #5345 的区别:该 PR 修改的是重构前的
service/convert.go路径;本 PR 针对当前relaykit架构下无finish_reason终止 chunk 的[DONE]/EOF 收尾路径,不改变正常终止 chunk 的 usage 处理逻辑。🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
📸 运行证明 / Proof of Work
Summary by CodeRabbit
[DONE]without a separate terminal chunk.end_turnstop reason.