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

1727 lines
62 KiB
Go

package cohere
import (
"fmt"
"strings"
"sync"
"time"
"github.com/maximhq/bifrost/core/providers/anthropic"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
"github.com/maximhq/bifrost/core/schemas"
"github.com/tidwall/gjson"
)
// CohereResponsesStreamState tracks state during streaming conversion for responses API
type CohereResponsesStreamState struct {
ContentIndexToOutputIndex map[int]int // Maps Cohere content_index to OpenAI output_index
ToolArgumentBuffers map[int]string // Maps output_index to accumulated tool argument JSON
ToolCallNames map[int]string // Maps output_index to tool name
ItemIDs map[int]string // Maps output_index to item ID for stable IDs
ReasoningContentIndices map[int]bool // Tracks which content indices are reasoning blocks
AnnotationIndexToContentIndex map[int]int // Maps annotation index to content index for citation pairing
CurrentOutputIndex int // Current output index counter
MessageID *string // Message ID from message_start
Model *string // Model name from message_start
CreatedAt int // Timestamp for created_at consistency
HasEmittedCreated bool // Whether we've emitted response.created
HasEmittedInProgress bool // Whether we've emitted response.in_progress
ToolPlanOutputIndex *int // Output index for tool plan text item (if created)
}
// cohereResponsesStreamStatePool provides a pool for Cohere responses stream state objects.
var cohereResponsesStreamStatePool = sync.Pool{
New: func() interface{} {
return &CohereResponsesStreamState{
ContentIndexToOutputIndex: make(map[int]int),
ToolArgumentBuffers: make(map[int]string),
ToolCallNames: make(map[int]string),
ItemIDs: make(map[int]string),
ReasoningContentIndices: make(map[int]bool),
AnnotationIndexToContentIndex: make(map[int]int),
CurrentOutputIndex: 0,
CreatedAt: int(time.Now().Unix()),
HasEmittedCreated: false,
HasEmittedInProgress: false,
ToolPlanOutputIndex: nil,
}
},
}
// acquireCohereResponsesStreamState gets a Cohere responses stream state from the pool.
func acquireCohereResponsesStreamState() *CohereResponsesStreamState {
state := cohereResponsesStreamStatePool.Get().(*CohereResponsesStreamState)
// 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.ToolCallNames == nil {
state.ToolCallNames = make(map[int]string)
} else {
clear(state.ToolCallNames)
}
if state.ItemIDs == nil {
state.ItemIDs = make(map[int]string)
} else {
clear(state.ItemIDs)
}
if state.ReasoningContentIndices == nil {
state.ReasoningContentIndices = make(map[int]bool)
} else {
clear(state.ReasoningContentIndices)
}
if state.AnnotationIndexToContentIndex == nil {
state.AnnotationIndexToContentIndex = make(map[int]int)
} else {
clear(state.AnnotationIndexToContentIndex)
}
// Reset other fields
state.CurrentOutputIndex = 0
state.MessageID = nil
state.Model = nil
state.CreatedAt = int(time.Now().Unix())
state.HasEmittedCreated = false
state.HasEmittedInProgress = false
state.ToolPlanOutputIndex = nil
return state
}
// releaseCohereResponsesStreamState returns a Cohere responses stream state to the pool.
func releaseCohereResponsesStreamState(state *CohereResponsesStreamState) {
if state != nil {
state.flush() // Clean before returning to pool
cohereResponsesStreamStatePool.Put(state)
}
}
// flush resets the state of the stream state to its initial values
func (state *CohereResponsesStreamState) 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.ToolCallNames == nil {
state.ToolCallNames = make(map[int]string)
} else {
clear(state.ToolCallNames)
}
if state.ItemIDs == nil {
state.ItemIDs = make(map[int]string)
} else {
clear(state.ItemIDs)
}
if state.ReasoningContentIndices == nil {
state.ReasoningContentIndices = make(map[int]bool)
} else {
clear(state.ReasoningContentIndices)
}
if state.AnnotationIndexToContentIndex == nil {
state.AnnotationIndexToContentIndex = make(map[int]int)
} else {
clear(state.AnnotationIndexToContentIndex)
}
state.CurrentOutputIndex = 0
state.MessageID = nil
state.Model = nil
state.CreatedAt = int(time.Now().Unix())
state.HasEmittedCreated = false
state.HasEmittedInProgress = false
state.ToolPlanOutputIndex = nil
}
// getOrCreateOutputIndex returns the output index for a given content index, creating a new one if needed
func (state *CohereResponsesStreamState) getOrCreateOutputIndex(contentIndex *int) int {
if contentIndex == nil {
// If no content index, create a new output index
outputIndex := state.CurrentOutputIndex
state.CurrentOutputIndex++
return outputIndex
}
if outputIndex, exists := state.ContentIndexToOutputIndex[*contentIndex]; exists {
return outputIndex
}
// Create new output index for this content index
outputIndex := state.CurrentOutputIndex
state.CurrentOutputIndex++
state.ContentIndexToOutputIndex[*contentIndex] = outputIndex
return outputIndex
}
// convertCohereContentBlockToBifrost converts CohereContentBlock to schemas.ContentBlock for Responses
func convertCohereContentBlockToBifrost(cohereBlock CohereContentBlock) schemas.ResponsesMessageContentBlock {
switch cohereBlock.Type {
case CohereContentBlockTypeText:
return schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeText,
Text: cohereBlock.Text,
ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{},
},
}
case CohereContentBlockTypeImage:
// For images, create a text block describing the image (should never happen)
if cohereBlock.ImageURL == nil {
// Skip invalid image blocks without ImageURL
return schemas.ResponsesMessageContentBlock{}
}
return schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesInputMessageContentBlockTypeImage,
ResponsesInputMessageContentBlockImage: &schemas.ResponsesInputMessageContentBlockImage{
ImageURL: &cohereBlock.ImageURL.URL,
},
}
case CohereContentBlockTypeThinking:
return schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: cohereBlock.Thinking,
}
default:
// Fallback to text block
return schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesInputMessageContentBlockTypeText,
Text: schemas.Ptr(string(cohereBlock.Type)),
ResponsesOutputMessageContentText: &schemas.ResponsesOutputMessageContentText{
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
Annotations: []schemas.ResponsesOutputMessageContentTextAnnotation{},
},
}
}
}
func (chunk *CohereStreamEvent) ToBifrostResponsesStream(sequenceNumber int, state *CohereResponsesStreamState) ([]*schemas.BifrostResponsesStreamResponse, *schemas.BifrostError, bool) {
switch chunk.Type {
case StreamEventMessageStart:
// Message start - emit response.created and response.in_progress (OpenAI-style lifecycle)
if chunk.ID != nil {
state.MessageID = chunk.ID
// Use the state's CreatedAt for consistency
if state.CreatedAt == 0 {
state.CreatedAt = int(time.Now().Unix())
}
var responses []*schemas.BifrostResponsesStreamResponse
// Emit response.created
if !state.HasEmittedCreated {
response := &schemas.BifrostResponsesResponse{
ID: state.MessageID,
CreatedAt: state.CreatedAt,
}
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
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeInProgress,
SequenceNumber: sequenceNumber + len(responses),
Response: response,
})
state.HasEmittedInProgress = true
}
if len(responses) > 0 {
return responses, nil, false
}
}
case StreamEventContentStart:
// Content block start - emit output_item.added (OpenAI-style)
// First, close tool plan message item if it's still open
var responses []*schemas.BifrostResponsesStreamResponse
if state.ToolPlanOutputIndex != nil {
outputIndex := *state.ToolPlanOutputIndex
itemID := state.ItemIDs[outputIndex]
// 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: schemas.Ptr(0),
ItemID: &itemID,
Text: &emptyText,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
})
// Emit content_part.done
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: schemas.Ptr(0),
ItemID: &itemID,
Part: part,
})
// Emit output_item.done
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: schemas.Ptr(0),
Item: doneItem,
})
state.ToolPlanOutputIndex = nil // Mark as closed
}
if chunk.Delta != nil && chunk.Index != nil && chunk.Delta.Message != nil && chunk.Delta.Message.Content != nil && chunk.Delta.Message.Content.CohereStreamContentObject != nil {
outputIndex := state.getOrCreateOutputIndex(chunk.Index)
switch chunk.Delta.Message.Content.CohereStreamContentObject.Type {
case CohereContentBlockTypeText:
// Text block - emit output_item.added with type "message"
messageType := schemas.ResponsesMessageTypeMessage
role := schemas.ResponsesInputMessageRoleAssistant
// 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
item := &schemas.ResponsesMessage{
ID: &itemID,
Type: &messageType,
Role: &role,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{}, // Empty blocks slice for mutation support
},
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemAdded,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: chunk.Index,
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: chunk.Index,
ItemID: &itemID,
Part: part,
})
return responses, nil, false
case CohereContentBlockTypeThinking:
// Thinking/reasoning content - emit as reasoning item
messageType := schemas.ResponsesMessageTypeReasoning
role := schemas.ResponsesInputMessageRoleAssistant
// Generate stable ID for reasoning item
itemID := "rs_" + providerUtils.GetRandomString(50)
state.ItemIDs[outputIndex] = itemID
item := &schemas.ResponsesMessage{
ID: &itemID,
Type: &messageType,
Role: &role,
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{},
},
}
// Track that this content index is a reasoning block
if chunk.Index != nil {
state.ReasoningContentIndices[*chunk.Index] = true
}
// Emit output_item.added
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemAdded,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: chunk.Index,
Item: item,
})
// Emit content_part.added with empty reasoning_text part
emptyText := ""
part := &schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: &emptyText,
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeContentPartAdded,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: chunk.Index,
ItemID: &itemID,
Part: part,
})
return responses, nil, false
}
}
if len(responses) > 0 {
return responses, nil, false
}
case StreamEventContentDelta:
if chunk.Index != nil && chunk.Delta != nil {
outputIndex := state.getOrCreateOutputIndex(chunk.Index)
// Handle text content delta
if chunk.Delta.Message != nil && chunk.Delta.Message.Content != nil && chunk.Delta.Message.Content.CohereStreamContentObject != nil && chunk.Delta.Message.Content.CohereStreamContentObject.Text != nil && *chunk.Delta.Message.Content.CohereStreamContentObject.Text != "" {
// Emit output_text.delta (not reasoning_summary_text.delta for regular text)
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputTextDelta,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: chunk.Index,
Delta: chunk.Delta.Message.Content.CohereStreamContentObject.Text,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
}
if itemID != "" {
response.ItemID = &itemID
}
return []*schemas.BifrostResponsesStreamResponse{response}, nil, false
}
// Handle thinking content delta
if chunk.Delta.Message != nil && chunk.Delta.Message.Content != nil && chunk.Delta.Message.Content.CohereStreamContentObject != nil && chunk.Delta.Message.Content.CohereStreamContentObject.Thinking != nil && *chunk.Delta.Message.Content.CohereStreamContentObject.Thinking != "" {
// Emit reasoning_summary_text.delta for thinking content
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDelta,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: chunk.Index,
Delta: chunk.Delta.Message.Content.CohereStreamContentObject.Thinking,
}
if itemID != "" {
response.ItemID = &itemID
}
return []*schemas.BifrostResponsesStreamResponse{response}, nil, false
}
}
return nil, nil, false
case StreamEventContentEnd:
// Content block is complete - emit output_text.done, content_part.done, and output_item.done (OpenAI-style)
if chunk.Index != nil {
outputIndex := state.getOrCreateOutputIndex(chunk.Index)
itemID := state.ItemIDs[outputIndex]
var responses []*schemas.BifrostResponsesStreamResponse
isReasoning := state.ReasoningContentIndices[*chunk.Index]
// Check if this content index is a reasoning block
if isReasoning {
// Emit reasoning_summary_text.done (reasoning equivalent of output_text.done)
emptyText := ""
reasoningDoneResponse := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeReasoningSummaryTextDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: chunk.Index,
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: chunk.Index,
Part: part,
}
if itemID != "" {
partDoneResponse.ItemID = &itemID
}
responses = append(responses, partDoneResponse)
// Clear the reasoning content index tracking
delete(state.ReasoningContentIndices, *chunk.Index)
} else {
// Regular text block - 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: chunk.Index,
ItemID: &itemID,
Text: &emptyText,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
})
// Emit content_part.done
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: chunk.Index,
ItemID: &itemID,
Part: part,
})
}
// Emit output_item.done for all content blocks (text, reasoning, etc.)
statusCompleted := "completed"
var doneItem *schemas.ResponsesMessage
if isReasoning {
messageType := schemas.ResponsesMessageTypeReasoning
role := schemas.ResponsesInputMessageRoleAssistant
doneItem = &schemas.ResponsesMessage{
Type: &messageType,
Role: &role,
Status: &statusCompleted,
ResponsesReasoning: &schemas.ResponsesReasoning{
Summary: []schemas.ResponsesReasoningSummary{},
},
}
} else {
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: chunk.Index,
Item: doneItem,
})
return responses, nil, false
}
case StreamEventToolPlanDelta:
if chunk.Delta != nil && chunk.Delta.Message != nil && chunk.Delta.Message.ToolPlan != nil && *chunk.Delta.Message.ToolPlan != "" {
// Tool plan delta - treat as normal text (Option A)
// Use output_index 0 for text message if it exists, otherwise create new
outputIndex := 0
var responses []*schemas.BifrostResponsesStreamResponse
if state.ToolPlanOutputIndex != nil {
outputIndex = *state.ToolPlanOutputIndex
} else {
// Create message item first if it doesn't exist
outputIndex = 0
state.ToolPlanOutputIndex = &outputIndex
state.ContentIndexToOutputIndex[0] = outputIndex
// Generate stable ID for text item
// 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
messageType := schemas.ResponsesMessageTypeMessage
role := schemas.ResponsesInputMessageRoleAssistant
item := &schemas.ResponsesMessage{
ID: &itemID,
Type: &messageType,
Role: &role,
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{},
},
}
// Emit output_item.added for text message
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemAdded,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: schemas.Ptr(0),
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: schemas.Ptr(0),
ItemID: &itemID,
Part: part,
})
}
// Emit output_text.delta (not reasoning_summary_text.delta)
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputTextDelta,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: schemas.Ptr(0), // Tool plan is typically at index 0
Delta: chunk.Delta.Message.ToolPlan,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
}
if itemID != "" {
response.ItemID = &itemID
}
responses = append(responses, response)
return responses, nil, false
}
return nil, nil, false
case StreamEventToolCallStart:
// First, close tool plan message item if it's still open
var responses []*schemas.BifrostResponsesStreamResponse
if state.ToolPlanOutputIndex != nil {
outputIndex := *state.ToolPlanOutputIndex
itemID := state.ItemIDs[outputIndex]
// 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: schemas.Ptr(0),
ItemID: &itemID,
Text: &emptyText,
LogProbs: []schemas.ResponsesOutputMessageContentTextLogProb{},
})
// Emit content_part.done
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: schemas.Ptr(0),
ItemID: &itemID,
Part: part,
})
// Emit output_item.done
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: schemas.Ptr(0),
Item: doneItem,
})
state.ToolPlanOutputIndex = nil // Mark as closed
}
if chunk.Index != nil && chunk.Delta != nil && chunk.Delta.Message != nil && chunk.Delta.Message.ToolCalls != nil && chunk.Delta.Message.ToolCalls.CohereToolCallObject != nil {
// Tool call start - emit output_item.added with type "function_call" and status "in_progress"
toolCall := chunk.Delta.Message.ToolCalls.CohereToolCallObject
if toolCall.Function != nil && toolCall.Function.Name != nil {
// Always use a new output index for tool calls to avoid collision with text items
// Use output_index 1 (or next available) to avoid collision with text at index 0
outputIndex := state.CurrentOutputIndex
if outputIndex == 0 {
outputIndex = 1 // Skip 0 if it's used for text
}
state.CurrentOutputIndex = outputIndex + 1
// Optionally map the content index if provided
if chunk.Index != nil {
state.ContentIndexToOutputIndex[*chunk.Index] = outputIndex
}
statusInProgress := "in_progress"
itemID := ""
if toolCall.ID != nil {
itemID = *toolCall.ID
state.ItemIDs[outputIndex] = itemID
}
if toolCall.Function.Name != nil {
state.ToolCallNames[outputIndex] = *toolCall.Function.Name
}
item := &schemas.ResponsesMessage{
ID: toolCall.ID,
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall),
Status: &statusInProgress,
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: toolCall.ID,
Name: toolCall.Function.Name,
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),
Item: item,
})
return responses, nil, false
}
}
if len(responses) > 0 {
return responses, nil, false
}
return nil, nil, false
case StreamEventToolCallDelta:
if chunk.Index != nil && chunk.Delta != nil && chunk.Delta.Message != nil && chunk.Delta.Message.ToolCalls != nil && chunk.Delta.Message.ToolCalls.CohereToolCallObject != nil {
// Tool call delta - handle function arguments streaming
toolCall := chunk.Delta.Message.ToolCalls.CohereToolCallObject
if toolCall.Function != nil {
outputIndex := state.getOrCreateOutputIndex(chunk.Index)
// Accumulate tool arguments in buffer
if _, exists := state.ToolArgumentBuffers[outputIndex]; !exists {
state.ToolArgumentBuffers[outputIndex] = ""
}
state.ToolArgumentBuffers[outputIndex] += toolCall.Function.Arguments
// Emit function_call_arguments.delta
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDelta,
SequenceNumber: sequenceNumber,
ContentIndex: chunk.Index,
OutputIndex: schemas.Ptr(outputIndex),
Delta: schemas.Ptr(toolCall.Function.Arguments),
}
if itemID != "" {
response.ItemID = &itemID
}
return []*schemas.BifrostResponsesStreamResponse{response}, nil, false
}
}
return nil, nil, false
case StreamEventToolCallEnd:
if chunk.Index != nil {
// Tool call end - emit function_call_arguments.done then output_item.done
outputIndex := state.getOrCreateOutputIndex(chunk.Index)
var responses []*schemas.BifrostResponsesStreamResponse
argsValue := ""
// Emit function_call_arguments.done with full accumulated JSON
if accumulatedArgs, hasArgs := state.ToolArgumentBuffers[outputIndex]; hasArgs && accumulatedArgs != "" {
argsValue = accumulatedArgs
itemID := state.ItemIDs[outputIndex]
response := &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeFunctionCallArgumentsDone,
SequenceNumber: sequenceNumber,
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: chunk.Index,
Arguments: &argsValue,
}
if itemID != "" {
response.ItemID = &itemID
}
responses = append(responses, response)
// Clear the buffer
delete(state.ToolArgumentBuffers, outputIndex)
}
// Emit output_item.done for the function call
statusCompleted := "completed"
itemID := state.ItemIDs[outputIndex]
callName, hasName := state.ToolCallNames[outputIndex]
var callNamePtr *string
if hasName && callName != "" {
callNamePtr = &callName
}
doneItem := &schemas.ResponsesMessage{
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall),
Status: &statusCompleted,
ResponsesToolMessage: &schemas.ResponsesToolMessage{
CallID: &itemID,
Name: callNamePtr,
Arguments: &argsValue,
},
}
if itemID != "" {
doneItem.ID = &itemID
}
responses = append(responses, &schemas.BifrostResponsesStreamResponse{
Type: schemas.ResponsesStreamResponseTypeOutputItemDone,
SequenceNumber: sequenceNumber + len(responses),
OutputIndex: schemas.Ptr(outputIndex),
ContentIndex: chunk.Index,
Item: doneItem,
})
return responses, nil, false
}
return nil, nil, false
case StreamEventCitationStart:
if chunk.Index != nil && chunk.Delta != nil && chunk.Delta.Message != nil && chunk.Delta.Message.Citations != nil {
// Citation start - create annotation for the citation
citation := chunk.Delta.Message.Citations.CohereStreamCitationObject
// Map Cohere citation to ResponsesOutputMessageContentTextAnnotation
annotation := &schemas.ResponsesOutputMessageContentTextAnnotation{
Type: "file_citation", // Default to file_citation
StartIndex: schemas.Ptr(citation.Start),
EndIndex: schemas.Ptr(citation.End),
}
// Set annotation type and metadata
if len(citation.Sources) > 0 {
source := citation.Sources[0]
if source.ID != nil {
annotation.FileID = source.ID
}
if source.Document != nil {
doc := []byte(*source.Document)
if t := providerUtils.GetJSONField(doc, "title"); t.Exists() && t.Type == gjson.String {
title := t.String()
annotation.Title = &title
}
if id := providerUtils.GetJSONField(doc, "id"); id.Exists() && id.Type == gjson.String && annotation.FileID == nil {
idStr := id.String()
annotation.FileID = &idStr
}
if s := providerUtils.GetJSONField(doc, "snippet"); s.Exists() && s.Type == gjson.String {
snippet := s.String()
annotation.Text = &snippet
}
if u := providerUtils.GetJSONField(doc, "url"); u.Exists() && u.Type == gjson.String {
url := u.String()
annotation.URL = &url
}
}
}
// Use output_index based on content index for citations (they're part of the text item)
outputIndex := 0
if citation.ContentIndex >= 0 {
contentIndexPtr := &citation.ContentIndex
outputIndex = state.getOrCreateOutputIndex(contentIndexPtr)
}
// Record mapping from annotation index to content index for citation pairing
if chunk.Index != nil && citation.ContentIndex >= 0 {
state.AnnotationIndexToContentIndex[*chunk.Index] = citation.ContentIndex
}
return []*schemas.BifrostResponsesStreamResponse{{
Type: schemas.ResponsesStreamResponseTypeOutputTextAnnotationAdded,
SequenceNumber: sequenceNumber,
ContentIndex: schemas.Ptr(citation.ContentIndex),
Annotation: annotation,
OutputIndex: schemas.Ptr(outputIndex),
AnnotationIndex: chunk.Index,
}}, nil, false
}
return nil, nil, false
case StreamEventCitationEnd:
if chunk.Index != nil {
// Citation end - indicate annotation is complete
// Look up the original content index from state using the annotation index
contentIndex, exists := state.AnnotationIndexToContentIndex[*chunk.Index]
if !exists {
// Fallback: if mapping not found, use annotation index (shouldn't happen in normal flow)
contentIndex = *chunk.Index
}
// Derive outputIndex from the content index
contentIndexPtr := &contentIndex
outputIndex := state.getOrCreateOutputIndex(contentIndexPtr)
return []*schemas.BifrostResponsesStreamResponse{{
Type: schemas.ResponsesStreamResponseTypeOutputTextAnnotationDone,
SequenceNumber: sequenceNumber,
ContentIndex: &contentIndex,
OutputIndex: schemas.Ptr(outputIndex),
AnnotationIndex: chunk.Index,
}}, nil, false
}
return nil, nil, false
case StreamEventMessageEnd:
// Message end - emit response.completed (OpenAI-style)
response := &schemas.BifrostResponsesResponse{
CreatedAt: state.CreatedAt,
}
if state.MessageID != nil {
response.ID = state.MessageID
}
if state.Model != nil {
response.Model = *state.Model
}
if chunk.Delta != nil {
if chunk.Delta.Usage != nil {
usage := &schemas.ResponsesResponseUsage{}
if chunk.Delta.Usage.Tokens != nil {
if chunk.Delta.Usage.Tokens.InputTokens != nil {
usage.InputTokens = *chunk.Delta.Usage.Tokens.InputTokens
}
if chunk.Delta.Usage.Tokens.OutputTokens != nil {
usage.OutputTokens = *chunk.Delta.Usage.Tokens.OutputTokens
}
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
}
if chunk.Delta.Usage.CachedTokens != nil {
usage.InputTokensDetails = &schemas.ResponsesResponseInputTokens{
CachedReadTokens: *chunk.Delta.Usage.CachedTokens,
}
}
response.Usage = usage
}
}
return []*schemas.BifrostResponsesStreamResponse{{
Type: schemas.ResponsesStreamResponseTypeCompleted,
SequenceNumber: sequenceNumber,
Response: response,
}}, nil, true
case StreamEventDebug:
return nil, nil, false
}
return nil, nil, false
}
// ConvertResponsesTextFormatToCohere converts Bifrost Responses Text.Format to Cohere's typed format
// Responses format: Text.Format with type "json_schema", "json_object", or "text"
// Cohere format: { type: "json_object", json_schema: {...} }
func convertResponsesTextFormatToCohere(textFormat *schemas.ResponsesTextConfigFormat) *CohereResponseFormat {
if textFormat == nil {
return nil
}
cohereFormat := &CohereResponseFormat{}
// Convert type
switch textFormat.Type {
case "text":
cohereFormat.Type = ResponseFormatTypeText
case "json_object":
cohereFormat.Type = ResponseFormatTypeJSONObject
case "json_schema":
cohereFormat.Type = ResponseFormatTypeJSONObject
// If schema is provided, extract it
if textFormat.JSONSchema != nil {
// Build schema map
schema := make(map[string]interface{})
if textFormat.JSONSchema.Type != nil {
schema["type"] = *textFormat.JSONSchema.Type
}
if textFormat.JSONSchema.Properties != nil {
schema["properties"] = *textFormat.JSONSchema.Properties
}
if len(textFormat.JSONSchema.Required) > 0 {
schema["required"] = textFormat.JSONSchema.Required
}
if textFormat.JSONSchema.AdditionalProperties != nil {
schema["additionalProperties"] = *textFormat.JSONSchema.AdditionalProperties
}
var schemaInterface interface{} = schema
cohereFormat.JSONSchema = &schemaInterface
}
default:
cohereFormat.Type = ResponseFormatTypeJSONObject
}
return cohereFormat
}
// ToCohereResponsesRequest converts a BifrostRequest (Responses structure) to CohereChatRequest
func ToCohereResponsesRequest(bifrostReq *schemas.BifrostResponsesRequest) (*CohereChatRequest, error) {
if bifrostReq == nil {
return nil, nil
}
cohereReq := &CohereChatRequest{
Model: bifrostReq.Model,
}
// Map basic parameters
if bifrostReq.Params != nil {
if bifrostReq.Params.MaxOutputTokens != nil {
cohereReq.MaxTokens = bifrostReq.Params.MaxOutputTokens
}
if bifrostReq.Params.Temperature != nil {
cohereReq.Temperature = bifrostReq.Params.Temperature
}
if bifrostReq.Params.TopP != nil {
cohereReq.P = bifrostReq.Params.TopP
}
// Convert reasoning
if bifrostReq.Params.Reasoning != nil {
if bifrostReq.Params.Reasoning.MaxTokens != nil {
thinking := &CohereThinking{
Type: ThinkingTypeEnabled,
}
if *bifrostReq.Params.Reasoning.MaxTokens == -1 {
// cohere does not support dynamic reasoning budget like gemini
// setting it to minimum reasoning budget
thinking.TokenBudget = schemas.Ptr(anthropic.MinimumReasoningMaxTokens)
} else {
thinking.TokenBudget = bifrostReq.Params.Reasoning.MaxTokens
}
cohereReq.Thinking = thinking
} else {
if bifrostReq.Params.Reasoning.Effort != nil && *bifrostReq.Params.Reasoning.Effort != "none" {
maxOutputTokens := providerUtils.GetMaxOutputTokensOrDefault(bifrostReq.Model, DefaultCompletionMaxTokens)
if bifrostReq.Params.MaxOutputTokens != nil {
maxOutputTokens = *bifrostReq.Params.MaxOutputTokens
}
budgetTokens, err := providerUtils.GetBudgetTokensFromReasoningEffort(*bifrostReq.Params.Reasoning.Effort, MinimumReasoningMaxTokens, maxOutputTokens)
if err != nil {
return nil, err
}
cohereReq.Thinking = &CohereThinking{
Type: ThinkingTypeEnabled,
TokenBudget: schemas.Ptr(budgetTokens),
}
} else {
cohereReq.Thinking = &CohereThinking{
Type: ThinkingTypeDisabled,
}
}
}
}
if bifrostReq.Params.Text != nil && bifrostReq.Params.Text.Format != nil {
cohereReq.ResponseFormat = convertResponsesTextFormatToCohere(bifrostReq.Params.Text.Format)
}
if bifrostReq.Params.ExtraParams != nil {
cohereReq.ExtraParams = bifrostReq.Params.ExtraParams
if topK, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["top_k"]); ok {
delete(cohereReq.ExtraParams, "top_k")
cohereReq.K = topK
}
if stop, ok := schemas.SafeExtractStringSlice(bifrostReq.Params.ExtraParams["stop"]); ok {
delete(cohereReq.ExtraParams, "stop")
cohereReq.StopSequences = stop
}
if frequencyPenalty, ok := schemas.SafeExtractFloat64Pointer(bifrostReq.Params.ExtraParams["frequency_penalty"]); ok {
delete(cohereReq.ExtraParams, "frequency_penalty")
cohereReq.FrequencyPenalty = frequencyPenalty
}
if presencePenalty, ok := schemas.SafeExtractFloat64Pointer(bifrostReq.Params.ExtraParams["presence_penalty"]); ok {
delete(cohereReq.ExtraParams, "presence_penalty")
cohereReq.PresencePenalty = presencePenalty
}
if thinkingParam, ok := schemas.SafeExtractFromMap(bifrostReq.Params.ExtraParams, "thinking"); ok {
if thinkingMap, ok := thinkingParam.(map[string]interface{}); ok {
thinking := &CohereThinking{}
if typeStr, ok := schemas.SafeExtractString(thinkingMap["type"]); ok {
delete(thinkingMap, "type")
thinking.Type = CohereThinkingType(typeStr)
}
if tokenBudget, ok := schemas.SafeExtractIntPointer(thinkingMap["token_budget"]); ok {
delete(thinkingMap, "token_budget")
thinking.TokenBudget = tokenBudget
}
cohereReq.Thinking = thinking
bifrostReq.Params.ExtraParams["thinking"] = thinkingMap
}
}
}
}
// Convert tools
if bifrostReq.Params != nil && bifrostReq.Params.Tools != nil {
var cohereTools []CohereChatRequestTool
for _, tool := range bifrostReq.Params.Tools {
if tool.ResponsesToolFunction != nil && tool.Name != nil {
cohereTool := CohereChatRequestTool{
Type: "function",
Function: CohereChatRequestFunction{
Name: *tool.Name,
Description: tool.Description,
Parameters: tool.ResponsesToolFunction.Parameters,
},
}
cohereTools = append(cohereTools, cohereTool)
}
}
if len(cohereTools) > 0 {
cohereReq.Tools = cohereTools
}
}
// Convert tool choice
if bifrostReq.Params != nil && bifrostReq.Params.ToolChoice != nil {
cohereReq.ToolChoice = convertBifrostToolChoiceToCohereToolChoice(*bifrostReq.Params.ToolChoice)
}
// Process ResponsesInput (which contains the Responses items)
if bifrostReq.Input != nil {
cohereReq.Messages = ConvertBifrostMessagesToCohereMessages(bifrostReq.Input, bifrostReq.Params)
}
return cohereReq, nil
}
// ToBifrostResponsesResponse converts CohereChatResponse to BifrostResponse (Responses structure)
func (response *CohereChatResponse) ToBifrostResponsesResponse() *schemas.BifrostResponsesResponse {
if response == nil {
return nil
}
bifrostResp := &schemas.BifrostResponsesResponse{
ID: schemas.Ptr(response.ID),
CreatedAt: int(time.Now().Unix()), // Set current timestamp
}
// Convert usage information
if response.Usage != nil {
usage := &schemas.ResponsesResponseUsage{}
if response.Usage.Tokens != nil {
if response.Usage.Tokens.InputTokens != nil {
usage.InputTokens = *response.Usage.Tokens.InputTokens
}
if response.Usage.Tokens.OutputTokens != nil {
usage.OutputTokens = *response.Usage.Tokens.OutputTokens
}
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
}
if response.Usage.CachedTokens != nil {
usage.InputTokensDetails = &schemas.ResponsesResponseInputTokens{
CachedReadTokens: *response.Usage.CachedTokens,
}
}
bifrostResp.Usage = usage
}
// Convert output message to Responses format
if response.Message != nil {
outputMessages := ConvertCohereMessagesToBifrostMessages([]CohereMessage{*response.Message}, true)
bifrostResp.Output = outputMessages
}
return bifrostResp
}
// ConvertBifrostMessagesToCohereMessages converts an array of Bifrost ResponsesMessage to Cohere message format
// This is the main conversion method from Bifrost to Cohere - handles all message types and returns messages
func ConvertBifrostMessagesToCohereMessages(bifrostMessages []schemas.ResponsesMessage, params *schemas.ResponsesParameters) []CohereMessage {
var cohereMessages []CohereMessage
var systemContent []string
var pendingReasoningContentBlocks []CohereContentBlock
var currentAssistantMessage *CohereMessage
for _, msg := range bifrostMessages {
// Handle nil Type with default
msgType := schemas.ResponsesMessageTypeMessage
if msg.Type != nil {
msgType = *msg.Type
}
switch msgType {
case schemas.ResponsesMessageTypeMessage:
// Handle nil Role with default
role := "user"
if msg.Role != nil {
role = string(*msg.Role)
}
if role == "system" {
// Collect system messages separately for Cohere
systemMsgs := convertBifrostMessageToCohereSystemContent(&msg)
systemContent = append(systemContent, systemMsgs...)
} else {
// Convert regular message
cohereMsg := convertBifrostMessageToCohereMessage(&msg)
if cohereMsg != nil {
if role == "assistant" {
// Add any pending reasoning content blocks to the message
if len(pendingReasoningContentBlocks) > 0 {
// copy the pending reasoning content blocks
copied := make([]CohereContentBlock, len(pendingReasoningContentBlocks))
copy(copied, pendingReasoningContentBlocks)
contentBlocks := copied
pendingReasoningContentBlocks = nil
// Add content blocks after pending reasoning content blocks are added
if msg.Content != nil {
if msg.Content.ContentStr != nil {
contentBlocks = append(contentBlocks, CohereContentBlock{
Type: CohereContentBlockTypeText,
Text: msg.Content.ContentStr,
})
} else if msg.Content.ContentBlocks != nil {
contentBlocks = append(contentBlocks, convertResponsesMessageContentBlocksToCohere(msg.Content.ContentBlocks)...)
}
}
cohereMsg.Content = NewBlocksContent(contentBlocks)
}
// Store assistant message for potential reasoning blocks
currentAssistantMessage = cohereMsg
} else {
// Flush any pending assistant message first for non-assistant messages
if currentAssistantMessage != nil {
if len(pendingReasoningContentBlocks) > 0 {
if currentAssistantMessage.Content == nil {
currentAssistantMessage.Content = NewBlocksContent(pendingReasoningContentBlocks)
} else if currentAssistantMessage.Content.BlocksContent != nil {
currentAssistantMessage.Content.BlocksContent = append(currentAssistantMessage.Content.BlocksContent, pendingReasoningContentBlocks...)
}
pendingReasoningContentBlocks = nil
}
cohereMessages = append(cohereMessages, *currentAssistantMessage)
currentAssistantMessage = nil
}
cohereMessages = append(cohereMessages, *cohereMsg)
}
}
}
case schemas.ResponsesMessageTypeReasoning:
// Handle reasoning as thinking content blocks
reasoningBlocks := convertBifrostReasoningToCohereThinking(&msg)
if len(reasoningBlocks) > 0 {
if currentAssistantMessage == nil {
currentAssistantMessage = &CohereMessage{
Role: "assistant",
}
}
pendingReasoningContentBlocks = append(pendingReasoningContentBlocks, reasoningBlocks...)
}
case schemas.ResponsesMessageTypeFunctionCall:
// Flush any pending reasoning blocks first
if len(pendingReasoningContentBlocks) > 0 && currentAssistantMessage != nil {
if currentAssistantMessage.Content == nil {
currentAssistantMessage.Content = NewBlocksContent(pendingReasoningContentBlocks)
} else if currentAssistantMessage.Content.BlocksContent != nil {
currentAssistantMessage.Content.BlocksContent = append(currentAssistantMessage.Content.BlocksContent, pendingReasoningContentBlocks...)
}
cohereMessages = append(cohereMessages, *currentAssistantMessage)
pendingReasoningContentBlocks = nil
currentAssistantMessage = nil
}
// Handle function calls from Responses
assistantMsg := convertBifrostFunctionCallToCohereMessage(&msg)
if assistantMsg != nil {
cohereMessages = append(cohereMessages, *assistantMsg)
}
case schemas.ResponsesMessageTypeFunctionCallOutput:
// Handle function call outputs
toolMsg := convertBifrostFunctionCallOutputToCohereMessage(&msg)
if toolMsg != nil {
cohereMessages = append(cohereMessages, *toolMsg)
}
}
}
// Flush any remaining pending reasoning blocks
if len(pendingReasoningContentBlocks) > 0 && currentAssistantMessage != nil {
if currentAssistantMessage.Content == nil {
currentAssistantMessage.Content = NewBlocksContent(pendingReasoningContentBlocks)
} else if currentAssistantMessage.Content.BlocksContent != nil {
currentAssistantMessage.Content.BlocksContent = append(currentAssistantMessage.Content.BlocksContent, pendingReasoningContentBlocks...)
}
cohereMessages = append(cohereMessages, *currentAssistantMessage)
} else if currentAssistantMessage != nil {
cohereMessages = append(cohereMessages, *currentAssistantMessage)
}
// Prepend system messages if any
if len(systemContent) > 0 {
systemMsg := CohereMessage{
Role: "system",
Content: NewStringContent(strings.Join(systemContent, "\n")),
}
cohereMessages = append([]CohereMessage{systemMsg}, cohereMessages...)
} else if params != nil && params.Instructions != nil {
// if no system messages, check if instructions are present
systemMsg := CohereMessage{
Role: "system",
Content: NewStringContent(*params.Instructions),
}
cohereMessages = append([]CohereMessage{systemMsg}, cohereMessages...)
}
return cohereMessages
}
// ConvertCohereMessagesToBifrostMessages converts an array of Cohere messages to Bifrost ResponsesMessage format
// This is the main conversion method from Cohere to Bifrost - handles all message types and content blocks
func ConvertCohereMessagesToBifrostMessages(cohereMessages []CohereMessage, isOutputMessage bool) []schemas.ResponsesMessage {
var bifrostMessages []schemas.ResponsesMessage
for _, msg := range cohereMessages {
convertedMessages := convertSingleCohereMessageToBifrostMessages(&msg, isOutputMessage)
bifrostMessages = append(bifrostMessages, convertedMessages...)
}
return bifrostMessages
}
// convertBifrostToolChoiceToCohere converts schemas.ToolChoice to CohereToolChoice
func convertBifrostToolChoiceToCohereToolChoice(toolChoice schemas.ResponsesToolChoice) *CohereToolChoice {
toolChoiceString := toolChoice.ResponsesToolChoiceStr
if toolChoiceString != nil {
switch *toolChoiceString {
case "none":
choice := ToolChoiceNone
return &choice
case "required", "function":
choice := ToolChoiceRequired
return &choice
case "auto":
choice := ToolChoiceAuto
return &choice
default:
choice := ToolChoiceRequired
return &choice
}
}
return nil
}
// Helper functions for converting individual Cohere message types
// convertBifrostMessageToCohereSystemContent converts a Bifrost system message to Cohere system content
func convertBifrostMessageToCohereSystemContent(msg *schemas.ResponsesMessage) []string {
var systemContent []string
if msg.Content != nil {
if msg.Content.ContentStr != nil {
systemContent = append(systemContent, *msg.Content.ContentStr)
} else if msg.Content.ContentBlocks != nil {
for _, block := range msg.Content.ContentBlocks {
if block.Text != nil {
systemContent = append(systemContent, *block.Text)
}
}
}
}
return systemContent
}
// convertBifrostMessageToCohereMessage converts a regular Bifrost message to Cohere message
func convertBifrostMessageToCohereMessage(msg *schemas.ResponsesMessage) *CohereMessage {
role := "user"
if msg.Role != nil {
role = string(*msg.Role)
}
cohereMsg := CohereMessage{
Role: role,
}
// Convert content - only if Content is not nil
if msg.Content != nil {
if msg.Content.ContentStr != nil {
cohereMsg.Content = NewStringContent(*msg.Content.ContentStr)
} else if msg.Content.ContentBlocks != nil {
contentBlocks := convertResponsesMessageContentBlocksToCohere(msg.Content.ContentBlocks)
cohereMsg.Content = NewBlocksContent(contentBlocks)
}
}
return &cohereMsg
}
// convertBifrostReasoningToCohereThinking converts a Bifrost reasoning message to Cohere thinking blocks
func convertBifrostReasoningToCohereThinking(msg *schemas.ResponsesMessage) []CohereContentBlock {
var thinkingBlocks []CohereContentBlock
if msg.Content != nil && msg.Content.ContentBlocks != nil {
for _, block := range msg.Content.ContentBlocks {
if block.Type == schemas.ResponsesOutputMessageContentTypeReasoning && block.Text != nil {
thinkingBlock := CohereContentBlock{
Type: CohereContentBlockTypeThinking,
Thinking: block.Text,
}
thinkingBlocks = append(thinkingBlocks, thinkingBlock)
}
}
} else if msg.ResponsesReasoning != nil {
if msg.ResponsesReasoning.Summary != nil {
for _, reasoningContent := range msg.ResponsesReasoning.Summary {
thinkingBlock := CohereContentBlock{
Type: CohereContentBlockTypeThinking,
Thinking: &reasoningContent.Text,
}
thinkingBlocks = append(thinkingBlocks, thinkingBlock)
}
} else if msg.ResponsesReasoning.EncryptedContent != nil {
// Cohere doesn't have a direct equivalent to encrypted content,
// so we'll store it as a regular thinking block with a special marker
encryptedText := fmt.Sprintf("[ENCRYPTED_REASONING: %s]", *msg.ResponsesReasoning.EncryptedContent)
thinkingBlock := CohereContentBlock{
Type: CohereContentBlockTypeThinking,
Thinking: &encryptedText,
}
thinkingBlocks = append(thinkingBlocks, thinkingBlock)
}
}
return thinkingBlocks
}
// convertBifrostFunctionCallToCohereMessage converts a Bifrost function call to Cohere message
func convertBifrostFunctionCallToCohereMessage(msg *schemas.ResponsesMessage) *CohereMessage {
assistantMsg := CohereMessage{
Role: "assistant",
}
// Extract function call details
var cohereToolCalls []CohereToolCall
toolCall := CohereToolCall{
Type: "function",
Function: &CohereFunction{},
}
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil {
toolCall.ID = msg.CallID
}
// Get function details from AssistantMessage
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.Arguments != nil {
toolCall.Function.Arguments = *msg.ResponsesToolMessage.Arguments
}
// Get name from ToolMessage if available
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.Name != nil {
toolCall.Function.Name = msg.ResponsesToolMessage.Name
}
cohereToolCalls = append(cohereToolCalls, toolCall)
if len(cohereToolCalls) > 0 {
assistantMsg.ToolCalls = cohereToolCalls
}
return &assistantMsg
}
// convertBifrostFunctionCallOutputToCohereMessage converts a Bifrost function call output to Cohere message
func convertBifrostFunctionCallOutputToCohereMessage(msg *schemas.ResponsesMessage) *CohereMessage {
if msg.ResponsesToolMessage != nil && msg.ResponsesToolMessage.CallID != nil {
toolMsg := CohereMessage{
Role: "tool",
}
// Extract content from ResponsesFunctionToolCallOutput if Content is not set
// This is needed for OpenAI Responses API which uses an "output" field
content := msg.Content
if content == nil && msg.ResponsesToolMessage.Output != nil {
content = &schemas.ResponsesMessageContent{}
if msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr != nil {
content.ContentStr = msg.ResponsesToolMessage.Output.ResponsesToolCallOutputStr
} else if msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks != nil {
content.ContentBlocks = msg.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks
}
}
// Convert content - only if Content is not nil
if content != nil {
if content.ContentStr != nil {
toolMsg.Content = NewStringContent(*content.ContentStr)
} else if content.ContentBlocks != nil {
contentBlocks := convertResponsesMessageContentBlocksToCohere(content.ContentBlocks)
toolMsg.Content = NewBlocksContent(contentBlocks)
}
}
toolMsg.ToolCallID = msg.ResponsesToolMessage.CallID
return &toolMsg
}
return nil
}
// convertSingleCohereMessageToBifrostMessages converts a single Cohere message to Bifrost messages
func convertSingleCohereMessageToBifrostMessages(cohereMsg *CohereMessage, isOutputMessage bool) []schemas.ResponsesMessage {
var outputMessages []schemas.ResponsesMessage
var reasoningContentBlocks []schemas.ResponsesMessageContentBlock
// Handle text content first
if cohereMsg.Content != nil {
var content schemas.ResponsesMessageContent
var contentBlocks []schemas.ResponsesMessageContentBlock
if cohereMsg.Content.StringContent != nil {
// Determine content block type based on message role and output flag
blockType := schemas.ResponsesInputMessageContentBlockTypeText
if isOutputMessage || cohereMsg.Role == "assistant" {
blockType = schemas.ResponsesOutputMessageContentTypeText
}
contentBlocks = append(contentBlocks, schemas.ResponsesMessageContentBlock{
Type: blockType,
Text: cohereMsg.Content.StringContent,
})
} else if cohereMsg.Content.BlocksContent != nil {
// Convert content blocks and separate reasoning blocks
for _, block := range cohereMsg.Content.BlocksContent {
if block.Type == CohereContentBlockTypeThinking {
// Collect reasoning blocks to create a single reasoning message
reasoningContentBlocks = append(reasoningContentBlocks, schemas.ResponsesMessageContentBlock{
Type: schemas.ResponsesOutputMessageContentTypeReasoning,
Text: block.Thinking,
})
} else {
converted := convertCohereContentBlockToBifrost(block)
if converted.Type != "" {
contentBlocks = append(contentBlocks, converted)
}
}
}
}
content.ContentBlocks = contentBlocks
// Create message output if we have content blocks
if len(contentBlocks) > 0 {
var role schemas.ResponsesMessageRoleType
switch cohereMsg.Role {
case "user":
role = schemas.ResponsesInputMessageRoleUser
case "assistant":
role = schemas.ResponsesInputMessageRoleAssistant
case "system":
role = schemas.ResponsesInputMessageRoleSystem
default:
role = schemas.ResponsesInputMessageRoleUser
}
outputMsg := schemas.ResponsesMessage{
Role: &role,
Content: &content,
Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage),
}
if isOutputMessage {
outputMsg.ID = schemas.Ptr("msg_" + fmt.Sprintf("%d", time.Now().UnixNano()))
outputMsg.Status = schemas.Ptr("completed")
}
outputMessages = append(outputMessages, outputMsg)
}
}
// 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...)
}
// Handle tool calls
if cohereMsg.ToolCalls != nil {
for _, toolCall := range cohereMsg.ToolCalls {
// Check if Function is nil to avoid nil pointer dereference
if toolCall.Function == nil {
// Skip this tool call if Function is nil
continue
}
// Safely extract function name and arguments
var functionName *string
var functionArguments *string
if toolCall.Function.Name != nil {
functionName = toolCall.Function.Name
} else {
// Use empty string if Name is nil
functionName = schemas.Ptr("")
}
// Arguments is a string, not a pointer, so it's safe to access directly
functionArguments = schemas.Ptr(toolCall.Function.Arguments)
toolCallMsg := schemas.ResponsesMessage{
ID: toolCall.ID,
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCall),
Status: schemas.Ptr("completed"),
ResponsesToolMessage: &schemas.ResponsesToolMessage{
Name: functionName,
CallID: toolCall.ID,
Arguments: functionArguments,
},
}
if isOutputMessage {
role := schemas.ResponsesInputMessageRoleAssistant
toolCallMsg.Role = &role
}
outputMessages = append(outputMessages, toolCallMsg)
}
}
return outputMessages
}
// convertBifrostContentBlocksToCohere converts Bifrost content blocks to Cohere format
func convertResponsesMessageContentBlocksToCohere(blocks []schemas.ResponsesMessageContentBlock) []CohereContentBlock {
var cohereBlocks []CohereContentBlock
for _, block := range blocks {
switch block.Type {
case schemas.ResponsesInputMessageContentBlockTypeText, schemas.ResponsesOutputMessageContentTypeText:
// Handle both input_text (user messages) and output_text (assistant messages)
if block.Text != nil {
cohereBlocks = append(cohereBlocks, CohereContentBlock{
Type: CohereContentBlockTypeText,
Text: block.Text,
})
}
case schemas.ResponsesInputMessageContentBlockTypeImage:
if block.ResponsesInputMessageContentBlockImage != nil && block.ResponsesInputMessageContentBlockImage.ImageURL != nil && *block.ResponsesInputMessageContentBlockImage.ImageURL != "" {
cohereBlocks = append(cohereBlocks, CohereContentBlock{
Type: CohereContentBlockTypeImage,
ImageURL: &CohereImageURL{
URL: *block.ResponsesInputMessageContentBlockImage.ImageURL,
},
})
}
case schemas.ResponsesOutputMessageContentTypeReasoning:
if block.Text != nil {
cohereBlocks = append(cohereBlocks, CohereContentBlock{
Type: CohereContentBlockTypeThinking,
Thinking: block.Text,
})
}
case schemas.ResponsesOutputMessageContentTypeCompaction:
// Convert compaction to text block for Cohere (compaction is Anthropic-specific)
if block.ResponsesOutputMessageContentCompaction != nil {
if summary := strings.TrimSpace(block.ResponsesOutputMessageContentCompaction.Summary); summary != "" {
cohereBlocks = append(cohereBlocks, CohereContentBlock{
Type: CohereContentBlockTypeText,
Text: schemas.Ptr(summary),
})
}
}
}
}
return cohereBlocks
}