Skip to content

fix: optimize Claude tool-use relay logic to prevent state reset and data loss#4204

Open
james-code-hash wants to merge 1 commit intoQuantumNous:mainfrom
james-code-hash:fix/claude-tool-call-empty-input
Open

fix: optimize Claude tool-use relay logic to prevent state reset and data loss#4204
james-code-hash wants to merge 1 commit intoQuantumNous:mainfrom
james-code-hash:fix/claude-tool-call-empty-input

Conversation

@james-code-hash
Copy link
Copy Markdown

@james-code-hash james-code-hash commented Apr 12, 2026

📝 变更描述

優化 Claude 協議 (Anthropic API) 下的工具調用 (tool-use) 轉發邏輯,解決以下問題:

  1. 防止狀態重置:引入 ContentBlockStartSentContentBlockStopSent 追蹤狀態,確保 content_block_start 事件在整個流中只發送一次。這解決了因上游重複發送工具名導致客戶端 input 被重置為 {} 的問題。
  2. 修復指針共享 Bug:在處理 tool_calls 循環時使用局部變量副本,確保 PartialJson 指向正確的地址。
  3. 防止最後一個 Chunk 丟失:調整 StreamResponseOpenAI2Claude 邏輯,確保在 finish_reason 到達時先處理當前 Chunk 的內容數據,再處理 usage 和關閉流。

🚀 变更类型

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

🔗 关联任务

✅ 提交前检查项

  • 人工确认: 我已親自撰寫此描述。
  • 深度理解: 我已完全理解這些更改的工作原理。
  • 本地验证: 已在本地環境透過 Docker 重建並使用 Kimi K2.5 (Nvidia NIM) 進行驗證。

📸 运行证明

使用 moonshotai/kimi-k2.5 測試輸出 SSE 片段:

data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"functions.Bash:0","name":"Bash","input":{}}}
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"command\": \"echo hello\"}"}}

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved event tracking during streaming to prevent duplicate content block notifications.
    • Enhanced message processing and completion handling for streamed responses.

Copilot AI review requested due to automatic review settings April 12, 2026 14:16
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 12, 2026

Walkthrough

This PR adds state tracking to prevent duplicate content_block_start and content_block_stop events during Claude streaming, fixes pointer aliasing in tool call data construction, and corrects usage handling to prevent early termination of event streams.

Changes

Cohort / File(s) Summary
State Tracking Fields
relay/common/relay_info.go
Added ContentBlockStartSent and ContentBlockStopSent maps to ClaudeConvertInfo to track which content block indices have already emitted start/stop events.
Conditional Event Emission & Pointer Aliasing Fix
service/convert.go
Initialized and conditionally emit content_block_start/content_block_stop based on state maps; fixed pointer aliasing by copying loop variables before deriving pointers; modified finish/usage handling to update state instead of early-returning when finish_reason arrives without usage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #2511: Modifies content\_block\_start/delta emission logic for tool calls with guards to skip blocks when name/args are absent.
  • #2854: Adds index-tracking fields and stop/advance helpers to ClaudeConvertInfo for managing content block state transitions.
  • #4128: Updates usage field handling during streaming to prevent data overwrites and selectively backfill token counts.

Suggested reviewers

  • seefs001

Poem

🐰 A hop, a skip, through streaming flows,
No more duplicate starts—the fix now knows!
Pointers align, no aliasing maze,
Tool calls complete through the final phase. 🛠️✨

🚥 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 'fix: optimize Claude tool-use relay logic to prevent state reset and data loss' clearly describes the main changes: fixing state reset issues and preventing data loss in Claude tool-use relay logic.
Linked Issues check ✅ Passed The PR addresses all three root causes from #4203: preventing repeated content_block_start via state tracking, fixing pointer aliasing in tool_calls loop, and handling final chunk data before early returns.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing the three issues identified in #4203: state tracking, pointer aliasing, and data loss prevention in Claude tool-use relay logic.

✏️ 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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the OpenAI→Claude streaming conversion logic (Anthropic Messages SSE) to make tool-use relaying more robust, addressing repeated content_block_start, pointer aliasing during streaming deltas, and end-of-stream chunk handling.

Changes:

  • Track per-index content_block_start / content_block_stop emission to avoid duplicate starts and repeated stops.
  • Fix pointer-aliasing risks by copying streamed delta strings before taking addresses.
  • Reorder finish/usage handling to avoid dropping the final content chunk when finish_reason arrives.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
service/convert.go Adds start/stop tracking for content blocks, fixes partial-json pointer aliasing, adjusts finish/usage closing logic.
relay/common/relay_info.go Extends ClaudeConvertInfo with maps used to track emitted block start/stop events.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread service/convert.go
Comment on lines 521 to 527
blockIndex := base + offset

