Skip to content

fix: emit contentBlockStop events on the Bedrock ConverseStream egress - #4923

Merged
akshaydeo merged 1 commit into
maximhq:devfrom
fus3r:fix-converse-stream-content-block-stop
Jul 5, 2026
Merged

fix: emit contentBlockStop events on the Bedrock ConverseStream egress#4923
akshaydeo merged 1 commit into
maximhq:devfrom
fus3r:fix-converse-stream-content-block-stop

Conversation

@fus3r

@fus3r fus3r commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #4262. The Bedrock ConverseStream egress (/bedrock/model/{modelId}/converse-stream) never emitted contentBlockStop events: content blocks were opened and filled with deltas, but never closed before messageStop. The AWS contract terminates every content block with a contentBlockStop event carrying the block's contentBlockIndex (a required field), and consumers that assemble the final message on block boundaries (the AWS SDK stream unions, frameworks like strands) only finalize a block when they receive it. Through Bifrost they received all the deltas and still ended up with an empty assembled message, so the break is silent: live delta readers see output, assembling consumers get nothing.

The invoke egress (invoke-with-response-stream, Anthropic Messages shape) already maps OutputItemDone to content_block_stop. The Converse path dropped the same event with a comment saying Bedrock has no done events, which holds for the part-level done events but not at the block level.

Changes

  • ToBedrockConverseStreamResponse now maps OutputItemDone to a contentBlockStop event carrying the same content block index the block's start and delta events use, mirroring the invoke path. OutputTextDone, ContentPartDone and ReasoningSummaryTextDone stay skipped, so each block is closed exactly once, on its item's done event.
  • BedrockStreamEvent gets an explicit ContentBlockStop marker: the wire payload of contentBlockStop is just {"contentBlockIndex": n}, so the flat event union had no way to represent it. The marker is never serialized; ToEncodedEvents builds the payload from ContentBlockIndex.
  • ToEncodedEvents encodes the new event after deltas and before messageStop, so a text stream now encodes as messageStart, contentBlockDelta (xN), contentBlockStop, messageStop, metadata.
  • Regression tests replay the Bifrost stream lifecycle for a text block, a tool use block and a reasoning block through the converter and the encoder, asserting the exact wire event sequences and the stop payloads.

Two notes on scope:

  • The issue also suggests emitting contentBlockStart for text blocks. Real ConverseStream responses do not do that: the ContentBlockStart union only has image, toolResult and toolUse members, so text blocks start implicitly with their first delta. The current behavior already matches the contract there, so this PR leaves the start events unchanged.
  • Nova models on invoke-with-response-stream delegate to the same Converse converter, so that path picks up the block terminator as well, which also matches Nova's native stream shape.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

cd core
go test ./providers/bedrock/ -run 'ConverseStream.*ContentBlockStop' -v

All three tests fail on dev without this change, with the exact event sequence from the issue report (messageStart, contentBlockDelta, contentBlockDelta, messageStop, metadata and no contentBlockStop), and pass with it. go build ./... and the full ./providers/bedrock/ package suite are green.

Screenshots/Recordings

N/A

Breaking changes

  • Yes
  • No

Consumers that only read contentBlockDelta are unaffected. The new event is part of the documented ConverseStreamOutput union, so Bedrock clients already handle it.

Related issues

Closes #4262

Security considerations

None. This only adds a lifecycle event to the outgoing stream encoding.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 14531dbc-3c0c-4223-ae3b-ba07dc92313c

📥 Commits

Reviewing files that changed from the base of the PR and between f244691 and c685752.

📒 Files selected for processing (4)
  • core/changelog.md
  • core/providers/bedrock/conversestreamstop_test.go
  • core/providers/bedrock/responses.go
  • core/providers/bedrock/types.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved Bedrock streaming so messages now include explicit block-ending events, helping downstream consumers assemble complete content correctly.
    • Updated stream output to preserve the correct event order for text, tool-use, and reasoning blocks.
  • Tests
    • Added coverage for stream event ordering and block-end payloads across multiple Bedrock response types.
  • Documentation
    • Added a changelog entry describing the streaming fix.

Walkthrough

Bedrock ConverseStream conversion now explicitly emits contentBlockStop events per content block instead of implicitly relying on "done" lifecycle markers. A new ContentBlockStop flag on BedrockStreamEvent drives this, with corresponding encoding logic, tests for text/tool-use/reasoning blocks, and a changelog entry.

Changes

ContentBlockStop Emission

