Skip to content

fix: prevent duplicate tool calls in Responses-to-Chat streaming - #6225

Merged
Calcium-Ion merged 1 commit into
QuantumNous:mainfrom
nikuleo:fix/responses-chat-duplicate-tool-call
Jul 18, 2026
Merged

fix: prevent duplicate tool calls in Responses-to-Chat streaming#6225
Calcium-Ion merged 1 commit into
QuantumNous:mainfrom
nikuleo:fix/responses-chat-duplicate-tool-call

Conversation

@nikuleo

@nikuleo nikuleo commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

📝 变更描述 / Description

Responses-to-Chat 流式转换里,工具调用的 key 有两种形态:流式事件带 output_index,注册成 output:<n>response.completed 的完整 outputterminalOutputChunks 里被构造成没有 output_index 的合成事件,算出来是 item:<id>ensureToolForEvent 之前只查 toolByKey,key 对不上就直接建新 tool——于是同一个 function call 拿到第二个 tool_calls index,完整 arguments 被重发一遍。对自动执行工具的 Agent 客户端,这等于同一个工具被调用两次。

而 OpenAI 官方的 response.completed 本来就携带完整 output(含已流式发送过的 function_call item),这是必须正确处理的正常事件形态。

修复:ensureToolForEvent 在直接 key 未命中时,先用已登记的 itemIDToKeycallIDToKey 反查已存在的 tool,找到就把新 key alias 过去;全部未命中才创建新 tool。这和同文件里非流式路径 ResponsesBufferedAccumulator.findToolIndex 的既有语义一致(它本来就会按 item ID 反查再新建)。terminal-only 工具调用(部分兼容上游只在终止事件里给出工具调用)不受影响,反查不到时仍走原有新建逻辑,对应的既有测试保持通过。

🚀 变更类型 / Type of change

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

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

新增回归测试 TestResponsesStreamEventToChatChunksDoesNotResendToolOnTerminalOutput(事件序列:output_item.added -> function_call_arguments.delta -> 携带完整 output 的 response.completed),修复前失败、修复后通过。

修复前(main @ a63364d):

--- FAIL: TestResponsesStreamEventToChatChunksDoesNotResendToolOnTerminalOutput (0.00s)
    Error: Not equal:
           expected: "{\"q\":\"x\"}"
           actual  : "{\"q\":\"x\"}{\"q\":\"x\"}"

修复后:

--- PASS: TestResponsesStreamEventToChatChunksDoesNotResendToolOnTerminalOutput (0.00s)

$ go vet ./service/relayconvert/...
$ go test ./service/relayconvert/... -count=1
ok      github.com/QuantumNous/new-api/service/relayconvert                           1.790s
ok      github.com/QuantumNous/new-api/service/relayconvert/internal/oai_chat        2.709s
ok      github.com/QuantumNous/new-api/service/relayconvert/internal/oai_responses   2.221s

既有测试全部通过,terminal-only 工具调用场景(TestResponsesStreamEventToChatChunksUsesTerminalDoneOutput)不受影响。另已本地验证:多个不同工具仍分配不同 index;流式只发送部分 arguments、终止事件携带完整 arguments 时,只在同一 index 下补发差量。

本 PR 由 AI 辅助完成(AI-assisted),我已逐行审阅并在本地验证。

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Prevented duplicate tool calls from appearing when a response stream ends with a completed event.
    • Preserved tool call arguments and final completion status correctly across streamed responses.

When a function call was already registered under its output_index key
via response.output_item.added, the synthetic events built from the
terminal response.completed output carry no output_index and resolve to
a different item-based key. ensureToolForEvent then created a second
tool index and resent the full arguments, so Chat Completions clients
received the same tool call twice.

Reuse the tool registered under itemIDToKey/callIDToKey before creating
a new one, and alias the new key to the existing tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 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: 54a1ac28-de0a-453a-8d13-5944ead42eb3

📥 Commits

Reviewing files that changed from the base of the PR and between a63364d and c38f2f5.

📒 Files selected for processing (2)
  • service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go
  • service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go

Walkthrough

The streaming Responses-to-Chat converter now reuses existing tool state when terminal events use a different key format. A unit test verifies that repeated terminal tool data produces one tool index, the expected arguments, and a tool_calls finish reason.

Changes

Responses-to-Chat tool deduplication

Layer / File(s) Summary
Tool reconciliation and regression coverage
service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go, service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go
ensureToolForEvent re-associates tools using item or call ID mappings, while the regression test verifies that terminal output does not duplicate tool indices or arguments.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: calcium-ion

Poem

I’m a rabbit with tools in my burrow tonight,
One call hops through the stream just right.
No doubled carrots, no arguments spun,
The terminal echo counts only one.
tool_calls says: the work is done!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the fix for duplicate tool calls in Responses-to-Chat streaming.
Linked Issues check ✅ Passed The code reuses existing tool state by item/call ID and the regression test covers the duplicate-terminal-output case in #6223.
Out of Scope Changes check ✅ Passed The changes are narrowly focused on the reported streaming duplication bug and its regression test.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@Calcium-Ion
Calcium-Ion merged commit 1086038 into QuantumNous:main Jul 18, 2026
2 checks passed
fux-dev pushed a commit to fux-dev/new-api that referenced this pull request Jul 19, 2026
…ntumNous#6225)

