Skip to content

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

@josh-williams

Description

@josh-williams

Description

Summary

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.

Steps to Reproduce

  • Configure streamText with a set of tools.
  • Have the model call a tool name that is not in the tools object.
  • The stream emits tool-input-start → tool-input-delta → tool-input-error → tool-output-error for the same toolCallId.
  • Observe that the resulting assistant message contains two parts with the same toolCallId.

Root Cause

In process-ui-message-stream.ts:

  • tool-input-start arrives without dynamic set (the tool isn't in the tools object, so tool?.type === 'dynamic' is undefined). Since chunk.dynamic is falsy, updateToolPart is called, which creates a static part (e.g. type: "tool-some-nonexistent-tool-name").
  • tool-input-error arrives with dynamic: true (set by parseToolCall's catch block for NoSuchToolError). The handler branches on chunk.dynamic and calls updateDynamicToolPart, which only searches for parts with type === 'dynamic-tool'. It doesn't find the existing static part, so it creates a second part with type: "dynamic-tool".

Expected Behavior

Only one message part should exist per toolCallId. When tool-input-error arrives for a toolCallId that already has a part (regardless of whether it's static or dynamic), it should update that existing part rather than creating a new one.

Actual Behavior

Two parts are created for the same toolCallId:
{ type: "tool-{toolName}", toolCallId: "...", state: "input-streaming", ... }
{ type: "dynamic-tool", toolCallId: "...", state: "output-error", ... }

Impact

When these messages are persisted and sent in a subsequent request, convertToModelMessages produces two tool results for the same toolCallId, which the Anthropic API (and likely others) rejects as invalid.

AI SDK Version

  • ai 6.0.49

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions