Skip to content

fix(ai-sdk): emit tool-call events for OpenRouter compatibility#11420

Closed
0xMink wants to merge 3 commits intoRooCodeInc:mainfrom
0xMink:fix/openrouter-tool-call-compat
Closed

fix(ai-sdk): emit tool-call events for OpenRouter compatibility#11420
0xMink wants to merge 3 commits intoRooCodeInc:mainfrom
0xMink:fix/openrouter-tool-call-compat

Conversation

@0xMink
Copy link
Copy Markdown
Contributor

@0xMink 0xMink commented Feb 11, 2026

Closes #11419

Summary

  • processAiSdkStreamPart() silently ignored AI SDK tool-call events, relying entirely on tool-input-start/delta/end lifecycle events for tool delivery. @openrouter/ai-sdk-provider v2.1.1 does not emit tool-input-end for incrementally streamed tool calls, and emits only tool-call with no tool-input-* events for flush-path calls. This caused tool calls to be silently dropped for all OpenRouter-proxied models.
  • Now emits tool-call as a tool_call chunk with string passthrough (avoids double-encoding when upstream JSON parse fails) and ?? {} fallback for undefined input.
  • Added deduplication in Task.ts tool_call handler: skips tools already finalized via streaming path, replaces in-flight partial tools, pushes new blocks for flush-only path. All three OpenRouter streaming paths are now handled correctly.

Test plan

  • Existing: 87 ai-sdk tests, 10 duplicate-tool-use-ids tests, 38 Task tests, 12 NativeToolCallParser tests -- all passing
  • New: 3 tests for tool-call emission (object input, undefined input, string passthrough)
  • New: 5 tests for tool_call dedup ordering (skip finalized, replace partial, flush-only push, tool_call-before-end ordering, mixed paths)
  • 155 total tests passing, 4 skipped (pre-existing)

processAiSdkStreamPart() silently ignored AI SDK tool-call events,
relying entirely on tool-input-start/delta/end for tool delivery.
@openrouter/ai-sdk-provider v2.1.1 does not emit tool-input-end for
incrementally streamed tool calls, and emits only tool-call (no
tool-input-* events) for flush-path tool calls. This caused tool
calls to be silently dropped, triggering false "no tools used" errors
for users proxying through OpenRouter.

- Emit tool-call as tool_call chunk instead of ignoring it
- String input passthrough to avoid double-encoding when upstream
  JSON parse fails
- Deduplicate in Task.ts tool_call handler: skip already-finalized
  tools, replace in-flight partial tools, push for flush-only path
- 8 new tests covering emission, serialization, and dedup ordering
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working labels Feb 11, 2026
@roomote-v0
Copy link
Copy Markdown
Contributor

roomote-v0 Bot commented Feb 11, 2026

Rooviewer Clock   See task

All previous issues resolved. Test updates in 90a3ee2 correctly align provider specs with the new tool-call emission behavior. No new issues found. LGTM.

  • Dedup check in tool_call handler only matches tool_use blocks, missing mcp_tool_use -- can produce duplicate MCP tool blocks when both streaming and tool-call events arrive
  • Missing text block finalization before pushing tool_use in flush-only path -- delays tool presentation until stream-end cleanup
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

Comment thread src/core/task/Task.ts
Comment on lines +3107 to +3112
const alreadyPresent = this.assistantMessageContent.some(
(block) =>
block.type === "tool_use" &&
!block.partial &&
(block as any).id === chunk.id,
)
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.

The dedup check only matches block.type === "tool_use", but NativeToolCallParser.finalizeStreamingToolCall() returns McpToolUse (type "mcp_tool_use") for dynamic MCP tools (mcp--serverName--toolName). When such a tool is already finalized via the streaming path, the block has type: "mcp_tool_use" and streamingToolCallIndices has been cleaned up, so both guards miss it. parseToolCall then returns a second McpToolUse which gets pushed as a duplicate. This only triggers when MCP tools are proxied through OpenRouter and both streaming events and tool-call are emitted for the same call.

Suggested change
const alreadyPresent = this.assistantMessageContent.some(
(block) =>
block.type === "tool_use" &&
!block.partial &&
(block as any).id === chunk.id,
)
const alreadyPresent = this.assistantMessageContent.some(
(block) =>
(block.type === "tool_use" || block.type === "mcp_tool_use") &&
!block.partial &&
(block as any).id === chunk.id,
)

Fix it with Roo Code or mention @roomote and request a fix.

Comment thread src/core/task/Task.ts
…lush-only tools

Address review feedback:
- Extend dedup predicate to match mcp_tool_use blocks, preventing
  duplicate MCP tool entries when both streaming and tool-call events
  arrive for the same call
- Finalize preceding partial text block in flush-only path to prevent
  stalling tool presentation until stream-end cleanup
- Add regression tests for both scenarios
@0xMink
Copy link
Copy Markdown
Contributor Author

0xMink commented Feb 12, 2026

Acknowledged — both issues addressed in 381faee:

  1. Extended the alreadyPresent guard to include mcp_tool_use blocks, preventing duplicates when MCP tools finalize via the streaming path
  2. Added partial text block finalization in the flush-only tool_call path (mirroring tool_call_start behavior) to avoid delaying tool presentation

Regression tests added for both scenarios (17/17 passing in duplicate-tool-use-ids suite, 128/128+4 skipped across ai-sdk and Task suites).

Four provider test suites (deepseek, mistral, moonshot, openrouter)
still asserted that tool-call events are ignored. Update them to
assert the new behavior: tool-call emits a tool_call chunk, with
deduplication handled by Task.ts.
@0xMink
Copy link
Copy Markdown
Contributor Author

0xMink commented Feb 12, 2026

Pushed CI fix 90a3ee2: updated provider test suites (DeepSeek, Mistral, Moonshot, OpenRouter) to expect tool_call chunk emission. Local: 107/107 passing across those suites.

@0xMink
Copy link
Copy Markdown
Contributor Author

0xMink commented Feb 12, 2026

CI green. All review feedback addressed across 3 commits. Broad Unicode/control-char scan clean (no bidi, zero-width, BOM, soft hyphen, or formatting chars). Ready for re-review.

@hannesrudolph
Copy link
Copy Markdown
Collaborator

Closing: PR lacks clear evidence of manual testing.

@github-project-automation github-project-automation Bot moved this from New to Done in Roo Code Roadmap Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenRouter proxy: tool calls silently dropped after AI SDK migration (tool-call event ignored)

2 participants