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, }, } }