Skip to content

fix: try to fix tool call issues - #2397

Merged
Calcium-Ion merged 1 commit into
QuantumNous:mainfrom
seefs001:fix/tool-call-claude
Dec 9, 2025
Merged

fix: try to fix tool call issues#2397
Calcium-Ion merged 1 commit into
QuantumNous:mainfrom
seefs001:fix/tool-call-claude

Conversation

@seefs001

@seefs001 seefs001 commented Dec 9, 2025

Copy link
Copy Markdown
Collaborator

fix #2292
同时处理工具调用要开thinking才能使用的问题

Summary by CodeRabbit

  • Bug Fixes

    • Stop streaming immediately when a response is marked complete to avoid extra output.
    • Ensure final stop signals and usage info are emitted consistently at conversation end.
  • Improvements

    • Tool-use now shows tool input arguments immediately after the tool marker.
    • Streaming better distinguishes intermediate "thinking" updates from final text for clearer messages.
    • More reliable termination behavior to prevent duplicate or missing end-of-response signals.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

StreamResponseOpenAI2Claude in service/convert.go changes streaming control flow: adds an early-exit when conversion is done, distinguishes tool-call vs non-tool deltas to emit appropriate content_block_start/delta variants (tool_use, thinking, text), and centralizes finish/stop and usage emission while updating Done and LastMessagesType.

Changes

Cohort / File(s) Summary
OpenAI → Claude streaming conversion
service/convert.go
Reworked StreamResponseOpenAI2Claude control flow: early return when info.ClaudeConvertInfo.Done is true; resolve tool calls from delta.tool_call or fallback to first tool call; emit content_block_start with tool_use and immediate content_block_delta for tool arguments when present; emit thinking or text blocks and deltas for non-tool content; propagate FinishReason to info.FinishReason, emit final stop and usage deltas, set ClaudeConvertInfo.Done, and adjust LastMessagesType and response indexing across branches.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect resolution of toolCall from delta vs fallback to avoid nil dereference.
  • Verify correct indexing and ordering of emitted content_block_start/delta and final stop/usage blocks.
  • Confirm FinishReason propagation, StopReason mapping, and that Done early-exit does not skip required finalization.
  • Check all updates to LastMessagesType for downstream consumers.

Possibly related PRs

Poem

🐇 I tap the stream with gentle paws,
Tool args snug in tiny clause.
Thinking hops, then text takes flight,
A little stop seals day to night.
Hooray — the bytes all bounce just right!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive Title partially relates to the changeset as it mentions tool call fixes, but is vague and overly broad without specifying which issues are being addressed. Make the title more specific by referencing the issue number or describing the specific tool call problem being resolved.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The code changes in StreamResponseOpenAI2Claude address tool call handling and thinking block support, which aligns with the PR objective to fix tool call issues and enable thinking for proper tool functionality.
Out of Scope Changes check ✅ Passed All changes in service/convert.go are focused on improving tool call handling and Claude response conversion, staying within scope of fixing tool call issues referenced in issue #2292.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1007a7 and 1cb2b6f.

📒 Files selected for processing (1)
  • service/convert.go (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jiajunly
Repo: QuantumNous/new-api PR: 1629
File: relay/channel/openai/relay-openai.go:170-174
Timestamp: 2025-08-21T06:31:11.073Z
Learning: In relay/channel/openai/relay-openai.go, the streaming logic for the AddThinkFirst feature is designed so that only the first chunk of a stream gets the "<think>\n" prefix. The final flush in the streaming handler intentionally uses addThink=false because the last chunk should never receive the prefix, even in single-chunk streams where the prefix would have been applied during normal processing.
📚 Learning: 2025-08-21T06:31:11.073Z
Learnt from: jiajunly
Repo: QuantumNous/new-api PR: 1629
File: relay/channel/openai/relay-openai.go:170-174
Timestamp: 2025-08-21T06:31:11.073Z
Learning: In relay/channel/openai/relay-openai.go, the streaming logic for the AddThinkFirst feature is designed so that only the first chunk of a stream gets the "<think>\n" prefix. The final flush in the streaming handler intentionally uses addThink=false because the last chunk should never receive the prefix, even in single-chunk streams where the prefix would have been applied during normal processing.

Applied to files:

  • service/convert.go
🔇 Additional comments (5)
service/convert.go (5)

204-206: LGTM! Early exit guard prevents duplicate processing.

The guard correctly short-circuits when conversion is complete, avoiding redundant event emission.


267-308: LGTM! First-chunk content handling is well-structured.

The code correctly distinguishes between reasoning (thinking) and text content in the first response chunk, emitting appropriate Claude event sequences and tracking state via LastMessagesType.


310-336: LGTM! First-chunk completion is handled correctly.

The code properly handles the edge case where the stream completes in the first chunk, emitting all required termination events (stop block, usage delta, message_stop) and setting the Done flag.


465-490: LGTM! Completion signaling is properly implemented.

The code correctly detects completion conditions, emits all required termination events (stop block, usage delta, message_stop), sets the Done flag, and returns to prevent further processing.


340-364: LGTM! No-choices completion case is handled correctly.

The code properly handles the edge case where there are no choices but conversion is done, emitting appropriate termination events for non-standard OpenAI responses.


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.

@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

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)

