Skip to content

Anthropic signed thinking blocks are reordered when a UIMessage interleaves provider-executed tools and thinking #910

Description

@georgesma

TanStack AI version

0.39.1

Framework/Library version

@tanstack/ai-anthropic: 0.16.0

Describe the bug and the steps to reproduce it

When an assistant UIMessage contains signed Anthropic thinking parts interleaved with provider-executed tools, convertMessagesToModelMessages does not preserve the original part order.

This matters for Anthropic because signed thinking / redacted_thinking blocks must be replayed exactly as they appeared in the original response. If they are moved relative to tool-use blocks, the next Anthropic request can fail with:

400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified. These blocks must remain as they were in the original response."}}

The shape that triggers it is:

assistant UIMessage parts:
  thinking(signature A)
  provider-executed web_search tool-call
  thinking(signature B)
  local tool-call
  local tool-result

The original order is valid because the second thinking block happened after the provider tool result. But after conversion to model messages, both thinking blocks are attached to the same assistant segment and replayed before both tool calls.

Reproduction Script

import { convertMessagesToModelMessages } from "@tanstack/ai"

const messages = [
  {
    id: "assistant-message",
    role: "assistant",
    parts: [
      {
        type: "thinking",
        content: "First signed thinking block",
        signature: "signature-1",
      },
      {
        type: "tool-call",
        id: "srvtoolu_search",
        name: "web_search",
        arguments: '{"query":"top AI companies"}',
        state: "input-complete",
        metadata: {
          providerExecuted: true,
          anthropic: {
            serverToolType: "web_search",
            resultBlockType: "web_search_tool_result",
            result: [
              {
                type: "web_search_result",
                title: "Example result",
                url: "https://example.com",
                encrypted_content: "opaque-provider-payload",
              },
            ],
          },
        },
      },
      {
        type: "thinking",
        content: "Second signed thinking block",
        signature: "signature-2",
      },
      {
        type: "tool-call",
        id: "toolu_create_block",
        name: "createBlock",
        arguments: '{"block":{"type":"entity-grid"}}',
        state: "complete",
        output: { ok: true },
      },
      {
        type: "tool-result",
        toolCallId: "toolu_create_block",
        content: '{"ok":true}',
        state: "complete",
      },
    ],
  },
]

const modelMessages = convertMessagesToModelMessages(messages)

console.log(
  JSON.stringify(
    modelMessages.map((message) => ({
      role: message.role,
      thinking: message.thinking?.map((part) => part.content),
      toolCalls: message.toolCalls?.map((toolCall) => toolCall.function.name),
      toolCallId: message.toolCallId,
      content: message.content,
    })),
    null,
    2,
  ),
)

Actual Output

Both signed thinking blocks are attached to the same assistant model message:

[
  {
    "role": "assistant",
    "thinking": [
      "First signed thinking block",
      "Second signed thinking block"
    ],
    "toolCalls": [
      "web_search",
      "createBlock"
    ],
    "content": null
  },
  {
    "role": "tool",
    "toolCallId": "toolu_create_block",
    "content": "{\"ok\":true}"
  }
]

With the Anthropic adapter, that means the signed thinking blocks are appended before the provider server_tool_use and the local tool_use, changing the original signed transcript order.

Expected Output

The conversion should split assistant segments when a later signed thinking block appears after an already accumulated tool call, so the original interleaving is preserved:

[
  {
    "role": "assistant",
    "thinking": [
      "First signed thinking block"
    ],
    "toolCalls": [
      "web_search"
    ],
    "content": null
  },
  {
    "role": "assistant",
    "thinking": [
      "Second signed thinking block"
    ],
    "toolCalls": [
      "createBlock"
    ],
    "content": null
  },
  {
    "role": "tool",
    "toolCallId": "toolu_create_block",
    "content": "{\"ok\":true}"
  }
]

Equivalently, the Anthropic adapter needs to replay signed thinking blocks in the same relative order as the original provider response content blocks.

Why This Is Blocking

This happens in normal multi-turn conversations that use Anthropic web search / web fetch plus thinking. The first turn succeeds and can be persisted as a UIMessage, but the next user turn fails because the stored assistant message is converted back into a modified Anthropic transcript.

The issue is especially easy to hit with provider-executed server tools because the provider result is stored in the tool-call metadata rather than as a normal tool-result part, so there is no tool-result boundary that forces a segment flush before the next thinking block.

Your Minimal, Reproducible Example - (Sandbox Highly Recommended)

See bug description

Screenshots or Videos (Optional)

No response

Do you intend to try to help solve this bug with your own PR?

No, because I do not know how

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    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