Skip to content

fix(opencode): filter empty text content blocks for all providers#17742

Open
RhoninSeiei wants to merge 3 commits intoanomalyco:devfrom
RhoninSeiei:fix/empty-text-content-bedrock
Open

fix(opencode): filter empty text content blocks for all providers#17742
RhoninSeiei wants to merge 3 commits intoanomalyco:devfrom
RhoninSeiei:fix/empty-text-content-bedrock

Conversation

@RhoninSeiei
Copy link
Copy Markdown

@RhoninSeiei RhoninSeiei commented Mar 16, 2026

Issue for this PR

Fixes #15715
Fixes #5028
Refs #2655

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

The existing empty-content filter in normalizeMessages (transform.ts) only runs for @ai-sdk/anthropic and @ai-sdk/amazon-bedrock. Users who connect through @ai-sdk/openai-compatible (custom Bedrock proxies, Databricks-hosted Claude, etc.) hit the same Bedrock ValidationException: messages: text content blocks must be non-empty because the filter does not cover their provider.

The root cause: during multi-turn conversations with tool calls, the streaming processor can create text parts with empty strings (from text-start events that receive no deltas). These empty parts propagate through the message pipeline and eventually reach the SDK converter, which sends content: "" to Bedrock.

This PR makes three changes:

  1. transform.ts - Apply empty text/reasoning filtering universally to all providers (not just Anthropic/Bedrock). Uses .trim() to also catch whitespace-only content. Empty text blocks are never useful for any provider.
  2. message-v2.ts - Skip empty/whitespace-only text and reasoning parts at the source when constructing UIMessages, preventing them from entering the pipeline at all.
  3. transform.test.ts - Replace the old "does not filter for non-anthropic providers" test with a new test that verifies universal filtering works for @ai-sdk/openai-compatible providers.

Note: the SDK itself also has a related issue where convertToOpenAICompatibleChatMessages outputs content: "" instead of content: null for assistant messages with only tool calls. A separate issue has been filed at vercel/ai#13466.

How did you verify your code works?

  1. Ran the full transform test suite (bun test test/provider/transform.test.ts) - all 115 tests pass with 220 expect() calls.
  2. Built a dev binary from the patched source and tested multi-turn conversations with a Bedrock-backed provider through @ai-sdk/openai-compatible. The ValidationException no longer occurs after 5+ rounds of tool-call-heavy conversation.

Screenshots / recordings

N/A - backend logic change, no UI impact.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Many providers (Anthropic, Bedrock, and proxies like openai-compatible
forwarding to Bedrock) reject messages with empty text content blocks.
The existing filter only applied to @ai-sdk/anthropic and
@ai-sdk/amazon-bedrock, but users connecting through
@ai-sdk/openai-compatible (e.g. custom Bedrock proxies, Databricks)
hit the same ValidationException in multi-turn conversations.

Changes:
- normalizeMessages: apply empty text/reasoning filtering universally
  instead of only for Anthropic/Bedrock providers. Also use .trim() to
  catch whitespace-only content.
- message-v2.ts: skip empty text and reasoning parts at the source
  when constructing UIMessages from stored parts.
- Update test to verify universal filtering for openai-compatible.

Fixes anomalyco#15715
Fixes anomalyco#5028
Refs anomalyco#2655
@github-actions github-actions bot added the needs:compliance This means the issue will auto-close after 2 hours. label Mar 16, 2026
@github-actions
Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Based on the search results, I found the following potentially related PRs:

Related/Duplicate PRs Found:

  1. fix(provider): handle empty content for bedrock/openai-compatible APIs #17396 - fix(provider): handle empty content for bedrock/openai-compatible APIs

    • Directly addresses the same issue of empty content handling for Bedrock and openai-compatible APIs
  2. fix(provider): drop empty content messages after interleaved reasoning filter #17712 - fix(provider): drop empty content messages after interleaved reasoning filter

    • Related to empty content filtering, appears to be part of the same effort
  3. fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748) #16750 - fix(provider): skip empty-text filtering for assistant messages in normalizeMessages

    • Previous attempt at addressing empty text filtering in normalizeMessages
  4. fix: expand Anthropic detection and strip whitespace-only text blocks #12634 - fix: expand Anthropic detection and strip whitespace-only text blocks

    • Related work on whitespace-only content filtering
  5. fix: empty tool-result content and cache control level for custom @ai… #17363 - fix: empty tool-result content and cache control level for custom @ai…

    • Addresses empty content in tool results for custom providers

Most likely duplicates: PR #17396 and PR #17712 appear to be addressing the same or very closely related issues. You should verify if this PR (#17742) is redundant with those existing work items.

@github-actions
Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@erichasinternet
Copy link
Copy Markdown

👋 Thanks for this PR @RhoninSeiei! This directly addresses an issue I've been experiencing.

Real-world impact

I encountered this exact problem using AWS Bedrock via LiteLLM proxy (@ai-sdk/openai-compatible). Empty text blocks in my database caused permanent ValidationException errors that made my sessions completely unusable.