225-225: Remove no-op append statement.

This line appends nothing to claudeResponses and serves no purpose. It appears to be leftover code from development.

Apply this diff to remove it:

 		claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
 			Type:    "message_start",
 			Message: msg,
 		})
-		claudeResponses = append(claudeResponses)
 		//claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
 		//	Type: "ping",
 		//})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ebc713 and ea13c0b.

📒 Files selected for processing (1)
  • service/convert.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
service/convert.go (6)
relay/common/relay_info.go (4)
  • ClaudeConvertInfo (33-39)
  • LastMessageTypeThinking (30-30)
  • LastMessageTypeText (28-28)
  • LastMessageTypeTools (29-29)
relay/helper/common.go (1)
  • Done (93-95)
dto/openai_response.go (2)
  • ToolCallResponse (121-127)
  • Usage (222-240)
dto/claude.go (4)
  • ClaudeResponse (450-464)
  • ClaudeMediaMessage (18-37)
  • Thinking (411-414)
  • ClaudeUsage (515-525)
dto/openai_request.go (1)
  • Input (900-904)
common/utils.go (1)
  • GetPointer (292-294)
🔇 Additional comments (5)
service/convert.go (5)

204-206: LGTM: Early exit prevents duplicate processing.

The early return when conversion is already done prevents duplicate message processing and ensures clean state management.


268-309: LGTM: Proper handling of reasoning and text content in first chunk.

The logic correctly differentiates between reasoning (thinking) and text content, creates appropriate content blocks, and updates the state tracker. This addresses the issue where tool calls required "thinking" to be enabled.


311-337: LGTM: Handles single-chunk completion correctly.

Properly handles the case where a response completes in the first chunk by emitting stop blocks, usage information, and setting the Done flag to prevent further processing.


416-458: LGTM: State transitions handled correctly.

The logic properly handles state transitions between thinking, text, and tool content types, including generating appropriate stop blocks and incrementing indices when switching content types.


466-490: LGTM: Finish handling is correct.

Properly handles completion by generating stop blocks, reporting usage statistics with appropriate fallback, and setting the Done flag to prevent further processing.

Comment thread service/convert.go
Comment thread service/convert.go

@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: 1

Caution

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

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

225-225: Remove no-op statement.

append(claudeResponses) with no additional arguments is a no-op that does nothing.

-		claudeResponses = append(claudeResponses)

341-365: Redundant Done check - this branch is unreachable.

The condition info.ClaudeConvertInfo.Done at line 344 will never be true because the function already returns at lines 204-206 when Done is true. This entire branch is dead code.

Consider whether this was intended as a fallback. If not, simplify by removing the dead branch:

 	if len(openAIResponse.Choices) == 0 {
 		// no choices
-		// 可能为非标准的 OpenAI 响应,判断是否已经完成
-		if info.ClaudeConvertInfo.Done {
-			claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
-			oaiUsage := info.ClaudeConvertInfo.Usage
-			if oaiUsage != nil {
-				claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
-					Type: "message_delta",
-					Usage: &dto.ClaudeUsage{
-						InputTokens:              oaiUsage.PromptTokens,
-						OutputTokens:             oaiUsage.CompletionTokens,
-						CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
-						CacheReadInputTokens:     oaiUsage.PromptTokensDetails.CachedTokens,
-					},
-					Delta: &dto.ClaudeMediaMessage{
-						StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
-					},
-				})
-			}
-			claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
-				Type: "message_stop",
-			})
-		}
+		// Empty choices with no content - skip
 		return claudeResponses
♻️ Duplicate comments (2)
service/convert.go (2)

231-263: Validate tool call data before creating content blocks.

This issue was flagged in a previous review but remains unaddressed. When GetFirstToolCall() returns nil, the code creates an empty ToolCallResponse{} (line 239), resulting in a tool_use content block with empty Id and Name fields. This may cause downstream issues when Claude clients process these invalid blocks.

Apply this diff to add validation:

 		if openAIResponse.IsToolCall() {
 			info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
 			var toolCall dto.ToolCallResponse
+			var hasValidToolCall bool
 			if len(openAIResponse.Choices) > 0 && len(openAIResponse.Choices[0].Delta.ToolCalls) > 0 {
 				toolCall = openAIResponse.Choices[0].Delta.ToolCalls[0]
+				hasValidToolCall = true
 			} else {
 				first := openAIResponse.GetFirstToolCall()
 				if first != nil {
 					toolCall = *first
+					hasValidToolCall = true
-				} else {
-					toolCall = dto.ToolCallResponse{}
 				}
 			}
+			if hasValidToolCall {
 				resp := &dto.ClaudeResponse{
 					Type: "content_block_start",
 					ContentBlock: &dto.ClaudeMediaMessage{
 						Id:    toolCall.ID,
 						Type:  "tool_use",
 						Name:  toolCall.Function.Name,
 						Input: map[string]interface{}{},
 					},
 				}
 				resp.SetIndex(0)
 				claudeResponses = append(claudeResponses, resp)
 				// 首块包含工具 delta,则追加 input_json_delta
 				if toolCall.Function.Arguments != "" {
 					claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
 						Index: &info.ClaudeConvertInfo.Index,
 						Type:  "content_block_delta",
 						Delta: &dto.ClaudeMediaMessage{
 							Type:        "input_json_delta",
 							PartialJson: &toolCall.Function.Arguments,
 						},
 					})
 				}
+			}
 		} else {

384-414: Index not incremented after tool call loop.

After processing multiple tool calls, info.ClaudeConvertInfo.Index is set to blockIndex inside the loop (line 413), leaving it at the last tool call's index. The next content block should start at the next available index.

For example: if three tool calls are processed at indices 0, 1, 2, the next non-tool block should use index 3, not 2.

Move the index update outside the loop and increment it:

 			for i, toolCall := range toolCalls {
 				blockIndex := info.ClaudeConvertInfo.Index
 				if toolCall.Index != nil {
 					blockIndex = *toolCall.Index
 				} else if len(toolCalls) > 1 {
 					blockIndex = info.ClaudeConvertInfo.Index + i
 				}

 				idx := blockIndex
 				claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
 					Index: &idx,
 					Type:  "content_block_start",
 					ContentBlock: &dto.ClaudeMediaMessage{
 						Id:    toolCall.ID,
 						Type:  "tool_use",
 						Name:  toolCall.Function.Name,
 						Input: map[string]interface{}{},
 					},
 				})

 				claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
 					Index: &idx,
 					Type:  "content_block_delta",
 					Delta: &dto.ClaudeMediaMessage{
 						Type:        "input_json_delta",
 						PartialJson: &toolCall.Function.Arguments,
 					},
 				})
-
-				info.ClaudeConvertInfo.Index = blockIndex
 			}
+			// Update index to point to the next available slot after all tool calls
+			if len(toolCalls) > 0 {
+				lastToolCall := toolCalls[len(toolCalls)-1]
+				if lastToolCall.Index != nil {
+					info.ClaudeConvertInfo.Index = *lastToolCall.Index + 1
+				} else {
+					info.ClaudeConvertInfo.Index = info.ClaudeConvertInfo.Index + len(toolCalls)
+				}
+			}
🧹 Nitpick comments (1)
service/convert.go (1)

264-266: Remove empty else block.

This empty else block serves no purpose and should be removed.

-		} else {
-
 		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b4796b and a1007a7.

📒 Files selected for processing (1)
  • service/convert.go (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: jiajunly
Repo: QuantumNous/new-api PR: 1629
File: relay/channel/openai/relay-openai.go:170-174
Timestamp: 2025-08-21T06:31:11.073Z
Learning: In relay/channel/openai/relay-openai.go, the streaming logic for the AddThinkFirst feature is designed so that only the first chunk of a stream gets the "<think>\n" prefix. The final flush in the streaming handler intentionally uses addThink=false because the last chunk should never receive the prefix, even in single-chunk streams where the prefix would have been applied during normal processing.
🔇 Additional comments (2)
service/convert.go (2)

204-206: LGTM! Early exit guard prevents duplicate processing.

This correctly short-circuits processing when the stream conversion has already completed.


466-491: Final stop block emission logic is correct.

The handling of finish reason and stop block emission is well-structured. Setting info.ClaudeConvertInfo.Done = true after emitting the stop blocks ensures the early exit guard will prevent duplicate processing.

Note: The info.ClaudeConvertInfo.Done condition at line 466 is redundant due to the early exit at lines 204-206, but it's harmless.

Comment thread service/convert.go
@seefs001
seefs001 force-pushed the fix/tool-call-claude branch from a1007a7 to 1cb2b6f Compare December 9, 2025 05:56
@seefs001
seefs001 marked this pull request as ready for review December 9, 2025 06:06
@Calcium-Ion
Calcium-Ion merged commit fca015c into QuantumNous:main Dec 9, 2025
1 check passed
ennnnny pushed a commit to ennnnny/new-api that referenced this pull request Mar 17, 2026
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.

new-api配置gemini3模型后,在cc中使用无输出 Claude 转 OpenRouter

2 participants