Skip to content

fix: finalize Claude streams after upstream EOF - #6714

Closed
kuangzhenfeng wants to merge 1 commit into
QuantumNous:mainfrom
kuangzhenfeng:dev
Closed

fix: finalize Claude streams after upstream EOF#6714
kuangzhenfeng wants to merge 1 commit into
QuantumNous:mainfrom
kuangzhenfeng:dev

Conversation

@kuangzhenfeng

@kuangzhenfeng kuangzhenfeng commented Aug 8, 2026

Copy link
Copy Markdown

📝 变更描述 / Description

修复 OpenAI 兼容流在发送 [DONE] 或直接 EOF、但没有可转换的终止 chunk 时,Claude Messages 流缺少结束事件的问题。

此前 HandleFinalResponse 只处理最后一个普通 chunk,随后直接将转换状态标记为完成。若上游没有提供 finish_reason 终止 chunk,当前 content block 不会关闭,message_deltamessage_stop 也不会发送,Claude Code 等严格等待 Anthropic SSE 结束事件的客户端会一直停留在生成状态。

本次改动在处理最后一个 chunk 后调用现有的 Claude 流 finalizer,由转换状态统一补齐仍未发送的 content_block_stopmessage_deltamessage_stop。正常已完成的流会由 Done 状态阻止重复结束事件。

同时增加端到端回归测试,覆盖 reasoning、text、[DONE] 的事件顺序及终止事件数量。

#5345 的区别:该 PR 修改的是重构前的 service/convert.go 路径;本 PR 针对当前 relaykit 架构下无 finish_reason 终止 chunk 的 [DONE]/EOF 收尾路径,不改变正常终止 chunk 的 usage 处理逻辑。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 非重复提交: 已检索现有 Issues 与 PRs,并在变更描述中说明与相近 PR 的差异。
  • Bug fix 说明: 已关联相同结束事件缺失症状的 Issue。
  • 变更理解: 已确认 EOF 收尾路径通过现有 finalizer 生成完整 Claude SSE 生命周期。
  • 范围聚焦: 本 PR 未包含与当前问题无关的代码改动。
  • 本地验证: 已运行定向回归测试并通过。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

$ GOWORK=off go test -vet=off ./relay/channel/openai -run TestOaiStreamHandlerFinalizesClaudeStreamWithoutTerminalChunk -count=1
ok  github.com/QuantumNous/new-api/relay/channel/openai  0.941s

Summary by CodeRabbit

  • Bug Fixes
    • Improved Claude-format streaming responses when the upstream stream ends with [DONE] without a separate terminal chunk.
    • Final response events are now emitted correctly, including the completed text, usage information, event ordering, and end_turn stop reason.
    • Ensures streamed Claude responses conclude reliably and consistently.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The Claude streaming path now finalizes responses when an OpenAI-compatible stream ends with [DONE] without a terminal chunk. A compatibility wrapper exposes finalization, and a test verifies event ordering, text, stop reason, usage, and completion status.

Changes

Claude stream finalization

Layer / File(s) Summary
Delegate Claude stream finalization
relaykit/relayconvert/response_compat.go, relay/channel/openai/helper.go
The compatibility layer exposes FinalizeStreamResponseOpenAI2Claude. The handler emits the returned terminal Claude responses.
Validate terminal stream behavior
relay/channel/openai/claude_stream_terminal_test.go
The test covers streams ending with [DONE] without a terminal chunk. It verifies event sequencing, final text, end_turn, usage, and successful completion.

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
Loading

Possibly related PRs

Suggested reviewers: calcium-ion

Poem

I’m a rabbit, hopping through the stream,
[DONE] now closes every dream.
Claude events line up in flight,
Text and end_turn land just right.
Usage marks the journey bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: finalizing Claude streams after upstream EOF.
✨ 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.

@coderabbitai coderabbitai 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 823e263 and ee718d1.

📒 Files selected for processing (3)
  • relay/channel/openai/claude_stream_terminal_test.go
  • relay/channel/openai/helper.go
  • relaykit/relayconvert/response_compat.go

Comment on lines +29 to +35
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")

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.

🎯 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.

Comment on lines +37 to +57
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,
},
}

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.

📐 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

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.

1 participant