When a function call was already registered under its output_index key
via response.output_item.added, the synthetic events built from the
terminal response.completed output carry no output_index and resolve to
a different item-based key. ensureToolForEvent then created a second
tool index and resent the full arguments, so Chat Completions clients
received the same tool call twice.

Reuse the tool registered under itemIDToKey/callIDToKey before creating
a new one, and alias the new key to the existing tool.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
qpwo005451 pushed a commit to qpwo005451/new-api that referenced this pull request Jul 21, 2026
…ntumNous#6225)

When a function call was already registered under its output_index key
via response.output_item.added, the synthetic events built from the
terminal response.completed output carry no output_index and resolve to
a different item-based key. ensureToolForEvent then created a second
tool index and resent the full arguments, so Chat Completions clients
received the same tool call twice.

Reuse the tool registered under itemIDToKey/callIDToKey before creating
a new one, and alias the new key to the existing tool.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jiweiyeah added a commit to jiweiyeah/yutou-api that referenced this pull request Jul 25, 2026
Synced upstream/main (8739c055a6c53d, 49 commits) into custom.

Conflicts resolved by keeping custom side per user instruction:
- controller/user.go: kept custom subscription filter + total_topup sort
- model/user.go: kept custom queryUsers delegation (GetAllUsers/GetAllUsersFiltered/SearchUsers)
- model/channel_cache.go: kept custom performance monitoring (RecordCacheSync)
- web/default/src/features/users/components/users-columns.tsx: kept custom imports (formatNumber, cn)
- web/default/src/features/users/components/users-table.tsx: kept custom controlled sorting + server-side ordering + subscriptionFilter

Removed upstream test model/user_pagination_test.go — depends on the
upstream sortOptions-based GetAllUsers/SearchUsers signatures that custom
side does not expose (custom uses queryUsers with subStatus/orderBy/orderDir).

Verified: go build ./... and web/default tsc --noEmit both pass.

Notable upstream fixes absorbed:
- QuantumNous#6225 prevent duplicate tool calls in Responses→Chat streaming
- QuantumNous#6194 server-side sorting for user list (fix paged-data client-sort bug)
- QuantumNous#6134 prevent large quota values from overflowing
- QuantumNous#6168 purge authentication data on hard user deletion
- QuantumNous#6096 golang.org/x/crypto 0.51.0 → 0.52.0
- Deadlock fix in channel_cache.go (InvalidatePricingCache lock ordering)
  NOT absorbed — custom side keeps performance monitoring branch.
  Manual review needed if pricing cache staleness is observed.
yiranxiaohui added a commit to yiranxiaohui/new-api that referenced this pull request Jul 25, 2026
Sync 14 upstream commits. The dominant change is structural: upstream
promoted the frontend from web/default to the web/ root and deleted the
classic theme entirely, so most of the diff is renames and deletions.

Notable upstream work:
- refactor(auth): stateless dashboard tokens replacing sessions (QuantumNous#6329)
- feat(channel): upstream model discovery for Codex and advanced custom
  channels (QuantumNous#6184, QuantumNous#5971)
- fix: CAS status update prevents duplicate suno task refunds (QuantumNous#6074)
- fix: no duplicate tool calls in Responses-to-Chat streaming (QuantumNous#6225)

Fork-side resolutions:
- Drop the classic theme, following upstream. electron/ and the
  release/electron-build workflows stay deleted as this fork already
  removed them; GHCR publishing continues via docker-build.yml.
- UserBase keeps the fork's per-user Ratio alongside upstream's new Role,
  AuthVersion and CacheSchema fields. GetUserCache adopts upstream's
  cache-population path, which returns ToBaseUser() and so still carries
  Ratio.
- web-router keeps the fork's dynamic index injector (SystemName/Logo
  templating) on top of upstream's renamed frontendFS. serveIndex collapses
  to the single-frontend WebAssets now that classic is gone.
- Restore the fork's invoices feature (5 files), which git's rename
  detection dropped during the web/default -> web/ move, and re-register
  its route in routeTree.gen.ts.

Backend builds and the full Go test suite passes. The frontend is not yet
type-checked or built locally.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
zhaodechao2008 pushed a commit to zhaodechao2008/new-api that referenced this pull request Jul 27, 2026
…ntumNous#6225)

When a function call was already registered under its output_index key
via response.output_item.added, the synthetic events built from the
terminal response.completed output carry no output_index and resolve to
a different item-based key. ensureToolForEvent then created a second
tool index and resent the full arguments, so Chat Completions clients
received the same tool call twice.

Reuse the tool registered under itemIDToKey/callIDToKey before creating
a new one, and alias the new key to the existing tool.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
330079598 pushed a commit to 330079598/new-api that referenced this pull request Aug 19, 2026
…ntumNous#6225)

When a function call was already registered under its output_index key
via response.output_item.added, the synthetic events built from the
terminal response.completed output carry no output_index and resolve to
a different item-based key. ensureToolForEvent then created a second
tool index and resent the full arguments, so Chat Completions clients
received the same tool call twice.

Reuse the tool registered under itemIDToKey/callIDToKey before creating
a new one, and alias the new key to the existing tool.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

Bug: Responses-to-Chat streaming duplicates tool calls on response.completed

2 participants