idx := blockIndex
if toolCall.Function.Name != "" {
if toolCall.Function.Name != "" && !info.ClaudeConvertInfo.ContentBlockStartSent[idx] {
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &idx,
Type: "content_block_start",
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

In the for i, toolCall := range toolCalls loop, idx := blockIndex is a single variable reused across iterations (scope is the loop body). Taking &idx means all appended responses can end up sharing the same pointer and ultimately the last index value. Use common.GetPointer(idx) / create a per-iteration copy (e.g., idxCopy := idx) before taking the address.

Copilot uses AI. Check for mistakes.
Comment thread service/convert.go
Comment on lines +629 to 633
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_stop",
})
info.ClaudeConvertInfo.Done = true
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

message_stop (and setting Done=true) is currently emitted only when oaiUsage != nil. If the upstream sends finish_reason but never sends usage (or usage was never captured), the Claude stream may never be closed. Emit message_stop unconditionally on doneChunk/Done, and keep the message_delta usage payload conditional.

Suggested change
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_stop",
})
info.ClaudeConvertInfo.Done = true
}
}
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_stop",
})
info.ClaudeConvertInfo.Done = true

Copilot uses AI. Check for mistakes.
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
service/convert.go (1)

612-633: ⚠️ Potential issue | 🟠 Major

Emit message_stop independently of usage.

This branch still closes the Claude message only when oaiUsage != nil. But GenRelayInfoClaude sets ShouldIncludeUsage = false, so normal multi-chunk Claude relays can hit doneChunk with no usage payload at all. In that case we send content_block_stop and then return without message_stop, which leaves downstream Anthropic clients hanging on an unterminated stream.
If you still want to forward a later usage-only chunk when one exists, that needs separate pending-finalization state; it shouldn't be the condition that decides whether the message is closed.

Suggested direction
 		if doneChunk || info.ClaudeConvertInfo.Done {
 			oaiUsage := openAIResponse.Usage
 			if oaiUsage == nil {
 				oaiUsage = info.ClaudeConvertInfo.Usage
 			}
 			if oaiUsage != nil {
 				claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
 					Type:  "message_delta",
 					Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
 					Delta: &dto.ClaudeMediaMessage{
 						StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
 					},
 				})
-				claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
-					Type: "message_stop",
-				})
-				info.ClaudeConvertInfo.Done = true
 			}
+			if doneChunk {
+				claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
+					Type: "message_stop",
+				})
+				info.ClaudeConvertInfo.Done = true
+			}
 			return claudeResponses
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/convert.go` around lines 612 - 633, Change the branch that handles
doneChunk / info.ClaudeConvertInfo.Done so that message termination
("message_stop") is emitted unconditionally when doneChunk is true (do not gate
it on oaiUsage != nil); still append a "message_delta" with Usage when either
openAIResponse.Usage or info.ClaudeConvertInfo.Usage is present (use
buildClaudeUsageFromOpenAIUsage and prefer openAIResponse.Usage if non-nil), and
append the StopReason delta (dto.ClaudeMediaMessage with
stopReasonOpenAI2Claude) as before; ensure you set info.ClaudeConvertInfo.Done
once the stop has been emitted to avoid duplicates so downstream Anthropic
clients always receive a terminating "message_stop" even if ShouldIncludeUsage
is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@service/convert.go`:
- Around line 612-633: Change the branch that handles doneChunk /
info.ClaudeConvertInfo.Done so that message termination ("message_stop") is
emitted unconditionally when doneChunk is true (do not gate it on oaiUsage !=
nil); still append a "message_delta" with Usage when either openAIResponse.Usage
or info.ClaudeConvertInfo.Usage is present (use buildClaudeUsageFromOpenAIUsage
and prefer openAIResponse.Usage if non-nil), and append the StopReason delta
(dto.ClaudeMediaMessage with stopReasonOpenAI2Claude) as before; ensure you set
info.ClaudeConvertInfo.Done once the stop has been emitted to avoid duplicates
so downstream Anthropic clients always receive a terminating "message_stop" even
if ShouldIncludeUsage is false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c3a2fa9c-07db-473c-82b1-45ba3a9ee292

📥 Commits

Reviewing files that changed from the base of the PR and between b4df995 and 0b7fa10.

📒 Files selected for processing (2)
  • relay/common/relay_info.go
  • service/convert.go

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.

fix: Claude 協議下工具調用 (tool_use) 出現空 input {} 或數據截斷問題

2 participants