Skip to content

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

Closed
RhoninSeiei wants to merge 7 commits into
anomalyco:devfrom
RhoninSeiei:fix/empty-text-content-bedrock
Closed

fix(opencode): filter empty text content blocks for all providers#17742
RhoninSeiei wants to merge 7 commits into
anomalyco: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

@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

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
@rekram1-node
Copy link
Copy Markdown
Collaborator

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

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

Labels

None yet

Projects

None yet

5 participants