Skip to content

fix(provider): add universal empty-content guard to message pipeline#23259

Closed
jpvelasco wants to merge 1 commit intoanomalyco:devfrom
jpvelasco:feat/normalize-messages-validation-guard
Closed

fix(provider): add universal empty-content guard to message pipeline#23259
jpvelasco wants to merge 1 commit intoanomalyco:devfrom
jpvelasco:feat/normalize-messages-validation-guard

Conversation

@jpvelasco
Copy link
Copy Markdown

@jpvelasco jpvelasco commented Apr 18, 2026

Fixes #23260

Problem

The message() pipeline in packages/opencode/src/provider/transform.ts applies a sequential series of transforms, but has no final validation step. Three passes can each produce messages with content: [] or content: "" that providers reject with hard errors:

Pass Code location Can produce
Pass 1 (Anthropic/Bedrock empty text filter) transform.ts:55-73 guarded locally -- but only for two providers
Pass 3 (Anthropic tool reorder flatMap) transform.ts:115-126 content: [] if the non-tool split half was all-empty
Pass 5 (interleaved reasoning filter) transform.ts:178-211 content: [] when message was reasoning-only
unsupportedParts() transform.ts:267-303 content: [] if all parts were unsupported modality

Bedrock's ConverseAPI rejects empty content with a fatal ValidationException that permanently breaks the active session. Other providers fail in their own ways.

This architectural gap has surfaced in multiple independent reports:

Fix

Add a single O(n) filter at the very end of message(), after all transformation passes and before return msgs:

msgs = msgs.filter((msg) => {
  if (typeof msg.content === "string") return msg.content !== ""
  if (Array.isArray(msg.content)) return msg.content.length > 0
  return true
})

This is additive -- existing pass-local guards (Pass 1) remain in place as defense-in-depth. Any future transformation pass that produces empty content is automatically caught without needing its own guard.

Tests

Added a new describe("ProviderTransform.message - universal empty-content guard") block to test/provider/transform.test.ts with 12 test cases:

  • Drop content: "" and content: [] at assistant/user/tool/system roles (non-Anthropic)
  • Guard does NOT reach inside arrays -- content: [{text:""}] is preserved (not our job to touch array internals outside Pass 1)
  • Pass 3 regression: Anthropic tool reorder split producing empty half is dropped
  • Pass 5 regression: interleaved-only assistant message (content:[{type:"reasoning"}]) is dropped
  • Preserves all non-empty messages unchanged
  • Cross-provider matrix: Bedrock, Anthropic, OpenAI, Google Vertex, Mistral, Gateway, OpenRouter, openai-compatible all drop empty-array content

Also updated one existing test that was asserting the previous pass-through behavior for empty-string content on non-Anthropic providers -- the description now accurately reflects that empty-string is dropped universally by the guard.

Compatibility with related PRs

Verification

cd packages/opencode
bun test test/provider/transform.test.ts
# 145 pass, 0 fail

Multiple transformation passes (Pass 3 Anthropic tool reorder, Pass 5
interleaved reasoning filter, unsupportedParts stripping) can each
produce messages with content:[] or content:"" that providers reject
with hard validation errors. Bedrock's ConverseAPI crashes the session
with a ValidationException on empty content.

Adds a single O(n) filter at the end of message() that drops any
message with empty string or empty array content, regardless of which
pass produced it. This is additive -- existing pass-local guards remain
as defense-in-depth.

Relates to: anomalyco#15715, anomalyco#16332, anomalyco#22364, anomalyco#17705
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions github-actions bot added the needs:compliance This means the issue will auto-close after 2 hours. label Apr 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions
Copy link
Copy Markdown
Contributor

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

Potential Duplicate PRs Found

The following open/related PRs address the same or overlapping issues:

  1. fix(provider): drop empty content messages after interleaved reasoning filter #17712 - fix(provider): drop empty content messages after interleaved reasoning filter

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

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

    • Directly related: Takes a similar approach (universal empty-text filtering) as the current PR
    • The PR description explicitly notes this as an alternative approach; current PR is rebased on dev with more comprehensive regression suite
  4. fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748) #16750 - fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748)

    • Related: Addresses empty-text filtering but in a different location (normalizeMessages vs. message pipeline)
    • May have been a precursor to the broader fix

Why they're related: All address the same root problem of empty content (content: "" or content: []) being rejected by providers like Bedrock, with multiple independent PRs attempting different scopes of fixes. The current PR (#23259) appears to take a comprehensive, universal approach that supersedes the narrower fixes.

@github-actions
Copy link
Copy Markdown
Contributor

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

Feel free to open a new pull request that follows our guidelines.

@github-actions github-actions bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Apr 18, 2026
@github-actions github-actions bot closed this Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: message pipeline has no final empty-content guard, causing ValidationException on Bedrock and hard failures on other providers

1 participant