Skip to content

fix(ai): Don't create duplicate tool parts when models call non-existent tools#12774

Merged
gr2m merged 1 commit intovercel:mainfrom
josh-williams:fix-duplicate-tool-parts
Feb 26, 2026
Merged

fix(ai): Don't create duplicate tool parts when models call non-existent tools#12774
gr2m merged 1 commit intovercel:mainfrom
josh-williams:fix-duplicate-tool-parts

Conversation

@josh-williams
Copy link
Copy Markdown
Contributor

Background

When a model calls a tool that doesn't exist in the tools object (e.g. a hallucinated tool name), processUIMessageStream creates two message parts for the same toolCallId — one static (tool-{toolName}) and one dynamic (dynamic-tool). This causes downstream failures when the conversation is continued, since the model API rejects requests containing two tool results for the same tool call ID.

Summary

Fixes a bug in processUIMessageStream where a model calling a non-existent tool produces two UI message parts for the same toolCallId — one static (tool-{toolName}) and one dynamic (dynamic-tool). This happens because tool-input-start creates a static part (when dynamic is undefined for an unknown tool), but the subsequent tool-input-error arrives with dynamic: true and only searches for a dynamic-tool part, missing the existing static one and creating a duplicate. The fix checks for an existing part by toolCallId before branching on chunk.dynamic, so the error updates the existing part in place instead of creating a second one.

Manual Verification

Manually verified against my own application. After the fix, I got a single tool part like:

            {
                "type": "tool-dashboard-page-create-dashboard",
                "state": "output-error",
                "rawInput":
                {
                    "name": "Staging Monitoring"
                },
                "errorText": "Model tried to call unavailable tool 'dashboard-page-create-dashboard'. Available tools: ....",
                "toolCallId": "toolu_015E6Kd6cwSMkw7SFpsa7w88",
                "callProviderMetadata":
                {
                    "anthropic":
                    {
                        "caller":
                        {
                            "type": "direct"
                        }
                    }
                }
            },

Checklist

  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root)
  • I have reviewed this pull request (self-review)

Related Issues

Fixes #12772

@tigent tigent bot added ai/ui anything UI related bug Something isn't working as documented labels Feb 23, 2026
Copy link
Copy Markdown
Collaborator

@gr2m gr2m left a comment

Choose a reason for hiding this comment

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

Looks good, thank you Josh!

@gr2m gr2m merged commit 5230482 into vercel:main Feb 26, 2026
25 of 26 checks passed
pawaca added a commit to pawaca/ai that referenced this pull request Mar 27, 2026
Follow-up to vercel#12774: apply the same existing-part lookup pattern to
the tool-input-available case so that a dynamic flag mismatch between
tool-input-start and tool-input-available does not create a duplicate
part.

Closes vercel#12772
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai/ui anything UI related bug Something isn't working as documented

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two tool parts with the same toolCallId created when the model calls a non-existent tool name

2 participants