2379 lines
76 KiB
Go
2379 lines
76 KiB
Go
package schemas
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// =============================================================================
|
|
// BIDIRECTIONAL CONVERSION METHODS
|
|
// =============================================================================
|
|
//
|
|
// This section contains methods for converting between Chat Completions API
|
|
// and Responses API formats. These methods are attached to the structs themselves
|
|
// for easy conversion in both directions.
|
|
//
|
|
// Key Features:
|
|
// 1. Bidirectional: Convert to and from both formats
|
|
// 2. Data preservation: All relevant data is preserved during conversion
|
|
// 3. Aggregation/Spreading: Handle tool messages properly for each format
|
|
// 4. Validation: Ensure data integrity during conversion
|
|
//
|
|
// =============================================================================
|
|
|
|
// =============================================================================
|
|
// TOOL CONVERSION METHODS
|
|
// =============================================================================
|
|
|
|
// ToResponsesTool converts a ChatTool to ResponsesTool format
|
|
func (ct *ChatTool) ToResponsesTool() *ResponsesTool {
|
|
if ct == nil {
|
|
return &ResponsesTool{}
|
|
}
|
|
|
|
rt := &ResponsesTool{
|
|
Type: ResponsesToolType(ct.Type),
|
|
}
|
|
|
|
// Convert function tools
|
|
if ct.Type == ChatToolTypeFunction && ct.Function != nil {
|
|
rt.Name = &ct.Function.Name
|
|
rt.Description = ct.Function.Description
|
|
|
|
// Create ResponsesToolFunction if needed
|
|
if ct.Function.Parameters != nil || ct.Function.Strict != nil {
|
|
rt.ResponsesToolFunction = &ResponsesToolFunction{
|
|
Parameters: ct.Function.Parameters,
|
|
Strict: ct.Function.Strict,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert custom tools
|
|
if ct.Type == ChatToolTypeCustom && ct.Custom != nil {
|
|
if ct.Custom.Format != nil {
|
|
rt.ResponsesToolCustom = &ResponsesToolCustom{
|
|
Format: &ResponsesToolCustomFormat{
|
|
Type: ct.Custom.Format.Type,
|
|
},
|
|
}
|
|
if ct.Custom.Format.Grammar != nil {
|
|
rt.ResponsesToolCustom.Format.Definition = &ct.Custom.Format.Grammar.Definition
|
|
rt.ResponsesToolCustom.Format.Syntax = &ct.Custom.Format.Grammar.Syntax
|
|
}
|
|
}
|
|
}
|
|
|
|
return rt
|
|
}
|
|
|
|
// ToChatTool converts a ResponsesTool to ChatTool format
|
|
func (rt *ResponsesTool) ToChatTool() *ChatTool {
|
|
if rt == nil {
|
|
return &ChatTool{}
|
|
}
|
|
|
|
ct := &ChatTool{
|
|
Type: ChatToolType(rt.Type),
|
|
}
|
|
|
|
// Convert function tools
|
|
if rt.Type == "function" {
|
|
ct.Function = &ChatToolFunction{}
|
|
|
|
if rt.Name != nil {
|
|
ct.Function.Name = *rt.Name
|
|
}
|
|
if rt.Description != nil {
|
|
ct.Function.Description = rt.Description
|
|
}
|
|
if rt.ResponsesToolFunction != nil {
|
|
ct.Function.Parameters = rt.ResponsesToolFunction.Parameters
|
|
ct.Function.Strict = rt.ResponsesToolFunction.Strict
|
|
}
|
|
}
|
|
|
|
// Convert custom tools
|
|
if rt.Type == "custom" && rt.ResponsesToolCustom != nil {
|
|
ct.Custom = &ChatToolCustom{}
|
|
if rt.ResponsesToolCustom.Format != nil {
|
|
ct.Custom.Format = &ChatToolCustomFormat{
|
|
Type: rt.ResponsesToolCustom.Format.Type,
|
|
}
|
|
if rt.ResponsesToolCustom.Format.Definition != nil && rt.ResponsesToolCustom.Format.Syntax != nil {
|
|
ct.Custom.Format.Grammar = &ChatToolCustomGrammarFormat{
|
|
Definition: *rt.ResponsesToolCustom.Format.Definition,
|
|
Syntax: *rt.ResponsesToolCustom.Format.Syntax,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ct
|
|
}
|
|
|
|
// ToChatAssistantMessageToolCall converts a ResponsesToolMessage to ChatAssistantMessageToolCall format.
|
|
// This is useful for executing Responses API tool calls using the Chat API tool executor.
|
|
//
|
|
// Returns:
|
|
// - *ChatAssistantMessageToolCall: The converted tool call in Chat API format
|
|
//
|
|
// Example:
|
|
//
|
|
// responsesToolMsg := &ResponsesToolMessage{
|
|
// CallID: Ptr("call-123"),
|
|
// Name: Ptr("calculate"),
|
|
// Arguments: Ptr("{\"x\": 10, \"y\": 20}"),
|
|
// }
|
|
// chatToolCall := responsesToolMsg.ToChatAssistantMessageToolCall()
|
|
func (rtm *ResponsesToolMessage) ToChatAssistantMessageToolCall() *ChatAssistantMessageToolCall {
|
|
if rtm == nil {
|
|
return nil
|
|
}
|
|
|
|
toolCall := &ChatAssistantMessageToolCall{
|
|
ID: rtm.CallID,
|
|
Type: Ptr("function"),
|
|
Function: ChatAssistantMessageToolCallFunction{
|
|
Name: rtm.Name,
|
|
Arguments: "{}", // Default to empty JSON object for valid JSON unmarshaling
|
|
},
|
|
}
|
|
|
|
// Extract arguments string
|
|
if rtm.Arguments != nil {
|
|
toolCall.Function.Arguments = *rtm.Arguments
|
|
}
|
|
|
|
return toolCall
|
|
}
|
|
|
|
// ToResponsesToolMessage converts a ChatToolMessage (tool execution result) to ResponsesToolMessage format.
|
|
// This creates a function_call_output message suitable for the Responses API.
|
|
//
|
|
// Returns:
|
|
// - *ResponsesMessage: A ResponsesMessage with type=function_call_output containing the tool result
|
|
//
|
|
// Example:
|
|
//
|
|
// chatToolMsg := &ChatMessage{
|
|
// Role: ChatMessageRoleTool,
|
|
// ChatToolMessage: &ChatToolMessage{
|
|
// ToolCallID: Ptr("call-123"),
|
|
// },
|
|
// Content: &ChatMessageContent{
|
|
// ContentStr: Ptr("Result: 30"),
|
|
// },
|
|
// }
|
|
// responsesMsg := chatToolMsg.ToResponsesToolMessage()
|
|
func (cm *ChatMessage) ToResponsesToolMessage() *ResponsesMessage {
|
|
if cm == nil || cm.ChatToolMessage == nil {
|
|
return nil
|
|
}
|
|
|
|
msgType := ResponsesMessageTypeFunctionCallOutput
|
|
|
|
respMsg := &ResponsesMessage{
|
|
Type: &msgType,
|
|
ResponsesToolMessage: &ResponsesToolMessage{
|
|
CallID: cm.ChatToolMessage.ToolCallID,
|
|
},
|
|
}
|
|
|
|
// Extract output from content
|
|
if cm.Content != nil {
|
|
if cm.Content.ContentStr != nil {
|
|
output := *cm.Content.ContentStr
|
|
respMsg.ResponsesToolMessage.Output = &ResponsesToolMessageOutputStruct{
|
|
ResponsesToolCallOutputStr: &output,
|
|
}
|
|
} else if len(cm.Content.ContentBlocks) > 0 {
|
|
// For structured content blocks, convert to ResponsesMessageContentBlock
|
|
respBlocks := make([]ResponsesMessageContentBlock, len(cm.Content.ContentBlocks))
|
|
for i, block := range cm.Content.ContentBlocks {
|
|
respBlocks[i] = ResponsesMessageContentBlock{
|
|
Type: ResponsesMessageContentBlockType(block.Type),
|
|
Text: block.Text,
|
|
CacheControl: block.CacheControl,
|
|
}
|
|
|
|
// Map image
|
|
if block.ImageURLStruct != nil {
|
|
respBlocks[i].ResponsesInputMessageContentBlockImage = &ResponsesInputMessageContentBlockImage{
|
|
ImageURL: &block.ImageURLStruct.URL,
|
|
Detail: block.ImageURLStruct.Detail,
|
|
}
|
|
}
|
|
|
|
// Map file
|
|
if block.File != nil {
|
|
respBlocks[i].FileID = block.File.FileID
|
|
respBlocks[i].ResponsesInputMessageContentBlockFile = &ResponsesInputMessageContentBlockFile{
|
|
FileData: block.File.FileData,
|
|
Filename: block.File.Filename,
|
|
FileType: block.File.FileType,
|
|
}
|
|
}
|
|
|
|
// Map audio
|
|
if block.InputAudio != nil {
|
|
format := ""
|
|
if block.InputAudio.Format != nil {
|
|
format = *block.InputAudio.Format
|
|
}
|
|
respBlocks[i].Audio = &ResponsesInputMessageContentBlockAudio{
|
|
Data: block.InputAudio.Data,
|
|
Format: format,
|
|
}
|
|
}
|
|
}
|
|
respMsg.ResponsesToolMessage.Output = &ResponsesToolMessageOutputStruct{
|
|
ResponsesFunctionToolCallOutputBlocks: respBlocks,
|
|
}
|
|
}
|
|
}
|
|
|
|
return respMsg
|
|
}
|
|
|
|
// =============================================================================
|
|
// TOOL CHOICE CONVERSION METHODS
|
|
// =============================================================================
|
|
|
|
// ToResponsesToolChoice converts a ChatToolChoice to ResponsesToolChoice format
|
|
func (ctc *ChatToolChoice) ToResponsesToolChoice() *ResponsesToolChoice {
|
|
if ctc == nil {
|
|
return &ResponsesToolChoice{}
|
|
}
|
|
|
|
rtc := &ResponsesToolChoice{}
|
|
|
|
// Handle string choice (e.g., "none", "auto", "required")
|
|
if ctc.ChatToolChoiceStr != nil {
|
|
rtc.ResponsesToolChoiceStr = ctc.ChatToolChoiceStr
|
|
return rtc
|
|
}
|
|
|
|
// Handle structured choice
|
|
if ctc.ChatToolChoiceStruct != nil {
|
|
rtc.ResponsesToolChoiceStruct = &ResponsesToolChoiceStruct{
|
|
Type: ResponsesToolChoiceType(ctc.ChatToolChoiceStruct.Type),
|
|
}
|
|
|
|
switch ctc.ChatToolChoiceStruct.Type {
|
|
case ChatToolChoiceTypeNone, ChatToolChoiceTypeAny, ChatToolChoiceTypeRequired:
|
|
// These map to mode field
|
|
modeStr := string(ctc.ChatToolChoiceStruct.Type)
|
|
rtc.ResponsesToolChoiceStruct.Mode = &modeStr
|
|
|
|
case ChatToolChoiceTypeFunction:
|
|
// Map function choice
|
|
if ctc.ChatToolChoiceStruct.Function != nil && ctc.ChatToolChoiceStruct.Function.Name != "" {
|
|
rtc.ResponsesToolChoiceStruct.Name = &ctc.ChatToolChoiceStruct.Function.Name
|
|
}
|
|
|
|
case ChatToolChoiceTypeAllowedTools:
|
|
// Map allowed tools
|
|
if ctc.ChatToolChoiceStruct.AllowedTools != nil && len(ctc.ChatToolChoiceStruct.AllowedTools.Tools) > 0 {
|
|
tools := make([]ResponsesToolChoiceAllowedToolDef, len(ctc.ChatToolChoiceStruct.AllowedTools.Tools))
|
|
for i, tool := range ctc.ChatToolChoiceStruct.AllowedTools.Tools {
|
|
tools[i] = ResponsesToolChoiceAllowedToolDef{
|
|
Type: tool.Type,
|
|
}
|
|
if tool.Function.Name != "" {
|
|
name := tool.Function.Name
|
|
tools[i].Name = &name
|
|
}
|
|
}
|
|
rtc.ResponsesToolChoiceStruct.Tools = tools
|
|
}
|
|
// Copy the mode (e.g., "auto", "required")
|
|
if ctc.ChatToolChoiceStruct.AllowedTools != nil && ctc.ChatToolChoiceStruct.AllowedTools.Mode != "" {
|
|
mode := ctc.ChatToolChoiceStruct.AllowedTools.Mode
|
|
rtc.ResponsesToolChoiceStruct.Mode = &mode
|
|
}
|
|
|
|
case ChatToolChoiceTypeCustom:
|
|
// Map custom choice
|
|
if ctc.ChatToolChoiceStruct.Custom != nil && ctc.ChatToolChoiceStruct.Custom.Name != "" {
|
|
rtc.ResponsesToolChoiceStruct.Name = &ctc.ChatToolChoiceStruct.Custom.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
return rtc
|
|
}
|
|
|
|
// ToChatToolChoice converts a ResponsesToolChoice to ChatToolChoice format
|
|
func (tc *ResponsesToolChoice) ToChatToolChoice() *ChatToolChoice {
|
|
if tc == nil {
|
|
return &ChatToolChoice{}
|
|
}
|
|
|
|
ctc := &ChatToolChoice{}
|
|
|
|
// Handle string choice
|
|
if tc.ResponsesToolChoiceStr != nil {
|
|
ctc.ChatToolChoiceStr = tc.ResponsesToolChoiceStr
|
|
return ctc
|
|
}
|
|
|
|
// Handle structured choice
|
|
if tc.ResponsesToolChoiceStruct != nil {
|
|
ctc.ChatToolChoiceStruct = &ChatToolChoiceStruct{
|
|
Type: ChatToolChoiceType(tc.ResponsesToolChoiceStruct.Type),
|
|
}
|
|
|
|
// Handle mode-based choices (none, auto, required)
|
|
if tc.ResponsesToolChoiceStruct.Mode != nil {
|
|
switch *tc.ResponsesToolChoiceStruct.Mode {
|
|
case "none":
|
|
ctc.ChatToolChoiceStruct.Type = ChatToolChoiceTypeNone
|
|
case "auto":
|
|
ctc.ChatToolChoiceStruct.Type = ChatToolChoiceTypeAny
|
|
case "required":
|
|
ctc.ChatToolChoiceStruct.Type = ChatToolChoiceTypeRequired
|
|
}
|
|
}
|
|
|
|
// Handle function choice
|
|
if tc.ResponsesToolChoiceStruct.Type == ResponsesToolChoiceTypeFunction && tc.ResponsesToolChoiceStruct.Name != nil {
|
|
ctc.ChatToolChoiceStruct.Function = &ChatToolChoiceFunction{
|
|
Name: *tc.ResponsesToolChoiceStruct.Name,
|
|
}
|
|
}
|
|
|
|
// Handle custom choice
|
|
if tc.ResponsesToolChoiceStruct.Type == ResponsesToolChoiceTypeCustom && tc.ResponsesToolChoiceStruct.Name != nil {
|
|
ctc.ChatToolChoiceStruct.Custom = &ChatToolChoiceCustom{
|
|
Name: *tc.ResponsesToolChoiceStruct.Name,
|
|
}
|
|
}
|
|
|
|
// Handle allowed tools
|
|
if len(tc.ResponsesToolChoiceStruct.Tools) > 0 {
|
|
ctc.ChatToolChoiceStruct.Type = ChatToolChoiceTypeAllowedTools
|
|
tools := make([]ChatToolChoiceAllowedToolsTool, len(tc.ResponsesToolChoiceStruct.Tools))
|
|
for i, tool := range tc.ResponsesToolChoiceStruct.Tools {
|
|
tools[i] = ChatToolChoiceAllowedToolsTool{
|
|
Type: tool.Type,
|
|
}
|
|
if tool.Name != nil {
|
|
tools[i].Function = ChatToolChoiceFunction{Name: *tool.Name}
|
|
}
|
|
}
|
|
// Copy the mode if present, otherwise default to "auto"
|
|
mode := "auto"
|
|
if tc.ResponsesToolChoiceStruct.Mode != nil && *tc.ResponsesToolChoiceStruct.Mode != "" {
|
|
mode = *tc.ResponsesToolChoiceStruct.Mode
|
|
}
|
|
ctc.ChatToolChoiceStruct.AllowedTools = &ChatToolChoiceAllowedTools{
|
|
Mode: mode,
|
|
Tools: tools,
|
|
}
|
|
}
|
|
|
|
return ctc
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// MESSAGE CONVERSION METHODS
|
|
// =============================================================================
|
|
|
|
// ToResponsesMessages converts a ChatMessage to one or more ResponsesMessages
|
|
// This handles the expansion of assistant messages with tool calls into separate function_call messages
|
|
func (cm *ChatMessage) ToResponsesMessages() []ResponsesMessage {
|
|
if cm == nil {
|
|
return []ResponsesMessage{}
|
|
}
|
|
|
|
var messages []ResponsesMessage
|
|
|
|
// Check if this is an assistant message with multiple tool calls that need expansion
|
|
if cm.ChatAssistantMessage != nil && cm.ChatAssistantMessage.ToolCalls != nil && len(cm.ChatAssistantMessage.ToolCalls) > 0 {
|
|
// Expand multiple tool calls into separate function_call items
|
|
for _, tc := range cm.ChatAssistantMessage.ToolCalls {
|
|
messageType := ResponsesMessageTypeFunctionCall
|
|
|
|
var callID *string
|
|
if tc.ID != nil && *tc.ID != "" {
|
|
callID = tc.ID
|
|
}
|
|
|
|
var namePtr *string
|
|
if tc.Function.Name != nil && *tc.Function.Name != "" {
|
|
namePtr = tc.Function.Name
|
|
}
|
|
|
|
// Create a copy of the arguments string to avoid range loop variable capture
|
|
var argumentsPtr *string
|
|
if tc.Function.Arguments != "" {
|
|
argumentsPtr = Ptr(tc.Function.Arguments)
|
|
}
|
|
|
|
rm := ResponsesMessage{
|
|
ID: Ptr("fc_" + GetRandomString(50)),
|
|
Type: &messageType,
|
|
Role: Ptr(ResponsesInputMessageRoleAssistant),
|
|
Status: Ptr("completed"),
|
|
ResponsesToolMessage: &ResponsesToolMessage{
|
|
CallID: callID,
|
|
Name: namePtr,
|
|
Arguments: argumentsPtr,
|
|
},
|
|
}
|
|
|
|
messages = append(messages, rm)
|
|
}
|
|
return messages
|
|
}
|
|
|
|
// Regular message conversion
|
|
messageType := ResponsesMessageTypeMessage
|
|
role := ResponsesInputMessageRoleUser
|
|
|
|
// Determine message type and role
|
|
switch cm.Role {
|
|
case ChatMessageRoleAssistant:
|
|
role = ResponsesInputMessageRoleAssistant
|
|
// Check for refusal
|
|
if cm.ChatAssistantMessage != nil && cm.ChatAssistantMessage.Refusal != nil {
|
|
messageType = ResponsesMessageTypeRefusal
|
|
}
|
|
case ChatMessageRoleUser:
|
|
role = ResponsesInputMessageRoleUser
|
|
case ChatMessageRoleSystem:
|
|
role = ResponsesInputMessageRoleSystem
|
|
case ChatMessageRoleTool:
|
|
messageType = ResponsesMessageTypeFunctionCallOutput
|
|
role = "" // tool call output messages don't include a role field
|
|
case ChatMessageRoleDeveloper:
|
|
role = ResponsesInputMessageRoleDeveloper
|
|
}
|
|
|
|
rm := ResponsesMessage{
|
|
Type: &messageType,
|
|
}
|
|
|
|
if role != "" {
|
|
rm.Role = &role
|
|
}
|
|
|
|
if role == ResponsesInputMessageRoleAssistant && messageType != ResponsesMessageTypeFunctionCallOutput {
|
|
rm.ID = Ptr("msg_" + GetRandomString(50))
|
|
}
|
|
|
|
// Handle refusal content specifically - use content blocks with ResponsesOutputMessageContentRefusal
|
|
if messageType == ResponsesMessageTypeRefusal && cm.ChatAssistantMessage != nil && cm.ChatAssistantMessage.Refusal != nil {
|
|
refusalBlock := ResponsesMessageContentBlock{
|
|
Type: ResponsesOutputMessageContentTypeRefusal,
|
|
ResponsesOutputMessageContentRefusal: &ResponsesOutputMessageContentRefusal{
|
|
Refusal: *cm.ChatAssistantMessage.Refusal,
|
|
},
|
|
}
|
|
rm.Content = &ResponsesMessageContent{
|
|
ContentBlocks: []ResponsesMessageContentBlock{refusalBlock},
|
|
}
|
|
} else if cm.Content != nil && cm.Content.ContentStr != nil {
|
|
// Convert regular string content (if input message then ContentStr, else ContentBlocks)
|
|
// Skip setting content for function_call_output - content should only be in output field
|
|
if messageType == ResponsesMessageTypeFunctionCallOutput {
|
|
// Don't set content for function_call_output - it will be set in ResponsesToolMessage.Output
|
|
} else if cm.Role == ChatMessageRoleAssistant {
|
|
rm.Content = &ResponsesMessageContent{
|
|
ContentBlocks: []ResponsesMessageContentBlock{
|
|
{
|
|
Type: ResponsesOutputMessageContentTypeText,
|
|
Text: cm.Content.ContentStr,
|
|
ResponsesOutputMessageContentText: &ResponsesOutputMessageContentText{
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
Annotations: []ResponsesOutputMessageContentTextAnnotation{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rm.Status = Ptr("completed")
|
|
} else {
|
|
rm.Content = &ResponsesMessageContent{
|
|
ContentStr: cm.Content.ContentStr,
|
|
}
|
|
}
|
|
} else if cm.Content != nil && cm.Content.ContentBlocks != nil {
|
|
// Convert content blocks
|
|
// Skip setting content blocks for function_call_output
|
|
if messageType == ResponsesMessageTypeFunctionCallOutput {
|
|
// Don't set content for function_call_output - it will be set in ResponsesToolMessage.Output
|
|
} else {
|
|
responseBlocks := make([]ResponsesMessageContentBlock, len(cm.Content.ContentBlocks))
|
|
for i, block := range cm.Content.ContentBlocks {
|
|
blockType := ResponsesMessageContentBlockType(block.Type)
|
|
|
|
switch block.Type {
|
|
case ChatContentBlockTypeText:
|
|
if cm.Role == ChatMessageRoleAssistant {
|
|
blockType = ResponsesOutputMessageContentTypeText
|
|
} else {
|
|
blockType = ResponsesInputMessageContentBlockTypeText
|
|
}
|
|
case ChatContentBlockTypeImage:
|
|
blockType = ResponsesInputMessageContentBlockTypeImage
|
|
case ChatContentBlockTypeFile:
|
|
blockType = ResponsesInputMessageContentBlockTypeFile
|
|
case ChatContentBlockTypeInputAudio:
|
|
blockType = ResponsesInputMessageContentBlockTypeAudio
|
|
}
|
|
|
|
responseBlocks[i] = ResponsesMessageContentBlock{
|
|
Type: blockType,
|
|
Text: block.Text,
|
|
}
|
|
|
|
// Convert specific block types
|
|
if block.ImageURLStruct != nil {
|
|
responseBlocks[i].ResponsesInputMessageContentBlockImage = &ResponsesInputMessageContentBlockImage{
|
|
ImageURL: &block.ImageURLStruct.URL,
|
|
Detail: block.ImageURLStruct.Detail,
|
|
}
|
|
}
|
|
if block.File != nil {
|
|
responseBlocks[i].ResponsesInputMessageContentBlockFile = &ResponsesInputMessageContentBlockFile{
|
|
FileData: block.File.FileData,
|
|
Filename: block.File.Filename,
|
|
}
|
|
responseBlocks[i].FileID = block.File.FileID
|
|
}
|
|
if block.InputAudio != nil {
|
|
format := ""
|
|
if block.InputAudio.Format != nil {
|
|
format = *block.InputAudio.Format
|
|
}
|
|
responseBlocks[i].Audio = &ResponsesInputMessageContentBlockAudio{
|
|
Data: block.InputAudio.Data,
|
|
Format: format,
|
|
}
|
|
}
|
|
}
|
|
rm.Content = &ResponsesMessageContent{
|
|
ContentBlocks: responseBlocks,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle tool messages
|
|
if cm.ChatToolMessage != nil {
|
|
rm.ResponsesToolMessage = &ResponsesToolMessage{}
|
|
if cm.ChatToolMessage.ToolCallID != nil {
|
|
rm.ResponsesToolMessage.CallID = cm.ChatToolMessage.ToolCallID
|
|
}
|
|
|
|
// If tool output content exists, add it to function_call_output
|
|
// For function_call_output, get content from cm.Content since rm.Content is not set
|
|
if messageType == ResponsesMessageTypeFunctionCallOutput && cm.Content != nil {
|
|
// Prefer ContentStr if present
|
|
if cm.Content.ContentStr != nil && *cm.Content.ContentStr != "" {
|
|
rm.ResponsesToolMessage.Output = &ResponsesToolMessageOutputStruct{
|
|
ResponsesToolCallOutputStr: cm.Content.ContentStr,
|
|
}
|
|
} else if len(cm.Content.ContentBlocks) > 0 {
|
|
// For structured content blocks, convert to ResponsesMessageContentBlock
|
|
respBlocks := make([]ResponsesMessageContentBlock, len(cm.Content.ContentBlocks))
|
|
for i, block := range cm.Content.ContentBlocks {
|
|
respBlocks[i] = ResponsesMessageContentBlock{
|
|
Type: ResponsesMessageContentBlockType(block.Type),
|
|
Text: block.Text,
|
|
CacheControl: block.CacheControl,
|
|
}
|
|
|
|
// Map image
|
|
if block.ImageURLStruct != nil {
|
|
respBlocks[i].ResponsesInputMessageContentBlockImage = &ResponsesInputMessageContentBlockImage{
|
|
ImageURL: &block.ImageURLStruct.URL,
|
|
Detail: block.ImageURLStruct.Detail,
|
|
}
|
|
}
|
|
|
|
// Map file
|
|
if block.File != nil {
|
|
respBlocks[i].FileID = block.File.FileID
|
|
respBlocks[i].ResponsesInputMessageContentBlockFile = &ResponsesInputMessageContentBlockFile{
|
|
FileData: block.File.FileData,
|
|
Filename: block.File.Filename,
|
|
FileType: block.File.FileType,
|
|
}
|
|
}
|
|
|
|
// Map audio
|
|
if block.InputAudio != nil {
|
|
format := ""
|
|
if block.InputAudio.Format != nil {
|
|
format = *block.InputAudio.Format
|
|
}
|
|
respBlocks[i].Audio = &ResponsesInputMessageContentBlockAudio{
|
|
Data: block.InputAudio.Data,
|
|
Format: format,
|
|
}
|
|
}
|
|
}
|
|
rm.ResponsesToolMessage.Output = &ResponsesToolMessageOutputStruct{
|
|
ResponsesFunctionToolCallOutputBlocks: respBlocks,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
messages = append(messages, rm)
|
|
return messages
|
|
}
|
|
|
|
// ToChatMessages converts a slice of ResponsesMessages back to ChatMessages
|
|
// This handles the aggregation of function_call messages back into assistant messages with tool calls
|
|
func ToChatMessages(rms []ResponsesMessage) []ChatMessage {
|
|
if len(rms) == 0 {
|
|
return []ChatMessage{}
|
|
}
|
|
|
|
var chatMessages []ChatMessage
|
|
var currentToolCalls []ChatAssistantMessageToolCall
|
|
|
|
for _, rm := range rms {
|
|
if rm.Type != nil && *rm.Type == ResponsesMessageTypeReasoning {
|
|
continue
|
|
}
|
|
|
|
// Handle function_call messages - collect them for aggregation
|
|
if rm.Type != nil && *rm.Type == ResponsesMessageTypeFunctionCall {
|
|
if rm.ResponsesToolMessage != nil {
|
|
tc := ChatAssistantMessageToolCall{
|
|
Type: Ptr("function"),
|
|
}
|
|
|
|
if rm.ResponsesToolMessage.CallID != nil {
|
|
tc.ID = rm.ResponsesToolMessage.CallID
|
|
}
|
|
|
|
tc.Function = ChatAssistantMessageToolCallFunction{}
|
|
if rm.ResponsesToolMessage.Name != nil {
|
|
tc.Function.Name = rm.ResponsesToolMessage.Name
|
|
}
|
|
if rm.ResponsesToolMessage.Arguments != nil {
|
|
tc.Function.Arguments = *rm.ResponsesToolMessage.Arguments
|
|
}
|
|
|
|
currentToolCalls = append(currentToolCalls, tc)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// If we have collected tool calls, create an assistant message with them
|
|
if len(currentToolCalls) > 0 {
|
|
// Create a copy of the slice to avoid shared slice header issues
|
|
toolCallsCopy := append([]ChatAssistantMessageToolCall(nil), currentToolCalls...)
|
|
chatMessages = append(chatMessages, ChatMessage{
|
|
Role: ChatMessageRoleAssistant,
|
|
ChatAssistantMessage: &ChatAssistantMessage{
|
|
ToolCalls: toolCallsCopy,
|
|
},
|
|
})
|
|
currentToolCalls = nil // Reset for next batch
|
|
}
|
|
|
|
// Convert regular message
|
|
cm := ChatMessage{}
|
|
|
|
// Set role
|
|
if rm.Role != nil {
|
|
switch *rm.Role {
|
|
case ResponsesInputMessageRoleAssistant:
|
|
cm.Role = ChatMessageRoleAssistant
|
|
case ResponsesInputMessageRoleUser:
|
|
cm.Role = ChatMessageRoleUser
|
|
case ResponsesInputMessageRoleSystem:
|
|
cm.Role = ChatMessageRoleSystem
|
|
case ResponsesInputMessageRoleDeveloper:
|
|
cm.Role = ChatMessageRoleDeveloper
|
|
}
|
|
}
|
|
|
|
// Handle special message types
|
|
if rm.Type != nil {
|
|
switch *rm.Type {
|
|
case ResponsesMessageTypeFunctionCallOutput:
|
|
cm.Role = ChatMessageRoleTool
|
|
if rm.ResponsesToolMessage != nil && rm.ResponsesToolMessage.CallID != nil {
|
|
cm.ChatToolMessage = &ChatToolMessage{
|
|
ToolCallID: rm.ResponsesToolMessage.CallID,
|
|
}
|
|
|
|
// Extract content from ResponsesFunctionToolCallOutput if present
|
|
// This is needed because OpenAI Responses API uses an "output" field
|
|
// which is stored in ResponsesFunctionToolCallOutput
|
|
if rm.ResponsesToolMessage.Output != nil {
|
|
if rm.Content == nil {
|
|
rm.Content = &ResponsesMessageContent{}
|
|
}
|
|
// If Content is not already set, extract from ResponsesFunctionToolCallOutput
|
|
if rm.Content.ContentStr == nil && rm.Content.ContentBlocks == nil {
|
|
if rm.ResponsesToolMessage.Output.ResponsesToolCallOutputStr != nil {
|
|
rm.Content.ContentStr = rm.ResponsesToolMessage.Output.ResponsesToolCallOutputStr
|
|
} else if rm.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks != nil {
|
|
rm.Content.ContentBlocks = rm.ResponsesToolMessage.Output.ResponsesFunctionToolCallOutputBlocks
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case ResponsesMessageTypeRefusal:
|
|
cm.ChatAssistantMessage = &ChatAssistantMessage{}
|
|
// Extract refusal from content blocks or ContentStr
|
|
if rm.Content != nil {
|
|
if rm.Content.ContentBlocks != nil {
|
|
// Look for refusal content block
|
|
for _, block := range rm.Content.ContentBlocks {
|
|
if block.Type == ResponsesOutputMessageContentTypeRefusal && block.ResponsesOutputMessageContentRefusal != nil {
|
|
refusalText := block.ResponsesOutputMessageContentRefusal.Refusal
|
|
cm.ChatAssistantMessage.Refusal = &refusalText
|
|
break
|
|
}
|
|
}
|
|
} else if rm.Content.ContentStr != nil {
|
|
// Fallback to ContentStr for backward compatibility
|
|
cm.ChatAssistantMessage.Refusal = rm.Content.ContentStr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert content (skip for refusal messages since refusal is already extracted)
|
|
if rm.Content != nil && (rm.Type == nil || *rm.Type != ResponsesMessageTypeRefusal) {
|
|
if rm.Content.ContentStr != nil ||
|
|
(len(rm.Content.ContentBlocks) == 1 &&
|
|
(rm.Content.ContentBlocks[0].Type == ResponsesInputMessageContentBlockTypeText || rm.Content.ContentBlocks[0].Type == ResponsesOutputMessageContentTypeText)) {
|
|
if rm.Content.ContentStr != nil {
|
|
cm.Content = &ChatMessageContent{
|
|
ContentStr: rm.Content.ContentStr,
|
|
}
|
|
} else {
|
|
cm.Content = &ChatMessageContent{
|
|
ContentStr: rm.Content.ContentBlocks[0].Text,
|
|
}
|
|
}
|
|
} else if rm.Content.ContentBlocks != nil {
|
|
chatBlocks := make([]ChatContentBlock, len(rm.Content.ContentBlocks))
|
|
for i, block := range rm.Content.ContentBlocks {
|
|
// Map ResponsesMessageContentBlockType to ChatContentBlockType
|
|
var chatBlockType ChatContentBlockType
|
|
switch block.Type {
|
|
case ResponsesInputMessageContentBlockTypeText:
|
|
chatBlockType = ChatContentBlockTypeText // "input_text" -> "text"
|
|
case ResponsesInputMessageContentBlockTypeImage:
|
|
chatBlockType = ChatContentBlockTypeImage // "input_image" -> "image_url"
|
|
case ResponsesInputMessageContentBlockTypeFile:
|
|
chatBlockType = ChatContentBlockTypeFile // "input_file" -> "file"
|
|
case ResponsesInputMessageContentBlockTypeAudio:
|
|
chatBlockType = ChatContentBlockTypeInputAudio // "input_audio" -> "input_audio" (same)
|
|
default:
|
|
// For unknown types, fall back to direct conversion
|
|
chatBlockType = ChatContentBlockType(block.Type)
|
|
}
|
|
|
|
chatBlocks[i] = ChatContentBlock{
|
|
Type: chatBlockType,
|
|
Text: block.Text,
|
|
}
|
|
|
|
// Convert specific block types
|
|
if block.ResponsesInputMessageContentBlockImage != nil {
|
|
chatBlocks[i].ImageURLStruct = &ChatInputImage{
|
|
Detail: block.ResponsesInputMessageContentBlockImage.Detail,
|
|
}
|
|
if block.ResponsesInputMessageContentBlockImage.ImageURL != nil {
|
|
chatBlocks[i].ImageURLStruct.URL = *block.ResponsesInputMessageContentBlockImage.ImageURL
|
|
}
|
|
}
|
|
if block.ResponsesInputMessageContentBlockFile != nil {
|
|
chatBlocks[i].File = &ChatInputFile{
|
|
FileData: block.ResponsesInputMessageContentBlockFile.FileData,
|
|
Filename: block.ResponsesInputMessageContentBlockFile.Filename,
|
|
FileID: block.FileID,
|
|
}
|
|
}
|
|
if block.Audio != nil {
|
|
chatBlocks[i].InputAudio = &ChatInputAudio{
|
|
Data: block.Audio.Data,
|
|
}
|
|
if block.Audio.Format != "" {
|
|
chatBlocks[i].InputAudio.Format = &block.Audio.Format
|
|
}
|
|
}
|
|
}
|
|
cm.Content = &ChatMessageContent{
|
|
ContentBlocks: chatBlocks,
|
|
}
|
|
}
|
|
}
|
|
|
|
chatMessages = append(chatMessages, cm)
|
|
}
|
|
|
|
// Handle any remaining tool calls at the end
|
|
if len(currentToolCalls) > 0 {
|
|
// Create a copy of the slice to avoid shared slice header issues
|
|
toolCallsCopy := append([]ChatAssistantMessageToolCall(nil), currentToolCalls...)
|
|
chatMessages = append(chatMessages, ChatMessage{
|
|
Role: ChatMessageRoleAssistant,
|
|
ChatAssistantMessage: &ChatAssistantMessage{
|
|
ToolCalls: toolCallsCopy,
|
|
},
|
|
})
|
|
}
|
|
|
|
return chatMessages
|
|
}
|
|
|
|
func (cu *BifrostLLMUsage) ToResponsesResponseUsage() *ResponsesResponseUsage {
|
|
if cu == nil {
|
|
return nil
|
|
}
|
|
|
|
usage := &ResponsesResponseUsage{
|
|
InputTokens: cu.PromptTokens,
|
|
OutputTokens: cu.CompletionTokens,
|
|
TotalTokens: cu.TotalTokens,
|
|
Cost: cu.Cost,
|
|
}
|
|
|
|
if cu.PromptTokensDetails != nil {
|
|
usage.InputTokensDetails = &ResponsesResponseInputTokens{
|
|
TextTokens: cu.PromptTokensDetails.TextTokens,
|
|
AudioTokens: cu.PromptTokensDetails.AudioTokens,
|
|
ImageTokens: cu.PromptTokensDetails.ImageTokens,
|
|
CachedReadTokens: cu.PromptTokensDetails.CachedReadTokens,
|
|
CachedWriteTokens: cu.PromptTokensDetails.CachedWriteTokens,
|
|
}
|
|
}
|
|
if cu.CompletionTokensDetails != nil {
|
|
usage.OutputTokensDetails = &ResponsesResponseOutputTokens{
|
|
TextTokens: cu.CompletionTokensDetails.TextTokens,
|
|
AcceptedPredictionTokens: cu.CompletionTokensDetails.AcceptedPredictionTokens,
|
|
AudioTokens: cu.CompletionTokensDetails.AudioTokens,
|
|
ReasoningTokens: cu.CompletionTokensDetails.ReasoningTokens,
|
|
RejectedPredictionTokens: cu.CompletionTokensDetails.RejectedPredictionTokens,
|
|
CitationTokens: cu.CompletionTokensDetails.CitationTokens,
|
|
NumSearchQueries: cu.CompletionTokensDetails.NumSearchQueries,
|
|
}
|
|
}
|
|
|
|
return usage
|
|
}
|
|
|
|
func (ru *ResponsesResponseUsage) ToBifrostLLMUsage() *BifrostLLMUsage {
|
|
if ru == nil {
|
|
return nil
|
|
}
|
|
|
|
usage := &BifrostLLMUsage{
|
|
PromptTokens: ru.InputTokens,
|
|
CompletionTokens: ru.OutputTokens,
|
|
TotalTokens: ru.TotalTokens,
|
|
Cost: ru.Cost,
|
|
}
|
|
|
|
if ru.InputTokensDetails != nil {
|
|
usage.PromptTokensDetails = &ChatPromptTokensDetails{
|
|
TextTokens: ru.InputTokensDetails.TextTokens,
|
|
AudioTokens: ru.InputTokensDetails.AudioTokens,
|
|
ImageTokens: ru.InputTokensDetails.ImageTokens,
|
|
CachedReadTokens: ru.InputTokensDetails.CachedReadTokens,
|
|
CachedWriteTokens: ru.InputTokensDetails.CachedWriteTokens,
|
|
}
|
|
}
|
|
if ru.OutputTokensDetails != nil {
|
|
usage.CompletionTokensDetails = &ChatCompletionTokensDetails{
|
|
TextTokens: ru.OutputTokensDetails.TextTokens,
|
|
AcceptedPredictionTokens: ru.OutputTokensDetails.AcceptedPredictionTokens,
|
|
AudioTokens: ru.OutputTokensDetails.AudioTokens,
|
|
ImageTokens: ru.OutputTokensDetails.ImageTokens,
|
|
ReasoningTokens: ru.OutputTokensDetails.ReasoningTokens,
|
|
RejectedPredictionTokens: ru.OutputTokensDetails.RejectedPredictionTokens,
|
|
CitationTokens: ru.OutputTokensDetails.CitationTokens,
|
|
NumSearchQueries: ru.OutputTokensDetails.NumSearchQueries,
|
|
}
|
|
}
|
|
|
|
return usage
|
|
}
|
|
|
|
// =============================================================================
|
|
// REQUEST CONVERSION METHODS
|
|
// =============================================================================
|
|
|
|
// ToResponsesRequest converts a BifrostChatRequest to BifrostResponsesRequest format
|
|
func (cr *BifrostChatRequest) ToResponsesRequest() *BifrostResponsesRequest {
|
|
if cr == nil {
|
|
return &BifrostResponsesRequest{}
|
|
}
|
|
|
|
brr := &BifrostResponsesRequest{
|
|
Provider: cr.Provider,
|
|
Model: cr.Model,
|
|
Fallbacks: cr.Fallbacks, // Copy fallbacks as-is
|
|
}
|
|
|
|
// Convert Input messages using existing ChatMessage.ToResponsesMessages()
|
|
var allResponsesMessages []ResponsesMessage
|
|
for _, chatMsg := range cr.Input {
|
|
responsesMessages := chatMsg.ToResponsesMessages()
|
|
allResponsesMessages = append(allResponsesMessages, responsesMessages...)
|
|
}
|
|
brr.Input = allResponsesMessages
|
|
|
|
// Convert Parameters
|
|
if cr.Params != nil {
|
|
brr.Params = &ResponsesParameters{
|
|
// Map common fields
|
|
ParallelToolCalls: cr.Params.ParallelToolCalls,
|
|
PromptCacheKey: cr.Params.PromptCacheKey,
|
|
SafetyIdentifier: cr.Params.SafetyIdentifier,
|
|
ServiceTier: cr.Params.ServiceTier,
|
|
Store: cr.Params.Store,
|
|
Temperature: cr.Params.Temperature,
|
|
TopLogProbs: cr.Params.TopLogProbs,
|
|
TopP: cr.Params.TopP,
|
|
ExtraParams: cr.Params.ExtraParams,
|
|
|
|
// Map specific fields
|
|
MaxOutputTokens: cr.Params.MaxCompletionTokens, // max_completion_tokens -> max_output_tokens
|
|
Metadata: cr.Params.Metadata,
|
|
}
|
|
|
|
// Convert StreamOptions
|
|
if cr.Params.StreamOptions != nil {
|
|
brr.Params.StreamOptions = &ResponsesStreamOptions{
|
|
IncludeObfuscation: cr.Params.StreamOptions.IncludeObfuscation,
|
|
}
|
|
}
|
|
|
|
// Convert Tools using existing ChatTool.ToResponsesTool()
|
|
if len(cr.Params.Tools) > 0 {
|
|
responsesTools := make([]ResponsesTool, 0, len(cr.Params.Tools))
|
|
for _, chatTool := range cr.Params.Tools {
|
|
responsesTool := chatTool.ToResponsesTool()
|
|
responsesTools = append(responsesTools, *responsesTool)
|
|
}
|
|
brr.Params.Tools = responsesTools
|
|
}
|
|
|
|
// Convert ToolChoice using existing ChatToolChoice.ToResponsesToolChoice()
|
|
if cr.Params.ToolChoice != nil {
|
|
responsesToolChoice := cr.Params.ToolChoice.ToResponsesToolChoice()
|
|
brr.Params.ToolChoice = responsesToolChoice
|
|
}
|
|
|
|
// Handle Reasoning from reasoning_effort
|
|
if cr.Params.Reasoning != nil && (cr.Params.Reasoning.Enabled != nil || cr.Params.Reasoning.Effort != nil || cr.Params.Reasoning.MaxTokens != nil) {
|
|
brr.Params.Reasoning = &ResponsesParametersReasoning{
|
|
Effort: cr.Params.Reasoning.Effort,
|
|
MaxTokens: cr.Params.Reasoning.MaxTokens,
|
|
}
|
|
}
|
|
|
|
// Handle Verbosity
|
|
if cr.Params.Verbosity != nil {
|
|
if brr.Params.Text == nil {
|
|
brr.Params.Text = &ResponsesTextConfig{}
|
|
}
|
|
brr.Params.Text.Verbosity = cr.Params.Verbosity
|
|
}
|
|
}
|
|
|
|
brr.RawRequestBody = cr.RawRequestBody
|
|
|
|
return brr
|
|
}
|
|
|
|
// ToChatRequest converts a BifrostResponsesRequest to BifrostChatRequest format
|
|
func (brr *BifrostResponsesRequest) ToChatRequest() *BifrostChatRequest {
|
|
if brr == nil {
|
|
return &BifrostChatRequest{}
|
|
}
|
|
|
|
bcr := &BifrostChatRequest{
|
|
Provider: brr.Provider,
|
|
Model: brr.Model,
|
|
Fallbacks: brr.Fallbacks, // Copy fallbacks as-is
|
|
}
|
|
|
|
// Convert Input messages using existing ToChatMessages()
|
|
bcr.Input = ToChatMessages(brr.Input)
|
|
normalizeDeveloperRoleForChatFallback(bcr.Input)
|
|
|
|
// Convert Parameters
|
|
if brr.Params != nil {
|
|
bcr.Params = &ChatParameters{
|
|
// Map common fields
|
|
ParallelToolCalls: brr.Params.ParallelToolCalls,
|
|
PromptCacheKey: brr.Params.PromptCacheKey,
|
|
SafetyIdentifier: brr.Params.SafetyIdentifier,
|
|
ServiceTier: brr.Params.ServiceTier,
|
|
Store: brr.Params.Store,
|
|
Temperature: brr.Params.Temperature,
|
|
TopLogProbs: brr.Params.TopLogProbs,
|
|
TopP: brr.Params.TopP,
|
|
ExtraParams: brr.Params.ExtraParams,
|
|
|
|
// Map specific fields
|
|
MaxCompletionTokens: brr.Params.MaxOutputTokens, // max_output_tokens -> max_completion_tokens
|
|
Metadata: brr.Params.Metadata,
|
|
}
|
|
|
|
// Convert StreamOptions
|
|
if brr.Params.StreamOptions != nil {
|
|
bcr.Params.StreamOptions = &ChatStreamOptions{
|
|
IncludeObfuscation: brr.Params.StreamOptions.IncludeObfuscation,
|
|
IncludeUsage: Ptr(true), // Default for Chat API
|
|
}
|
|
}
|
|
|
|
// Responses -> Chat fallback only supports function tools in a valid chat shape.
|
|
bcr.Params.Tools = sanitizeResponsesToolsForChatFallback(brr.Params.Tools)
|
|
|
|
// Convert ToolChoice using existing ResponsesToolChoice.ToChatToolChoice()
|
|
if brr.Params.ToolChoice != nil {
|
|
chatToolChoice := brr.Params.ToolChoice.ToChatToolChoice()
|
|
bcr.Params.ToolChoice = sanitizeChatToolChoiceForFallback(chatToolChoice, bcr.Params.Tools)
|
|
}
|
|
|
|
// Handle Reasoning from Reasoning
|
|
if brr.Params.Reasoning != nil {
|
|
bcr.Params.Reasoning = &ChatReasoning{
|
|
Effort: brr.Params.Reasoning.Effort,
|
|
MaxTokens: brr.Params.Reasoning.MaxTokens,
|
|
}
|
|
}
|
|
|
|
// Handle Verbosity from Text config
|
|
if brr.Params.Text != nil && brr.Params.Text.Verbosity != nil {
|
|
bcr.Params.Verbosity = brr.Params.Text.Verbosity
|
|
}
|
|
}
|
|
|
|
bcr.RawRequestBody = brr.RawRequestBody
|
|
|
|
return bcr
|
|
}
|
|
|
|
func sanitizeResponsesToolsForChatFallback(tools []ResponsesTool) []ChatTool {
|
|
if len(tools) == 0 {
|
|
return nil
|
|
}
|
|
|
|
chatTools := make([]ChatTool, 0, len(tools))
|
|
for _, responsesTool := range tools {
|
|
if responsesTool.Type != ResponsesToolTypeFunction {
|
|
continue
|
|
}
|
|
if responsesTool.Name == nil || strings.TrimSpace(*responsesTool.Name) == "" {
|
|
continue
|
|
}
|
|
|
|
chatTool := responsesTool.ToChatTool()
|
|
if chatTool == nil || chatTool.Function == nil || strings.TrimSpace(chatTool.Function.Name) == "" {
|
|
continue
|
|
}
|
|
|
|
chatTool.Type = ChatToolTypeFunction
|
|
chatTool.Custom = nil
|
|
chatTools = append(chatTools, *chatTool)
|
|
}
|
|
|
|
if len(chatTools) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return chatTools
|
|
}
|
|
|
|
func normalizeDeveloperRoleForChatFallback(messages []ChatMessage) {
|
|
for i := range messages {
|
|
if messages[i].Role == ChatMessageRoleDeveloper {
|
|
messages[i].Role = ChatMessageRoleSystem
|
|
}
|
|
}
|
|
}
|
|
|
|
func sanitizeChatToolChoiceForFallback(toolChoice *ChatToolChoice, tools []ChatTool) *ChatToolChoice {
|
|
if toolChoice == nil {
|
|
return nil
|
|
}
|
|
if len(tools) == 0 {
|
|
return nil
|
|
}
|
|
|
|
validToolNames := make(map[string]struct{}, len(tools))
|
|
for _, tool := range tools {
|
|
if tool.Function != nil && strings.TrimSpace(tool.Function.Name) != "" {
|
|
validToolNames[tool.Function.Name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
if toolChoice.ChatToolChoiceStruct != nil {
|
|
switch toolChoice.ChatToolChoiceStruct.Type {
|
|
case ChatToolChoiceTypeFunction:
|
|
if toolChoice.ChatToolChoiceStruct.Function == nil {
|
|
return nil
|
|
}
|
|
if _, ok := validToolNames[toolChoice.ChatToolChoiceStruct.Function.Name]; !ok {
|
|
return nil
|
|
}
|
|
case ChatToolChoiceTypeAllowedTools, ChatToolChoiceTypeCustom:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return toolChoice
|
|
}
|
|
|
|
// =============================================================================
|
|
// RESPONSE CONVERSION METHODS
|
|
// =============================================================================
|
|
|
|
func responsesStatusFromChatFinishReason(finishReason string) (status string, incompleteDetails *ResponsesResponseIncompleteDetails, mapped bool) {
|
|
switch finishReason {
|
|
case string(BifrostFinishReasonLength):
|
|
return "incomplete", &ResponsesResponseIncompleteDetails{Reason: "max_output_tokens"}, true
|
|
case string(BifrostFinishReasonStop), string(BifrostFinishReasonToolCalls):
|
|
return "completed", nil, true
|
|
default:
|
|
return "", nil, false
|
|
}
|
|
}
|
|
|
|
func responsesTerminalFromChatFinishReason(finishReason *string) (eventType ResponsesStreamResponseType, status string, incompleteDetails *ResponsesResponseIncompleteDetails) {
|
|
// Unknown/empty finish reasons preserve prior behavior: treat as completed.
|
|
eventType = ResponsesStreamResponseTypeCompleted
|
|
status = "completed"
|
|
|
|
if finishReason == nil || *finishReason == "" {
|
|
return eventType, status, nil
|
|
}
|
|
|
|
mappedStatus, mappedIncompleteDetails, mapped := responsesStatusFromChatFinishReason(*finishReason)
|
|
if !mapped {
|
|
return eventType, status, nil
|
|
}
|
|
|
|
if mappedStatus == "incomplete" {
|
|
eventType = ResponsesStreamResponseTypeIncomplete
|
|
}
|
|
|
|
return eventType, mappedStatus, mappedIncompleteDetails
|
|
}
|
|
|
|
// ToBifrostResponsesResponse converts the BifrostChatResponse to BifrostResponsesResponse format
|
|
// This converts Chat-style fields (Choices) to Responses API format
|
|
func (cr *BifrostChatResponse) ToBifrostResponsesResponse() *BifrostResponsesResponse {
|
|
if cr == nil {
|
|
return nil
|
|
}
|
|
|
|
// Create new BifrostResponsesResponse from Chat fields
|
|
responsesResp := &BifrostResponsesResponse{
|
|
ID: Ptr(cr.ID),
|
|
Object: "response",
|
|
CreatedAt: cr.Created,
|
|
Model: cr.Model,
|
|
Citations: cr.Citations,
|
|
SearchResults: cr.SearchResults,
|
|
Videos: cr.Videos,
|
|
}
|
|
|
|
// Convert Choices to Output messages
|
|
var outputMessages []ResponsesMessage
|
|
for _, choice := range cr.Choices {
|
|
if choice.ChatNonStreamResponseChoice != nil && choice.ChatNonStreamResponseChoice.Message != nil {
|
|
// Convert ChatMessage to ResponsesMessages
|
|
responsesMessages := choice.ChatNonStreamResponseChoice.Message.ToResponsesMessages()
|
|
outputMessages = append(outputMessages, responsesMessages...)
|
|
}
|
|
}
|
|
|
|
if len(outputMessages) > 0 {
|
|
responsesResp.Output = outputMessages
|
|
}
|
|
|
|
// Convert Usage if needed
|
|
if cr.Usage != nil {
|
|
responsesResp.Usage = cr.Usage.ToResponsesResponseUsage()
|
|
}
|
|
|
|
// Map finish reason to Responses status.
|
|
hasCompletedFinishReason := false
|
|
for _, choice := range cr.Choices {
|
|
if choice.FinishReason == nil || *choice.FinishReason == "" {
|
|
continue
|
|
}
|
|
status, incompleteDetails, mapped := responsesStatusFromChatFinishReason(*choice.FinishReason)
|
|
if !mapped {
|
|
continue
|
|
}
|
|
if status == "incomplete" {
|
|
responsesResp.Status = Ptr(status)
|
|
responsesResp.IncompleteDetails = incompleteDetails
|
|
hasCompletedFinishReason = false
|
|
break
|
|
}
|
|
hasCompletedFinishReason = true
|
|
}
|
|
if responsesResp.Status == nil && hasCompletedFinishReason {
|
|
responsesResp.Status = Ptr("completed")
|
|
}
|
|
|
|
// Copy other relevant fields
|
|
responsesResp.ExtraFields = cr.ExtraFields
|
|
responsesResp.ExtraFields.RequestType = ResponsesRequest
|
|
|
|
return responsesResp
|
|
}
|
|
|
|
// ToBifrostChatResponse converts a BifrostResponsesResponse to BifrostChatResponse format
|
|
// This converts Responses API format to Chat-style fields (Choices)
|
|
func (responsesResp *BifrostResponsesResponse) ToBifrostChatResponse() *BifrostChatResponse {
|
|
if responsesResp == nil {
|
|
return nil
|
|
}
|
|
|
|
// Create new BifrostChatResponse from Responses fields
|
|
chatResp := &BifrostChatResponse{
|
|
Created: responsesResp.CreatedAt,
|
|
Object: "chat.completion",
|
|
Model: responsesResp.Model,
|
|
Citations: responsesResp.Citations,
|
|
SearchResults: responsesResp.SearchResults,
|
|
Videos: responsesResp.Videos,
|
|
}
|
|
|
|
if responsesResp.ID != nil {
|
|
chatResp.ID = *responsesResp.ID
|
|
}
|
|
|
|
// Create Choices from ResponsesResponse
|
|
if len(responsesResp.Output) > 0 {
|
|
// Convert ResponsesMessages back to ChatMessages
|
|
chatMessages := ToChatMessages(responsesResp.Output)
|
|
|
|
// Create choices from chat messages
|
|
choices := make([]BifrostResponseChoice, 0, len(chatMessages))
|
|
for i, chatMsg := range chatMessages {
|
|
choice := BifrostResponseChoice{
|
|
Index: i,
|
|
ChatNonStreamResponseChoice: &ChatNonStreamResponseChoice{
|
|
Message: &chatMsg,
|
|
},
|
|
}
|
|
choices = append(choices, choice)
|
|
}
|
|
|
|
chatResp.Choices = choices
|
|
}
|
|
|
|
// Convert Usage if needed
|
|
if responsesResp.Usage != nil {
|
|
// Map Responses usage to Chat usage
|
|
chatResp.Usage = responsesResp.Usage.ToBifrostLLMUsage()
|
|
}
|
|
|
|
// Copy other relevant fields
|
|
chatResp.ExtraFields = responsesResp.ExtraFields
|
|
chatResp.ExtraFields.RequestType = ChatCompletionRequest
|
|
chatResp.ExtraFields.Provider = responsesResp.ExtraFields.Provider
|
|
|
|
return chatResp
|
|
}
|
|
|
|
// ChatToResponsesStreamState tracks state during Chat-to-Responses streaming conversion
|
|
type ChatToResponsesStreamState struct {
|
|
ToolArgumentBuffers map[string]string // Maps tool call ID to accumulated argument JSON
|
|
ItemIDs map[string]string // Maps tool call ID to item ID
|
|
ToolCallNames map[string]string // Maps tool call ID to tool name
|
|
ToolCallIndexToID map[uint16]string // Maps tool call index to tool call ID (for lookups when ID is missing)
|
|
MessageID *string // Message ID from first chunk
|
|
Model *string // Model name
|
|
CreatedAt int // Timestamp for created_at consistency
|
|
HasEmittedCreated bool // Whether we've emitted response.created
|
|
HasEmittedInProgress bool // Whether we've emitted response.in_progress
|
|
TextItemAdded bool // Whether text item has been added
|
|
TextItemClosed bool // Whether text item has been closed
|
|
TextItemHasContent bool // Whether text item has received any content deltas
|
|
TextBuffer strings.Builder // Accumulated text deltas for output_text.done/content_part.done
|
|
CurrentOutputIndex int // Current output index counter
|
|
ToolCallOutputIndices map[string]int // Maps tool call ID to output index
|
|
SequenceNumber int // Monotonic sequence number across all chunks
|
|
}
|
|
|
|
// chatToResponsesStreamStatePool provides a pool for ChatToResponsesStreamState objects.
|
|
var chatToResponsesStreamStatePool = sync.Pool{
|
|
New: func() interface{} {
|
|
return &ChatToResponsesStreamState{
|
|
ToolArgumentBuffers: make(map[string]string),
|
|
ItemIDs: make(map[string]string),
|
|
ToolCallNames: make(map[string]string),
|
|
ToolCallIndexToID: make(map[uint16]string),
|
|
CreatedAt: int(time.Now().Unix()),
|
|
CurrentOutputIndex: 0,
|
|
ToolCallOutputIndices: make(map[string]int),
|
|
SequenceNumber: 0,
|
|
HasEmittedCreated: false,
|
|
HasEmittedInProgress: false,
|
|
TextItemAdded: false,
|
|
TextItemClosed: false,
|
|
TextItemHasContent: false,
|
|
TextBuffer: strings.Builder{},
|
|
}
|
|
},
|
|
}
|
|
|
|
// AcquireChatToResponsesStreamState gets a ChatToResponsesStreamState from the pool.
|
|
func AcquireChatToResponsesStreamState() *ChatToResponsesStreamState {
|
|
state := chatToResponsesStreamStatePool.Get().(*ChatToResponsesStreamState)
|
|
// Clear maps (they're already initialized from New or previous flush)
|
|
// Only initialize if nil (shouldn't happen, but defensive)
|
|
if state.ToolArgumentBuffers == nil {
|
|
state.ToolArgumentBuffers = make(map[string]string)
|
|
} else {
|
|
clear(state.ToolArgumentBuffers)
|
|
}
|
|
if state.ItemIDs == nil {
|
|
state.ItemIDs = make(map[string]string)
|
|
} else {
|
|
clear(state.ItemIDs)
|
|
}
|
|
if state.ToolCallNames == nil {
|
|
state.ToolCallNames = make(map[string]string)
|
|
} else {
|
|
clear(state.ToolCallNames)
|
|
}
|
|
if state.ToolCallIndexToID == nil {
|
|
state.ToolCallIndexToID = make(map[uint16]string)
|
|
} else {
|
|
clear(state.ToolCallIndexToID)
|
|
}
|
|
if state.ToolCallOutputIndices == nil {
|
|
state.ToolCallOutputIndices = make(map[string]int)
|
|
} else {
|
|
clear(state.ToolCallOutputIndices)
|
|
}
|
|
// 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.TextItemAdded = false
|
|
state.TextItemClosed = false
|
|
state.TextItemHasContent = false
|
|
state.TextBuffer = strings.Builder{}
|
|
state.SequenceNumber = 0
|
|
return state
|
|
}
|
|
|
|
// ReleaseChatToResponsesStreamState returns a ChatToResponsesStreamState to the pool.
|
|
func ReleaseChatToResponsesStreamState(state *ChatToResponsesStreamState) {
|
|
if state != nil {
|
|
// Clear maps before returning to pool
|
|
if state.ToolArgumentBuffers != nil {
|
|
clear(state.ToolArgumentBuffers)
|
|
}
|
|
if state.ItemIDs != nil {
|
|
clear(state.ItemIDs)
|
|
}
|
|
if state.ToolCallNames != nil {
|
|
clear(state.ToolCallNames)
|
|
}
|
|
if state.ToolCallIndexToID != nil {
|
|
clear(state.ToolCallIndexToID)
|
|
}
|
|
if state.ToolCallOutputIndices != nil {
|
|
clear(state.ToolCallOutputIndices)
|
|
}
|
|
// 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.TextItemAdded = false
|
|
state.TextItemClosed = false
|
|
state.TextItemHasContent = false
|
|
state.TextBuffer = strings.Builder{}
|
|
state.SequenceNumber = 0
|
|
chatToResponsesStreamStatePool.Put(state)
|
|
}
|
|
}
|
|
|
|
// ToBifrostResponsesStreamResponse converts the BifrostChatResponse from Chat streaming format to Responses streaming format
|
|
// This converts Chat stream chunks (Choices with Deltas) to BifrostResponsesStreamResponse format
|
|
// Returns a slice of responses to support cases where a single event produces multiple responses
|
|
func (cr *BifrostChatResponse) ToBifrostResponsesStreamResponse(state *ChatToResponsesStreamState) []*BifrostResponsesStreamResponse {
|
|
if cr == nil || state == nil {
|
|
return nil
|
|
}
|
|
|
|
// If no choices to convert, return early
|
|
if len(cr.Choices) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Convert first streaming choice to BifrostResponsesStreamResponse
|
|
// Note: Chat API typically has one choice per chunk in streaming
|
|
choice := cr.Choices[0]
|
|
if choice.ChatStreamResponseChoice == nil || choice.ChatStreamResponseChoice.Delta == nil {
|
|
return nil
|
|
}
|
|
|
|
delta := choice.ChatStreamResponseChoice.Delta
|
|
var responses []*BifrostResponsesStreamResponse
|
|
|
|
// Store message ID and model from first chunk
|
|
if state.MessageID == nil && cr.ID != "" {
|
|
state.MessageID = &cr.ID
|
|
}
|
|
if state.Model == nil && cr.Model != "" {
|
|
state.Model = &cr.Model
|
|
}
|
|
|
|
// Emit lifecycle events on first chunk with role
|
|
if delta.Role != nil && !state.HasEmittedCreated {
|
|
// Emit response.created
|
|
response := &BifrostResponsesResponse{
|
|
ID: state.MessageID,
|
|
CreatedAt: state.CreatedAt,
|
|
}
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeCreated,
|
|
SequenceNumber: state.SequenceNumber,
|
|
Response: response,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
state.HasEmittedCreated = true
|
|
|
|
// Emit response.in_progress
|
|
response = &BifrostResponsesResponse{
|
|
ID: state.MessageID,
|
|
CreatedAt: state.CreatedAt,
|
|
}
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeInProgress,
|
|
SequenceNumber: state.SequenceNumber,
|
|
Response: response,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
state.HasEmittedInProgress = true
|
|
}
|
|
|
|
// Handle different types of streaming content
|
|
hasContent := delta.Content != nil && *delta.Content != ""
|
|
hasReasoning := delta.Reasoning != nil && *delta.Reasoning != ""
|
|
|
|
// Create output items if we have content OR reasoning (for reasoning-only models)
|
|
if hasContent || (hasReasoning && !state.TextItemAdded) {
|
|
// Text content delta (or reasoning-only response)
|
|
if !state.TextItemAdded {
|
|
// Add text item if not already added
|
|
outputIndex := 0
|
|
// 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["text"] = itemID
|
|
|
|
messageType := ResponsesMessageTypeMessage
|
|
role := ResponsesInputMessageRoleAssistant
|
|
|
|
item := &ResponsesMessage{
|
|
ID: &itemID,
|
|
Type: &messageType,
|
|
Role: &role,
|
|
Content: &ResponsesMessageContent{
|
|
ContentBlocks: []ResponsesMessageContentBlock{},
|
|
},
|
|
}
|
|
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeOutputItemAdded,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(0),
|
|
Item: item,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
state.TextItemAdded = true
|
|
|
|
// Emit content_part.added with empty output_text part
|
|
emptyText := ""
|
|
part := &ResponsesMessageContentBlock{
|
|
Type: ResponsesOutputMessageContentTypeText,
|
|
Text: &emptyText,
|
|
ResponsesOutputMessageContentText: &ResponsesOutputMessageContentText{
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
Annotations: []ResponsesOutputMessageContentTextAnnotation{},
|
|
},
|
|
}
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeContentPartAdded,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(0),
|
|
ItemID: &itemID,
|
|
Part: part,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
}
|
|
|
|
// Emit text delta - at least one is required for lifecycle validation
|
|
// Even for reasoning-only responses, we emit an empty delta on the first chunk
|
|
if hasContent || (!state.TextItemHasContent && (hasReasoning || hasContent)) {
|
|
itemID := state.ItemIDs["text"]
|
|
|
|
var contentDelta string
|
|
if hasContent {
|
|
contentDelta = *delta.Content
|
|
state.TextBuffer.WriteString(contentDelta)
|
|
} else {
|
|
// For reasoning-only responses, emit empty delta on first chunk
|
|
contentDelta = ""
|
|
}
|
|
|
|
response := &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeOutputTextDelta,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(0),
|
|
ContentIndex: Ptr(0),
|
|
Delta: &contentDelta,
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
ExtraFields: cr.ExtraFields,
|
|
}
|
|
if itemID != "" {
|
|
response.ItemID = &itemID
|
|
}
|
|
responses = append(responses, response)
|
|
state.SequenceNumber++
|
|
state.TextItemHasContent = true
|
|
}
|
|
}
|
|
|
|
if len(delta.ToolCalls) > 0 {
|
|
// Tool call delta - handle function call arguments
|
|
toolCall := delta.ToolCalls[0] // Take first tool call
|
|
contentIndex := 1 // Tool calls use content_index:1
|
|
|
|
// Determine tool call ID: use ID if present, otherwise look up by index
|
|
var toolCallID string
|
|
if toolCall.ID != nil && *toolCall.ID != "" {
|
|
toolCallID = *toolCall.ID
|
|
} else {
|
|
// Look up ID by index for subsequent chunks that don't include the ID
|
|
if id, exists := state.ToolCallIndexToID[toolCall.Index]; exists {
|
|
toolCallID = id
|
|
} else {
|
|
// No ID and no mapping found - skip this chunk
|
|
// This can happen if the stream is malformed or out of order
|
|
return responses
|
|
}
|
|
}
|
|
|
|
// Check if this is a new tool call (only when ID is present)
|
|
if toolCall.ID != nil && *toolCall.ID != "" {
|
|
if _, exists := state.ToolCallOutputIndices[toolCallID]; !exists {
|
|
// Close text item if still open and has content
|
|
if state.TextItemAdded && !state.TextItemClosed && state.TextItemHasContent {
|
|
outputIndex := 0
|
|
itemID := state.ItemIDs["text"]
|
|
|
|
finalText := state.TextBuffer.String()
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeOutputTextDone,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(0),
|
|
ItemID: &itemID,
|
|
Text: &finalText,
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
|
|
// Emit content_part.done
|
|
part := &ResponsesMessageContentBlock{
|
|
Type: ResponsesOutputMessageContentTypeText,
|
|
Text: &finalText,
|
|
ResponsesOutputMessageContentText: &ResponsesOutputMessageContentText{
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
Annotations: []ResponsesOutputMessageContentTextAnnotation{},
|
|
},
|
|
}
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeContentPartDone,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(0),
|
|
ItemID: &itemID,
|
|
Part: part,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
|
|
// Emit output_item.done
|
|
statusCompleted := "completed"
|
|
messageType := ResponsesMessageTypeMessage
|
|
role := ResponsesInputMessageRoleAssistant
|
|
textType := ResponsesOutputMessageContentTypeText
|
|
doneItem := &ResponsesMessage{
|
|
Type: &messageType,
|
|
Role: &role,
|
|
Status: &statusCompleted,
|
|
Content: &ResponsesMessageContent{
|
|
ContentBlocks: []ResponsesMessageContentBlock{
|
|
{
|
|
Type: textType,
|
|
Text: &finalText,
|
|
ResponsesOutputMessageContentText: &ResponsesOutputMessageContentText{
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
Annotations: []ResponsesOutputMessageContentTextAnnotation{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if itemID != "" {
|
|
doneItem.ID = &itemID
|
|
}
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeOutputItemDone,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(0),
|
|
Item: doneItem,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
state.TextItemClosed = true
|
|
}
|
|
|
|
// Assign new output index for tool call
|
|
outputIndex := state.CurrentOutputIndex
|
|
if outputIndex == 0 {
|
|
outputIndex = 1 // Skip 0 if text is using it
|
|
}
|
|
state.CurrentOutputIndex = outputIndex + 1
|
|
state.ToolCallOutputIndices[toolCallID] = outputIndex
|
|
|
|
// Store tool call info and index mapping
|
|
state.ItemIDs[toolCallID] = toolCallID
|
|
state.ToolCallIndexToID[toolCall.Index] = toolCallID
|
|
if toolCall.Function.Name != nil {
|
|
state.ToolCallNames[toolCallID] = *toolCall.Function.Name
|
|
}
|
|
|
|
// Initialize argument buffer
|
|
state.ToolArgumentBuffers[toolCallID] = ""
|
|
|
|
// Emit output_item.added for function call
|
|
statusInProgress := "in_progress"
|
|
item := &ResponsesMessage{
|
|
ID: &toolCallID,
|
|
Type: Ptr(ResponsesMessageTypeFunctionCall),
|
|
Status: &statusInProgress,
|
|
ResponsesToolMessage: &ResponsesToolMessage{
|
|
CallID: &toolCallID,
|
|
Name: toolCall.Function.Name,
|
|
Arguments: Ptr(""), // Arguments will be filled by deltas
|
|
},
|
|
}
|
|
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeOutputItemAdded,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(contentIndex),
|
|
Item: item,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
}
|
|
}
|
|
|
|
// Accumulate and emit function call arguments delta
|
|
// This works for both chunks with ID and chunks without ID (using looked-up ID)
|
|
if toolCall.Function.Arguments != "" {
|
|
outputIndex := state.ToolCallOutputIndices[toolCallID]
|
|
state.ToolArgumentBuffers[toolCallID] += toolCall.Function.Arguments
|
|
|
|
itemID := state.ItemIDs[toolCallID]
|
|
response := &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeFunctionCallArgumentsDelta,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(contentIndex),
|
|
Delta: &toolCall.Function.Arguments,
|
|
ExtraFields: cr.ExtraFields,
|
|
}
|
|
if itemID != "" {
|
|
response.ItemID = &itemID
|
|
}
|
|
responses = append(responses, response)
|
|
state.SequenceNumber++
|
|
}
|
|
}
|
|
|
|
if delta.Reasoning != nil && *delta.Reasoning != "" {
|
|
// Reasoning/thought content delta (for models that support reasoning)
|
|
response := &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeReasoningSummaryTextDelta,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(0),
|
|
Delta: delta.Reasoning,
|
|
ExtraFields: cr.ExtraFields,
|
|
}
|
|
responses = append(responses, response)
|
|
state.SequenceNumber++
|
|
}
|
|
|
|
if delta.Refusal != nil && *delta.Refusal != "" {
|
|
// Refusal delta
|
|
response := &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeRefusalDelta,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(0),
|
|
Refusal: delta.Refusal,
|
|
ExtraFields: cr.ExtraFields,
|
|
}
|
|
responses = append(responses, response)
|
|
state.SequenceNumber++
|
|
}
|
|
|
|
// Check if this is a completion chunk with finish_reason
|
|
if choice.FinishReason != nil {
|
|
terminalEventType, terminalStatus, terminalIncompleteDetails := responsesTerminalFromChatFinishReason(choice.FinishReason)
|
|
|
|
// Close text item if still open (regardless of whether it has content, to support reasoning-only responses)
|
|
if state.TextItemAdded && !state.TextItemClosed {
|
|
outputIndex := 0
|
|
itemID := state.ItemIDs["text"]
|
|
|
|
finalText := state.TextBuffer.String()
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeOutputTextDone,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(0),
|
|
ItemID: &itemID,
|
|
Text: &finalText,
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
|
|
// Emit content_part.done
|
|
part := &ResponsesMessageContentBlock{
|
|
Type: ResponsesOutputMessageContentTypeText,
|
|
Text: &finalText,
|
|
ResponsesOutputMessageContentText: &ResponsesOutputMessageContentText{
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
Annotations: []ResponsesOutputMessageContentTextAnnotation{},
|
|
},
|
|
}
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeContentPartDone,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(0),
|
|
ItemID: &itemID,
|
|
Part: part,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
|
|
// Emit output_item.done
|
|
statusFinal := terminalStatus
|
|
messageType := ResponsesMessageTypeMessage
|
|
role := ResponsesInputMessageRoleAssistant
|
|
textType := ResponsesOutputMessageContentTypeText
|
|
doneItem := &ResponsesMessage{
|
|
Type: &messageType,
|
|
Role: &role,
|
|
Status: &statusFinal,
|
|
Content: &ResponsesMessageContent{
|
|
ContentBlocks: []ResponsesMessageContentBlock{
|
|
{
|
|
Type: textType,
|
|
Text: &finalText,
|
|
ResponsesOutputMessageContentText: &ResponsesOutputMessageContentText{
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
Annotations: []ResponsesOutputMessageContentTextAnnotation{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if itemID != "" {
|
|
doneItem.ID = &itemID
|
|
}
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeOutputItemDone,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(0),
|
|
Item: doneItem,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
state.TextItemClosed = true
|
|
}
|
|
|
|
// Close any open tool call items and emit function_call_arguments.done
|
|
for toolCallID, args := range state.ToolArgumentBuffers {
|
|
if args != "" {
|
|
outputIndex := state.ToolCallOutputIndices[toolCallID]
|
|
itemID := state.ItemIDs[toolCallID]
|
|
contentIndex := 1 // Tool calls use content_index:1
|
|
argsCopy := args
|
|
// Emit function_call_arguments.done with full arguments (no item field, just item_id and arguments)
|
|
response := &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeFunctionCallArgumentsDone,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(contentIndex),
|
|
Arguments: &argsCopy,
|
|
ExtraFields: cr.ExtraFields,
|
|
}
|
|
if itemID != "" {
|
|
response.ItemID = &itemID
|
|
}
|
|
responses = append(responses, response)
|
|
state.SequenceNumber++
|
|
|
|
// Emit output_item.done for function call
|
|
statusFinal := terminalStatus
|
|
messageType := ResponsesMessageTypeFunctionCall
|
|
callName, hasName := state.ToolCallNames[toolCallID]
|
|
var callNamePtr *string
|
|
if hasName && callName != "" {
|
|
callNamePtr = &callName
|
|
}
|
|
argsValue := args
|
|
outputItemDone := &ResponsesMessage{
|
|
Type: &messageType,
|
|
Status: &statusFinal,
|
|
ResponsesToolMessage: &ResponsesToolMessage{
|
|
CallID: &toolCallID,
|
|
Name: callNamePtr,
|
|
Arguments: &argsValue,
|
|
},
|
|
}
|
|
if itemID != "" {
|
|
outputItemDone.ID = &itemID
|
|
}
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: ResponsesStreamResponseTypeOutputItemDone,
|
|
SequenceNumber: state.SequenceNumber,
|
|
OutputIndex: Ptr(outputIndex),
|
|
ContentIndex: Ptr(contentIndex),
|
|
Item: outputItemDone,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
}
|
|
}
|
|
|
|
// Emit terminal response event.
|
|
var usage *ResponsesResponseUsage
|
|
if cr.Usage != nil {
|
|
usage = cr.Usage.ToResponsesResponseUsage()
|
|
}
|
|
|
|
responseStatus := terminalStatus
|
|
|
|
response := &BifrostResponsesResponse{
|
|
ID: state.MessageID,
|
|
CreatedAt: state.CreatedAt,
|
|
Usage: usage,
|
|
Status: &responseStatus,
|
|
IncompleteDetails: terminalIncompleteDetails,
|
|
}
|
|
|
|
if state.Model != nil {
|
|
response.Model = *state.Model
|
|
}
|
|
var allOutput []ResponsesMessage
|
|
|
|
if state.TextItemAdded {
|
|
statusFinal := terminalStatus
|
|
messageType := ResponsesMessageTypeMessage
|
|
role := ResponsesInputMessageRoleAssistant
|
|
textType := ResponsesOutputMessageContentTypeText
|
|
finalText := state.TextBuffer.String()
|
|
itemID := state.ItemIDs["text"]
|
|
|
|
msg := ResponsesMessage{
|
|
Type: &messageType,
|
|
Role: &role,
|
|
Status: &statusFinal,
|
|
Content: &ResponsesMessageContent{
|
|
ContentBlocks: []ResponsesMessageContentBlock{
|
|
{
|
|
Type: textType,
|
|
Text: &finalText,
|
|
ResponsesOutputMessageContentText: &ResponsesOutputMessageContentText{
|
|
LogProbs: []ResponsesOutputMessageContentTextLogProb{},
|
|
Annotations: []ResponsesOutputMessageContentTextAnnotation{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if itemID != "" {
|
|
msg.ID = &itemID
|
|
}
|
|
allOutput = append(allOutput, msg)
|
|
}
|
|
|
|
// Collect tool call IDs sorted by outputIndex for deterministic order
|
|
type toolCallEntry struct {
|
|
toolCallID string
|
|
outputIndex int
|
|
}
|
|
var toolCallEntries []toolCallEntry
|
|
for toolCallID, outputIndex := range state.ToolCallOutputIndices {
|
|
toolCallEntries = append(toolCallEntries, toolCallEntry{toolCallID: toolCallID, outputIndex: outputIndex})
|
|
}
|
|
sort.Slice(toolCallEntries, func(i, j int) bool {
|
|
return toolCallEntries[i].outputIndex < toolCallEntries[j].outputIndex
|
|
})
|
|
|
|
for _, entry := range toolCallEntries {
|
|
toolCallID := entry.toolCallID
|
|
statusFinal := terminalStatus
|
|
messageType := ResponsesMessageTypeFunctionCall
|
|
callName, hasName := state.ToolCallNames[toolCallID]
|
|
var callNamePtr *string
|
|
if hasName && callName != "" {
|
|
callNamePtr = &callName
|
|
}
|
|
args := state.ToolArgumentBuffers[toolCallID]
|
|
fcMsg := ResponsesMessage{
|
|
Type: &messageType,
|
|
Status: &statusFinal,
|
|
ResponsesToolMessage: &ResponsesToolMessage{
|
|
CallID: &toolCallID,
|
|
Name: callNamePtr,
|
|
Arguments: &args,
|
|
},
|
|
}
|
|
itemID := state.ItemIDs[toolCallID]
|
|
if itemID != "" {
|
|
fcMsg.ID = &itemID
|
|
}
|
|
allOutput = append(allOutput, fcMsg)
|
|
}
|
|
|
|
if len(allOutput) > 0 {
|
|
response.Output = allOutput
|
|
}
|
|
|
|
responses = append(responses, &BifrostResponsesStreamResponse{
|
|
Type: terminalEventType,
|
|
SequenceNumber: state.SequenceNumber,
|
|
Response: response,
|
|
ExtraFields: cr.ExtraFields,
|
|
})
|
|
state.SequenceNumber++
|
|
}
|
|
|
|
// Set RequestType for all responses
|
|
for _, resp := range responses {
|
|
if resp != nil {
|
|
resp.ExtraFields.RequestType = ResponsesStreamRequest
|
|
// Copy other extra fields
|
|
resp.SearchResults = cr.SearchResults
|
|
resp.Videos = cr.Videos
|
|
resp.Citations = cr.Citations
|
|
}
|
|
}
|
|
|
|
return responses
|
|
}
|
|
|
|
// ToBifrostChatResponse converts a BifrostResponsesStreamResponse chunk to a BifrostChatResponse (chat.completion.chunk).
|
|
func (rsr *BifrostResponsesStreamResponse) ToBifrostChatResponse() *BifrostChatResponse {
|
|
if rsr == nil {
|
|
return nil
|
|
}
|
|
|
|
extraFields := rsr.ExtraFields
|
|
extraFields.RequestType = ChatCompletionStreamRequest
|
|
|
|
resp := &BifrostChatResponse{
|
|
Object: "chat.completion.chunk",
|
|
ExtraFields: extraFields,
|
|
SearchResults: rsr.SearchResults,
|
|
Videos: rsr.Videos,
|
|
Citations: rsr.Citations,
|
|
}
|
|
|
|
if rsr.Response != nil {
|
|
if rsr.Response.ID != nil {
|
|
resp.ID = *rsr.Response.ID
|
|
}
|
|
resp.Created = rsr.Response.CreatedAt
|
|
resp.Model = rsr.Response.Model
|
|
}
|
|
|
|
switch rsr.Type {
|
|
case ResponsesStreamResponseTypeOutputTextDelta:
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{
|
|
Content: rsr.Delta,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
|
|
case ResponsesStreamResponseTypeReasoningSummaryTextDelta:
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{
|
|
Reasoning: rsr.Delta,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
|
|
case ResponsesStreamResponseTypeRefusalDelta:
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{
|
|
Refusal: rsr.Refusal,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
|
|
case ResponsesStreamResponseTypeOutputItemAdded:
|
|
if rsr.Item == nil || rsr.Item.Type == nil {
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
}
|
|
|
|
switch *rsr.Item.Type {
|
|
case ResponsesMessageTypeFunctionCall:
|
|
if rsr.Item.ResponsesToolMessage == nil {
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
}
|
|
funcType := "function"
|
|
var idx uint16
|
|
if rsr.OutputIndex != nil && *rsr.OutputIndex > 0 {
|
|
idx = uint16(*rsr.OutputIndex - 1)
|
|
}
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{
|
|
ToolCalls: []ChatAssistantMessageToolCall{
|
|
{
|
|
Index: idx,
|
|
Type: &funcType,
|
|
ID: rsr.Item.ResponsesToolMessage.CallID,
|
|
Function: ChatAssistantMessageToolCallFunction{
|
|
Name: rsr.Item.ResponsesToolMessage.Name,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
|
|
case ResponsesMessageTypeMessage:
|
|
role := "assistant"
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{
|
|
Role: &role,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
|
|
default:
|
|
// reasoning, file_search_call, web_search_call, etc. — no chat equivalent,
|
|
// actual content arrives via separate delta events.
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
}
|
|
|
|
case ResponsesStreamResponseTypeFunctionCallArgumentsDelta:
|
|
if rsr.Delta == nil {
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
}
|
|
var idx uint16
|
|
if rsr.OutputIndex != nil && *rsr.OutputIndex > 0 {
|
|
idx = uint16(*rsr.OutputIndex - 1)
|
|
}
|
|
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{
|
|
ToolCalls: []ChatAssistantMessageToolCall{
|
|
{
|
|
Index: idx,
|
|
Function: ChatAssistantMessageToolCallFunction{
|
|
Arguments: *rsr.Delta,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
|
|
case ResponsesStreamResponseTypeCompleted, ResponsesStreamResponseTypeIncomplete:
|
|
finishReason := string(BifrostFinishReasonStop)
|
|
if rsr.Type == ResponsesStreamResponseTypeIncomplete {
|
|
finishReason = string(BifrostFinishReasonLength)
|
|
}
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
FinishReason: &finishReason,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{},
|
|
},
|
|
},
|
|
}
|
|
if rsr.Response != nil {
|
|
if rsr.Response.Usage != nil {
|
|
resp.Usage = rsr.Response.Usage.ToBifrostLLMUsage()
|
|
}
|
|
// Check for tool_calls finish reason
|
|
if rsr.Type == ResponsesStreamResponseTypeCompleted {
|
|
for _, output := range rsr.Response.Output {
|
|
if output.Type != nil && *output.Type == ResponsesMessageTypeFunctionCall {
|
|
finishReason = string(BifrostFinishReasonToolCalls)
|
|
resp.Choices[0].FinishReason = &finishReason
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return resp
|
|
|
|
default:
|
|
// Lifecycle events (created, in_progress, content_part.added/done, output_text.done,
|
|
// output_item.done, function_call_arguments.done, etc.) → empty chat chunk with no content.
|
|
resp.Choices = []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
ChatStreamResponseChoice: &ChatStreamResponseChoice{
|
|
Delta: &ChatStreamResponseChoiceDelta{},
|
|
},
|
|
},
|
|
}
|
|
return resp
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// RESPONSE CONVERSION METHODS
|
|
// =============================================================================
|
|
|
|
// ToBifrostTextCompletionResponse converts a BifrostChatResponse to a BifrostTextCompletionResponse
|
|
func (cr *BifrostChatResponse) ToBifrostTextCompletionResponse() *BifrostTextCompletionResponse {
|
|
if cr == nil {
|
|
return nil
|
|
}
|
|
|
|
if len(cr.Choices) == 0 {
|
|
return &BifrostTextCompletionResponse{
|
|
ID: cr.ID,
|
|
Model: cr.Model,
|
|
Object: "text_completion",
|
|
SystemFingerprint: cr.SystemFingerprint,
|
|
Usage: cr.Usage,
|
|
ExtraFields: BifrostResponseExtraFields{
|
|
RequestType: TextCompletionRequest,
|
|
ChunkIndex: cr.ExtraFields.ChunkIndex,
|
|
Provider: cr.ExtraFields.Provider,
|
|
OriginalModelRequested: cr.ExtraFields.OriginalModelRequested,
|
|
Latency: cr.ExtraFields.Latency,
|
|
RawResponse: cr.ExtraFields.RawResponse,
|
|
CacheDebug: cr.ExtraFields.CacheDebug,
|
|
ProviderResponseHeaders: cr.ExtraFields.ProviderResponseHeaders,
|
|
},
|
|
}
|
|
}
|
|
|
|
choice := cr.Choices[0]
|
|
|
|
// Handle streaming response choice
|
|
if choice.ChatStreamResponseChoice != nil && choice.ChatStreamResponseChoice.Delta != nil {
|
|
return &BifrostTextCompletionResponse{
|
|
ID: cr.ID,
|
|
Model: cr.Model,
|
|
Object: "text_completion",
|
|
SystemFingerprint: cr.SystemFingerprint,
|
|
Choices: []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
TextCompletionResponseChoice: &TextCompletionResponseChoice{
|
|
Text: choice.ChatStreamResponseChoice.Delta.Content,
|
|
},
|
|
FinishReason: choice.FinishReason,
|
|
LogProbs: choice.LogProbs,
|
|
},
|
|
},
|
|
Usage: cr.Usage,
|
|
ExtraFields: BifrostResponseExtraFields{
|
|
RequestType: TextCompletionRequest,
|
|
ChunkIndex: cr.ExtraFields.ChunkIndex,
|
|
Provider: cr.ExtraFields.Provider,
|
|
OriginalModelRequested: cr.ExtraFields.OriginalModelRequested,
|
|
Latency: cr.ExtraFields.Latency,
|
|
RawResponse: cr.ExtraFields.RawResponse,
|
|
CacheDebug: cr.ExtraFields.CacheDebug,
|
|
ProviderResponseHeaders: cr.ExtraFields.ProviderResponseHeaders,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Handle non-streaming response choice
|
|
if choice.ChatNonStreamResponseChoice != nil {
|
|
msg := choice.ChatNonStreamResponseChoice.Message
|
|
var textContent *string
|
|
if msg != nil && msg.Content != nil {
|
|
if msg.Content.ContentStr != nil {
|
|
textContent = msg.Content.ContentStr
|
|
} else if len(msg.Content.ContentBlocks) > 0 {
|
|
var sb strings.Builder
|
|
for _, block := range msg.Content.ContentBlocks {
|
|
if block.Text != nil {
|
|
sb.WriteString(*block.Text)
|
|
}
|
|
}
|
|
if sb.Len() > 0 {
|
|
s := sb.String()
|
|
textContent = &s
|
|
}
|
|
}
|
|
}
|
|
return &BifrostTextCompletionResponse{
|
|
ID: cr.ID,
|
|
Model: cr.Model,
|
|
Object: "text_completion",
|
|
SystemFingerprint: cr.SystemFingerprint,
|
|
Choices: []BifrostResponseChoice{
|
|
{
|
|
Index: 0,
|
|
TextCompletionResponseChoice: &TextCompletionResponseChoice{
|
|
Text: textContent,
|
|
},
|
|
FinishReason: choice.FinishReason,
|
|
LogProbs: choice.LogProbs,
|
|
},
|
|
},
|
|
Usage: cr.Usage,
|
|
ExtraFields: BifrostResponseExtraFields{
|
|
RequestType: TextCompletionRequest,
|
|
ChunkIndex: cr.ExtraFields.ChunkIndex,
|
|
Provider: cr.ExtraFields.Provider,
|
|
OriginalModelRequested: cr.ExtraFields.OriginalModelRequested,
|
|
Latency: cr.ExtraFields.Latency,
|
|
RawResponse: cr.ExtraFields.RawResponse,
|
|
CacheDebug: cr.ExtraFields.CacheDebug,
|
|
ProviderResponseHeaders: cr.ExtraFields.ProviderResponseHeaders,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Fallback case - return basic response structure
|
|
return &BifrostTextCompletionResponse{
|
|
ID: cr.ID,
|
|
Model: cr.Model,
|
|
Object: "text_completion",
|
|
SystemFingerprint: cr.SystemFingerprint,
|
|
Usage: cr.Usage,
|
|
ExtraFields: BifrostResponseExtraFields{
|
|
RequestType: TextCompletionRequest,
|
|
ChunkIndex: cr.ExtraFields.ChunkIndex,
|
|
Provider: cr.ExtraFields.Provider,
|
|
OriginalModelRequested: cr.ExtraFields.OriginalModelRequested,
|
|
Latency: cr.ExtraFields.Latency,
|
|
RawResponse: cr.ExtraFields.RawResponse,
|
|
CacheDebug: cr.ExtraFields.CacheDebug,
|
|
ProviderResponseHeaders: cr.ExtraFields.ProviderResponseHeaders,
|
|
},
|
|
}
|
|
}
|