Database analysis results:

  • 26 empty parts in part table (text: "")
  • 2 empty messages in message table (content: [])
  • Sessions permanently broken after corruption occurred

Evidence this affects openai-compatible:

The existing filter in transform.ts only checked:

if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock")

But my setup uses @ai-sdk/openai-compatible pointing to a LiteLLM proxy, so the filter never ran. Your universal approach fixes this.

Why this PR is important

Your two-part fix is exactly right:

  1. Prevention (transform.ts): Universal filtering stops empty parts from reaching any provider
  2. Source filtering (message-v2.ts): Prevents empty parts from entering the pipeline at all

The .trim() addition is crucial - it catches whitespace-only content that would also fail validation.

Recovery aspect

I've created a Python repair script to fix already-corrupted databases and opened issue #19309 to track the recovery/repair aspect. This PR prevents future corruption but doesn't fix existing corrupted databases. Both solutions are needed for complete coverage.

Offer to help

I'm happy to:

  • Test this branch with my LiteLLM + Bedrock setup
  • Verify it prevents new empty parts from being created
  • Confirm existing corrupted parts don't cause crashes (if combined with defensive read-time filtering)

Let me know if there's anything I can do to help get this merged!

Environment:

  • OpenCode: v1.3.x (dev branch)
  • Provider: AWS Bedrock via LiteLLM
  • SDK: @ai-sdk/openai-compatible
  • Model: anthropic.claude-sonnet-4-5-20250929-v1:0

erichasinternet pushed a commit to erichasinternet/opencode that referenced this pull request Mar 26, 2026
…tabase

Empty text and reasoning parts with blank or whitespace-only text can
be stored in the database during streaming (from text-start events that
receive no deltas, or when streams are interrupted). These empty parts
cause permanent ValidationException errors with providers like AWS
Bedrock, especially when using LiteLLM proxy (@ai-sdk/openai-compatible).

This fix adds defensive filtering at two levels:
1. When hydrating parts from database (filters on load)
2. When converting to model messages (filters during conversion)

Both filters use .trim() to catch whitespace-only content.

Fixes anomalyco#19309
Complements anomalyco#17742 (prevention) with recovery for existing corruption
erichasinternet pushed a commit to erichasinternet/opencode that referenced this pull request Mar 26, 2026
…tabase

Empty text and reasoning parts with blank or whitespace-only text can
be stored in the database during streaming (from text-start events that
receive no deltas, or when streams are interrupted). These empty parts
cause permanent ValidationException errors with providers like AWS
Bedrock, especially when using LiteLLM proxy (@ai-sdk/openai-compatible).

This fix adds defensive filtering at two levels:
1. When hydrating parts from database (filters on load)
2. When converting to model messages (filters during conversion)

Both filters use .trim() to catch whitespace-only content.

Fixes anomalyco#19309
Complements anomalyco#17742 (prevention) with recovery for existing corruption
@robinmordasiewicz
Copy link
Copy Markdown

Hey @RhoninSeiei — we've been working on the same bug independently (our closed PR was #17565) and wanted to flag something we discovered.

Your .trim() expansion in normalizeMessages() applies to all message roles including assistant. This can break Anthropic adaptive thinking (Opus 4.6, Sonnet 4.6) — see issue #16748 for the detailed writeup.

The problem: Anthropic's adaptive thinking emits whitespace-only text parts between reasoning blocks with cryptographic signatures. The signatures are positionally sensitive — removing the empty/whitespace text part changes the block arrangement and invalidates them. The API then rejects with:

thinking blocks in the latest assistant message cannot be modified

The fix we landed on: Skip the empty/whitespace filter for assistant messages that contain reasoning parts:

```typescript
const hasReasoning =
msg.role === "assistant" && msg.content.some((part) => part.type === "reasoning")
if (hasReasoning) return msg
```

This preserves the signature-sensitive block arrangement while still filtering empty content from user, tool, system, and assistant-without-reasoning messages.

PR #16750 independently arrived at the same approach from the #16748 investigation. You might want to coordinate with that PR to avoid conflicting fixes.

Hope this helps — your root cause analysis in the PR description is excellent and matches what we found.

@RhoninSeiei
Copy link
Copy Markdown
Author

@erichasinternet Thanks for the concrete Bedrock-via-LiteLLM confirmation. That proxy path was one of the main targets for this PR, so the real-world validation is helpful.

@RhoninSeiei
Copy link
Copy Markdown
Author

@robinmordasiewicz Good catch. The adaptive-thinking replay case from #16748 does apply here, so I pushed ef378fa to preserve assistant reasoning separators for Anthropic 4.6-style messages and added a regression test for it. Checks are rerunning now.

@glassdimly
Copy link
Copy Markdown

glassdimly commented Apr 1, 2026

Built and tested this on 1.3.13 (dev branch + cherry-pick). Working fix — sessions no longer break with the whitespace text error. I had AI clean up old sessions in the DB to fix resume.

Here's a gist with AI step-by-step build-from-source instructions if anyone else needs to apply this before it's released: https://gist.github.com/glassdimly/0988483d5de6a09b20e34df7d1d71afe

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

Labels

None yet

Projects

None yet

4 participants