Layer / File(s) Summary
ContentBlockStop marker
core/providers/bedrock/types.go
Adds a non-serialized ContentBlockStop boolean field to BedrockStreamEvent as an explicit discriminator for contentBlockStop events.
Conversion and encoding logic
core/providers/bedrock/responses.go
ToBedrockConverseStreamResponse explicitly ignores output_text.done/content_part.done/reasoning_summary_text.done and sets ContentBlockStop/ContentBlockIndex on output_item.done; ToEncodedEvents emits a contentBlockStop event when the flag is set.
Tests and changelog
core/providers/bedrock/conversestreamstop_test.go, core/changelog.md
Adds helpers and three tests validating emitted event sequences and contentBlockStop payloads for text, tool-use, and reasoning blocks; adds a changelog entry closing #4262.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Stream as BifrostResponsesStreamResponse
  participant Converter as ToBedrockConverseStreamResponse
  participant Event as BedrockStreamEvent
  participant Encoder as ToEncodedEvents

  Stream->>Converter: output_item.done
  Converter->>Event: set ContentBlockStop=true, ContentBlockIndex
  Event->>Encoder: ToEncodedEvents()
  Encoder->>Encoder: append contentBlockStop event with ContentBlockIndex
Loading

Suggested reviewers: danpiths, akshaydeo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: emitting contentBlockStop events for Bedrock ConverseStream.
Description check ✅ Passed The description follows the template well and includes all major sections: summary, changes, testing, breaking changes, related issues, and security.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — the change is a targeted addition of a lifecycle event that was contractually required but missing; it does not alter any existing event path.

The fix is narrow: one new case in a switch and one new branch in ToEncodedEvents. Every other block-index-carrying event uses the same ContentIndex defaulting pattern so the new code is consistent. The three new regression tests fail on the base branch and pass on this branch, directly reproducing the reported issue. The converter remains a pure transformation function with no side effects, and the new ContentBlockStop marker carries json:"-" so it cannot leak into any serialised wire payload.

No files require special attention.

Important Files Changed

Filename Overview
core/providers/bedrock/responses.go OutputItemDone case now sets ContentBlockStop=true and ContentBlockIndex, emitting contentBlockStop in ToEncodedEvents after deltas and before messageStop; change is consistent with how every other block-index-carrying event is assembled.
core/providers/bedrock/types.go Adds ContentBlockStop bool with json:"-" as an explicit marker in the flat BedrockStreamEvent union; the tag correctly prevents direct serialization and the field is only consumed by ToEncodedEvents.
core/providers/bedrock/conversestreamstop_test.go New regression tests cover text, tool-use, and reasoning block lifecycles, asserting both the exact event-type sequence and the contentBlockStop wire payload for each block type.
core/changelog.md Changelog entry added for the contentBlockStop fix.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Bifrost as Bifrost Stream
    participant Conv as ToBedrockConverseStreamResponse
    participant Enc as ToEncodedEvents
    participant Client as AWS SDK Consumer

    Bifrost->>Conv: ResponsesStreamResponseTypeCreated
    Conv->>Enc: "BedrockStreamEvent{Role: assistant}"
    Enc->>Client: messageStart

    Bifrost->>Conv: ResponsesStreamResponseTypeOutputItemAdded (tool)
    Conv->>Enc: "BedrockStreamEvent{Start: toolUse, ContentBlockIndex: n}"
    Enc->>Client: contentBlockStart

    Bifrost->>Conv: ResponsesStreamResponseTypeOutputTextDelta / FunctionCallArgumentsDelta
    Conv->>Enc: "BedrockStreamEvent{Delta: ..., ContentBlockIndex: n}"
    Enc->>Client: contentBlockDelta

    Note over Conv,Client: Bug fix: OutputItemDone now closes the block
    Bifrost->>Conv: ResponsesStreamResponseTypeOutputItemDone
    Conv->>Enc: "BedrockStreamEvent{ContentBlockStop: true, ContentBlockIndex: n}"
    Enc->>Client: contentBlockStop

    Bifrost->>Conv: ResponsesStreamResponseTypeCompleted
    Conv->>Enc: "BedrockStreamEvent{StopReason: ..., Usage: ...}"
    Enc->>Client: messageStop
    Enc->>Client: metadata
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Bifrost as Bifrost Stream
    participant Conv as ToBedrockConverseStreamResponse
    participant Enc as ToEncodedEvents
    participant Client as AWS SDK Consumer

    Bifrost->>Conv: ResponsesStreamResponseTypeCreated
    Conv->>Enc: "BedrockStreamEvent{Role: assistant}"
    Enc->>Client: messageStart

    Bifrost->>Conv: ResponsesStreamResponseTypeOutputItemAdded (tool)
    Conv->>Enc: "BedrockStreamEvent{Start: toolUse, ContentBlockIndex: n}"
    Enc->>Client: contentBlockStart

    Bifrost->>Conv: ResponsesStreamResponseTypeOutputTextDelta / FunctionCallArgumentsDelta
    Conv->>Enc: "BedrockStreamEvent{Delta: ..., ContentBlockIndex: n}"
    Enc->>Client: contentBlockDelta

    Note over Conv,Client: Bug fix: OutputItemDone now closes the block
    Bifrost->>Conv: ResponsesStreamResponseTypeOutputItemDone
    Conv->>Enc: "BedrockStreamEvent{ContentBlockStop: true, ContentBlockIndex: n}"
    Enc->>Client: contentBlockStop

    Bifrost->>Conv: ResponsesStreamResponseTypeCompleted
    Conv->>Enc: "BedrockStreamEvent{StopReason: ..., Usage: ...}"
    Enc->>Client: messageStop
    Enc->>Client: metadata
Loading

Reviews (1): Last reviewed commit: "fix: emit contentBlockStop events on the..." | Re-trigger Greptile

akshaydeo commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jul 5, 9:59 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 5, 9:59 PM UTC: Graphite couldn't merge this PR because it failed for an unknown reason (Fast-forward merges are not supported for forked repositories. Please create a branch in the target repository in order to merge).

@akshaydeo
akshaydeo merged commit 47570e4 into maximhq:dev Jul 5, 2026
6 checks passed
@fus3r
fus3r deleted the fix-converse-stream-content-block-stop branch July 5, 2026 22:06
@TejasGhatte TejasGhatte mentioned this pull request Jul 6, 2026
17 tasks
Pratham-Mishra04 pushed a commit that referenced this pull request Jul 6, 2026
## Summary

Adds end-to-end test coverage for a set of provider egress streaming and truncation correctness bugs. The new tests assert that Bedrock `converse-stream` properly closes content blocks before terminating, that Anthropic normalized and Claude Code passthrough streams emit contiguous `content_block_start` indices starting from 0, and that Bedrock Responses API truncated responses correctly signal `status=incomplete` with `reason=max_output_tokens` in both streaming and non-streaming modes.

## Changes

- Added a `contentBlockStop`-before-`messageStop` assertion to the existing Bedrock `converse-stream` basic test to catch #4923.
- Added a `content_block_start` index contiguity check to the existing Anthropic normalized streaming test to catch gaps introduced by server-tool rewrites (#4890 / #4932).
- Added a new **section 17 – Provider Egress Streaming/Truncation Guards** with four requests:
  - Bedrock forced-tool `converse-stream` verifies `toolUse`, `contentBlockStop`, and `messageStop` ordering (#4923).
  - Anthropic normalized `web_fetch` streaming verifies contiguous `content_block_start` indices (#4932).
  - Bedrock Responses non-streaming truncation verifies `status=incomplete` and `incomplete_details.reason=max_output_tokens` (#4680).
  - Bedrock Responses streaming truncation verifies `response.incomplete` is emitted and `response.completed` is absent (#4680).
- Added a new **section 18 – Claude Code Passthrough server-tool streaming index contiguity** with three requests covering `web_search` (normal results), `web_search` (zero results), and `web_fetch` via the `claude-cli` User-Agent passthrough path (#4890).

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Run the Postman/Newman collection against a live Bifrost instance:

```sh
newman run tests/e2e/api/collections/provider-harness.json \
  --env-var baseUrl=<BIFROST_URL> \
  --env-var bedrockModel=<BEDROCK_MODEL_ID> \
  --env-var anthropicKey=<ANTHROPIC_API_KEY>
```

All tests in sections 17 and 18 should pass. Specifically:
- Bedrock `converse-stream` responses must contain `contentBlockStop` before `messageStop`.
- All Anthropic streaming responses must have `content_block_start` indices `[0, 1, 2, …]` with no gaps.
- Bedrock Responses truncated (non-streaming) must return `status=incomplete` with `incomplete_details.reason=max_output_tokens`.
- Bedrock Responses truncated (streaming) must emit `response.incomplete` and must **not** emit `response.completed`.

## Breaking changes

- [x] No

## Related issues

Closes #4923, #4932, #4890, #4680

## Security considerations

None. These are read-only test assertions against existing API endpoints; no new credentials or secrets are introduced beyond those already required by the collection.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
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]: Bedrock ConverseStream egress never emits contentBlockStop (breaks strands streaming)

3 participants