Skip to content

[fix]: Bedrock provider - emit message_stop event for Anthropic invoke stream#2429

Merged
akshaydeo merged 3 commits intomaximhq:mainfrom
tefimov:fix/bedrock-invoke-message-stop-conversion
Apr 15, 2026
Merged

[fix]: Bedrock provider - emit message_stop event for Anthropic invoke stream#2429
akshaydeo merged 3 commits intomaximhq:mainfrom
tefimov:fix/bedrock-invoke-message-stop-conversion

Conversation

@tefimov
Copy link
Copy Markdown
Contributor

@tefimov tefimov commented Mar 31, 2026

Summary

Fixes the Bedrock invoke-with-response-stream conversion to emit both message_delta and message_stop events when converting from Bifrost internal format to Anthropic Messages API SSE format. Previously only message_delta was emitted, causing clients like Claude Code to hang indefinitely waiting for the stream termination signal.

Changes

  • Rename InvokeModelRawChunk to InvokeModelRawChunks ([][]byte) to support multiple events from a single Bifrost event
  • Update toAnthropicInvokeStreamBytes to return [][]byte instead of []byte
  • For Completed events, emit both message_delta and message_stop as separate events
  • Update ToEncodedEvents to iterate over multiple chunks
  • Add test verifying both events are emitted
  • Update OpenAPI schema for the field rename

Affected packages:

  • core/providers/bedrock/types.go - Field rename
  • core/providers/bedrock/invoke.go - Multi-event emission logic
  • core/providers/bedrock/responses.go - Iterate over chunks
  • core/providers/bedrock/bedrock_test.go - New test + error handling in helpers
  • transports/bifrost-http/integrations/bedrock.go - Use new field name
  • docs/openapi/schemas/integrations/bedrock/converse.yaml - Schema update
  • docs/openapi/openapi.json - Schema update

Type of change

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

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (Next.js)
  • Docs

How to test

# Run the specific tests
cd core
go test ./providers/bedrock/... -run TestToBedrockInvokeMessagesStreamResponse -v

# Expected output:
# === RUN   TestToBedrockInvokeMessagesStreamResponse_NoDuplicateContentBlockStop
# --- PASS: TestToBedrockInvokeMessagesStreamResponse_NoDuplicateContentBlockStop
# === RUN   TestToBedrockInvokeMessagesStreamResponse_MessageStopEmitted
# --- PASS: TestToBedrockInvokeMessagesStreamResponse_MessageStopEmitted

Breaking changes

  • Yes
  • No

Impact: The BedrockStreamEvent.InvokeModelRawChunk field has been renamed to InvokeModelRawChunks and changed from []byte to [][]byte. Any code directly accessing this field will need to be updated.

Migration: Change event.InvokeModelRawChunk to event.InvokeModelRawChunks[0] for single-event access, or iterate over the slice for multi-event handling.

Related issues

Security considerations

None - this is a bug fix for event emission.

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

🤖 Generated with Claude Code

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 31, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ tefimov
❌ akshaydeo
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03e71385-6bfe-4ffb-89a9-8a7ded8ada6c

📥 Commits

Reviewing files that changed from the base of the PR and between 51d4bde and 064f0ad.

📒 Files selected for processing (3)
  • core/providers/bedrock/bedrock_test.go
  • docs/openapi/openapi.json
  • docs/openapi/schemas/integrations/bedrock/converse.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/openapi/openapi.json
  • docs/openapi/schemas/integrations/bedrock/converse.yaml
  • core/providers/bedrock/bedrock_test.go

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Bedrock provider now emits stream completion events for Anthropic model invocations, ensuring proper stream termination signaling.
  • Improvements

    • Updated streaming response structure to support multiple message chunks per event, improving event handling for complex scenarios.
  • Documentation

    • Refreshed API schema documentation to reflect updated streaming behavior and multi-chunk event support.

Walkthrough

Changes restructure the Bedrock provider's stream event handling to support multiple raw chunks per event, enabling the Completed response to emit separate message_delta and message_stop Anthropic SSE events instead of a single event. The BedrockStreamEvent type transitions from holding a single InvokeModelRawChunk []byte field to InvokeModelRawChunks [][]byte.

Changes

Cohort / File(s) Summary
Type Definition
core/providers/bedrock/types.go
Changed BedrockStreamEvent.InvokeModelRawChunk from []byte to InvokeModelRawChunks [][]byte to support multiple raw chunks per stream event.
Core Provider Logic
core/providers/bedrock/invoke.go
Modified toAnthropicInvokeStreamBytes to return [][]byte (multiple SSE event payloads). Updated ResponsesStreamResponseTypeCompleted mapping to emit two separate Anthropic events (message_delta and message_stop) as distinct marshaled payloads. Non-Completed events are wrapped into single-element [][]byte slices.
Stream Response Handling
core/providers/bedrock/responses.go
Updated ToEncodedEvents() to iterate over each element in InvokeModelRawChunks and emit a separate "chunk" event for each raw payload.
Test Helpers
core/providers/bedrock/bedrock_test.go
Enhanced mustMarshalJSON and mustMarshalToolParams error handling to capture and panic with descriptive error messages instead of silently discarding json.Marshal errors.
Transport Integration
transports/bifrost-http/integrations/bedrock.go
Updated the invoke-with-response-stream route to populate InvokeModelRawChunks with a slice containing the raw response bytes instead of the single InvokeModelRawChunk field.
Documentation and Schemas
docs/openapi/openapi.json, docs/openapi/schemas/integrations/bedrock/converse.yaml
Replaced legacy schema field invokeModelRawChunk (single base64-encoded byte string) with invokeModelRawChunks (array of byte strings) to document support for multiple raw chunks when Bifrost events map to multiple Anthropic SSE events.
Changelog Entries
core/changelog.md, transports/changelog.md
Added entries documenting the fix to emit message_stop events and support for multi-event behavior via InvokeModelRawChunks.

Sequence Diagram

sequenceDiagram
    participant Client
    participant BedrockProvider as Bedrock Provider
    participant AnthropicAPI as Anthropic SSE
    
    Client->>BedrockProvider: Invoke with response stream
    BedrockProvider->>AnthropicAPI: Forward request
    AnthropicAPI-->>BedrockProvider: Response event: Completed
    
    Note over BedrockProvider: Completed → multiple chunks
    BedrockProvider->>BedrockProvider: Marshal message_delta payload
    BedrockProvider->>BedrockProvider: Marshal message_stop payload
    BedrockProvider->>BedrockProvider: Collect as [][]byte chunks
    
    BedrockProvider->>BedrockProvider: ToEncodedEvents()
    loop For each chunk in InvokeModelRawChunks
        BedrockProvider-->>Client: Emit "chunk" event with raw bytes
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Chunks multiply like carrots in spring,
One Completed event now splits into two,
message_delta hops, then message_stop takes wing,
The raw bytes flow through arrays so new!
Bedrock streams dance—multiple and true. 🌿

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: emitting a message_stop event for Anthropic invoke streams in the Bedrock provider.
Description check ✅ Passed The description comprehensively covers all required template sections including summary, changes, type of change, affected areas, testing instructions, breaking changes with migration guidance, related issues, and security considerations.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 31, 2026

Confidence Score: 4/5

Safe to merge after fixing the broken test and adding the missing message_stop coverage test.

The production logic change in invoke.go and responses.go is correct and well-scoped. However, an existing test will now always fail due to a stale field name in its local unmarshaling struct, and the promised new test covering the core fix is absent — both are concrete test failures that need resolving before CI passes.

core/providers/bedrock/bedrock_test.go — stale field name in NoDuplicateContentBlockStop test and missing MessageStopEmitted test.

Important Files Changed

Filename Overview
core/providers/bedrock/invoke.go Correctly extends toAnthropicInvokeStreamBytes to return [][]byte and emits both message_delta and message_stop for Completed events; logic and early-return guards look correct.
core/providers/bedrock/responses.go ToEncodedEvents updated to iterate over InvokeModelRawChunks slice; change is straightforward and correct.
core/providers/bedrock/types.go Field renamed from InvokeModelRawChunk []byte to InvokeModelRawChunks [][]byte with clear documentation comment.
core/providers/bedrock/bedrock_test.go Existing test NoDuplicateContentBlockStop not updated to use new field name (invokeModelRawChunks) — will fail at runtime. Promised new MessageStopEmitted test is absent from the diff.
transports/bifrost-http/integrations/bedrock.go Updated passthrough path to wrap single raw bytes in a one-element slice matching the new InvokeModelRawChunks field.
docs/openapi/openapi.json Four schema locations updated consistently from invokeModelRawChunk (byte) to invokeModelRawChunks (array of byte).
docs/openapi/schemas/integrations/bedrock/converse.yaml YAML schema updated to reflect the renamed array field with a helpful description.

Comments Outside Diff (2)

  1. core/providers/bedrock/bedrock_test.go, line 3649-3670 (link)

    Stale field name breaks this test

    The local bedrockChunk struct still uses the old JSON tag invokeModelRawChunk, but the field was renamed to invokeModelRawChunks (plural). When the test marshals a *BedrockStreamEvent and unmarshals it into this local struct, InvokeModelRawChunk will always be nil because the JSON key no longer matches. As a result, stopCount stays at 0 and assert.Equal(t, 1, stopCount, ...) will always fail.

  2. core/providers/bedrock/bedrock_test.go, line 17-23 (link)

    Missing message_stop emission test

    The PR description and "How to test" section mention a new test TestToBedrockInvokeMessagesStreamResponse_MessageStopEmitted that verifies both message_delta and message_stop are emitted on Completed events, but this test is not present in the diff. The core fix in invoke.go is untested by any new test case.

Reviews (7): Last reviewed commit: "Merge branch 'main' into fix/bedrock-inv..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
core/providers/bedrock/types.go (1)

600-603: Consider a transitional decode shim for the old JSON field name.

Line 603 is a breaking wire-format rename; if any external/fixture JSON still sends invokeModelRawChunk, payloads will be silently dropped. A one-release dual-read fallback can smooth migration.

Proposed compatibility patch
 type BedrockStreamEvent struct {
@@
 	// For InvokeModelWithResponseStream (Legacy API)
 	// InvokeModelRawChunks holds one or more raw byte payloads for legacy invoke stream.
 	// Multiple chunks are needed when a single Bifrost event maps to multiple Anthropic SSE events
 	// (e.g., Completed → message_delta + message_stop).
 	InvokeModelRawChunks [][]byte `json:"invokeModelRawChunks,omitempty"`
 }
+
+// UnmarshalJSON supports transitional compatibility for the legacy singular field.
+func (event *BedrockStreamEvent) UnmarshalJSON(data []byte) error {
+	type Alias BedrockStreamEvent
+	aux := &struct {
+		InvokeModelRawChunk  []byte   `json:"invokeModelRawChunk,omitempty"`
+		InvokeModelRawChunks [][]byte `json:"invokeModelRawChunks,omitempty"`
+		*Alias
+	}{
+		Alias: (*Alias)(event),
+	}
+
+	if err := sonic.Unmarshal(data, aux); err != nil {
+		return err
+	}
+	if len(aux.InvokeModelRawChunks) > 0 {
+		event.InvokeModelRawChunks = aux.InvokeModelRawChunks
+	} else if len(aux.InvokeModelRawChunk) > 0 {
+		event.InvokeModelRawChunks = [][]byte{aux.InvokeModelRawChunk}
+	}
+	return nil
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/bedrock/types.go` around lines 600 - 603, The Rename of the
JSON field to InvokeModelRawChunks is breaking for payloads that still send the
old "invokeModelRawChunk" key — add a transitional decode shim: implement a
custom UnmarshalJSON on the struct that owns the InvokeModelRawChunks field to
accept either "invokeModelRawChunks" (new array form) or the legacy
"invokeModelRawChunk" (single chunk) and populate InvokeModelRawChunks
accordingly (wrap the single value into a slice when the legacy key is present);
keep the existing json tag/output using the new "invokeModelRawChunks" name so
only decoding is dual-read for one release.
core/providers/bedrock/bedrock_test.go (1)

17-20: mustMarshalJSON ignores the error from json.Marshal.

Based on retrieved learnings, must* helper functions in bedrock tests should handle json.Marshal errors by panicking or failing fast, not by discarding them with _.

♻️ Proposed fix
 func mustMarshalJSON(v interface{}) json.RawMessage {
-	b, _ := json.Marshal(v)
+	b, err := json.Marshal(v)
+	if err != nil {
+		panic(fmt.Sprintf("mustMarshalJSON: %v", err))
+	}
 	return json.RawMessage(b)
 }

Note: A similar fix should be applied to mustMarshalToolParams at lines 42-45.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/bedrock/bedrock_test.go` around lines 17 - 20, mustMarshalJSON
currently discards the error from json.Marshal; change it to check the error and
fail fast (panic) when Marshal returns an error, returning the json.RawMessage
only on success; include the error text in the panic message for debugging.
Apply the same pattern to mustMarshalToolParams so both helpers validate the
json.Marshal error instead of ignoring it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@core/providers/bedrock/bedrock_test.go`:
- Around line 17-20: mustMarshalJSON currently discards the error from
json.Marshal; change it to check the error and fail fast (panic) when Marshal
returns an error, returning the json.RawMessage only on success; include the
error text in the panic message for debugging. Apply the same pattern to
mustMarshalToolParams so both helpers validate the json.Marshal error instead of
ignoring it.

In `@core/providers/bedrock/types.go`:
- Around line 600-603: The Rename of the JSON field to InvokeModelRawChunks is
breaking for payloads that still send the old "invokeModelRawChunk" key — add a
transitional decode shim: implement a custom UnmarshalJSON on the struct that
owns the InvokeModelRawChunks field to accept either "invokeModelRawChunks" (new
array form) or the legacy "invokeModelRawChunk" (single chunk) and populate
InvokeModelRawChunks accordingly (wrap the single value into a slice when the
legacy key is present); keep the existing json tag/output using the new
"invokeModelRawChunks" name so only decoding is dual-read for one release.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2cf8b955-d896-4914-a2d9-4e4511e1c3da

📥 Commits

Reviewing files that changed from the base of the PR and between a7f83b4 and 8651cd6.

📒 Files selected for processing (7)
  • core/changelog.md
  • core/providers/bedrock/bedrock_test.go
  • core/providers/bedrock/invoke.go
  • core/providers/bedrock/responses.go
  • core/providers/bedrock/types.go
  • transports/bifrost-http/integrations/bedrock.go
  • transports/changelog.md

coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 31, 2026
The Bedrock invoke-with-response-stream endpoint converts Bifrost internal
format to Anthropic Messages API SSE events. Previously, the Completed event
only emitted message_delta but not message_stop, causing clients like Claude
Code to hang indefinitely waiting for the stream termination signal.

This change:
- Renames InvokeModelRawChunk to InvokeModelRawChunks ([][]byte) to support
  multiple events from a single Bifrost event
- Updates toAnthropicInvokeStreamBytes to return [][]byte
- For Completed events, emits both message_delta and message_stop
- Updates ToEncodedEvents to iterate over multiple chunks
- Adds test verifying both events are emitted

This fixes cross-provider routing (e.g., OpenAI → Bedrock) where the
conversion layer is required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tefimov tefimov force-pushed the fix/bedrock-invoke-message-stop-conversion branch from 1a5f377 to 51d4bde Compare April 1, 2026 16:29
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/providers/bedrock/invoke.go (1)

835-849: ⚠️ Potential issue | 🟠 Major

Preserve tool_use in the final message_delta.

Line 837 falls back to end_turn whenever IncompleteDetails is nil, but toBedrockInvokeAnthropicResponse() already infers tool_use from the final output in that case. A streamed invoke completion that ends in a tool call will now emit both terminal events but still advertise a different stop reason than the non-streaming path.

Suggested fix
 		stopReason := "end_turn"
 		if resp.Response != nil && resp.Response.IncompleteDetails != nil {
 			stopReason = resp.Response.IncompleteDetails.Reason
+		} else if resp.Response != nil {
+			for _, item := range resp.Response.Output {
+				if item.ResponsesToolMessage != nil {
+					stopReason = "tool_use"
+					break
+				}
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/bedrock/invoke.go` around lines 835 - 849, The final streamed
completion currently always sets stop_reason to "end_turn" when
resp.Response.IncompleteDetails is nil, which loses the inferred tool-use info
produced by toBedrockInvokeAnthropicResponse; update the message_delta
construction in the ResponsesStreamResponseTypeCompleted case to preserve any
tool_use captured on the response (e.g., use resp.Response.ToolUse or the field
populated by toBedrockInvokeAnthropicResponse) by adding a "tool_use" entry to
the delta when present (fall back to omitting it only if no tool_use is
available). Locate the ResponsesStreamResponseTypeCompleted case in invoke.go
(variable resp) and add the tool_use key into the messageDelta["delta"] map so
the streamed completion emits the same tool_use as the non-streaming path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@core/providers/bedrock/invoke.go`:
- Around line 835-849: The final streamed completion currently always sets
stop_reason to "end_turn" when resp.Response.IncompleteDetails is nil, which
loses the inferred tool-use info produced by toBedrockInvokeAnthropicResponse;
update the message_delta construction in the
ResponsesStreamResponseTypeCompleted case to preserve any tool_use captured on
the response (e.g., use resp.Response.ToolUse or the field populated by
toBedrockInvokeAnthropicResponse) by adding a "tool_use" entry to the delta when
present (fall back to omitting it only if no tool_use is available). Locate the
ResponsesStreamResponseTypeCompleted case in invoke.go (variable resp) and add
the tool_use key into the messageDelta["delta"] map so the streamed completion
emits the same tool_use as the non-streaming path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4bedee72-bf87-4817-a2e0-1617089bc071

📥 Commits

Reviewing files that changed from the base of the PR and between 8651cd6 and 51d4bde.

📒 Files selected for processing (10)
  • core/changelog.md
  • core/providers/bedrock/batch.go
  • core/providers/bedrock/bedrock_test.go
  • core/providers/bedrock/invoke.go
  • core/providers/bedrock/responses.go
  • core/providers/bedrock/types.go
  • docs/openapi/openapi.json
  • docs/openapi/schemas/integrations/bedrock/converse.yaml
  • transports/bifrost-http/integrations/bedrock.go
  • transports/changelog.md
✅ Files skipped from review due to trivial changes (4)
  • core/changelog.md
  • transports/changelog.md
  • core/providers/bedrock/batch.go
  • core/providers/bedrock/responses.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • transports/bifrost-http/integrations/bedrock.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 1, 2026
@tefimov tefimov force-pushed the fix/bedrock-invoke-message-stop-conversion branch from 51d4bde to 98ed28a Compare April 1, 2026 16:48
- Add error handling to mustMarshalJSON and mustMarshalToolParams test helpers
- Update OpenAPI schema: rename invokeModelRawChunk to invokeModelRawChunks
  (array type) in converse.yaml and openapi.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tefimov tefimov force-pushed the fix/bedrock-invoke-message-stop-conversion branch from 98ed28a to 064f0ad Compare April 1, 2026 16:52
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 1, 2026
@akshaydeo
Copy link
Copy Markdown
Contributor

@greptile

akshaydeo
akshaydeo previously approved these changes Apr 15, 2026
@akshaydeo akshaydeo dismissed stale reviews from coderabbitai[bot] and themself via f662c87 April 15, 2026 12:06
@akshaydeo akshaydeo merged commit b423d7d into maximhq:main Apr 15, 2026
1 of 3 checks passed
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.

5 participants