Skip to content

fix(ai): preserve empty text parts in multi-step response messages#14070

Closed
nelsonauner wants to merge 3 commits intovercel:mainfrom
nelsonauner:nelson/fix-empty-text-thinking-blocks
Closed

fix(ai): preserve empty text parts in multi-step response messages#14070
nelsonauner wants to merge 3 commits intovercel:mainfrom
nelsonauner:nelson/fix-empty-text-thinking-blocks

Conversation

@nelsonauner
Copy link
Copy Markdown

Summary

  • Fix: toResponseMessages() was filtering out empty text blocks (text: ""), which broke multi-step generateText calls when Bedrock/Claude returned an empty text block between reasoning/thinking blocks.
  • Bedrock validates that the content block structure of assistant messages is unchanged between steps. Dropping an empty text block shifted all subsequent content indices, causing Bedrock to reject step 2 with: "thinking blocks cannot be modified. These blocks must remain as they were in the original response."
  • The bug was intermittent — Claude only sometimes emits empty text blocks between reasoning blocks.

Root cause

Bedrock Converse API response (step 1):

content[0]: reasoningContent (analysis + signature)
content[1]: {"text": ""}          ← empty text block
content[2]: reasoningContent (plan + signature)
content[3]: text
content[4]: toolUse

After toResponseMessages() filtered the empty text, step 2 sent:

content[0]: reasoningContent ✓
content[1]: reasoningContent ✗ (Bedrock expected empty text at this index)
content[2]: text
content[3]: toolUse

Changes

  1. to-response-messages.ts — Removed the empty text filter. Empty text parts are structurally significant and must be preserved for round-tripping.
  2. to-response-messages.test.ts — Updated existing test, added new test reproducing the exact production scenario (reasoning + empty text + reasoning + tool-call).

Test plan

  • to-response-messages.test.ts — 21 tests pass
  • generate-text.test.ts — 184 tests pass
  • Verify no regressions in other providers that may have relied on empty text filtering

🤖 Generated with Claude Code

When Bedrock/Claude returns an empty text block between reasoning
blocks, toResponseMessages() was filtering it out. This shifted all
subsequent content block indices, causing Bedrock to reject step 2
with "thinking blocks cannot be modified" because the block structure
no longer matched the original response.

The fix removes the empty text filter. Empty text parts are
structurally significant when reasoning/thinking blocks are present,
as providers validate that the content block structure is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tigent tigent bot added ai/core core functions like generateText, streamText, etc. Provider utils, and provider spec. ai/provider related to a provider package. Must be assigned together with at least one `provider/*` label bug Something isn't working as documented provider/amazon-bedrock Issues related to the @ai-sdk/amazon-bedrock provider provider/anthropic Issues related to the @ai-sdk/anthropic provider labels Apr 2, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rompt

A second filter in convert-to-language-model-prompt.ts was also
stripping empty text blocks from assistant messages. This filter
must be removed for the same reason: empty text blocks are
structurally significant when reasoning/thinking blocks are present.

Also adds an end-to-end reproduction test in generate-text.test.ts
that verifies empty text blocks between reasoning blocks survive
the full multi-step round-trip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nelsonauner nelsonauner marked this pull request as draft April 2, 2026 22:05
@nelsonauner nelsonauner marked this pull request as ready for review April 3, 2026 17:34
@gr2m
Copy link
Copy Markdown
Collaborator

gr2m commented Apr 3, 2026

I ran this example 100 times in an attempt to reproduce the problem, but no luck so far

import { bedrock } from '@ai-sdk/amazon-bedrock';
import { isStepCount, streamText, tool } from 'ai';
import { z } from 'zod';
import { run } from '../../lib/run';
import { saveRawChunks } from '../../lib/save-raw-chunks';

// When Bedrock Claude with thinking enabled returns an empty text block
// between reasoning blocks (e.g. [reasoning, "", reasoning, text, tool_call]),
// toResponseMessages() filters it out. On the next step, Bedrock rejects the
// request because the content structure no longer matches the original response.
//
// The bug is intermittent — Claude only sometimes emits empty text blocks
// between reasoning blocks. Run this example multiple times to reproduce.

