Files
bifrost/core/providers/bedrock/responses.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

3395 lines
118 KiB
Go

package bedrock
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"github.com/bytedance/sonic"
"github.com/google/uuid"
"github.com/maximhq/bifrost/core/providers/anthropic"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
"github.com/maximhq/bifrost/core/schemas"
)
// BedrockResponsesStreamState tracks state during streaming conversion for responses API
type BedrockResponsesStreamState struct {
ContentIndexToOutputIndex map[int]int // Maps Bedrock contentBlockIndex to OpenAI output_index
ToolArgumentBuffers map[int]string // Maps output_index to accumulated tool argument JSON
ItemIDs map[int]string // Maps output_index to item ID for stable IDs
ToolCallIDs map[int]string // Maps output_index to tool call ID (callID)
ToolCallNames map[int]string // Maps output_index to tool call name
ReasoningContentIndices map[int]bool // Tracks which content indices are reasoning blocks
CompletedOutputIndices map[int]bool // Tracks which output indices have been completed
CurrentOutputIndex int // Current output index counter
MessageID *string // Message ID (generated)
Model *string // Model name
StopReason *string // Stop reason for the message
CreatedAt int // Timestamp for created_at consistency
HasEmittedCreated bool // Whether we've emitted response.created
HasEmittedInProgress bool // Whether we've emitted response.in_progress
}
// bedrockResponsesStreamStatePool provides a pool for Bedrock responses stream state objects.
var bedrockResponsesStreamStatePool = sync.Pool{
New: func() interface{} {
return &BedrockResponsesStreamState{
ContentIndexToOutputIndex: make(map[int]int),
ToolArgumentBuffers: make(map[int]string),
ItemIDs: make(map[int]string),
ToolCallIDs: make(map[int]string),
ToolCallNames: make(map[int]string),
ReasoningContentIndices: make(map[int]bool),
CompletedOutputIndices: make(map[int]bool),
CurrentOutputIndex: 0,
CreatedAt: int(time.Now().Unix()),
HasEmittedCreated: false,
HasEmittedInProgress: false,
}
},
}
// acquireBedrockResponsesStreamState gets a Bedrock responses stream state from the pool.
func acquireBedrockResponsesStreamState() *BedrockResponsesStreamState {
state := bedrockResponsesStreamStatePool.Get().(*BedrockResponsesStreamState)
// Clear maps (they're already initialized from New or previous flush)
// Only initialize if nil (shouldn't happen, but defensive)
if state.ContentIndexToOutputIndex == nil {
state.ContentIndexToOutputIndex = make(map[int]int)
} else {
clear(state.ContentIndexToOutputIndex)
}
if state.ToolArgumentBuffers == nil {
state.ToolArgumentBuffers = make(map[int]string)
} else {
clear(state.ToolArgumentBuffers)
}
if state.ItemIDs == nil {
state.ItemIDs = make(map[int]string)
} else {
clear(state.ItemIDs)
}
if state.ToolCallIDs == nil {
state.ToolCallIDs = make(map[int]string)
} else {
clear(state.ToolCallIDs)
}
if state.ToolCallNames == nil {
state.ToolCallNames = make(map[int]string)
} else {
clear(state.ToolCallNames)
}
if state.ReasoningContentIndices == nil {
state.ReasoningContentIndices = make(map[int]bool)
} else {
clear(state.ReasoningContentIndices)
}
if state.CompletedOutputIndices == nil {
state.CompletedOutputIndices = make(map[int]bool)
} else {
clear(state.CompletedOutputIndices)
}
// Reset other fields
state.CurrentOutputIndex = 0
state.MessageID = nil
state.Model = nil
state.StopReason = nil
state.CreatedAt = int(time.Now().Unix())
state.HasEmittedCreated = false
state.HasEmittedInProgress = false
return state
}
// releaseBedrockResponsesStreamState returns a Bedrock responses stream state to the pool.
func releaseBedrockResponsesStreamState(state *BedrockResponsesStreamState) {
if state != nil {
state.flush() // Clean before returning to pool
bedrockResponsesStreamStatePool.Put(state)
}
}
func (state *BedrockResponsesStreamState) flush() {
// Clear maps (reuse if already initialized, otherwise initialize)
if state.ContentIndexToOutputIndex == nil {
state.ContentIndexToOutputIndex = make(map[int]int)
} else {
clear(state.ContentIndexToOutputIndex)
}
if state.ToolArgumentBuffers == nil {
state.ToolArgumentBuffers = make(map[int]string)
} else {
clear(state.ToolArgumentBuffers)
}
if state.ItemIDs == nil {
state.ItemIDs = make(map[int]string)
} else {
clear(state.ItemIDs)
}
if state.ToolCallIDs == nil {
state.ToolCallIDs = make(map[int]string)
} else {
clear(state.ToolCallIDs)
}
if state.ToolCallNames == nil {
state.ToolCallNames = make(map[int]string)
} else {
clear(state.ToolCallNames)
}
if state.ReasoningContentIndices == nil {
state.ReasoningContentIndices = make(map[int]bool)
} else {
clear(state.ReasoningContentIndices)
}
if state.CompletedOutputIndices == nil {
state.CompletedOutputIndices = make(map[int]bool)
} else {
clear(state.CompletedOutputIndices)
}
state.CurrentOutputIndex = 0
state.MessageID = nil
state.Model = nil
state.StopReason = nil
state.CreatedAt = int(time.Now().Unix())
state.HasEmittedCreated = false
state.HasEmittedInProgress = false
}
// ToBifrostResponsesStream converts a Bedrock stream event to a Bifrost Responses Stream response
// Returns a slice of responses to support cases where a single event produces multiple responses
func (chunk *BedrockStreamEvent) ToBifrostResponsesStream(sequenceNumber int, state *BedrockResponsesStreamState) ([]*schemas.BifrostResponsesStreamResponse, *schemas.BifrostError, bool) {
switch {
case chunk.Role != nil:
// Message start - emit response.created and response.in_progress (OpenAI-style lifecycle)
var responses []*schemas.BifrostResponsesStreamResponse
// Generate message ID if not already set
if state.MessageID == nil {
messageID := fmt.Sprintf("msg_%d", state.CreatedAt)
state.MessageID = &messageID
}
// Emit response.created
if !state.HasEmittedCreated {
response := &schemas.BifrostResponsesResponse{
ID: state.MessageID,
CreatedAt: state.CreatedAt,
}
if state.Model != nil {
response.Model = *state.Model
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeCreated,
SequenceNumber: sequenceNumber,
Response: response,
})
state.HasEmittedCreated = true
}
// Emit response.in_progress
if !state.HasEmittedInProgress {
response := &schemas.BifrostResponsesResponse{
ID: state.MessageID,
CreatedAt: state.CreatedAt, // Use same timestamp
}
if state.Model != nil {
response.Model = *state.Model
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeInProgress,
SequenceNumber: sequenceNumber + len(responses),
Response: response,
})
state.HasEmittedInProgress = true
}
// Don't pre-create any items here - let each content block create its own item when it first appears
if len(responses) > 0 {
return responses, nil, false
}
case chunk.Start != nil:
// Handle content block start (text content or tool use)
contentBlockIndex := 0
if chunk.ContentBlockIndex != nil {
contentBlockIndex = *chunk.ContentBlockIndex
}
// Check if this is a tool use start
if chunk.Start.ToolUse != nil {
var responses []*schemas.BifrostResponsesStreamResponse
// Close any open reasoning blocks first (Anthropic sends content_block_stop before starting new blocks)
for prevContentIndex := range state.ReasoningContentIndices {
prevOutputIndex, prevExists := state.ContentIndexToOutputIndex[prevContentIndex]
if !prevExists {
continue
}
// Skip already completed output indices
if state.CompletedOutputIndices[prevOutputIndex] {
continue
}
itemID := state.ItemIDs[prevOutputIndex]
// For reasoning items, content_index is always 0
reasoningContentIndex := 0
// Emit reasoning_summary_text.done
emptyText := ""
reasoningDoneResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &reasoningContentIndex,
Text: &emptyText,
}
if itemID != "" {
reasoningDoneResponse.ItemID = &itemID
}
responses = append(responses, reasoningDoneResponse)
// Emit content_part.done for reasoning
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: &emptyText,
}
partDoneResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &reasoningContentIndex,
Part: part,
}
if itemID != "" {
partDoneResponse.ItemID = &itemID
}
responses = append(responses, partDoneResponse)
// Emit output_item.done for reasoning
statusCompleted := "completed"
messageType := schemas.ResponsesMessageTypeReasoning
role := schemas.ResponsesInputMessageRoleAssistant
doneItem := &schemas.ResponsesMessage{
Type: &messageType,
Role: &role,
Status: &statusCompleted,
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{},
},
}
if itemID != "" {
doneItem.ID = &itemID
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &reasoningContentIndex,
Item: doneItem,
})
// Mark this output index as completed
state.CompletedOutputIndices[prevOutputIndex] = true
}
// Clear reasoning content indices after closing them
clear(state.ReasoningContentIndices)
// Close any open text blocks before starting tool calls
// This ensures all text content is closed before tool calls begin
for prevContentIndex, prevOutputIndex := range state.ContentIndexToOutputIndex {
// Skip reasoning blocks (already handled above)
if state.ReasoningContentIndices[prevContentIndex] {
continue
}
// Skip already completed output indices
if state.CompletedOutputIndices[prevOutputIndex] {
continue
}
// Check if this is a text block (not a tool call)
prevToolCallID := state.ToolCallIDs[prevOutputIndex]
if prevToolCallID != "" {
continue // This is a tool call, skip it for now
}
// This is a text block - close it
prevItemID := state.ItemIDs[prevOutputIndex]
if prevItemID == "" {
continue
}
// Emit output_text.done
emptyText := ""
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputTextDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &prevContentIndex,
ItemID: &prevItemID,
Text: &emptyText,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
})
// Emit content_part.done for text
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: &emptyText,
ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{},
},
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &prevContentIndex,
ItemID: &prevItemID,
Part: part,
})
// Emit output_item.done for text
statusCompleted := "completed"
messageType := schemas.ResponsesMessageTypeMessage
role := schemas.ResponsesInputMessageRoleAssistant
doneItem := &schemas.ResponsesMessage{
Type: &messageType,
Role: &role,
Status: &statusCompleted,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{},
},
}
if prevItemID != "" {
doneItem.ID = &prevItemID
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &prevContentIndex,
Item: doneItem,
})
// Mark this output index as completed
state.CompletedOutputIndices[prevOutputIndex] = true
}
// Close any open tool call blocks before starting a new one (Anthropic completes each block before starting next)
for prevContentIndex, prevOutputIndex := range state.ContentIndexToOutputIndex {
// Skip reasoning blocks (already handled above)
if state.ReasoningContentIndices[prevContentIndex] {
continue
}
// Skip already completed output indices
if state.CompletedOutputIndices[prevOutputIndex] {
continue
}
// Check if this is a tool call
prevToolCallID := state.ToolCallIDs[prevOutputIndex]
if prevToolCallID == "" {
continue // Not a tool call
}
prevItemID := state.ItemIDs[prevOutputIndex]
prevToolName := state.ToolCallNames[prevOutputIndex]
accumulatedArgs := state.ToolArgumentBuffers[prevOutputIndex]
// Emit content_part.done for tool call
emptyText := ""
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: &emptyText,
ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{},
},
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: schemas.Ptr(prevContentIndex),
ItemID: &prevItemID,
Part: part,
})
// Emit function_call_arguments.done with full arguments
if accumulatedArgs != "" {
var doneItem *schemas.ResponsesMessage
if prevToolCallID != "" || prevToolName != "" {
doneItem = &schemas.ResponsesMessage{
ResponsesToolMessage: &schemas.ResponsesToolMessage{},
}
if prevToolCallID != "" {
doneItem.ResponsesToolMessage.CallID = &prevToolCallID
}
if prevToolName != "" {
doneItem.ResponsesToolMessage.Name = &prevToolName
}
}
argsDoneResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
Arguments: &accumulatedArgs,
}
if prevItemID != "" {
argsDoneResponse.ItemID = &prevItemID
}
if doneItem != nil {
argsDoneResponse.Item = doneItem
}
responses = append(responses, argsDoneResponse)
}
// Emit output_item.done for tool call
statusCompleted := "completed"
toolDoneItem := &schemas.ResponsesMessage{
ID: &prevItemID,
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall),
Status: &statusCompleted,
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: &prevToolCallID,
Name: &prevToolName,
Arguments: &accumulatedArgs,
},
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: schemas.Ptr(prevContentIndex),
ItemID: &prevItemID,
Item: toolDoneItem,
})
// Mark this output index as completed
state.CompletedOutputIndices[prevOutputIndex] = true
}
// Create new output index for this tool use
outputIndex := state.CurrentOutputIndex
state.ContentIndexToOutputIndex[contentBlockIndex] = outputIndex
state.CurrentOutputIndex++ // Increment for next use
// Store tool use ID as item ID and call ID
toolUseID := chunk.Start.ToolUse.ToolUseID
toolName := chunk.Start.ToolUse.Name
state.ItemIDs[outputIndex] = toolUseID
state.ToolCallIDs[outputIndex] = toolUseID
state.ToolCallNames[outputIndex] = toolName
statusInProgress := "in_progress"
item := &schemas.ResponsesMessage{
ID: &toolUseID,
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall),
Status: &statusInProgress,
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: &toolUseID,
Name: &toolName,
Arguments: schemas.Ptr(""), // Arguments will be filled by deltas
},
}
// Initialize argument buffer for this tool call
state.ToolArgumentBuffers[outputIndex] = ""
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemAdded,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: schemas.Ptr(contentBlockIndex),
Item: item,
})
return responses, nil, false
}
// Text content start is handled by Role event, so we can ignore Start for text
case chunk.ContentBlockIndex != nil && chunk.Delta != nil:
// Handle contentBlockDelta event
contentBlockIndex := *chunk.ContentBlockIndex
outputIndex, exists := state.ContentIndexToOutputIndex[contentBlockIndex]
if !exists {
// Check if this is a new content block that should close previous reasoning blocks
var responses []*schemas.BifrostResponsesStreamResponse
// If this is a text delta with a new content block index, close any open reasoning blocks
if chunk.Delta.Text != nil && contentBlockIndex > 0 {
for prevContentIndex := range state.ReasoningContentIndices {
if prevContentIndex < contentBlockIndex {
prevOutputIndex, prevExists := state.ContentIndexToOutputIndex[prevContentIndex]
if !prevExists {
continue
}
itemID := state.ItemIDs[prevOutputIndex]
// For reasoning items, content_index is always 0
reasoningContentIndex := 0
// Emit reasoning_summary_text.done
emptyText := ""
reasoningDoneResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &reasoningContentIndex,
Text: &emptyText,
}
if itemID != "" {
reasoningDoneResponse.ItemID = &itemID
}
responses = append(responses, reasoningDoneResponse)
// Emit content_part.done for reasoning
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: &emptyText,
}
partDoneResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &reasoningContentIndex,
Part: part,
}
if itemID != "" {
partDoneResponse.ItemID = &itemID
}
responses = append(responses, partDoneResponse)
// Emit output_item.done for reasoning
statusCompleted := "completed"
messageType := schemas.ResponsesMessageTypeReasoning
role := schemas.ResponsesInputMessageRoleAssistant
doneItem := &schemas.ResponsesMessage{
Type: &messageType,
Role: &role,
Status: &statusCompleted,
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{},
},
}
if itemID != "" {
doneItem.ID = &itemID
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(prevOutputIndex),
ContentIndex: &reasoningContentIndex,
Item: doneItem,
})
// Clear the reasoning content index tracking
delete(state.ReasoningContentIndices, prevContentIndex)
// Mark this output index as completed
state.CompletedOutputIndices[prevOutputIndex] = true
}
}
}
// Create new output index for this content block
outputIndex = state.CurrentOutputIndex
state.CurrentOutputIndex++
state.ContentIndexToOutputIndex[contentBlockIndex] = outputIndex
// If this is a text delta for a new content block, create the text item
if chunk.Delta.Text != nil {
// Generate stable ID for text item
var itemID string
if state.MessageID == nil {
itemID = fmt.Sprintf("item_%d", outputIndex)
} else {
itemID = fmt.Sprintf("msg_%s_item_%d", *state.MessageID, outputIndex)
}
state.ItemIDs[outputIndex] = itemID
// Create text item
messageType := schemas.ResponsesMessageTypeMessage
role := schemas.ResponsesInputMessageRoleAssistant
item := &schemas.ResponsesMessage{
ID: &itemID,
Type: &messageType,
Role: &role,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{}, // Empty blocks slice for mutation support
},
}
// Emit output_item.added for text
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemAdded,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
Item: item,
})
// Emit content_part.added with empty output_text part
emptyText := ""
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: &emptyText,
ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{},
},
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartAdded,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
ItemID: &itemID,
Part: part,
})
}
// If this is a text delta for a new content block, also emit the text delta in the same batch
if chunk.Delta.Text != nil && *chunk.Delta.Text != "" {
text := *chunk.Delta.Text
itemID := state.ItemIDs[outputIndex]
textDeltaResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputTextDelta,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
Delta: &text,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
}
if itemID != "" {
textDeltaResponse.ItemID = &itemID
}
responses = append(responses, textDeltaResponse)
}
// If we have responses to return (either from closing reasoning or creating text item), return them first
if len(responses) > 0 {
return responses, nil, false
}
}
switch {
case chunk.Delta.Text != nil:
// Handle text delta
text := *chunk.Delta.Text
if text != "" {
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputTextDelta,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
Delta: &text,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
}
if itemID != "" {
response.ItemID = &itemID
}
return []*schemas.BifrostResponsesStreamResponse{response}, nil, false
}
case chunk.Delta.ToolUse != nil:
// Handle tool use delta - function call arguments
toolUseDelta := chunk.Delta.ToolUse
if toolUseDelta.Input != "" {
// Accumulate argument deltas
state.ToolArgumentBuffers[outputIndex] += toolUseDelta.Input
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDelta,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
Delta: &toolUseDelta.Input,
}
if itemID != "" {
response.ItemID = &itemID
}
return []*schemas.BifrostResponsesStreamResponse{response}, nil, false
}
case chunk.Delta.ReasoningContent != nil:
// Handle reasoning content delta
reasoningDelta := chunk.Delta.ReasoningContent
// Check if this is the first reasoning delta for this content block
if !state.ReasoningContentIndices[contentBlockIndex] {
// First reasoning delta - emit output_item.added and content_part.added
var responses []*schemas.BifrostResponsesStreamResponse
// Generate stable ID for reasoning item
var itemID string
if state.MessageID == nil {
itemID = fmt.Sprintf("reasoning_%d", outputIndex)
} else {
itemID = fmt.Sprintf("msg_%s_reasoning_%d", *state.MessageID, outputIndex)
}
state.ItemIDs[outputIndex] = itemID
// Create reasoning item
messageType := schemas.ResponsesMessageTypeReasoning
role := schemas.ResponsesInputMessageRoleAssistant
item := &schemas.ResponsesMessage{
ID: &itemID,
Type: &messageType,
Role: &role,
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{},
},
}
// Preserve signature if present
if reasoningDelta.Signature != nil {
item.ResponsesReasoning.EncryptedContent = reasoningDelta.Signature
}
// Track that this content index is a reasoning block
state.ReasoningContentIndices[contentBlockIndex] = true
// Emit output_item.added
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemAdded,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
Item: item,
})
// Emit content_part.added with empty reasoning_text part
emptyText := ""
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: &emptyText,
}
// Preserve signature in the content part if present
if reasoningDelta.Signature != nil {
part.Signature = reasoningDelta.Signature
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartAdded,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
ItemID: &itemID,
Part: part,
})
// If there's text content, also emit the delta
if reasoningDelta.Text != nil && *reasoningDelta.Text != "" {
deltaResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
Delta: reasoningDelta.Text,
ItemID: &itemID,
}
responses = append(responses, deltaResponse)
}
return responses, nil, false
} else {
// Subsequent reasoning deltas - just emit the delta
if reasoningDelta.Text != nil && *reasoningDelta.Text != "" {
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
Delta: reasoningDelta.Text,
}
if itemID != "" {
response.ItemID = &itemID
}
return []*schemas.BifrostResponsesStreamResponse{response}, nil, false
}
// Handle signature deltas
if reasoningDelta.Signature != nil {
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentBlockIndex,
Signature: reasoningDelta.Signature, // Use signature field instead of delta
}
if itemID != "" {
response.ItemID = &itemID
}
return []*schemas.BifrostResponsesStreamResponse{response}, nil, false
}
}
}
case chunk.StopReason != nil:
// Stop reason - track it for the final response
var stopReason string
switch *chunk.StopReason {
case "tool_use":
stopReason = "tool_calls"
case "end_turn":
stopReason = "stop"
case "max_tokens":
stopReason = "length"
default:
stopReason = *chunk.StopReason
}
state.StopReason = &stopReason
// Items should be closed explicitly when content blocks end
return nil, nil, false
}
return nil, nil, false
}
// FinalizeBedrockStream finalizes the stream by closing any open items and emitting completed event
func FinalizeBedrockStream(state *BedrockResponsesStreamState, sequenceNumber int, usage *schemas.ResponsesResponseUsage) []*schemas.BifrostResponsesStreamResponse {
var responses []*schemas.BifrostResponsesStreamResponse
// Close any open items (text items and tool calls)
for contentIndex, outputIndex := range state.ContentIndexToOutputIndex {
// Skip reasoning blocks
if state.ReasoningContentIndices[contentIndex] {
continue
}
// Skip already completed output indices
if state.CompletedOutputIndices[outputIndex] {
continue
}
itemID := state.ItemIDs[outputIndex]
if itemID == "" {
continue
}
// Check if this is a tool call by looking at the tool call IDs
toolCallID := state.ToolCallIDs[outputIndex]
isToolCall := toolCallID != ""
if isToolCall {
// This is a tool call that needs to be closed
// Emit content_part.done for tool call
emptyText := ""
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: &emptyText,
ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{},
},
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentIndex,
ItemID: &itemID,
Part: part,
})
// Emit function_call_arguments.done with full arguments
toolName := state.ToolCallNames[outputIndex]
accumulatedArgs := state.ToolArgumentBuffers[outputIndex]
if accumulatedArgs != "" {
var doneItem *schemas.ResponsesMessage
if toolCallID != "" || toolName != "" {
doneItem = &schemas.ResponsesMessage{
ResponsesToolMessage: &schemas.ResponsesToolMessage{},
}
if toolCallID != "" {
doneItem.ResponsesToolMessage.CallID = &toolCallID
}
if toolName != "" {
doneItem.ResponsesToolMessage.Name = &toolName
}
}
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
Arguments: &accumulatedArgs,
}
if itemID != "" {
response.ItemID = &itemID
}
if doneItem != nil {
response.Item = doneItem
}
responses = append(responses, response)
}
// Emit output_item.done for tool call
statusCompleted := "completed"
doneItem := &schemas.ResponsesMessage{
ID: &itemID,
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall),
Status: &statusCompleted,
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: &toolCallID,
Name: &toolName,
Arguments: &accumulatedArgs,
},
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentIndex,
ItemID: &itemID,
Item: doneItem,
})
} else {
// This is likely a text item that needs to be closed
// Emit output_text.done (without accumulated text, just the event)
emptyText := ""
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputTextDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentIndex,
ItemID: &itemID,
Text: &emptyText,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
})
// Emit content_part.done for text
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: &emptyText,
ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{},
},
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentIndex,
ItemID: &itemID,
Part: part,
})
// Emit output_item.done for text
statusCompleted := "completed"
messageType := schemas.ResponsesMessageTypeMessage
role := schemas.ResponsesInputMessageRoleAssistant
doneItem := &schemas.ResponsesMessage{
Type: &messageType,
Role: &role,
Status: &statusCompleted,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{},
},
}
if itemID != "" {
doneItem.ID = &itemID
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &contentIndex,
Item: doneItem,
})
}
// Mark this output index as completed
state.CompletedOutputIndices[outputIndex] = true
}
// Close any open reasoning items
for contentIndex := range state.ReasoningContentIndices {
outputIndex, exists := state.ContentIndexToOutputIndex[contentIndex]
if !exists {
continue
}
// Skip already completed output indices
if state.CompletedOutputIndices[outputIndex] {
continue
}
itemID := state.ItemIDs[outputIndex]
// For reasoning items, content_index is always 0 (reasoning content is the first and only content part)
reasoningContentIndex := 0
// Emit reasoning_summary_text.done
emptyText := ""
reasoningDoneResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &reasoningContentIndex,
Text: &emptyText,
}
if itemID != "" {
reasoningDoneResponse.ItemID = &itemID
}
responses = append(responses, reasoningDoneResponse)
// Emit content_part.done for reasoning
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: &emptyText,
}
partDoneResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &reasoningContentIndex,
Part: part,
}
if itemID != "" {
partDoneResponse.ItemID = &itemID
}
responses = append(responses, partDoneResponse)
// Emit output_item.done for reasoning
statusCompleted := "completed"
messageType := schemas.ResponsesMessageTypeReasoning
role := schemas.ResponsesInputMessageRoleAssistant
doneItem := &schemas.ResponsesMessage{
Type: &messageType,
Role: &role,
Status: &statusCompleted,
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{},
},
}
if itemID != "" {
doneItem.ID = &itemID
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: &reasoningContentIndex,
Item: doneItem,
})
// Mark this output index as completed
state.CompletedOutputIndices[outputIndex] = true
}
// Note: Tool calls are already closed in the first loop above.
// This section is intentionally left empty to avoid duplicate events.
if usage.InputTokensDetails != nil {
usage.InputTokens = usage.InputTokens + usage.InputTokensDetails.CachedReadTokens + usage.InputTokensDetails.CachedWriteTokens
}
// Emit response.completed
response := &schemas.BifrostResponsesResponse{
ID: state.MessageID,
CreatedAt: state.CreatedAt,
Usage: usage,
}
if state.Model != nil {
response.Model = *state.Model
}
if state.StopReason != nil {
response.StopReason = state.StopReason
} else {
// Infer stop reason based on whether tool calls are present
hasToolCalls := false
for _, toolCallID := range state.ToolCallIDs {
if toolCallID != "" {
hasToolCalls = true
break
}
}
if hasToolCalls {
response.StopReason = schemas.Ptr("tool_calls")
} else {
response.StopReason = schemas.Ptr("stop")
}
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeCompleted,
SequenceNumber: sequenceNumber + len(responses),
Response: response,
})
return responses
}
// ToBedrockConverseStreamResponse converts a Bifrost Responses stream response to Bedrock streaming format
// Returns a BedrockStreamEvent that represents the streaming event in Bedrock's format
func ToBedrockConverseStreamResponse(bifrostResp *schemas.BifrostResponsesStreamResponse) (*BedrockStreamEvent, error) {
if bifrostResp == nil {
return nil, fmt.Errorf("bifrost stream response is nil")
}
event := &BedrockStreamEvent{}
switch bifrostResp.Type {
case schemas.ResponsesStreamResponseTypeCreated:
// Message start - emit role event
// Always set role for message start event
role := "assistant"
event.Role = &role
case schemas.ResponsesStreamResponseTypeInProgress:
// In progress - no-op for Bedrock (it doesn't have an explicit in_progress event)
// Return nil to skip this event
return nil, nil
case schemas.ResponsesStreamResponseTypeOutputItemAdded:
// Content block start
if bifrostResp.Item != nil && bifrostResp.Item.ResponsesToolMessage != nil {
// Tool use start
if bifrostResp.Item.ResponsesToolMessage.Name != nil && bifrostResp.Item.ResponsesToolMessage.CallID != nil {
contentBlockIndex := 0
if bifrostResp.ContentIndex != nil {
contentBlockIndex = *bifrostResp.ContentIndex
}
event.ContentBlockIndex = &contentBlockIndex
event.Start = &BedrockContentBlockStart{
ToolUse: &BedrockToolUseStart{
ToolUseID: *bifrostResp.Item.ResponsesToolMessage.CallID,
Name: *bifrostResp.Item.ResponsesToolMessage.Name,
},
}
}
} else if bifrostResp.Item != nil {
// Text item added - Bedrock doesn't have an explicit text start event, so we skip it
// Check if it's a text message (has content blocks or is a message type)
if bifrostResp.Item.Content != nil || (bifrostResp.Item.Type != nil && *bifrostResp.Item.Type == schemas.ResponsesMessageTypeMessage) {
return nil, nil
}
}
case schemas.ResponsesStreamResponseTypeOutputTextDelta:
// Text delta
if bifrostResp.Delta != nil && *bifrostResp.Delta != "" {
contentBlockIndex := 0
if bifrostResp.ContentIndex != nil {
contentBlockIndex = *bifrostResp.ContentIndex
}
event.ContentBlockIndex = &contentBlockIndex
event.Delta = &BedrockContentBlockDelta{
Text: bifrostResp.Delta,
}
} else {
// Skip empty deltas
return nil, nil
}
case schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDelta:
// Tool use delta (function call arguments)
if bifrostResp.Delta != nil {
contentBlockIndex := 0
if bifrostResp.ContentIndex != nil {
contentBlockIndex = *bifrostResp.ContentIndex
}
event.ContentBlockIndex = &contentBlockIndex
event.Delta = &BedrockContentBlockDelta{
ToolUse: &BedrockToolUseDelta{
Input: *bifrostResp.Delta,
},
}
}
case schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta:
// Reasoning content delta
contentBlockIndex := 0
if bifrostResp.ContentIndex != nil {
contentBlockIndex = *bifrostResp.ContentIndex
}
event.ContentBlockIndex = &contentBlockIndex
// Check if this is a signature delta or text delta
if bifrostResp.Signature != nil {
// This is a signature delta
event.Delta = &BedrockContentBlockDelta{
ReasoningContent: &BedrockReasoningContentText{
Signature: bifrostResp.Signature,
},
}
} else if bifrostResp.Delta != nil && *bifrostResp.Delta != "" {
// This is reasoning text delta
event.Delta = &BedrockContentBlockDelta{
ReasoningContent: &BedrockReasoningContentText{
Text: bifrostResp.Delta,
},
}
} else {
// Skip empty deltas
return nil, nil
}
case schemas.ResponsesStreamResponseTypeOutputTextDone,
schemas.ResponsesStreamResponseTypeContentPartDone,
schemas.ResponsesStreamResponseTypeReasoningSummaryTextDone:
// Content block done - Bedrock doesn't have explicit done events, so we skip them
return nil, nil
case schemas.ResponsesStreamResponseTypeOutputItemDone:
// Item done - Bedrock doesn't have explicit done events, so we skip them
return nil, nil
case schemas.ResponsesStreamResponseTypeCompleted:
// Message stop - always set stopReason
stopReason := "end_turn"
if bifrostResp.Response != nil && bifrostResp.Response.IncompleteDetails != nil {
stopReason = bifrostResp.Response.IncompleteDetails.Reason
}
event.StopReason = &stopReason
// Add usage if available
if bifrostResp.Response != nil && bifrostResp.Response.Usage != nil {
event.Usage = &BedrockTokenUsage{
InputTokens: bifrostResp.Response.Usage.InputTokens,
OutputTokens: bifrostResp.Response.Usage.OutputTokens,
TotalTokens: bifrostResp.Response.Usage.TotalTokens,
}
}
case schemas.ResponsesStreamResponseTypeError:
// Error - errors are handled separately by the router via BifrostError in the stream chunk
// Return nil to skip this chunk
return nil, nil
default:
// Unknown type - skip
return nil, nil
}
return event, nil
}
// BedrockEncodedEvent represents a single event ready for encoding to AWS Event Stream
type BedrockEncodedEvent struct {
EventType string
Payload interface{}
}
// BedrockInvokeStreamChunkEvent represents the chunk event for invoke-with-response-stream
type BedrockInvokeStreamChunkEvent struct {
Bytes []byte `json:"bytes"`
}
// ToEncodedEvents converts the flat BedrockStreamEvent into a sequence of specific events
func (event *BedrockStreamEvent) ToEncodedEvents() []BedrockEncodedEvent {
var events []BedrockEncodedEvent
for _, rawChunk := range event.InvokeModelRawChunks {
events = append(events, BedrockEncodedEvent{
EventType: "chunk",
Payload: BedrockInvokeStreamChunkEvent{
Bytes: rawChunk,
},
})
}
if event.Role != nil {
events = append(events, BedrockEncodedEvent{
EventType: "messageStart",
Payload: BedrockMessageStartEvent{
Role: *event.Role,
},
})
}
if event.Start != nil {
events = append(events, BedrockEncodedEvent{
EventType: "contentBlockStart",
Payload: struct {
Start *BedrockContentBlockStart `json:"start"`
ContentBlockIndex *int `json:"contentBlockIndex"`
}{
Start: event.Start,
ContentBlockIndex: event.ContentBlockIndex,
},
})
}
if event.Delta != nil {
events = append(events, BedrockEncodedEvent{
EventType: "contentBlockDelta",
Payload: struct {
Delta *BedrockContentBlockDelta `json:"delta"`
ContentBlockIndex *int `json:"contentBlockIndex"`
}{
Delta: event.Delta,
ContentBlockIndex: event.ContentBlockIndex,
},
})
}
if event.StopReason != nil {
events = append(events, BedrockEncodedEvent{
EventType: "messageStop",
Payload: BedrockMessageStopEvent{
StopReason: *event.StopReason,
},
})
}
if event.Usage != nil || event.Metrics != nil {
events = append(events, BedrockEncodedEvent{
EventType: "metadata",
Payload: BedrockMetadataEvent{
Usage: event.Usage,
Metrics: event.Metrics,
Trace: event.Trace,
},
})
}
return events
}
// ToBifrostResponsesRequest converts a BedrockConverseRequest to Bifrost Responses Request format
func (request *BedrockConverseRequest) ToBifrostResponsesRequest(ctx *schemas.BifrostContext) (*schemas.BifrostResponsesRequest, error) {
if request == nil {
return nil, fmt.Errorf("bedrock request is nil")
}
// Extract provider from model ID (format: "bedrock/model-name")
provider, model := schemas.ParseModelString(request.ModelID, providerUtils.CheckAndSetDefaultProvider(ctx, schemas.Bedrock))
bifrostReq := &schemas.BifrostResponsesRequest{
Provider: provider,
Model: model,
Params: &schemas.ResponsesParameters{},
Fallbacks: schemas.ParseFallbacks(request.Fallbacks),
}
// Convert messages using the new conversion method
convertedMessages := ConvertBedrockMessagesToBifrostMessages(ctx, request.Messages, request.System, false)
bifrostReq.Input = convertedMessages
// Convert inference config to parameters
if request.InferenceConfig != nil {
if request.InferenceConfig.MaxTokens != nil {
bifrostReq.Params.MaxOutputTokens = request.InferenceConfig.MaxTokens
}
if request.InferenceConfig.Temperature != nil {
bifrostReq.Params.Temperature = request.InferenceConfig.Temperature
}
if request.InferenceConfig.TopP != nil {
bifrostReq.Params.TopP = request.InferenceConfig.TopP
}
if len(request.InferenceConfig.StopSequences) > 0 {
if bifrostReq.Params.ExtraParams == nil {
bifrostReq.Params.ExtraParams = make(map[string]interface{})
}
bifrostReq.Params.ExtraParams["stop"] = request.InferenceConfig.StopSequences
}
}
// Convert tool config
if request.ToolConfig != nil && len(request.ToolConfig.Tools) > 0 {
for _, tool := range request.ToolConfig.Tools {
if tool.ToolSpec != nil {
bifrostTool := schemas.ResponsesTool{
Type: schemas.ResponsesToolTypeFunction,
Name: &tool.ToolSpec.Name,
Description: tool.ToolSpec.Description,
ResponsesToolFunction: &schemas.ResponsesToolFunction{},
}
// Handle different types for InputSchema.JSON
if len(tool.ToolSpec.InputSchema.JSON) > 0 {
var params schemas.ToolFunctionParameters
if err := sonic.Unmarshal(tool.ToolSpec.InputSchema.JSON, &params); err == nil {
bifrostTool.ResponsesToolFunction.Parameters = &params
} else {
// Fallback: unmarshal as map and convert
var paramsMap map[string]interface{}
if err := sonic.Unmarshal(tool.ToolSpec.InputSchema.JSON, &paramsMap); err == nil {
params := convertMapToToolFunctionParameters(paramsMap)
bifrostTool.ResponsesToolFunction.Parameters = params
}
}
}
bifrostReq.Params.Tools = append(bifrostReq.Params.Tools, bifrostTool)
} else if tool.CachePoint != nil && !schemas.IsNovaModel(bifrostReq.Model) {
// add cache control to last tool in tools array
if len(bifrostReq.Params.Tools) > 0 {
bifrostReq.Params.Tools[len(bifrostReq.Params.Tools)-1].CacheControl = &schemas.CacheControl{
Type: schemas.CacheControlTypeEphemeral,
}
}
}
}
}
// Convert tool choice
if request.ToolConfig != nil && request.ToolConfig.ToolChoice != nil {
toolChoice := request.ToolConfig.ToolChoice
if toolChoice.Auto != nil {
autoStr := string(schemas.ResponsesToolChoiceTypeAuto)
bifrostReq.Params.ToolChoice = &schemas.ResponsesToolChoice{
ResponsesToolChoiceStr: &autoStr,
}
} else if toolChoice.Any != nil {
anyStr := string(schemas.ResponsesToolChoiceTypeAny)
bifrostReq.Params.ToolChoice = &schemas.ResponsesToolChoice{
ResponsesToolChoiceStr: &anyStr,
}
} else if toolChoice.Tool != nil {
bifrostReq.Params.ToolChoice = &schemas.ResponsesToolChoice{
ResponsesToolChoiceStruct: &schemas.ResponsesToolChoiceStruct{
Type: schemas.ResponsesToolChoiceTypeFunction,
Name: &toolChoice.Tool.Name,
},
}
}
}
// Convert guardrail config to extra params
if request.GuardrailConfig != nil {
if bifrostReq.Params.ExtraParams == nil {
bifrostReq.Params.ExtraParams = make(map[string]interface{})
}
guardrailMap := map[string]interface{}{
"guardrailIdentifier": request.GuardrailConfig.GuardrailIdentifier,
"guardrailVersion": request.GuardrailConfig.GuardrailVersion,
}
if request.GuardrailConfig.Trace != nil {
guardrailMap["trace"] = *request.GuardrailConfig.Trace
}
bifrostReq.Params.ExtraParams["guardrailConfig"] = guardrailMap
}
// Convert additional model request fields to extra params
if request.AdditionalModelRequestFields.Len() > 0 {
// Handle Anthropic thinking/reasoning_config format
reasoningConfig, ok := request.AdditionalModelRequestFields.Get("thinking")
if !ok {
reasoningConfig, ok = request.AdditionalModelRequestFields.Get("reasoning_config")
}
if ok {
if reasoningConfigMap, ok := reasoningConfig.(map[string]interface{}); ok {
if typeStr, ok := schemas.SafeExtractString(reasoningConfigMap["type"]); ok {
if typeStr == "enabled" || typeStr == "adaptive" {
var summary *string
if summaryValue, ok := schemas.SafeExtractStringPointer(request.ExtraParams["reasoning_summary"]); ok {
summary = summaryValue
}
var (
effortStr string
found bool
)
// Check for native output_config.effort first.
// output_config may be preserved as OrderedMap by the merge path.
if outputConfig, ok := request.AdditionalModelRequestFields.Get("output_config"); ok {
if outputConfigOrderedMap, ok := schemas.SafeExtractOrderedMap(outputConfig); ok && outputConfigOrderedMap != nil {
if effortValue, exists := outputConfigOrderedMap.Get("effort"); exists {
effortStr, found = schemas.SafeExtractString(effortValue)
}
} else if outputConfigMap, ok := outputConfig.(map[string]interface{}); ok {
effortStr, found = schemas.SafeExtractString(outputConfigMap["effort"])
}
}
if found {
var maxTokens *int
if budgetTokens, ok := schemas.SafeExtractInt(reasoningConfigMap["budget_tokens"]); ok {
maxTokens = schemas.Ptr(budgetTokens)
}
bifrostReq.Params.Reasoning = &schemas.ResponsesParametersReasoning{
Effort: schemas.Ptr(effortStr),
MaxTokens: maxTokens,
Summary: summary,
}
} else if maxTokens, ok := schemas.SafeExtractInt(reasoningConfigMap["budget_tokens"]); ok {
// Fallback: convert budget_tokens to effort
minBudgetTokens := 0
defaultMaxTokens := providerUtils.GetMaxOutputTokensOrDefault(bifrostReq.Model, DefaultCompletionMaxTokens)
if request.InferenceConfig != nil && request.InferenceConfig.MaxTokens != nil {
defaultMaxTokens = *request.InferenceConfig.MaxTokens
}
if schemas.IsAnthropicModel(bifrostReq.Model) {
minBudgetTokens = anthropic.MinimumReasoningMaxTokens
}
effort := providerUtils.GetReasoningEffortFromBudgetTokens(maxTokens, minBudgetTokens, defaultMaxTokens)
bifrostReq.Params.Reasoning = &schemas.ResponsesParametersReasoning{
Effort: schemas.Ptr(effort),
MaxTokens: schemas.Ptr(maxTokens),
Summary: summary,
}
} else {
// Adaptive with no explicit effort — default to "high"
bifrostReq.Params.Reasoning = &schemas.ResponsesParametersReasoning{
Effort: schemas.Ptr("high"),
Summary: summary,
}
}
} else {
bifrostReq.Params.Reasoning = &schemas.ResponsesParametersReasoning{
Effort: schemas.Ptr("none"),
}
}
}
}
}
// Handle Nova reasoningConfig format (camelCase)
if novaReasoningConfig, ok := request.AdditionalModelRequestFields.Get("reasoningConfig"); ok {
if novaReasoningConfigMap, ok := novaReasoningConfig.(map[string]interface{}); ok {
if typeStr, ok := schemas.SafeExtractString(novaReasoningConfigMap["type"]); ok {
if typeStr == "enabled" {
// Extract maxReasoningEffort from Nova format
if effortStr, ok := schemas.SafeExtractString(novaReasoningConfigMap["maxReasoningEffort"]); ok {
bifrostReq.Params.Reasoning = &schemas.ResponsesParametersReasoning{
Effort: schemas.Ptr(effortStr),
}
}
} else if typeStr == "disabled" {
bifrostReq.Params.Reasoning = &schemas.ResponsesParametersReasoning{
Effort: schemas.Ptr("none"),
}
}
}
}
}
}
if include, ok := schemas.SafeExtractStringSlice(request.ExtraParams["include"]); ok {
bifrostReq.Params.Include = include
}
// Convert performance config to extra params
if request.PerformanceConfig != nil {
if bifrostReq.Params.ExtraParams == nil {
bifrostReq.Params.ExtraParams = make(map[string]interface{})
}
perfConfigMap := map[string]interface{}{}
if request.PerformanceConfig.Latency != nil {
perfConfigMap["latency"] = *request.PerformanceConfig.Latency
}
if len(perfConfigMap) > 0 {
bifrostReq.Params.ExtraParams["performanceConfig"] = perfConfigMap
}
}
// Convert prompt variables to extra params
if len(request.PromptVariables) > 0 {
if bifrostReq.Params.ExtraParams == nil {
bifrostReq.Params.ExtraParams = make(map[string]interface{})
}
promptVarsMap := make(map[string]interface{})
for key, value := range request.PromptVariables {
varMap := map[string]interface{}{}
if value.Text != nil {
varMap["text"] = *value.Text
}
if len(varMap) > 0 {
promptVarsMap[key] = varMap
}
}
if len(promptVarsMap) > 0 {
bifrostReq.Params.ExtraParams["promptVariables"] = promptVarsMap
}
}
// Convert request metadata to extra params
if len(request.RequestMetadata) > 0 {
if bifrostReq.Params.ExtraParams == nil {
bifrostReq.Params.ExtraParams = make(map[string]interface{})
}
bifrostReq.Params.ExtraParams["requestMetadata"] = request.RequestMetadata
}
// Convert additional model request fields to extra params
if request.AdditionalModelRequestFields.Len() > 0 {
if bifrostReq.Params.ExtraParams == nil {
bifrostReq.Params.ExtraParams = make(map[string]interface{})
}
bifrostReq.Params.ExtraParams["additionalModelRequestFieldPaths"] = request.AdditionalModelRequestFields
}
// Convert additional model response field paths to extra params
if len(request.AdditionalModelResponseFieldPaths) > 0 {
if bifrostReq.Params.ExtraParams == nil {
bifrostReq.Params.ExtraParams = make(map[string]interface{})
}
bifrostReq.Params.ExtraParams["additionalModelResponseFieldPaths"] = request.AdditionalModelResponseFieldPaths
}
return bifrostReq, nil
}
// ToBedrockResponsesRequest converts a BifrostRequest (Responses structure) back to BedrockConverseRequest
func ToBedrockResponsesRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.BifrostResponsesRequest) (*BedrockConverseRequest, error) {
if bifrostReq == nil {
return nil, fmt.Errorf("bifrost request is nil")
}
// Validate tools are supported by Bedrock
if bifrostReq.Params != nil && bifrostReq.Params.Tools != nil {
if toolErr := anthropic.ValidateToolsForProvider(bifrostReq.Params.Tools, schemas.Bedrock); toolErr != nil {
return nil, toolErr
}
}
bedrockReq := &BedrockConverseRequest{
ModelID: bifrostReq.Model,
}
// map bifrost messages to bedrock messages using the new conversion method
if bifrostReq.Input != nil {
messages, systemMessages, err := ConvertBifrostMessagesToBedrockMessages(bifrostReq.Input)
if err != nil {
return nil, fmt.Errorf("failed to convert Responses messages: %w", err)
}
bedrockReq.Messages = messages
if len(systemMessages) > 0 {
bedrockReq.System = systemMessages
} else {
if bifrostReq.Params != nil && bifrostReq.Params.Instructions != nil {
// if no system messages, check if instructions are present
bedrockReq.System = []BedrockSystemMessage{
{
Text: bifrostReq.Params.Instructions,
},
}
}
}
}
var responsesStructuredOutputTool *BedrockTool
// Map basic parameters to inference config
if bifrostReq.Params != nil {
inferenceConfig := &BedrockInferenceConfig{}
if bifrostReq.Params.MaxOutputTokens != nil {
inferenceConfig.MaxTokens = bifrostReq.Params.MaxOutputTokens
}
if bifrostReq.Params.Temperature != nil {
inferenceConfig.Temperature = bifrostReq.Params.Temperature
}
if bifrostReq.Params.TopP != nil {
inferenceConfig.TopP = bifrostReq.Params.TopP
}
if bifrostReq.Params.Reasoning != nil {
if bedrockReq.AdditionalModelRequestFields == nil {
bedrockReq.AdditionalModelRequestFields = schemas.NewOrderedMap()
}
if bifrostReq.Params.Reasoning.MaxTokens != nil {
tokenBudget := *bifrostReq.Params.Reasoning.MaxTokens
if *bifrostReq.Params.Reasoning.MaxTokens == -1 {
// bedrock does not support dynamic reasoning budget like gemini
// setting it to default max tokens
tokenBudget = anthropic.MinimumReasoningMaxTokens
}
if schemas.IsAnthropicModel(bifrostReq.Model) && tokenBudget < anthropic.MinimumReasoningMaxTokens {
return nil, fmt.Errorf("reasoning.max_tokens must be >= %d for anthropic", anthropic.MinimumReasoningMaxTokens)
}
if schemas.IsAnthropicModel(bifrostReq.Model) {
bedrockReq.AdditionalModelRequestFields.Set("thinking", map[string]any{
"type": "enabled",
"budget_tokens": tokenBudget,
})
} else if schemas.IsNovaModel(bifrostReq.Model) {
minBudgetTokens := MinimumReasoningMaxTokens
modelDefaultMaxTokens := providerUtils.GetMaxOutputTokensOrDefault(bifrostReq.Model, DefaultCompletionMaxTokens)
defaultMaxTokens := modelDefaultMaxTokens
if inferenceConfig.MaxTokens != nil {
defaultMaxTokens = *inferenceConfig.MaxTokens
} else {
inferenceConfig.MaxTokens = schemas.Ptr(modelDefaultMaxTokens)
}
maxReasoningEffort := providerUtils.GetReasoningEffortFromBudgetTokens(tokenBudget, minBudgetTokens, defaultMaxTokens)
typeStr := "enabled"
switch maxReasoningEffort {
case "high":
inferenceConfig.MaxTokens = nil
inferenceConfig.Temperature = nil
inferenceConfig.TopP = nil
case "minimal":
maxReasoningEffort = "low"
case "none":
typeStr = "disabled"
}
config := map[string]any{
"type": typeStr,
}
if typeStr != "disabled" {
config["maxReasoningEffort"] = maxReasoningEffort
}
bedrockReq.AdditionalModelRequestFields.Set("reasoningConfig", config)
}
} else {
if bifrostReq.Params.Reasoning.Effort != nil && *bifrostReq.Params.Reasoning.Effort != "none" {
if schemas.IsNovaModel(bifrostReq.Model) {
effort := *bifrostReq.Params.Reasoning.Effort
typeStr := "enabled"
switch effort {
case "high":
// for nova models we need to unset these fields at high effort
inferenceConfig.MaxTokens = nil
inferenceConfig.Temperature = nil
inferenceConfig.TopP = nil
case "low", "medium":
// no special handling needed for low and medium
case "minimal":
effort = "low"
case "none":
typeStr = "disabled"
}
config := map[string]any{
"type": typeStr,
}
if typeStr != "disabled" {
config["maxReasoningEffort"] = effort
}
bedrockReq.AdditionalModelRequestFields.Set("reasoningConfig", config)
} else if schemas.IsAnthropicModel(bifrostReq.Model) {
if anthropic.SupportsAdaptiveThinking(bifrostReq.Model) {
// Opus 4.6+: adaptive thinking + output_config.effort
effort := anthropic.MapBifrostEffortToAnthropic(*bifrostReq.Params.Reasoning.Effort)
bedrockReq.AdditionalModelRequestFields.Set("thinking", map[string]any{
"type": "adaptive",
})
setOutputConfigField(bedrockReq.AdditionalModelRequestFields, "effort", effort)
} else {
// Opus 4.5 and older Anthropic models: budget_tokens thinking
modelDefaultMaxTokens := providerUtils.GetMaxOutputTokensOrDefault(bifrostReq.Model, DefaultCompletionMaxTokens)
defaultMaxTokens := modelDefaultMaxTokens
if inferenceConfig.MaxTokens != nil {
defaultMaxTokens = *inferenceConfig.MaxTokens
} else {
inferenceConfig.MaxTokens = schemas.Ptr(modelDefaultMaxTokens)
}
budgetTokens, err := providerUtils.GetBudgetTokensFromReasoningEffort(*bifrostReq.Params.Reasoning.Effort, anthropic.MinimumReasoningMaxTokens, defaultMaxTokens)
if err != nil {
return nil, err
}
bedrockReq.AdditionalModelRequestFields.Set("thinking", map[string]any{
"type": "enabled",
"budget_tokens": budgetTokens,
})
}
} else {
// Non-Anthropic, non-Nova models: budget_tokens only
minBudgetTokens := MinimumReasoningMaxTokens
modelDefaultMaxTokens := providerUtils.GetMaxOutputTokensOrDefault(bifrostReq.Model, DefaultCompletionMaxTokens)
defaultMaxTokens := modelDefaultMaxTokens
if inferenceConfig.MaxTokens != nil {
defaultMaxTokens = *inferenceConfig.MaxTokens
} else {
inferenceConfig.MaxTokens = schemas.Ptr(modelDefaultMaxTokens)
}
budgetTokens, err := providerUtils.GetBudgetTokensFromReasoningEffort(*bifrostReq.Params.Reasoning.Effort, minBudgetTokens, defaultMaxTokens)
if err != nil {
return nil, err
}
bedrockReq.AdditionalModelRequestFields.Set("reasoning_config", map[string]any{
"type": "enabled",
"budget_tokens": budgetTokens,
})
}
} else {
if schemas.IsAnthropicModel(bifrostReq.Model) {
bedrockReq.AdditionalModelRequestFields.Set("thinking", map[string]any{
"type": "disabled",
})
} else if schemas.IsNovaModel(bifrostReq.Model) {
bedrockReq.AdditionalModelRequestFields.Set("reasoningConfig", map[string]any{
"type": "disabled",
})
} else {
bedrockReq.AdditionalModelRequestFields.Set("reasoning_config", map[string]any{
"type": "disabled",
})
}
}
}
}
if bifrostReq.Params.Text != nil {
if bifrostReq.Params.Text.Format != nil {
responseFormatTool, anthropicOutputFormat := convertTextFormatToTool(ctx, bifrostReq.Model, bifrostReq.Params.Text)
if anthropicOutputFormat != nil {
if bedrockReq.AdditionalModelRequestFields == nil {
bedrockReq.AdditionalModelRequestFields = schemas.NewOrderedMap()
}
setOutputConfigField(bedrockReq.AdditionalModelRequestFields, "format", anthropicOutputFormat)
}
// Defer synthetic tool injection until after normal tool/tool_choice conversion
// so the structured-output tool is not overwritten by the later pass.
if responseFormatTool != nil {
responsesStructuredOutputTool = responseFormatTool
}
}
}
if bifrostReq.Params.ExtraParams != nil {
bedrockReq.ExtraParams = bifrostReq.Params.ExtraParams
if stop, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["stop"]); ok {
delete(bedrockReq.ExtraParams, "stop")
inferenceConfig.StopSequences = stop
}
if requestFields, exists := bifrostReq.Params.ExtraParams["additionalModelRequestFieldPaths"]; exists {
if orderedFields, ok := schemas.SafeExtractOrderedMap(requestFields); ok {
delete(bedrockReq.ExtraParams, "additionalModelRequestFieldPaths")
bedrockReq.AdditionalModelRequestFields = mergeAdditionalModelRequestFields(
bedrockReq.AdditionalModelRequestFields,
orderedFields,
)
}
}
if responseFields, exists := bifrostReq.Params.ExtraParams["additionalModelResponseFieldPaths"]; exists {
if fields, ok := responseFields.([]string); ok {
bedrockReq.AdditionalModelResponseFieldPaths = fields
} else if fieldsInterface, ok := responseFields.([]interface{}); ok {
stringFields := make([]string, 0, len(fieldsInterface))
for _, field := range fieldsInterface {
if fieldStr, ok := field.(string); ok {
stringFields = append(stringFields, fieldStr)
}
}
if len(stringFields) > 0 {
bedrockReq.AdditionalModelResponseFieldPaths = stringFields
}
}
if len(bedrockReq.AdditionalModelResponseFieldPaths) > 0 {
delete(bedrockReq.ExtraParams, "additionalModelResponseFieldPaths")
}
}
}
bedrockReq.InferenceConfig = inferenceConfig
if bifrostReq.Params.ServiceTier != nil {
bedrockReq.ServiceTier = &BedrockServiceTier{
Type: *bifrostReq.Params.ServiceTier,
}
}
}
// Convert tools
if bifrostReq.Params != nil && bifrostReq.Params.Tools != nil {
var bedrockTools []BedrockTool
for _, tool := range bifrostReq.Params.Tools {
if tool.ResponsesToolFunction != nil {
// Create the complete schema object that Bedrock expects
var schemaObject interface{}
if tool.ResponsesToolFunction.Parameters != nil {
schemaObject = tool.ResponsesToolFunction.Parameters
} else {
// Fallback to empty object schema if no parameters
schemaObject = map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
}
}
if tool.Name == nil || *tool.Name == "" {
return nil, fmt.Errorf("responses tool is missing required name for Bedrock function conversion")
}
name := *tool.Name
// Use the tool description if available, otherwise use a generic description
description := "Function tool"
if tool.Description != nil {
description = *tool.Description
}
schemaObjectBytes, err := providerUtils.MarshalSorted(schemaObject)
if err != nil {
return nil, fmt.Errorf("failed to serialize tool schema %q: %w", name, err)
}
bedrockTool := BedrockTool{
ToolSpec: &BedrockToolSpec{
Name: name,
Description: &description,
InputSchema: BedrockToolInputSchema{
JSON: json.RawMessage(schemaObjectBytes),
},
},
}
bedrockTools = append(bedrockTools, bedrockTool)
if tool.CacheControl != nil && !schemas.IsNovaModel(bifrostReq.Model) {
bedrockTools = append(bedrockTools, BedrockTool{
CachePoint: &BedrockCachePoint{
Type: BedrockCachePointTypeDefault,
},
})
}
}
}
if len(bedrockTools) > 0 {
bedrockReq.ToolConfig = &BedrockToolConfig{
Tools: bedrockTools,
}
}
}
// Convert tool choice
if bifrostReq.Params != nil && bifrostReq.Params.ToolChoice != nil {
bedrockToolChoice := convertResponsesToolChoice(*bifrostReq.Params.ToolChoice)
if bedrockToolChoice != nil {
if bedrockReq.ToolConfig == nil {
bedrockReq.ToolConfig = &BedrockToolConfig{}
}
bedrockReq.ToolConfig.ToolChoice = bedrockToolChoice
}
}
// If text.format was converted to a synthetic tool, inject it after the normal
// tool/tool_choice pass so it is not overwritten by the above conversion.
if responsesStructuredOutputTool != nil {
if bedrockReq.ToolConfig == nil {
bedrockReq.ToolConfig = &BedrockToolConfig{}
}
bedrockReq.ToolConfig.Tools = append([]BedrockTool{*responsesStructuredOutputTool}, bedrockReq.ToolConfig.Tools...)
bedrockReq.ToolConfig.ToolChoice = &BedrockToolChoice{
Tool: &BedrockToolChoiceTool{
Name: responsesStructuredOutputTool.ToolSpec.Name,
},
}
}
// Ensure tool config is present when tool content exists (similar to Chat Completions)
ensureResponsesToolConfigForConversation(bifrostReq, bedrockReq)
return bedrockReq, nil
}
// ToBifrostResponsesResponse converts BedrockConverseResponse to BifrostResponsesResponse
func (response *BedrockConverseResponse) ToBifrostResponsesResponse(ctx *schemas.BifrostContext) (*schemas.BifrostResponsesResponse, error) {
if response == nil {
return nil, fmt.Errorf("bedrock response is nil")
}
bifrostResp := &schemas.BifrostResponsesResponse{
ID: schemas.Ptr(uuid.New().String()),
CreatedAt: int(time.Now().Unix()),
}
// Convert output message to Responses format using the new conversion method
if response.Output != nil && response.Output.Message != nil {
outputMessages := ConvertBedrockMessagesToBifrostMessages(ctx, []BedrockMessage{*response.Output.Message}, []BedrockSystemMessage{}, true)
bifrostResp.Output = outputMessages
}
if response.Usage != nil {
// Convert usage information
bifrostResp.Usage = &schemas.ResponsesResponseUsage{
InputTokens: response.Usage.InputTokens,
OutputTokens: response.Usage.OutputTokens,
TotalTokens: response.Usage.TotalTokens,
}
// Handle cached tokens if present
if response.Usage.CacheReadInputTokens > 0 {
if bifrostResp.Usage.InputTokensDetails == nil {
bifrostResp.Usage.InputTokensDetails = &schemas.ResponsesResponseInputTokens{}
}
bifrostResp.Usage.InputTokensDetails.CachedReadTokens = response.Usage.CacheReadInputTokens
bifrostResp.Usage.InputTokens = bifrostResp.Usage.InputTokens + response.Usage.CacheReadInputTokens
}
if response.Usage.CacheWriteInputTokens > 0 {
if bifrostResp.Usage.InputTokensDetails == nil {
bifrostResp.Usage.InputTokensDetails = &schemas.ResponsesResponseInputTokens{}
}
bifrostResp.Usage.InputTokensDetails.CachedWriteTokens = response.Usage.CacheWriteInputTokens
bifrostResp.Usage.InputTokens = bifrostResp.Usage.InputTokens + response.Usage.CacheWriteInputTokens
}
}
if response.ServiceTier != nil && response.ServiceTier.Type != "" {
bifrostResp.ServiceTier = &response.ServiceTier.Type
}
return bifrostResp, nil
}
// ToBedrockConverseResponse converts Bifrost Responses response to Bedrock Converse response
func ToBedrockConverseResponse(bifrostResp *schemas.BifrostResponsesResponse) (*BedrockConverseResponse, error) {
if bifrostResp == nil {
return nil, fmt.Errorf("bifrost response is nil")
}
bedrockResp := &BedrockConverseResponse{
Output: &BedrockConverseOutput{},
Usage: &BedrockTokenUsage{},
Metrics: &BedrockConverseMetrics{},
}
var hasToolUse bool
message := &BedrockMessage{
Role: BedrockMessageRoleAssistant,
Content: []BedrockContentBlock{},
}
if len(bifrostResp.Output) > 0 {
// Convert Bifrost messages back to Bedrock messages using the new conversion method
bedrockMessages, _, err := ConvertBifrostMessagesToBedrockMessages(bifrostResp.Output)
if err != nil {
return nil, fmt.Errorf("failed to convert bifrost output messages: %w", err)
}
// Merge all content blocks from converted messages into a single message
for _, bedrockMsg := range bedrockMessages {
message.Content = append(message.Content, bedrockMsg.Content...)
}
// Check for tool use in the content blocks
for _, block := range message.Content {
if block.ToolUse != nil {
hasToolUse = true
break
}
}
}
bedrockResp.Output.Message = message
// Find stop reason from incomplete details or derive from response
// Priority: IncompleteDetails > tool_use detection > end_turn
stopReason := "end_turn"
if bifrostResp.IncompleteDetails != nil {
stopReason = bifrostResp.IncompleteDetails.Reason
} else if hasToolUse {
stopReason = "tool_use"
}
bedrockResp.StopReason = stopReason
// Convert usage stats
if bifrostResp.Usage != nil {
bedrockResp.Usage.InputTokens = bifrostResp.Usage.InputTokens
bedrockResp.Usage.OutputTokens = bifrostResp.Usage.OutputTokens
bedrockResp.Usage.TotalTokens = bifrostResp.Usage.TotalTokens
if bifrostResp.Usage.InputTokensDetails != nil {
if bifrostResp.Usage.InputTokensDetails.CachedReadTokens > 0 {
bedrockResp.Usage.CacheReadInputTokens = bifrostResp.Usage.InputTokensDetails.CachedReadTokens
bedrockResp.Usage.InputTokens = bedrockResp.Usage.InputTokens - bifrostResp.Usage.InputTokensDetails.CachedReadTokens
}
if bifrostResp.Usage.InputTokensDetails.CachedWriteTokens > 0 {
bedrockResp.Usage.CacheWriteInputTokens = bifrostResp.Usage.InputTokensDetails.CachedWriteTokens
bedrockResp.Usage.InputTokens = bedrockResp.Usage.InputTokens - bifrostResp.Usage.InputTokensDetails.CachedWriteTokens
}
}
}
// Set metrics
if bifrostResp.ExtraFields.Latency > 0 {
bedrockResp.Metrics.LatencyMs = bifrostResp.ExtraFields.Latency
}
return bedrockResp, nil
}
// Helper functions
// ensureResponsesToolConfigForConversation ensures toolConfig is present when tool content exists
func ensureResponsesToolConfigForConversation(bifrostReq *schemas.BifrostResponsesRequest, bedrockReq *BedrockConverseRequest) {
if bedrockReq.ToolConfig != nil {
return // Already has tool config
}
hasToolContent, tools := extractToolsFromResponsesConversationHistory(bifrostReq.Input)
if hasToolContent && len(tools) > 0 {
bedrockReq.ToolConfig = &BedrockToolConfig{Tools: tools}
}
}
// extractToolsFromResponsesConversationHistory extracts tools from Responses conversation history
func extractToolsFromResponsesConversationHistory(messages []schemas.ResponsesMessage) (bool, []BedrockTool) {
var hasToolContent bool
toolMap := make(map[string]*schemas.ResponsesTool) // Use map to deduplicate by name
for _, msg := range messages {
// Check if message contains tool use or tool result
if msg.Type != nil {
switch *msg.Type {
case schemas.ResponsesMessageTypeFunctionCall, schemas.ResponsesMessageTypeFunctionCallOutput:
hasToolContent = true
// Try to infer tool definition from tool call/result
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.Name != nil {
toolName := *msg.ResponsesToolMessage.Name
if _, exists := toolMap[toolName]; !exists {
// Create a minimal tool definition
toolMap[toolName] = &schemas.ResponsesTool{
Type: "function",
Name: &toolName,
ResponsesToolFunction: &schemas.ResponsesToolFunction{
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: schemas.NewOrderedMap(),
},
},
}
}
}
}
}
}
// Convert map to slice
var tools []BedrockTool
for _, tool := range toolMap {
if tool.Name != nil && tool.ResponsesToolFunction != nil {
schemaObject := tool.ResponsesToolFunction.Parameters
if schemaObject == nil {
schemaObject = &schemas.ToolFunctionParameters{
Type: "object",
Properties: schemas.NewOrderedMap(),
}
}
description := "Function tool"
if tool.Description != nil {
description = *tool.Description
}
schemaObjectBytes2, _ := providerUtils.MarshalSorted(schemaObject)
bedrockTool := BedrockTool{
ToolSpec: &BedrockToolSpec{
Name: *tool.Name,
Description: &description,
InputSchema: BedrockToolInputSchema{
JSON: json.RawMessage(schemaObjectBytes2),
},
},
}
tools = append(tools, bedrockTool)
}
}
return hasToolContent, tools
}
func convertResponsesToolChoice(toolChoice schemas.ResponsesToolChoice) *BedrockToolChoice {
// Check if it's a string choice
if toolChoice.ResponsesToolChoiceStr != nil {
switch schemas.ResponsesToolChoiceType(*toolChoice.ResponsesToolChoiceStr) {
case schemas.ResponsesToolChoiceTypeAuto:
return &BedrockToolChoice{
Auto: &BedrockToolChoiceAuto{},
}
case schemas.ResponsesToolChoiceTypeAny, schemas.ResponsesToolChoiceTypeRequired:
return &BedrockToolChoice{
Any: &BedrockToolChoiceAny{},
}
case schemas.ResponsesToolChoiceTypeNone:
// Bedrock doesn't have explicit "none" - just don't include tools
return nil
}
}
// Check if it's a struct choice
if toolChoice.ResponsesToolChoiceStruct != nil {
switch toolChoice.ResponsesToolChoiceStruct.Type {
case schemas.ResponsesToolChoiceTypeFunction:
// Extract the actual function name from the struct
if toolChoice.ResponsesToolChoiceStruct.Name != nil && *toolChoice.ResponsesToolChoiceStruct.Name != "" {
return &BedrockToolChoice{
Tool: &BedrockToolChoiceTool{
Name: *toolChoice.ResponsesToolChoiceStruct.Name,
},
}
}
// If Name is nil or empty, return nil as we can't construct a valid tool choice
return nil
case schemas.ResponsesToolChoiceTypeAuto:
return &BedrockToolChoice{
Auto: &BedrockToolChoiceAuto{},
}
case schemas.ResponsesToolChoiceTypeAny, schemas.ResponsesToolChoiceTypeRequired:
return &BedrockToolChoice{
Any: &BedrockToolChoiceAny{},
}
case schemas.ResponsesToolChoiceTypeNone:
return nil
}
}
return nil
}
// ToolCallState represents the state of a single tool call in the conversion process
type ToolCallState string
const (
// Tool call states
ToolCallStateInitialized ToolCallState = "initialized" // Tool call message received
ToolCallStateQueued ToolCallState = "queued" // Tool call queued for emission
ToolCallStateEmitted ToolCallState = "emitted" // Tool call emitted in assistant message
ToolCallStateAwaitingResult ToolCallState = "awaiting_result" // Waiting for tool result
ToolCallStateCompleted ToolCallState = "completed" // Tool call + result complete
)
// ToolCall represents a tool call with its full lifecycle state
type ToolCall struct {
CallID string
ToolName string
Arguments string
State ToolCallState
AssistantMsgIndex int // Index in final bedrockMessages where this call was emitted
Result *ToolResult
CacheControl *schemas.CacheControl
}
// ToolResult represents the result of a tool call
type ToolResult struct {
CallID string
Content []BedrockContentBlock
Status string
Emitted bool
CacheControl *schemas.CacheControl
}
// ToolCallBatch tracks a group of tool calls that should be emitted together
type ToolCallBatch struct {
ID string // Unique batch identifier
ToolCalls map[string]*ToolCall // Maps CallID to ToolCall
State ToolCallState
AssistantMsgIndex int // Where this batch's assistant message is in bedrockMessages
}
// ToolCallStateManager manages the lifecycle of tool calls through conversion
type ToolCallStateManager struct {
// All tool calls indexed by ID
toolCalls map[string]*ToolCall
// Current batch being accumulated
currentBatch *ToolCallBatch
batches []*ToolCallBatch
// Pending operations
pendingToolCallIDs []string // Tool calls waiting to be emitted
pendingResults map[string]*ToolResult // Results waiting to be matched
}
// NewToolCallStateManager creates a new state manager
func NewToolCallStateManager() *ToolCallStateManager {
return &ToolCallStateManager{
toolCalls: make(map[string]*ToolCall),
pendingResults: make(map[string]*ToolResult),
}
}
// RegisterToolCall registers a new tool call in the system
func (m *ToolCallStateManager) RegisterToolCall(callID, toolName, arguments string, cacheControl *schemas.CacheControl) {
if m.toolCalls[callID] != nil {
// Tool call already registered, skip
return
}
toolCall := &ToolCall{
CallID: callID,
ToolName: toolName,
Arguments: arguments,
State: ToolCallStateInitialized,
AssistantMsgIndex: -1,
CacheControl: cacheControl,
}
m.toolCalls[callID] = toolCall
m.pendingToolCallIDs = append(m.pendingToolCallIDs, callID)
}
// RegisterToolResult registers a tool result
func (m *ToolCallStateManager) RegisterToolResult(callID string, content []BedrockContentBlock, status string, cacheControl *schemas.CacheControl) {
// Attemp to deduplicate the result similar to tool call. Need to check in 2 places, since after moving
// on from pendingResults into a completed toolCall, the same ID might come again.
if _, ok := m.pendingResults[callID]; ok {
return
}
if toolCall, exists := m.toolCalls[callID]; exists && toolCall.Result != nil {
// Tool result already processed for this call ID, skip
return
}
result := &ToolResult{
CallID: callID,
Content: content,
Status: status,
Emitted: false,
CacheControl: cacheControl,
}
m.pendingResults[callID] = result
// If we have the corresponding tool call, attach the result
if toolCall, exists := m.toolCalls[callID]; exists {
toolCall.Result = result
if toolCall.State == ToolCallStateEmitted {
toolCall.State = ToolCallStateCompleted
} else if toolCall.State == ToolCallStateAwaitingResult {
toolCall.State = ToolCallStateCompleted
}
}
}
// EmitPendingToolCalls prepares all pending tool calls for emission as an assistant message
func (m *ToolCallStateManager) EmitPendingToolCalls() []string {
if len(m.pendingToolCallIDs) == 0 {
return nil
}
// Create a new batch for these tool calls
batchID := fmt.Sprintf("batch_%d", len(m.batches))
batch := &ToolCallBatch{
ID: batchID,
ToolCalls: make(map[string]*ToolCall),
State: ToolCallStateQueued,
}
// Mark all pending tool calls as queued
for _, callID := range m.pendingToolCallIDs {
if toolCall, exists := m.toolCalls[callID]; exists {
toolCall.State = ToolCallStateQueued
batch.ToolCalls[callID] = toolCall
}
}
m.batches = append(m.batches, batch)
m.currentBatch = batch
// Return the IDs that should be emitted
emitIDs := make([]string, len(m.pendingToolCallIDs))
copy(emitIDs, m.pendingToolCallIDs)
m.pendingToolCallIDs = nil
return emitIDs
}
// MarkToolCallsEmitted marks tool calls as having been emitted in an assistant message
func (m *ToolCallStateManager) MarkToolCallsEmitted(callIDs []string, assistantMsgIndex int) {
for _, callID := range callIDs {
if toolCall, exists := m.toolCalls[callID]; exists {
toolCall.State = ToolCallStateEmitted
toolCall.AssistantMsgIndex = assistantMsgIndex
}
}
if m.currentBatch != nil {
m.currentBatch.State = ToolCallStateEmitted
m.currentBatch.AssistantMsgIndex = assistantMsgIndex
}
}
// GetPendingResults returns all pending results that are ready to be emitted
func (m *ToolCallStateManager) GetPendingResults() map[string]*ToolResult {
return m.pendingResults
}
// MarkResultsEmitted marks results as having been emitted in a user message
func (m *ToolCallStateManager) MarkResultsEmitted(callIDs []string) {
for _, callID := range callIDs {
if result, exists := m.pendingResults[callID]; exists {
result.Emitted = true
delete(m.pendingResults, callID)
// Update tool call state
if toolCall, exists := m.toolCalls[callID]; exists {
toolCall.State = ToolCallStateCompleted
}
}
}
}
// HasPendingToolCalls checks if there are tool calls waiting to be emitted
func (m *ToolCallStateManager) HasPendingToolCalls() bool {
return len(m.pendingToolCallIDs) > 0
}
// HasPendingResults checks if there are results waiting to be emitted
func (m *ToolCallStateManager) HasPendingResults() bool {
return len(m.pendingResults) > 0
}
// ConvertBifrostMessagesToBedrockMessages converts an array of Bifrost ResponsesMessage to Bedrock message format
// This is the main conversion method from Bifrost to Bedrock - handles all message types and returns messages + system messages
// Uses a state machine to properly track and manage tool call lifecycles
func ConvertBifrostMessagesToBedrockMessages(bifrostMessages []schemas.ResponsesMessage) ([]BedrockMessage, []BedrockSystemMessage, error) {
var bedrockMessages []BedrockMessage
var systemMessages []BedrockSystemMessage
var pendingReasoningContentBlocks []BedrockContentBlock
// Initialize the state manager for tracking tool calls and results
stateManager := NewToolCallStateManager()
// Helper to flush pending tool result blocks into user messages using state manager
flushPendingToolResults := func() {
// Emit any pending results from the state manager
if stateManager.HasPendingResults() {
pendingResults := stateManager.GetPendingResults()
var resultBlocks []BedrockContentBlock
resultIDs := []string{}
for callID, result := range pendingResults {
resultBlocks = append(resultBlocks, BedrockContentBlock{
ToolResult: &BedrockToolResult{
ToolUseID: callID,
Content: result.Content,
Status: schemas.Ptr(result.Status),
},
})
if result.CacheControl != nil {
resultBlocks = append(resultBlocks, BedrockContentBlock{
CachePoint: &BedrockCachePoint{Type: BedrockCachePointTypeDefault},
})
}
resultIDs = append(resultIDs, callID)
}
if len(resultBlocks) > 0 {
bedrockMessages = append(bedrockMessages, BedrockMessage{
Role: BedrockMessageRoleUser,
Content: resultBlocks,
})
stateManager.MarkResultsEmitted(resultIDs)
}
}
}
// Helper to flush pending tool call blocks into a single assistant message using state manager
flushPendingToolCalls := func() {
if stateManager.HasPendingToolCalls() {
callIDs := stateManager.EmitPendingToolCalls()
// Create assistant message with tool calls
var contentBlocks []BedrockContentBlock
// Prepend pending reasoning blocks first (Bedrock requires reasoning before tool_use)
if len(pendingReasoningContentBlocks) > 0 {
contentBlocks = append(contentBlocks, pendingReasoningContentBlocks...)
pendingReasoningContentBlocks = nil
}
// Add tool use blocks
for _, callID := range callIDs {
if toolCall, exists := stateManager.toolCalls[callID]; exists {
toolUseBlock := &BedrockContentBlock{
ToolUse: &BedrockToolUse{
ToolUseID: toolCall.CallID,
Name: toolCall.ToolName,
},
}
// Preserve original key ordering of tool arguments for prompt caching.
var input json.RawMessage
var buf bytes.Buffer
if err := json.Compact(&buf, []byte(toolCall.Arguments)); err == nil {
input = buf.Bytes()
} else {
input = json.RawMessage("{}")
}
toolUseBlock.ToolUse.Input = input
contentBlocks = append(contentBlocks, *toolUseBlock)
if toolCall.CacheControl != nil {
contentBlocks = append(contentBlocks, BedrockContentBlock{
CachePoint: &BedrockCachePoint{Type: BedrockCachePointTypeDefault},
})
}
}
}
if len(contentBlocks) > 0 {
bedrockMessages = append(bedrockMessages, BedrockMessage{
Role: BedrockMessageRoleAssistant,
Content: contentBlocks,
})
stateManager.MarkToolCallsEmitted(callIDs, len(bedrockMessages)-1)
}
}
}
for i, msg := range bifrostMessages {
// Handle nil Type as regular message
msgType := schemas.ResponsesMessageTypeMessage
if msg.Type != nil {
msgType = *msg.Type
}
// If we're processing a non-reasoning message and have pending reasoning blocks,
// flush them into the previous assistant message (if it exists)
if msgType != schemas.ResponsesMessageTypeReasoning && len(pendingReasoningContentBlocks) > 0 {
if len(bedrockMessages) > 0 && bedrockMessages[len(bedrockMessages)-1].Role == BedrockMessageRoleAssistant {
// Prepend reasoning blocks to the last assistant message
lastMsg := &bedrockMessages[len(bedrockMessages)-1]
lastMsg.Content = append(pendingReasoningContentBlocks, lastMsg.Content...)
pendingReasoningContentBlocks = nil
}
}
switch msgType {
case schemas.ResponsesMessageTypeFunctionCall:
// Register tool call in state manager
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil {
toolName := ""
if msg.ResponsesToolMessage.Name != nil {
toolName = *msg.ResponsesToolMessage.Name
}
arguments := ""
if msg.ResponsesToolMessage.Arguments != nil {
arguments = *msg.ResponsesToolMessage.Arguments
}
stateManager.RegisterToolCall(*msg.ResponsesToolMessage.CallID, toolName, arguments, msg.CacheControl)
}
case schemas.ResponsesMessageTypeFunctionCallOutput:
// Register tool result in state manager
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil {
resultContent := []BedrockContentBlock{}
status := "success"
if msg.Status != nil && *msg.Status != "" {
// Validate status is one of the allowed values
switch *msg.Status {
case "success", "error":
status = *msg.Status
default:
// Default to success for unknown status values
status = "success"
}
}
// Convert result content to Bedrock format
if msg.ResponsesToolMessage.Output != nil {
if msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr != nil {
resultContent = append(resultContent, tryParseJSONIntoContentBlock(*msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr))
} else if msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks != nil {
// Handle structured output blocks
for _, block := range msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks {
if block.Text != nil {
resultContent = append(resultContent, tryParseJSONIntoContentBlock(*block.Text))
} else if block.Type == schemas.ResponsesInputMessageContentBlockTypeImage &&
block.ResponsesInputMessageContentBlockImage != nil &&
block.ResponsesInputMessageContentBlockImage.ImageURL != nil {
imageSource, err := convertImageToBedrockSource(*block.ResponsesInputMessageContentBlockImage.ImageURL)
if err != nil {
// Bedrock only supports base64 data URIs for images. If conversion
// fails (e.g. remote URL), the image is dropped from the tool result
// which silently degrades the model's ability to see tool output.
_ = fmt.Errorf("bedrock: converting tool result image: %w", err)
} else {
resultContent = append(resultContent, BedrockContentBlock{Image: imageSource})
}
}
}
}
}
stateManager.RegisterToolResult(*msg.ResponsesToolMessage.CallID, resultContent, status, msg.CacheControl)
}
// Check if next message is not a function call output - if so, flush tool calls and results
isLastResultInSequence := true
if i+1 < len(bifrostMessages) {
nextMsg := bifrostMessages[i+1]
nextMsgType := schemas.ResponsesMessageTypeMessage
if nextMsg.Type != nil {
nextMsgType = *nextMsg.Type
}
if nextMsgType == schemas.ResponsesMessageTypeFunctionCallOutput {
isLastResultInSequence = false
}
}
// If this is the last result in a sequence, flush tool calls and results together
if isLastResultInSequence {
// Emit pending tool calls first
if stateManager.HasPendingToolCalls() {
callIDs := stateManager.EmitPendingToolCalls()
var contentBlocks []BedrockContentBlock
// Prepend pending reasoning blocks first (Bedrock requires reasoning before tool_use)
if len(pendingReasoningContentBlocks) > 0 {
contentBlocks = append(contentBlocks, pendingReasoningContentBlocks...)
pendingReasoningContentBlocks = nil
}
// Add tool use blocks
for _, callID := range callIDs {
if toolCall, exists := stateManager.toolCalls[callID]; exists {
toolUseBlock := &BedrockContentBlock{
ToolUse: &BedrockToolUse{
ToolUseID: toolCall.CallID,
Name: toolCall.ToolName,
},
}
// Preserve original key ordering of tool arguments for prompt caching.
var input json.RawMessage
var buf bytes.Buffer
if err := json.Compact(&buf, []byte(toolCall.Arguments)); err == nil {
input = buf.Bytes()
} else {
input = json.RawMessage("{}")
}
toolUseBlock.ToolUse.Input = input
contentBlocks = append(contentBlocks, *toolUseBlock)
if toolCall.CacheControl != nil {
contentBlocks = append(contentBlocks, BedrockContentBlock{
CachePoint: &BedrockCachePoint{Type: BedrockCachePointTypeDefault},
})
}
}
}
if len(contentBlocks) > 0 {
bedrockMessages = append(bedrockMessages, BedrockMessage{
Role: BedrockMessageRoleAssistant,
Content: contentBlocks,
})
stateManager.MarkToolCallsEmitted(callIDs, len(bedrockMessages)-1)
}
}
// Emit pending results after tool calls
if stateManager.HasPendingResults() {
pendingResults := stateManager.GetPendingResults()
var resultBlocks []BedrockContentBlock
resultIDs := []string{}
for callID, result := range pendingResults {
resultBlocks = append(resultBlocks, BedrockContentBlock{
ToolResult: &BedrockToolResult{
ToolUseID: callID,
Content: result.Content,
Status: schemas.Ptr(result.Status),
},
})
if result.CacheControl != nil {
resultBlocks = append(resultBlocks, BedrockContentBlock{
CachePoint: &BedrockCachePoint{Type: BedrockCachePointTypeDefault},
})
}
resultIDs = append(resultIDs, callID)
}
if len(resultBlocks) > 0 {
bedrockMessages = append(bedrockMessages, BedrockMessage{
Role: BedrockMessageRoleUser,
Content: resultBlocks,
})
stateManager.MarkResultsEmitted(resultIDs)
}
}
}
case schemas.ResponsesMessageTypeMessage:
// Check if Role is present, skip message if not
if msg.Role == nil {
continue
}
// Extract role from the Responses message structure
role := *msg.Role
// Always flush pending tool calls and results before processing a new message
// This ensures tool calls and results are properly paired
if stateManager.HasPendingToolCalls() {
callIDs := stateManager.EmitPendingToolCalls()
// Create assistant message with tool calls
var toolUseBlocks []BedrockContentBlock
for _, callID := range callIDs {
if toolCall, exists := stateManager.toolCalls[callID]; exists {
toolUseBlock := &BedrockContentBlock{
ToolUse: &BedrockToolUse{
ToolUseID: toolCall.CallID,
Name: toolCall.ToolName,
},
}
// Preserve original key ordering of tool arguments for prompt caching.
var input json.RawMessage
var buf bytes.Buffer
if err := json.Compact(&buf, []byte(toolCall.Arguments)); err == nil {
input = buf.Bytes()
} else {
input = json.RawMessage("{}")
}
toolUseBlock.ToolUse.Input = input
toolUseBlocks = append(toolUseBlocks, *toolUseBlock)
}
}
if len(toolUseBlocks) > 0 {
bedrockMessages = append(bedrockMessages, BedrockMessage{
Role: BedrockMessageRoleAssistant,
Content: toolUseBlocks,
})
stateManager.MarkToolCallsEmitted(callIDs, len(bedrockMessages)-1)
}
}
// Emit any pending results after tool calls
if stateManager.HasPendingResults() {
pendingResults := stateManager.GetPendingResults()
var resultBlocks []BedrockContentBlock
resultIDs := []string{}
for callID, result := range pendingResults {
resultBlocks = append(resultBlocks, BedrockContentBlock{
ToolResult: &BedrockToolResult{
ToolUseID: callID,
Content: result.Content,
Status: schemas.Ptr(result.Status),
},
})
resultIDs = append(resultIDs, callID)
}
if len(resultBlocks) > 0 {
bedrockMessages = append(bedrockMessages, BedrockMessage{
Role: BedrockMessageRoleUser,
Content: resultBlocks,
})
stateManager.MarkResultsEmitted(resultIDs)
}
}
// Convert regular message
if role == schemas.ResponsesInputMessageRoleSystem {
// Convert to system message
systemMsgs := convertBifrostMessageToBedrockSystemMessages(&msg)
systemMessages = append(systemMessages, systemMsgs...)
} else {
// Convert user/assistant text message
bedrockMsg := convertBifrostMessageToBedrockMessage(&msg)
if bedrockMsg != nil {
bedrockMessages = append(bedrockMessages, *bedrockMsg)
}
}
case schemas.ResponsesMessageTypeReasoning:
// Handle reasoning as content in next assistant message
// For now, just add to pending content blocks
reasoningBlocks := convertBifrostReasoningToBedrockReasoning(&msg)
if len(reasoningBlocks) > 0 {
pendingReasoningContentBlocks = append(pendingReasoningContentBlocks, reasoningBlocks...)
}
}
}
// Flush any remaining pending tool calls
flushPendingToolCalls()
// Flush any remaining pending tool results
flushPendingToolResults()
// For Bedrock compatibility, reasoning blocks must not be the final block in an assistant message
// If we have pending reasoning blocks and the last message is an assistant message,
// merge them into a single message with reasoning first
if len(pendingReasoningContentBlocks) > 0 {
if len(bedrockMessages) > 0 && bedrockMessages[len(bedrockMessages)-1].Role == BedrockMessageRoleAssistant {
// Last message is an assistant message - prepend reasoning blocks to it
lastMsg := &bedrockMessages[len(bedrockMessages)-1]
lastMsg.Content = append(pendingReasoningContentBlocks, lastMsg.Content...)
pendingReasoningContentBlocks = nil
}
// If no assistant message to merge into, discard the reasoning blocks
// (they cannot exist alone in Bedrock without violating the constraint)
}
// Merge consecutive messages with the same role
// This ensures document blocks are in the same message as text blocks (Bedrock requirement)
mergedMessages := []BedrockMessage{}
for i := 0; i < len(bedrockMessages); i++ {
currentMsg := bedrockMessages[i]
// Merge any consecutive messages with the same role
for i+1 < len(bedrockMessages) && bedrockMessages[i+1].Role == currentMsg.Role {
i++
currentMsg.Content = append(currentMsg.Content, bedrockMessages[i].Content...)
}
mergedMessages = append(mergedMessages, currentMsg)
}
bedrockMessages = mergedMessages
return bedrockMessages, systemMessages, nil
}
// ConvertBedrockMessagesToBifrostMessages converts an array of Bedrock messages to Bifrost ResponsesMessage format
// This is the main conversion method from Bedrock to Bifrost - handles all message types and content blocks
func ConvertBedrockMessagesToBifrostMessages(ctx *schemas.BifrostContext, bedrockMessages []BedrockMessage, systemMessages []BedrockSystemMessage, isOutputMessage bool) []schemas.ResponsesMessage {
var bifrostMessages []schemas.ResponsesMessage
// Convert system messages first
systemBifrostMsgs := convertBedrockSystemMessageToBifrostMessages(systemMessages)
if len(systemBifrostMsgs) > 0 {
bifrostMessages = append(bifrostMessages, systemBifrostMsgs...)
}
// Convert regular messages
for _, msg := range bedrockMessages {
convertedMessages := convertSingleBedrockMessageToBifrostMessages(ctx, &msg, isOutputMessage)
bifrostMessages = append(bifrostMessages, convertedMessages...)
}
return bifrostMessages
}
// Helper functions for converting individual Bedrock message types
// convertBifrostMessageToBedrockSystemMessages converts a Bifrost system message to Bedrock system messages
func convertBifrostMessageToBedrockSystemMessages(msg *schemas.ResponsesMessage) []BedrockSystemMessage {
var systemMessages []BedrockSystemMessage
if msg.Content != nil {
if msg.Content.ContentStr != nil {
systemMessages = append(systemMessages, BedrockSystemMessage{
Text: msg.Content.ContentStr,
})
} else if msg.Content.ContentBlocks != nil {
for _, block := range msg.Content.ContentBlocks {
if block.Text != nil {
systemMessages = append(systemMessages, BedrockSystemMessage{
Text: block.Text,
})
if block.CacheControl != nil {
systemMessages = append(systemMessages, BedrockSystemMessage{
CachePoint: &BedrockCachePoint{
Type: BedrockCachePointTypeDefault,
},
})
}
}
}
}
}
return systemMessages
}
// convertBifrostMessageToBedrockMessage converts a regular Bifrost message to Bedrock message
func convertBifrostMessageToBedrockMessage(msg *schemas.ResponsesMessage) *BedrockMessage {
// Ensure Content is present
if msg.Content == nil {
return nil
}
bedrockMsg := BedrockMessage{
Role: BedrockMessageRole(*msg.Role),
}
// Convert content
contentBlocks, err := convertBifrostResponsesMessageContentBlocksToBedrockContentBlocks(*msg.Content)
if err != nil {
return nil
}
bedrockMsg.Content = contentBlocks
return &bedrockMsg
}
// convertBedrockSystemMessageToBifrostMessages converts a Bedrock system message to Bifrost messages
func convertBedrockSystemMessageToBifrostMessages(systemMessages []BedrockSystemMessage) []schemas.ResponsesMessage {
var bifrostMessages []schemas.ResponsesMessage
for _, sysMsg := range systemMessages {
if sysMsg.CachePoint != nil {
// add it to last content block of last message
if len(bifrostMessages) > 0 {
lastMessage := &bifrostMessages[len(bifrostMessages)-1]
if lastMessage.Content != nil && len(lastMessage.Content.ContentBlocks) > 0 {
lastMessage.Content.ContentBlocks[len(lastMessage.Content.ContentBlocks)-1].CacheControl = &schemas.CacheControl{
Type: schemas.CacheControlTypeEphemeral,
}
}
}
}
if sysMsg.Text != nil {
systemRole := schemas.ResponsesInputMessageRoleSystem
msgType := schemas.ResponsesMessageTypeMessage
bifrostMessages = append(bifrostMessages, schemas.ResponsesMessage{
Type: &msgType,
Role: &systemRole,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesInputMessageContentBlockTypeText,
Text: sysMsg.Text,
},
},
},
})
}
}
return bifrostMessages
}
// Helper to convert Bedrock role to Bifrost role
func convertBedrockRoleToBifrostRole(bedrockRole BedrockMessageRole) schemas.ResponsesMessageRoleType {
switch bedrockRole {
case BedrockMessageRoleUser:
return schemas.ResponsesInputMessageRoleUser
case BedrockMessageRoleAssistant:
return schemas.ResponsesInputMessageRoleAssistant
default:
return schemas.ResponsesInputMessageRoleUser
}
}
// Helper to create a text message
func createTextMessage(
text *string,
role schemas.ResponsesMessageRoleType,
textBlockType schemas.ResponsesMessageContentBlockType,
isOutputMessage bool,
) schemas.ResponsesMessage {
contentBlock := schemas.ResponsesMessageContentBlock{
Type: textBlockType,
Text: text,
}
if textBlockType == schemas.ResponsesOutputMessageContentTypeText {
contentBlock.ResponsesOutputMessageContentText = &schemas.ResponsesOutputMessageContentText{
Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{},
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
}
}
bifrostMsg := schemas.ResponsesMessage{
Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage),
Status: schemas.Ptr("completed"),
Role: &role,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{contentBlock},
},
}
if isOutputMessage {
bifrostMsg.ID = schemas.Ptr("msg_" + fmt.Sprintf("%d", time.Now().UnixNano()))
}
return bifrostMsg
}
// convertSingleBedrockMessageToBifrostMessages converts a single Bedrock message to Bifrost messages
func convertSingleBedrockMessageToBifrostMessages(ctx *schemas.BifrostContext, msg *BedrockMessage, isOutputMessage bool) []schemas.ResponsesMessage {
var outputMessages []schemas.ResponsesMessage
var reasoningContentBlocks []schemas.ResponsesMessageContentBlock
// Check if we have a structured output tool
var structuredOutputToolName string
if ctx != nil {
if toolName, ok := ctx.Value(schemas.BifrostContextKeyStructuredOutputToolName).(string); ok {
structuredOutputToolName = toolName
}
}
for _, block := range msg.Content {
if block.Text != nil {
// Text content
role := convertBedrockRoleToBifrostRole(msg.Role)
// For assistant messages (previous model outputs), use output_text type
// For user/system messages, use input_text type
textBlockType := schemas.ResponsesInputMessageContentBlockTypeText
if isOutputMessage || msg.Role == BedrockMessageRoleAssistant {
textBlockType = schemas.ResponsesOutputMessageContentTypeText
}
bifrostMsg := createTextMessage(block.Text, role, textBlockType, isOutputMessage)
if isOutputMessage {
bifrostMsg.ID = schemas.Ptr("msg_" + fmt.Sprintf("%d", time.Now().UnixNano()))
}
outputMessages = append(outputMessages, bifrostMsg)
} else if block.ReasoningContent != nil {
// Reasoning content - collect to create a single reasoning message
if block.ReasoningContent.ReasoningText != nil {
reasoningContentBlocks = append(reasoningContentBlocks, schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: block.ReasoningContent.ReasoningText.Text,
Signature: block.ReasoningContent.ReasoningText.Signature,
})
}
} else if block.ToolUse != nil {
// Tool use content
// Create copies of the values to avoid range loop variable capture
toolUseID := block.ToolUse.ToolUseID
toolUseName := block.ToolUse.Name
// Check if this is a structured output tool - if so, convert to text content
if structuredOutputToolName != "" && toolUseName == structuredOutputToolName {
// This is a structured output tool - convert to text message
role := convertBedrockRoleToBifrostRole(msg.Role)
// Marshal the tool input to JSON string
var contentStr string
if block.ToolUse.Input != nil {
contentStr = string(block.ToolUse.Input)
} else {
contentStr = "{}"
}
bifrostMsg := createTextMessage(&contentStr, role, schemas.ResponsesOutputMessageContentTypeText, isOutputMessage)
if isOutputMessage {
bifrostMsg.ID = schemas.Ptr("msg_" + fmt.Sprintf("%d", time.Now().UnixNano()))
}
outputMessages = append(outputMessages, bifrostMsg)
} else {
// Normal tool call message
arguments := "{}"
if block.ToolUse.Input != nil {
arguments = string(block.ToolUse.Input)
}
toolMsg := schemas.ResponsesMessage{
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall),
Status: schemas.Ptr("completed"),
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: &toolUseID,
Name: &toolUseName,
Arguments: schemas.Ptr(arguments),
},
}
if isOutputMessage {
toolMsg.ID = schemas.Ptr("msg_" + fmt.Sprintf("%d", time.Now().UnixNano()))
role := schemas.ResponsesInputMessageRoleAssistant
toolMsg.Role = &role
}
outputMessages = append(outputMessages, toolMsg)
}
} else if block.Document != nil {
// Document content
role := convertBedrockRoleToBifrostRole(msg.Role)
// Convert document to file block
fileBlock := schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesInputMessageContentBlockTypeFile,
ResponsesInputMessageContentBlockFile: &schemas.ResponsesInputMessageContentBlockFile{},
}
// Set filename from document name
if block.Document.Name != "" {
fileBlock.ResponsesInputMessageContentBlockFile.Filename = &block.Document.Name
}
fileType := "application/pdf"
// Set file type based on format
if block.Document.Format != "" {
switch block.Document.Format {
case "pdf":
fileType = "application/pdf"
case "txt", "md", "html", "csv":
fileType = "text/plain"
default:
fileType = "application/pdf" // Default to PDF
}
fileBlock.ResponsesInputMessageContentBlockFile.FileType = &fileType
}
// Convert document source data
if block.Document.Source != nil {
if block.Document.Source.Text != nil {
// Plain text content
fileBlock.ResponsesInputMessageContentBlockFile.FileData = block.Document.Source.Text
} else if block.Document.Source.Bytes != nil {
// Base64 encoded bytes (PDF)
fileDataURL := *block.Document.Source.Bytes
if !strings.HasPrefix(fileDataURL, "data:") {
fileDataURL = fmt.Sprintf("data:%s;base64,%s", fileType, fileDataURL)
}
fileBlock.ResponsesInputMessageContentBlockFile.FileData = &fileDataURL
}
}
bifrostMsg := schemas.ResponsesMessage{
Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage),
Role: &role,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{fileBlock},
},
}
if isOutputMessage {
bifrostMsg.ID = schemas.Ptr("msg_" + fmt.Sprintf("%d", time.Now().UnixNano()))
}
outputMessages = append(outputMessages, bifrostMsg)
} else if block.ToolResult != nil {
// Tool result content - typically not in assistant output but handled for completeness
// Prefer JSON payloads without unmarshalling; fallback to text
var resultContent string
if len(block.ToolResult.Content) > 0 {
// JSON first (no unmarshal; just one marshal to string when present)
for _, c := range block.ToolResult.Content {
if c.JSON != nil {
resultContent = string(c.JSON)
break
}
}
// Fallback to first available text block
if resultContent == "" {
for _, c := range block.ToolResult.Content {
if c.Text != nil {
resultContent = *c.Text
break
}
}
}
}
// Create a copy of the value to avoid range loop variable capture
toolResultID := block.ToolResult.ToolUseID
resultMsg := schemas.ResponsesMessage{
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCallOutput),
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: &toolResultID,
Output: &schemas.ResponsesToolMessageOutputStruct{
ResponsesToolCallOutputStr: &resultContent,
},
},
}
if isOutputMessage {
resultMsg.ID = schemas.Ptr("msg_" + fmt.Sprintf("%d", time.Now().UnixNano()))
role := schemas.ResponsesInputMessageRoleAssistant
resultMsg.Role = &role
resultMsg.Content = &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: &resultContent,
},
},
}
}
outputMessages = append(outputMessages, resultMsg)
} else if block.CachePoint != nil {
// Add cache control to last message
if len(outputMessages) > 0 {
lastMessage := &outputMessages[len(outputMessages)-1]
// First try: set on last content block (for text/image messages)
if lastMessage.Content != nil && len(lastMessage.Content.ContentBlocks) > 0 {
lastMessage.Content.ContentBlocks[len(lastMessage.Content.ContentBlocks)-1].CacheControl = &schemas.CacheControl{
Type: schemas.CacheControlTypeEphemeral,
}
} else {
// Fallback: set on message itself (for function_call/function_call_output)
lastMessage.CacheControl = &schemas.CacheControl{
Type: schemas.CacheControlTypeEphemeral,
}
}
}
}
}
// Handle reasoning blocks - prepend reasoning message if we collected any
if len(reasoningContentBlocks) > 0 {
reasoningMessage := schemas.ResponsesMessage{
ID: schemas.Ptr("rs_" + fmt.Sprintf("%d", time.Now().UnixNano())),
Type: schemas.Ptr(schemas.ResponsesMessageTypeReasoning),
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{},
},
Content: &schemas.ResponsesMessageContent{
ContentBlocks: reasoningContentBlocks,
},
}
// Prepend the reasoning message to the start of the messages list
outputMessages = append([]schemas.ResponsesMessage{reasoningMessage}, outputMessages...)
}
return outputMessages
}
// convertBifrostReasoningToBedrockReasoning converts a Bifrost reasoning message to Bedrock reasoning blocks
func convertBifrostReasoningToBedrockReasoning(msg *schemas.ResponsesMessage) []BedrockContentBlock {
var reasoningBlocks []BedrockContentBlock
if msg.Content != nil && msg.Content.ContentBlocks != nil {
for _, block := range msg.Content.ContentBlocks {
if block.Type == schemas.ResponsesOutputMessageContentTypeReasoning && block.Text != nil {
reasoningBlock := BedrockContentBlock{
ReasoningContent: &BedrockReasoningContent{
ReasoningText: &BedrockReasoningContentText{
Text: block.Text,
Signature: block.Signature,
},
},
}
reasoningBlocks = append(reasoningBlocks, reasoningBlock)
}
}
} else if msg.ResponsesReasoning != nil {
if msg.ResponsesReasoning.Summary != nil {
for _, reasoningContent := range msg.ResponsesReasoning.Summary {
reasoningBlock := BedrockContentBlock{
ReasoningContent: &BedrockReasoningContent{
ReasoningText: &BedrockReasoningContentText{
Text: &reasoningContent.Text,
},
},
}
reasoningBlocks = append(reasoningBlocks, reasoningBlock)
}
} else if msg.ResponsesReasoning.EncryptedContent != nil {
// Bedrock doesn't have a direct equivalent to encrypted content,
// so we'll store it as a regular reasoning block with a special marker
encryptedText := fmt.Sprintf("[ENCRYPTED_REASONING: %s]", *msg.ResponsesReasoning.EncryptedContent)
reasoningBlock := BedrockContentBlock{
ReasoningContent: &BedrockReasoningContent{
ReasoningText: &BedrockReasoningContentText{
Text: &encryptedText,
},
},
}
reasoningBlocks = append(reasoningBlocks, reasoningBlock)
}
}
return reasoningBlocks
}
// convertBifrostResponsesMessageContentBlocksToBedrockContentBlocks converts Bifrost content to Bedrock content blocks
func convertBifrostResponsesMessageContentBlocksToBedrockContentBlocks(content schemas.ResponsesMessageContent) ([]BedrockContentBlock, error) {
var blocks []BedrockContentBlock
if content.ContentStr != nil {
blocks = append(blocks, BedrockContentBlock{
Text: content.ContentStr,
})
} else if content.ContentBlocks != nil {
for _, block := range content.ContentBlocks {
bedrockBlock := BedrockContentBlock{}
switch block.Type {
case schemas.ResponsesInputMessageContentBlockTypeText, schemas.ResponsesOutputMessageContentTypeText:
bedrockBlock.Text = block.Text
case schemas.ResponsesInputMessageContentBlockTypeImage:
if block.ResponsesInputMessageContentBlockImage != nil && block.ResponsesInputMessageContentBlockImage.ImageURL != nil {
imageSource, err := convertImageToBedrockSource(*block.ResponsesInputMessageContentBlockImage.ImageURL)
if err != nil {
return nil, fmt.Errorf("failed to convert image in responses content block: %w", err)
}
bedrockBlock.Image = imageSource
}
case schemas.ResponsesOutputMessageContentTypeReasoning:
if block.Text != nil {
bedrockBlock.ReasoningContent = &BedrockReasoningContent{
ReasoningText: &BedrockReasoningContentText{
Text: block.Text,
Signature: block.Signature,
},
}
}
case schemas.ResponsesOutputMessageContentTypeCompaction:
// Convert compaction to text block for Bedrock (compaction is Anthropic-specific)
if block.ResponsesOutputMessageContentCompaction != nil {
bedrockBlock.Text = &block.ResponsesOutputMessageContentCompaction.Summary
}
case schemas.ResponsesInputMessageContentBlockTypeFile:
if block.ResponsesInputMessageContentBlockFile != nil {
doc := &BedrockDocumentSource{
Name: "document", // Default
Format: "pdf", // Default
Source: &BedrockDocumentSourceData{},
}
// Set filename (normalized for Bedrock)
if block.ResponsesInputMessageContentBlockFile.Filename != nil {
doc.Name = normalizeBedrockFilename(*block.ResponsesInputMessageContentBlockFile.Filename)
}
// Determine format: text or PDF based on FileType
isTextFile := false
if block.ResponsesInputMessageContentBlockFile.FileType != nil {
fileType := *block.ResponsesInputMessageContentBlockFile.FileType
// Check if it's a text type
if strings.HasPrefix(fileType, "text/") ||
fileType == "txt" || fileType == "md" ||
fileType == "html" {
doc.Format = "txt"
isTextFile = true
} else if strings.Contains(fileType, "pdf") || fileType == "pdf" {
doc.Format = "pdf"
}
}
// Handle file data
if block.ResponsesInputMessageContentBlockFile.FileData != nil {
fileData := *block.ResponsesInputMessageContentBlockFile.FileData
// Check if it's a data URL (e.g., "data:application/pdf;base64,...")
if strings.HasPrefix(fileData, "data:") {
urlInfo := schemas.ExtractURLTypeInfo(fileData)
if urlInfo.DataURLWithoutPrefix != nil {
// PDF or other binary - keep as base64
doc.Source.Bytes = urlInfo.DataURLWithoutPrefix
bedrockBlock.Document = doc
break
}
}
// Not a data URL - use as-is
if isTextFile {
// bytes is necessary for bedrock
// base64 string of the text
doc.Source.Text = &fileData
encoded := base64.StdEncoding.EncodeToString([]byte(fileData))
doc.Source.Bytes = &encoded
} else {
doc.Source.Bytes = &fileData
}
bedrockBlock.Document = doc
}
}
default:
// Don't add anything for unknown types
continue
}
// Only append if at least one required field is set
if bedrockBlock.Text != nil ||
bedrockBlock.Image != nil ||
bedrockBlock.Document != nil ||
bedrockBlock.ToolUse != nil ||
bedrockBlock.ToolResult != nil ||
bedrockBlock.ReasoningContent != nil ||
bedrockBlock.CachePoint != nil ||
bedrockBlock.JSON != nil ||
bedrockBlock.GuardContent != nil {
blocks = append(blocks, bedrockBlock)
}
if block.CacheControl != nil {
blocks = append(blocks, BedrockContentBlock{
CachePoint: &BedrockCachePoint{
Type: BedrockCachePointTypeDefault,
},
})
}
}
}
return blocks, nil
}