Finalize open thinking blocks on messageStop for Bedrock Converse streaming#14
Conversation
…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>
|
🤖 Supernova Code Review — View trace |
There was a problem hiding this comment.
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.
| block = finalize_thinking_block(state) | ||
| block ? [block] : nil | ||
| finalize_thinking_block(state) | ||
| end.presence |
There was a problem hiding this comment.
.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…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>
Summary
content_block_stopfor every reasoning-content index before the stream ends — observed for a redacted thinking block immediately superseded by the next block'scontent_block_startwith no stop in between.thinking.blocksempty for such turns, forcing replay ontoformat_thinking_blocks' lossy single-block reconstruction, which Anthropic rejects on the next request withInvalid \data` in `redacted_thinking` block`.messageStopnow 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 failuresbundle exec rubocop lib/ruby_llm/protocols/converse/streaming.rb spec/ruby_llm/protocols/converse/streaming_spec.rb— no offenses🤖 Generated with Claude Code