run(async () => {
  const result = streamText({
    model: bedrock('us.anthropic.claude-sonnet-4-5-20250929-v1:0'),
    stopWhen: isStepCount(5),
    reasoning: 'low',
    includeRawChunks: true,
    tools: {
      weather: tool({
        description: 'Get the current weather in a location',
        inputSchema: z.object({
          location: z.string().describe('The location to get the weather for'),
        }),
        execute: async ({ location }) => ({
          location,
          temperature: 72 + Math.floor(Math.random() * 21) - 10,
          condition: 'partly cloudy',
        }),
      }),
      search: tool({
        description: 'Search for information on the web',
        inputSchema: z.object({
          query: z.string().describe('The search query'),
        }),
        execute: async ({ query }) => ({
          results: [
            {
              title: `Result for: ${query}`,
              snippet: 'This is a mock search result.',
            },
          ],
        }),
      }),
    },
    prompt:
      'What is the weather in San Francisco and New York? Also search for "best restaurants in San Francisco". Think carefully about how to approach this.',
  });

  await saveRawChunks({
    result,
    filename: 'bedrock-thinking-tool-call',
  });
});

Anything we could do to adapt the example to make it closer to your own AI SDK use?

@nelsonauner
Copy link
Copy Markdown
Author

Anything we could do to adapt the example to make it closer to your own AI SDK use?

Hard to say - we've observed this 13 times out of millions of requests so I think repro'ing might be damn near impossible. Let me talk with the bedrock team

@nelsonauner
Copy link
Copy Markdown
Author

nelsonauner commented Apr 3, 2026

Me general point though (discussed in Slack) is it seems very odd to be dropping messages that are returned by the LLM provider. It messes up the whole ordering (e.g. grabbing by index) of the conversation/messages.

@nelsonauner
Copy link
Copy Markdown
Author

Here is the response from the Bedrock team asserting that empty text outputs are valid:

t's a pleasure to meet you! I hope you're doing well. Thank you for contacting AWS Premium Support. My name is Eduardo from the ML team and it's my pleasure to assist you with this case today.

I understand that you're experiencing an issue where Claude Sonnet 4.6 (using the Converse API) occasionally returns empty text blocks ({"text":""}) between reasoning/thinking content blocks in the response. This is causing your application and the Vercel AI SDK to crash because they don't expect empty text outputs between thinking blocks, and you're seeking guidance on whether this is a valid response pattern. Please feel free to let me know if I may have misunderstood anything here.

First and foremost, thank you so much for taking the time to thoroughly explain the situation. Your detailed description is incredibly helpful!

After consulting with the internal Bedrock team, the team shared that the Bedrock Converse API ContentBlock [1] documentation indicates that none of the content fields are required, meaning empty text blocks can occur as part of normal API behavior. As such, it's recommended to Implement logic in your application to filter out or handle empty text blocks between reasoning outputs.

Please kindly note that I will further check with the internal engineering team to confirm if there are any additional considerations or workarounds specific to this behavior. If I find any further insights, I'll follow up. Otherwise, the recommendation above should address the issue.

Please kindly rest assured that I am with you on this and if at any moment you have any additional information or questions, please don't hesitate to reach out. I will be more than happy to help you at the earliest with the most appropriate information!

Thank you and I sincerely wish you a wonderful rest of your day!

References:
[1] https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ContentBlock.html

@lgrammel
Copy link
Copy Markdown
Collaborator

lgrammel commented Apr 7, 2026

a few notes:

a) the filtering was introduced in a time when there were no content parts, just text, to prevent unnecessary ui updates (see https://github.com/vercel/ai/pull/2966/changes ). this is less relevant now that there are content parts. if a provider roundtrip breaks because of the filtering, it needs to be removed.
b) there may be ui regressions caused by empty text parts depending on how rendering is done, so this could be considered a breaking change (could introduce ui flickering perceived as bug)
c) there may be regressions with other providers, so we need to manually test at least the major providers and ensure they still work end-to-end
d) this needs a changeset

@gr2m
Copy link
Copy Markdown
Collaborator

gr2m commented Apr 7, 2026

Also relevant, Anthropic breaks the other way around

@nelsonauner for now, can we try to contain this fix to bedrock? I think if we add .providerMetadata to an empty text chunk it will not be removed, it's something we would need to implement in the bedrock provider.

For the upcoming v7 we will look into removing the current behavior of removing empty text chunks by default, instead implement workarounds where needed (like the one for anthropic)

@nelsonauner
Copy link
Copy Markdown
Author

@gr2m @lgrammel Thank you both for the kind response.

It's unfortunate that the bedrock anthropic and anthropic first party seems to have divergent behavior. I agree with keeping this a bedrock-only fix and ideally they and anthropic determine a single way this behavior should work

@nelsonauner
Copy link
Copy Markdown
Author

This was merged in via #14175, closing this PR

@nelsonauner nelsonauner closed this Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai/core core functions like generateText, streamText, etc. Provider utils, and provider spec. ai/provider related to a provider package. Must be assigned together with at least one `provider/*` label bug Something isn't working as documented provider/amazon-bedrock Issues related to the @ai-sdk/amazon-bedrock provider provider/anthropic Issues related to the @ai-sdk/anthropic provider

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants