Skip to content

refactor: extract StreamAnthropicChatEvents/StreamAnthropicResponsesEvents and route Bedrock Claude streams through shared Anthropic loop via bedrockAnthropicEventReader - #4360

Closed
Pratham-Mishra04 wants to merge 1 commit into
05-08-feat_shifted_to_anthropic_endpoints_in_bedrock_for_claude_models_and_refactorsfrom
06-13-feat_adds_anthropic_stream_delegation_in_bedrock_messages_endpoints
Closed

refactor: extract StreamAnthropicChatEvents/StreamAnthropicResponsesEvents and route Bedrock Claude streams through shared Anthropic loop via bedrockAnthropicEventReader#4360
Pratham-Mishra04 wants to merge 1 commit into
05-08-feat_shifted_to_anthropic_endpoints_in_bedrock_for_claude_models_and_refactorsfrom
06-13-feat_adds_anthropic_stream_delegation_in_bedrock_messages_endpoints

Conversation

@Pratham-Mishra04

@Pratham-Mishra04 Pratham-Mishra04 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Claude-on-Bedrock streaming previously duplicated the entire Anthropic SSE event loop inline, maintaining its own per-chunk state and accumulating usage independently. This PR replaces that duplicated path with a shared bedrockAnthropicEventReader adapter that translates Bedrock's binary AWS EventStream framing into the SSEEventReader contract, allowing both ChatCompletionStream and ResponsesStream for Anthropic models on Bedrock to be driven by the canonical StreamAnthropicChatEvents / StreamAnthropicResponsesEvents loops.

Changes

  • Extracted the Anthropic SSE event loop out of the inline goroutine closure in HandleAnthropicChatCompletionStreaming and HandleAnthropicResponsesStream into standalone exported functions StreamAnthropicChatEvents and StreamAnthropicResponsesEvents. These accept any SSEEventReader, decoupling wire framing from event processing.
  • Introduced StreamReaderError, a sentinel error type that lets a custom SSEEventReader surface a fully-formed *schemas.BifrostError through the ReadEvent contract. The shared loops detect it via errors.As and forward the embedded error verbatim, preserving status codes so the retry gate behaves correctly. The native SSE reader never returns this type, so existing SSE callers are unaffected.
  • Added bedrockAnthropicEventReader (in core/providers/bedrock/anthropicstream.go), which decodes AWS EventStream frames, unwraps the inner Anthropic SSE chunk from the {"bytes": ...} payload, and derives the SSE-style event type from the chunk's own type field. AWS exception frames are classified as retryable or terminal and surfaced as *StreamReaderError or plain errors respectively, preserving Bedrock's retry semantics.
  • In BedrockProvider.ChatCompletionStream and BedrockProvider.ResponsesStream, Anthropic model families now early-exit into StreamAnthropicChatEvents / StreamAnthropicResponsesEvents via the adapter reader. A post-response converter on the chat path stamps each chunk with a locally generated ID and the requested model ID. The Converse API path for non-Anthropic models is unchanged.
  • Removed the now-redundant inline Anthropic stream state (anthropicChatStreamState, anthropicStreamState), duplicated usage accumulation, and the per-chunk ToBifrostChatCompletionStream / ToBifrostResponsesStream dispatch that previously lived inside the Bedrock event loop for Anthropic models.
  • Fixed a minor ordering issue in the chat event loop: response.ID = messageID is now assigned before postResponseConverter runs, giving converters (e.g. Bedrock's ID/model override) the final say on those fields.

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

go test ./...
  • Invoke a Claude model via Bedrock with ChatCompletionStream and verify streamed chunks arrive with correct IDs, model names, usage, and finish reasons.
  • Invoke a Claude model via Bedrock with ResponsesStream and verify the same.
  • Trigger a throttling or serviceUnavailableException from Bedrock and confirm the retry gate fires as expected (retryable status code preserved).
  • Invoke a non-Anthropic Bedrock model (e.g. Titan, Llama) and confirm the Converse API path is unaffected.
  • Run a structured output request through both the Anthropic and Bedrock Claude paths and confirm the finish reason is stop and content is correctly assembled.

Breaking changes

  • Yes
  • No

Related issues

Security considerations

None. No new auth surfaces, secrets handling, or PII exposure introduced.

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

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling and recovery for streaming failures across Anthropic and Bedrock providers.
  • Refactor

    • Consolidated streaming logic for Anthropic models in Bedrock to reduce code duplication and ensure consistent behavior.
    • Enhanced structured-output handling during streaming responses for improved reliability.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extracts shared Anthropic streaming event-loop logic into reusable handlers and bridges them to Bedrock via an eventstream adapter, eliminating duplicate code and enabling proper error propagation through a new StreamReaderError wrapper type.

Changes

Streaming Refactoring and Integration

Layer / File(s) Summary
StreamReaderError contract for error propagation
core/providers/anthropic/anthropic.go
Adds StreamReaderError wrapper type allowing custom readers to surface fully-formed Bifrost errors with preserved status and retry semantics.
Anthropic chat event-loop extraction
core/providers/anthropic/anthropic.go
Extracts inline chat streaming into StreamAnthropicChatEvents handler that manages SSE parsing, usage aggregation, finish-reason mapping (with structured-output overrides), tool-call delta interception, chunk emission, and response conversion/attachment.
Anthropic responses event-loop extraction
core/providers/anthropic/anthropic.go
Extracts inline responses streaming into StreamAnthropicResponsesEvents handler that maintains stream state, aggregates usage, converts events to streaming response chunks, supports raw passthrough, and attaches final usage/latency.
Bedrock→Anthropic eventstream adapter
core/providers/bedrock/anthropicstream.go
Implements bedrockAnthropicEventReader decoding AWS Bedrock eventstream frames into the ReadEvent contract, with explicit EOF/transport-error wrapping, retryable exception classification, and embedded Anthropic SSE extraction.
Bedrock chat streaming integration
core/providers/bedrock/bedrock.go
Routes Anthropic models through the shared chat handler via the Bedrock adapter in an early-return branch; remaining Converse path is reworked to handle events directly with structured-output tool interception and cached-token aggregation.
Bedrock responses streaming integration
core/providers/bedrock/bedrock.go
Routes Anthropic models through the shared responses handler via the Bedrock adapter in an early-return branch; remaining Converse path is reworked to parse events, aggregate cached-token usage, intercept structured-output tool deltas, and emit final responses with usage attachment.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • maximhq/bifrost#3838: Fixes Anthropic responses stream "done" event payloads by adding per-outputIndex TextBuffers—both target the same Anthropic responses streaming event emission logic.

Suggested reviewers

  • akshaydeo

Poem

🐰 A streaming refactor so neat,
Where Bedrock and Anthro compete—
No duplication now blooms,
In adapter-shaped rooms,
With errors that flow, oh so sweet! 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main refactoring: extracting shared Anthropic event stream handlers and routing Bedrock Claude through them via a new adapter.
Description check ✅ Passed The description covers all template sections including summary, changes, type of change, affected areas, testing approach, breaking changes, and security considerations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 06-13-feat_adds_anthropic_stream_delegation_in_bedrock_messages_endpoints

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"


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

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 3/5

The Anthropic-native and Bedrock-Claude streaming paths look correct; there is a token-accounting inconsistency in the Bedrock Converse path that affects non-Anthropic models with cached inputs.

The Converse-API ChatCompletionStream loop lost the usage.TotalTokens cached-token adjustment while keeping the identical adjustment on usage.PromptTokens. Requests that hit the cache through non-Anthropic Bedrock models will emit a final usage chunk where total_tokens is lower than prompt_tokens + completion_tokens, a data inconsistency shipped to callers on every cached request through that path.

core/providers/bedrock/bedrock.go — Converse-API ChatCompletionStream cached-token accounting (lines 1528–1530); core/providers/bedrock/anthropicstream.go — new adapter warrants integration tests for the Anthropic-via-Bedrock streaming path.

Important Files Changed

Filename Overview
core/providers/anthropic/anthropic.go Extracts the Anthropic chat and responses streaming event loops into exported StreamAnthropicChatEvents / StreamAnthropicResponsesEvents functions accepting any SSEEventReader; adds the StreamReaderError sentinel type. Logic is faithfully preserved from the inlined goroutine closures.
core/providers/bedrock/anthropicstream.go New adapter that wraps Bedrock's binary AWS EventStream framing as an SSEEventReader. Retryable exception classification and transport-error mapping look correct; the empty-payload guard silently skips exception frames with no body, and the payloadBuf hint is not captured after expansion.
core/providers/bedrock/bedrock.go Routes Claude-on-Bedrock streams through the shared Anthropic loops and removes duplicated inline state. A regression was introduced in the Converse-API ChatCompletionStream path: the usage.TotalTokens adjustment for cached tokens was dropped while usage.PromptTokens still receives it, producing inconsistent usage figures for non-Anthropic Bedrock models that use prompt caching.

Reviews (1): Last reviewed commit: "feat: adds anthropic stream delegation i..." | Re-trigger Greptile

Comment on lines +1528 to +1530
if usage.PromptTokensDetails != nil {
usage.PromptTokens = usage.PromptTokens + usage.PromptTokensDetails.CachedReadTokens + usage.PromptTokensDetails.CachedWriteTokens
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing TotalTokens adjustment for cached tokens in Converse path

The Converse-path ChatCompletionStream loop previously adjusted both usage.PromptTokens and usage.TotalTokens by CachedReadTokens + CachedWriteTokens. After the refactor the TotalTokens line was dropped, so for any Bedrock Converse-API request that carries cached tokens (non-Anthropic models) the emitted final chunk will report a total_tokens that is lower than prompt_tokens + completion_tokens, breaking any downstream consumer that relies on the relationship between those two fields.

Suggested change
if usage.PromptTokensDetails != nil {
usage.PromptTokens = usage.PromptTokens + usage.PromptTokensDetails.CachedReadTokens + usage.PromptTokensDetails.CachedWriteTokens
}
if usage.PromptTokensDetails != nil {
usage.PromptTokens = usage.PromptTokens + usage.PromptTokensDetails.CachedReadTokens + usage.PromptTokensDetails.CachedWriteTokens
usage.TotalTokens = usage.TotalTokens + usage.PromptTokensDetails.CachedReadTokens + usage.PromptTokensDetails.CachedWriteTokens
}

Comment on lines +69 to +74
if len(message.Payload) == 0 {
continue
}

// Non-"event" message types carry AWS exception details in their headers.
if msgTypeHeader := message.Headers.Get(":message-type"); msgTypeHeader != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Empty-payload check precedes :message-type header check

An AWS exception frame with an empty body is unusual but not impossible. The current ordering silently continues on it rather than inspecting the :message-type / :exception-type headers, so a terminal accessDeniedException or validationException with an empty payload would be swallowed and the loop would stall waiting for more frames until the stream eventually closes.

Comment on lines +34 to +40
func newBedrockAnthropicEventReader(src io.Reader, providerName schemas.ModelProvider) *bedrockAnthropicEventReader {
return &bedrockAnthropicEventReader{
decoder: eventstream.NewDecoder(),
src: src,
payloadBuf: make([]byte, 0, 1024*1024), // 1MB payload buffer
providerName: providerName,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 payloadBuf expansion is never captured between calls

eventstream.Decoder.Decode may return a larger backing slice when the payload exceeds the initial capacity, but the result is not stored back into r.payloadBuf. Subsequent calls always pass the original 1 MB slice, so for streams with payloads larger than 1 MB the pre-allocation hint provides no benefit. The existing Converse path has the same pattern, so this is not a regression, but capturing the returned slice would make the comment accurate.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/anthropic/anthropic.go`:
- Around line 684-685: The stream startTime is currently captured before the
HTTP handshake, inflating streaming latency; move the startTime initialization
to immediately after the activeClient.Do(req, resp) returns (i.e., after the
handshake) and pass that fresh startTime into StreamAnthropicChatEvents (and the
other new helper call referenced in the comment) instead of the earlier
startTime from HandleAnthropic*Stream so first/final-chunk latency measures only
post-handshake streaming time.

In `@core/providers/bedrock/anthropicstream.go`:
- Around line 69-75: Reorder the check so we inspect message.Headers
(specifically message.Headers.Get(":message-type") and the ":exception-type"
header) before skipping frames with an empty payload: if a non-"event"
:message-type is present (or an :exception-type header), treat it as a Bedrock
exception frame and handle/return it instead of continuing; only skip the frame
when there is no payload AND no exception/ non-"event" header. Move or adjust
the existing len(message.Payload) == 0 guard accordingly so header-only
exception frames are not dropped.

In `@core/providers/bedrock/bedrock.go`:
- Around line 1953-1973: The synthetic text-delta branch in the
streamEvent.Delta.ToolUse handling creates a BifrostResponsesStreamResponse but
omits the per-item routing fields (OutputIndex, ContentIndex, and current item
identity) used downstream to attach deltas to the active output item; fix it by
populating the same routing fields that
streamEvent.ToBifrostResponsesStream(...) would include: copy streamEvent's
OutputIndex and ContentIndex and the current item identifier (e.g.,
streamEvent.Item.ID or equivalent field present on streamEvent) into
response.ExtraFields (and any top-level fields if the normal converter sets
them), or obtain a base response from streamEvent.ToBifrostResponsesStream and
merge its routing fields into the synthesized response before calling
providerUtils.ProcessAndSendResponse.
- Around line 1528-1530: The code is repeatedly adding cached token counts into
usage.PromptTokens on every EventStream message, inflating reported prompt
tokens; to fix, stop folding CachedReadTokens/CachedWriteTokens into
usage.PromptTokens inside the per-event loop and instead perform a one-time
aggregation immediately before calling CreateBifrostChatCompletionChunkResponse
by checking usage.PromptTokensDetails and adding usage.PromptTokens +=
usage.PromptTokensDetails.CachedReadTokens +
usage.PromptTokensDetails.CachedWriteTokens once, then proceed to
CreateBifrostChatCompletionChunkResponse; remove or guard the in-loop adjustment
around usage.PromptTokensDetails so cached tokens are not added repeatedly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7bff2855-1dd1-4781-a385-258b6eb4ce02

📥 Commits

Reviewing files that changed from the base of the PR and between 5e5d2ab and 314bfd0.

📒 Files selected for processing (3)
  • core/providers/anthropic/anthropic.go
  • core/providers/bedrock/anthropicstream.go
  • core/providers/bedrock/bedrock.go

Comment on lines +684 to +685
StreamAnthropicChatEvents(ctx, sseReader, responseChan, jsonBody, startTime, sendBackRawRequest, sendBackRawResponse, providerName, postHookRunner, postResponseConverter, logger, postHookSpanFinalizer)
}()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Capture the shared stream timer after the handshake.

Both new helper calls inherit startTime from HandleAnthropic*Stream, but that timestamp is still taken before activeClient.Do(req, resp). This makes first/final chunk latency include connection/header wait for both native Anthropic streams and the new Bedrock paths that reuse these helpers, so the refactor now spreads inflated latency metrics across both implementations.

Based on learnings, initialize streaming startTime after client.Do so final-chunk latency reflects post-handshake streaming time.

Also applies to: 1189-1190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/anthropic/anthropic.go` around lines 684 - 685, The stream
startTime is currently captured before the HTTP handshake, inflating streaming
latency; move the startTime initialization to immediately after the
activeClient.Do(req, resp) returns (i.e., after the handshake) and pass that
fresh startTime into StreamAnthropicChatEvents (and the other new helper call
referenced in the comment) instead of the earlier startTime from
HandleAnthropic*Stream so first/final-chunk latency measures only post-handshake
streaming time.

Source: Learnings

Comment on lines +69 to +75
if len(message.Payload) == 0 {
continue
}

// Non-"event" message types carry AWS exception details in their headers.
if msgTypeHeader := message.Headers.Get(":message-type"); msgTypeHeader != nil {
if msgType := msgTypeHeader.String(); msgType != "event" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Inspect EventStream headers before skipping empty payloads.

This drops any frame whose payload is empty before checking :message-type / :exception-type. Bedrock exception frames are classified from those headers, so a header-only exception is silently ignored and the shared Anthropic loop can fall through to EOF as if the stream ended normally instead of surfacing or retrying the failure.

💡 Minimal fix
-		if len(message.Payload) == 0 {
-			continue
-		}
-
-		// Non-"event" message types carry AWS exception details in their headers.
 		if msgTypeHeader := message.Headers.Get(":message-type"); msgTypeHeader != nil {
 			if msgType := msgTypeHeader.String(); msgType != "event" {
 				excType := msgType
 				if excHeader := message.Headers.Get(":exception-type"); excHeader != nil {
 					if v := excHeader.String(); v != "" {
@@
 				return "", nil, fmt.Errorf("%s stream %s: %s", r.providerName, excType, errMsg)
 			}
 		}
+
+		if len(message.Payload) == 0 {
+			continue
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/bedrock/anthropicstream.go` around lines 69 - 75, Reorder the
check so we inspect message.Headers (specifically
message.Headers.Get(":message-type") and the ":exception-type" header) before
skipping frames with an empty payload: if a non-"event" :message-type is present
(or an :exception-type header), treat it as a Bedrock exception frame and
handle/return it instead of continuing; only skip the frame when there is no
payload AND no exception/ non-"event" header. Move or adjust the existing
len(message.Payload) == 0 guard accordingly so header-only exception frames are
not dropped.

Comment on lines +1528 to +1530
if usage.PromptTokensDetails != nil {
usage.PromptTokens = usage.PromptTokens + usage.PromptTokensDetails.CachedReadTokens + usage.PromptTokensDetails.CachedWriteTokens
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only fold cached tokens into PromptTokens once.

This runs inside the per-event loop, so every later EventStream message re-adds the same cached counts and inflates the final usage block. That will overstate streamed token usage for any consumer using the terminal chunk for accounting.

🩹 Suggested fix
-				if usage.PromptTokensDetails != nil {
-					usage.PromptTokens = usage.PromptTokens + usage.PromptTokensDetails.CachedReadTokens + usage.PromptTokensDetails.CachedWriteTokens
-				}

Apply that adjustment once, immediately before CreateBifrostChatCompletionChunkResponse(...).

Based on learnings: in the Bedrock provider, cached tokens should be aggregated into input tokens as a single step, and TotalTokens already includes cached token counts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/bedrock/bedrock.go` around lines 1528 - 1530, The code is
repeatedly adding cached token counts into usage.PromptTokens on every
EventStream message, inflating reported prompt tokens; to fix, stop folding
CachedReadTokens/CachedWriteTokens into usage.PromptTokens inside the per-event
loop and instead perform a one-time aggregation immediately before calling
CreateBifrostChatCompletionChunkResponse by checking usage.PromptTokensDetails
and adding usage.PromptTokens += usage.PromptTokensDetails.CachedReadTokens +
usage.PromptTokensDetails.CachedWriteTokens once, then proceed to
CreateBifrostChatCompletionChunkResponse; remove or guard the in-loop adjustment
around usage.PromptTokensDetails so cached tokens are not added repeatedly.

Source: Learnings

Comment on lines +1953 to +1973
if streamEvent.Delta != nil && streamEvent.Delta.ToolUse != nil && isAccumulatingStructuredOutput {
// Convert tool use delta to text delta
content := streamEvent.Delta.ToolUse.Input
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputTextDelta,
SequenceNumber: chunkIndex,
Delta: &content,
ExtraFields: schemas.BifrostResponseExtraFields{
ChunkIndex: chunkIndex,
Latency: time.Since(lastChunkTime).Milliseconds(),
RawResponse: string(chunkPayload.Bytes),
ChunkIndex: chunkIndex,
Latency: time.Since(lastChunkTime).Milliseconds(),
},
}
lastChunkTime = time.Now()
chunkIndex++
providerUtils.ProcessAndSendResponse(ctx, postHookRunner,
providerUtils.GetBifrostResponseForStreamResponse(nil, nil, passthroughResp, nil, nil, nil),
responseChan, postHookSpanFinalizer)
}
for i, response := range responses {
if response != nil {
response.ExtraFields = schemas.BifrostResponseExtraFields{
ChunkIndex: chunkIndex,
Latency: time.Since(lastChunkTime).Milliseconds(),
}
chunkIndex++
lastChunkTime = time.Now()
// Only attach raw response to the last response of the incoming event
if providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse) && i == len(responses)-1 {
response.ExtraFields.RawResponse = string(chunkPayload.Bytes)
}
// Finalize the very last chunk of the stream with accumulated usage,
// raw request, total latency, and the stream-end indicator.
if isLastChunk && i == len(responses)-1 {
if response.Response == nil {
response.Response = &schemas.BifrostResponsesResponse{}
}
if usage.InputTokensDetails != nil {
usage.InputTokens = usage.InputTokens + usage.InputTokensDetails.CachedReadTokens + usage.InputTokensDetails.CachedWriteTokens
usage.TotalTokens = usage.TotalTokens + usage.InputTokensDetails.CachedReadTokens + usage.InputTokensDetails.CachedWriteTokens
}
response.Response.Usage = usage
if providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest) {
providerUtils.ParseAndSetRawRequest(&response.ExtraFields, jsonData)
}
response.ExtraFields.Latency = time.Since(startTime).Milliseconds()
ctx.SetValue(schemas.BifrostContextKeyStreamEndIndicator, true)
}
providerUtils.ProcessAndSendResponse(ctx, postHookRunner, providerUtils.GetBifrostResponseForStreamResponse(nil, nil, response, nil, nil, nil), responseChan, postHookSpanFinalizer)
lastChunkTime = time.Now()

if providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse) {
response.ExtraFields.RawResponse = string(message.Payload)
}
}
if isLastChunk {
return
}
} else {
// Converse API path: parse Bedrock Converse-specific stream events
var streamEvent BedrockStreamEvent
if err := sonic.Unmarshal(message.Payload, &streamEvent); err != nil {
provider.logger.Debug("Failed to parse JSON from event buffer: %v, data: %s", err, string(message.Payload))
providerUtils.ProcessAndSendError(ctx, postHookRunner, err, responseChan, provider.logger, postHookSpanFinalizer)
return
}

if streamEvent.Trace != nil {
streamTrace = streamEvent.Trace
providerUtils.ProcessAndSendResponse(ctx, postHookRunner, providerUtils.GetBifrostResponseForStreamResponse(nil, nil, response, nil, nil, nil), responseChan, postHookSpanFinalizer)
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Carry the normal item/content routing fields on synthetic text deltas.

These structured-output chunks bypass streamEvent.ToBifrostResponsesStream(...) and emit only Type, SequenceNumber, and Delta. That drops the per-item/per-content fields (OutputIndex, ContentIndex, and current item identity when available) that downstream Responses clients use to attach deltas to the active output item.

Mirror the routing fields the normal converter emits when synthesizing these response.output_text.delta events.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/bedrock/bedrock.go` around lines 1953 - 1973, The synthetic
text-delta branch in the streamEvent.Delta.ToolUse handling creates a
BifrostResponsesStreamResponse but omits the per-item routing fields
(OutputIndex, ContentIndex, and current item identity) used downstream to attach
deltas to the active output item; fix it by populating the same routing fields
that streamEvent.ToBifrostResponsesStream(...) would include: copy streamEvent's
OutputIndex and ContentIndex and the current item identifier (e.g.,
streamEvent.Item.ID or equivalent field present on streamEvent) into
response.ExtraFields (and any top-level fields if the normal converter sets
them), or obtain a base response from streamEvent.ToBifrostResponsesStream and
merge its routing fields into the synthesized response before calling
providerUtils.ProcessAndSendResponse.

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.

2 participants