Skip to content

fix: 修复 Claude→OpenAI 流式转换中 tool_call index 偏移导致工具调用丢失 - #5229

Open
feitianbubu wants to merge 1 commit into
QuantumNous:mainfrom
feitianbubu:pr/872584608934464b69e0fc18e3cfcb85d74f20ce
Open

fix: 修复 Claude→OpenAI 流式转换中 tool_call index 偏移导致工具调用丢失#5229
feitianbubu wants to merge 1 commit into
QuantumNous:mainfrom
feitianbubu:pr/872584608934464b69e0fc18e3cfcb85d74f20ce

Conversation

@feitianbubu

@feitianbubu feitianbubu commented Jun 1, 2026

Copy link
Copy Markdown
Member

背景 / 现象

通过 OpenAI 兼容端点(/v1/chat/completions)调用 Claude 渠道(请求转换为 OpenAI Compatible → Claude Messages)、流式 + 带 tools 时,客户端会偶发收到:

  • finish_reason = "tool_calls"
  • tool_calls 为空(toolCallCount = 0),只收到了模型的前导文本

在 claude-opus-4-8 上尤其容易复现,且时好时坏、难以稳定重现。

fix: 移除 fcIdx -1 偏移,修复并发工具调用撞键问题 (#5095)

没有彻底修复, 只是把常量偏移从 -1 改成 0,两个版本都不正确:常量偏移无法表达真实映射关系,只是各自"刚好"覆盖了一种场景。

根因

StreamResponseClaude2OpenAI 直接把 Claude 的内容块序号 当作 OpenAI 的 tool_calls[].index:

  • Claude 的 index所有内容块(text / thinking / tool_use)统一连续计数;
  • OpenAI 的 tool_calls[].index 必须只针对工具调用、从 0 起连续。

因此只要 tool_use 前面存在任何非工具块(一段前导文本,或 thinking 块),tool_use 就落在块 index ≥ 1。转换后发出的 OpenAI tool_call delta 携带 index: 1(或更大),且不存在 index: 0。严格按 0 基下标累积 tool_calls 的客户端无法正确归并,直接丢弃该工具调用;而 finish_reason=tool_calls(由 message_delta
stop_reason=tool_use 映射)仍照常下发——于是出现「有 finish_reason、无 tool_calls」的矛盾。

claude-opus-4-8 等模型在调用工具前常先输出一句前导文本或先思考,因此高频命中;当模型直接以 tool_use(块 0)起始时则正常——这正是"时好时坏"的原因。

修复方案

不再使用常量偏移,改为在 ClaudeResponseInfo 中维护一张 Claude 块序号 → 0 基工具序号 的映射(首次见到该块时分配下一个序号),并仅对 tool_use 块(content_block_start 的 tool_use 及其 input_json_delta)调用;text / thinking 块不占用序号。

效果:

上游块序列 修复前 修复后
text(0), tool(1) index=1 → 被客户端丢弃 index=0
thinking(0), tool(1), tool(2) 1, 2 0, 1
tool(0), tool(1)(#5095 的撞键场景) 0, 1 0, 1 ✓(不撞键)

影响范围

  • Claude 原生渠道、流式、RelayFormat=OpenAI(即 OpenAI 格式客户端调用 Claude 模型)。
  • AWS Bedrock 的 Claude 流式复用同一处理逻辑,一并受益。
  • 非流式、Claude→Claude 透传路径不受影响。

测试

新增回归用例覆盖三种场景并断言 OpenAI tool_calls[].index 为 0 基连续:

修复前:
image
修复后:
image

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed tool-call index assignment in streaming responses when non-tool content blocks precede tool invocations, ensuring correct and collision-free indices.
  • Tests

    • Added regression tests for edge cases in tool-call index mapping.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 51e7b744-8beb-4b9f-8b85-cfc75c98bd29

📥 Commits

Reviewing files that changed from the base of the PR and between 9a2e60d and 1ad39db.

📒 Files selected for processing (2)
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go

Walkthrough

This PR updates Claude-to-OpenAI streaming tool-call index mapping by introducing a per-response ToolIndexMap in ClaudeResponseInfo. Instead of using sequential tool-event counters, the streaming converter now assigns OpenAI tool-call indices deterministically based on Claude block positions, ensuring correct indices when non-tool content blocks precede tool-use blocks. The call site is updated to pass claudeInfo, and three regression tests validate the new behavior.

Changes

Tool Index Mapping for Claude Streaming

Layer / File(s) Summary
ToolIndexMap Data Structure and Helper
relay/channel/claude/relay-claude.go
ClaudeResponseInfo gains a ToolIndexMap map[int]int field and a toolCallIndex(blockIndex int) int helper that maps Claude block indices to OpenAI tool-call ordinals, memoizing the mappings for reuse across streaming events.
Tool Index Mapping Implementation in Streaming Converter
relay/channel/claude/relay-claude.go
StreamResponseClaude2OpenAI now accepts a claudeInfo *ClaudeResponseInfo parameter, computes blockIdx from the response index, and applies claudeInfo.toolCallIndex(blockIdx) to assign OpenAI tool-call indices for both content_block_start and content_block_delta tool events.
Streaming Handler Integration
relay/channel/claude/relay-claude.go
The OpenAI relay format streaming path updates its call to StreamResponseClaude2OpenAI to pass claudeInfo, enabling the per-response tool index mapping.
Tool Index Mapping Test Coverage
relay/channel/claude/relay_claude_test.go
Added mkToolUseStart helper and three regression tests: one verifying leading text blocks do not shift tool indices, one verifying leading thinking_delta blocks do not occupy a tool index, and one ensuring multiple tools without leading blocks produce collision-free indices starting at 0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • QuantumNous/new-api#5095: Directly modifies the same StreamResponseClaude2OpenAI tool-call index logic to prevent collisions by removing a -1 offset.
  • QuantumNous/new-api#2854: Also adjusts Claude-to-OpenAI streaming tool-call indexing via a different approach using ClaudeConvertInfo base/max offset state.
  • QuantumNous/new-api#3080: Modifies the same StreamResponseClaude2OpenAI tool-call streaming pipeline by adding ToolCallStreamStates to ClaudeResponseInfo for emission and ID-name alignment.

Suggested reviewers

  • seefs001
  • Calcium-Ion

Poem

🐰 A rabbit hops through tool-call streams so bright,
Mapping Claude blocks to indices just right,
No more collisions when texts lead the way,
Each tool finds its home, hooray, hooray!
Index maps dance in the OpenAI relay. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.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 directly addresses the core issue: fixing tool_call index offset in Claude→OpenAI streaming conversion that causes tool calls to be lost. It accurately reflects the main change in the PR.
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.

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

@Calcium-Ion
Calcium-Ion force-pushed the main branch 2 times, most recently from 51fdfc5 to 2b6f1df Compare August 30, 2026 15:03
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