1727 lines
62 KiB
Go
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
|
|
}
|