Skip to content

Finalize open thinking blocks on messageStop for Bedrock Converse streaming#14

Merged
mattwebbio merged 4 commits into
mainfrom
fix-bedrock-converse-redacted-thinking-messagestop
Jul 13, 2026
Merged

Finalize open thinking blocks on messageStop for Bedrock Converse streaming#14
mattwebbio merged 4 commits into
mainfrom
fix-bedrock-converse-redacted-thinking-messagestop

Conversation

@mattwebbio

Copy link
Copy Markdown

Summary

  • Bedrock's Converse stream doesn't reliably send a content_block_stop for every reasoning-content index before the stream ends — observed for a redacted thinking block immediately superseded by the next block's content_block_start with no stop in between.
  • That left thinking.blocks empty for such turns, forcing replay onto format_thinking_blocks' lossy single-block reconstruction, which Anthropic rejects on the next request with Invalid \data` in `redacted_thinking` block`.
  • messageStop now finalizes any block still open at that point, but only when it's structurally complete (has a signature or redacted-content marker present) — a genuinely truncated block with neither is still dropped, unchanged from existing behavior.

Test plan

  • bundle exec rspec spec/ruby_llm/protocols/converse/streaming_spec.rb spec/ruby_llm/stream_accumulator_spec.rb — 18 examples, 0 failures (includes new regression test + the pre-existing truncation-drop test, both green)
  • bundle exec rspec spec/ruby_llm/protocols/ — 309 examples, 0 failures
  • bundle exec rubocop lib/ruby_llm/protocols/converse/streaming.rb spec/ruby_llm/protocols/converse/streaming_spec.rb — no offenses

🤖 Generated with Claude Code

…entBlockStop

Bedrock's Converse stream doesn't always emit a content_block_stop for every
reasoning-content index before the stream ends — observed for a redacted
thinking block immediately superseded by the next block's content_block_start
with no stop in between. That left thinking.blocks empty, forcing replay to
fall back to format_thinking_blocks' lossy single-block reconstruction, which
Anthropic rejects with "Invalid `data` in `redacted_thinking` block".

messageStop now finalizes any block still open at that point, but only if it's
structurally complete (has a signature or redacted-content marker) — a
genuinely truncated block with neither is still dropped, matching existing
behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattwebbio mattwebbio requested a review from a team as a code owner July 13, 2026 21:35
@staging-supernova-dx-appf-io

Copy link
Copy Markdown

🤖 Supernova Code Review — View trace

@staging-supernova-dx-appf-io staging-supernova-dx-appf-io Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One bug found: the new finalize_remaining_thinking_blocks method calls .presence on an Array (line 234). This is an ActiveSupport method — it isn't part of Ruby core, and activesupport is not a declared runtime dependency of this gem. Tests pass only because spec/spec_helper.rb loads active_support/core_ext. In any non-Rails consumer, the call will raise NoMethodError at exactly the moment the fix is supposed to kick in (i.e., when messageStop arrives with an un-stopped thinking block).

See the inline comment for a drop-in fix.


🔁 Re-run this review

block = finalize_thinking_block(state)
block ? [block] : nil
finalize_thinking_block(state)
end.presence

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.presence is an ActiveSupport extension method — it isn't available in plain Ruby. The gem's gemspec has no activesupport runtime dependency, so any consumer using ruby_llm outside of a Rails context (or without manually loading ActiveSupport) will hit NoMethodError: undefined method 'presence' for Array the first time messageStop fires with leftover thinking state (i.e., exactly the scenario this fix is designed to handle).

Tests pass only because spec/spec_helper.rb explicitly require 'active_support/core_ext'.

Replace with an equivalent Ruby-native guard:

def finalize_remaining_thinking_blocks(thinking_state)
  blocks = thinking_state.keys.sort.filter_map do |index|
    state = thinking_state.delete(index)
    next unless state[:redacted] || state[:signature]

    finalize_thinking_block(state)
  end
  blocks.empty? ? nil : blocks
end

mattwebbio and others added 3 commits July 13, 2026 14:46
…blocks

activesupport isn't a runtime dependency of this gem, so #presence isn't
available to non-Rails consumers — it only worked in specs because
spec_helper loads active_support/core_ext. Use a plain empty? check instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
simplecov 1.0.0 removed SimpleCov.running, which bin/rspec-queue's custom
test-queue runner depends on in cleanup_worker for per-worker coverage
bookkeeping. The prior >= 0.21 constraint let CI's bundle install float onto
the new major version, crashing every test job after a clean run (0 real
failures) with "undefined method 'running' for module SimpleCov" — unrelated
to this branch's actual change, but breaking CI for any PR right now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bundle exec appraisal generate — each gemfiles/rails_*.gemfile is a
standalone materialized copy of the main Gemfile's dependencies, not an
eval_gemfile include, so the simplecov constraint had to be regenerated
into each one to actually take effect in CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattwebbio mattwebbio merged commit 27c9a7e into main Jul 13, 2026
21 checks passed
@mattwebbio mattwebbio deleted the fix-bedrock-converse-redacted-thinking-messagestop branch July 13, 2026 22:11
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.

1 participant