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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions