package gemini import ( "encoding/base64" "fmt" "strings" "github.com/maximhq/bifrost/core/schemas" ) // ToGeminiChatCompletionRequest converts a BifrostChatRequest to Gemini's generation request format for chat completion func ToGeminiChatCompletionRequest(bifrostReq *schemas.BifrostChatRequest) (*GeminiGenerationRequest, error) { if bifrostReq == nil { return nil, nil } // Create the base Gemini generation request geminiReq := &GeminiGenerationRequest{ Model: bifrostReq.Model, } // Convert parameters to generation config if bifrostReq.Params != nil { geminiReq.ExtraParams = bifrostReq.Params.ExtraParams var err error geminiReq.GenerationConfig, err = convertParamsToGenerationConfig(bifrostReq.Params, []string{}, bifrostReq.Model) if err != nil { return nil, err } // Handle tool-related parameters if len(bifrostReq.Params.Tools) > 0 { geminiReq.Tools = convertBifrostToolsToGemini(bifrostReq.Params.Tools) // Convert tool choice to tool config if bifrostReq.Params.ToolChoice != nil { geminiReq.ToolConfig = convertToolChoiceToToolConfig(bifrostReq.Params.ToolChoice) } } // Handle extra parameters if bifrostReq.Params.ExtraParams != nil { // Safety settings if safetySettings, ok := schemas.SafeExtractFromMap(bifrostReq.Params.ExtraParams, "safety_settings"); ok { delete(geminiReq.ExtraParams, "safety_settings") if settings, ok := SafeExtractSafetySettings(safetySettings); ok { geminiReq.SafetySettings = settings } } // Cached content if cachedContent, ok := schemas.SafeExtractString(bifrostReq.Params.ExtraParams["cached_content"]); ok { delete(geminiReq.ExtraParams, "cached_content") geminiReq.CachedContent = cachedContent } // Labels if labels, ok := schemas.SafeExtractFromMap(bifrostReq.Params.ExtraParams, "labels"); ok { delete(geminiReq.ExtraParams, "labels") if labelMap, ok := schemas.SafeExtractStringMap(labels); ok { geminiReq.Labels = labelMap } } } } // Convert chat completion messages to Gemini format contents, systemInstruction := convertBifrostMessagesToGemini(bifrostReq.Input) if systemInstruction != nil { geminiReq.SystemInstruction = systemInstruction } geminiReq.Contents = contents return geminiReq, nil } // ToBifrostChatResponse converts a GenerateContentResponse to a BifrostChatResponse func (response *GenerateContentResponse) ToBifrostChatResponse() *schemas.BifrostChatResponse { bifrostResp := &schemas.BifrostChatResponse{ ID: response.ResponseID, Model: response.ModelVersion, Object: "chat.completion", } // Set creation timestamp if available if !response.CreateTime.IsZero() { bifrostResp.Created = int(response.CreateTime.Unix()) } // Handle empty candidates (filtered/malformed responses) if len(response.Candidates) == 0 { finishReason := ConvertGeminiFinishReasonToBifrost(FinishReasonMalformedFunctionCall) return createErrorResponse(response, finishReason, false) } candidate := response.Candidates[0] // Check for filtered finish reasons that indicate errors if isErrorFinishReason(candidate.FinishReason) { finishReason := ConvertGeminiFinishReasonToBifrost(candidate.FinishReason) return createErrorResponse(response, finishReason, false) } // Collect all content and tool calls into a single message var toolCalls []schemas.ChatAssistantMessageToolCall var contentBlocks []schemas.ChatContentBlock var reasoningDetails []schemas.ChatReasoningDetails var contentStr *string // Process candidate content to extract text, tool calls, and reasoning if candidate.Content != nil && len(candidate.Content.Parts) > 0 { for _, part := range candidate.Content.Parts { // Handle thought/reasoning text separately - add to reasoning details if part.Text != "" && part.Thought { reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{ Index: len(reasoningDetails), Type: schemas.BifrostReasoningDetailsTypeText, Text: &part.Text, }) continue } // Handle regular text if part.Text != "" { contentBlocks = append(contentBlocks, schemas.ChatContentBlock{ Type: schemas.ChatContentBlockTypeText, Text: &part.Text, }) // Add thought signature to reasoning details if present with text if len(part.ThoughtSignature) > 0 { thoughtSig := base64.StdEncoding.EncodeToString(part.ThoughtSignature) reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{ Index: len(reasoningDetails), Type: schemas.BifrostReasoningDetailsTypeEncrypted, Signature: &thoughtSig, }) } } if part.FunctionCall != nil { function := schemas.ChatAssistantMessageToolCallFunction{ Name: &part.FunctionCall.Name, } if len(part.FunctionCall.Args) > 0 { function.Arguments = string(part.FunctionCall.Args) } callID := part.FunctionCall.Name if part.FunctionCall.ID != "" { callID = part.FunctionCall.ID } // Embed thought signature into CallID if present (matches responses.go pattern) if len(part.ThoughtSignature) > 0 && !strings.Contains(callID, thoughtSignatureSeparator) { encoded := base64.RawURLEncoding.EncodeToString(part.ThoughtSignature) callID = fmt.Sprintf("%s%s%s", callID, thoughtSignatureSeparator, encoded) } toolCall := schemas.ChatAssistantMessageToolCall{ Index: uint16(len(toolCalls)), Type: schemas.Ptr(string(schemas.ChatToolChoiceTypeFunction)), ID: &callID, Function: function, } toolCalls = append(toolCalls, toolCall) // Also add to reasoning details for backward compatibility if len(part.ThoughtSignature) > 0 { thoughtSig := base64.StdEncoding.EncodeToString(part.ThoughtSignature) // Extract base ID without signature for reasoning detail lookup baseCallID := callID if strings.Contains(callID, thoughtSignatureSeparator) { parts := strings.SplitN(callID, thoughtSignatureSeparator, 2) if len(parts) == 2 { baseCallID = parts[0] } } reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{ Index: len(reasoningDetails), Type: schemas.BifrostReasoningDetailsTypeEncrypted, Signature: &thoughtSig, ID: schemas.Ptr(fmt.Sprintf("tool_call_%s", baseCallID)), }) } } if part.FunctionResponse != nil { // Extract the output from the response output := extractFunctionResponseOutput(part.FunctionResponse) // Add as text content block if output != "" { contentBlocks = append(contentBlocks, schemas.ChatContentBlock{ Type: schemas.ChatContentBlockTypeText, Text: &output, }) } } // Handle code execution results if part.CodeExecutionResult != nil { output := part.CodeExecutionResult.Output if part.CodeExecutionResult.Outcome != OutcomeOK { output = "Error: " + output } if output != "" { contentBlocks = append(contentBlocks, schemas.ChatContentBlock{ Type: schemas.ChatContentBlockTypeText, Text: &output, }) } } // Handle executable code if part.ExecutableCode != nil { codeContent := "```" + part.ExecutableCode.Language + "\n" + part.ExecutableCode.Code + "\n```" contentBlocks = append(contentBlocks, schemas.ChatContentBlock{ Type: schemas.ChatContentBlockTypeText, Text: &codeContent, }) } // Handle standalone thought signature (not associated with function call or text) if len(part.ThoughtSignature) > 0 && part.FunctionCall == nil && part.Text == "" { thoughtSig := base64.StdEncoding.EncodeToString(part.ThoughtSignature) reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{ Index: len(reasoningDetails), Type: schemas.BifrostReasoningDetailsTypeEncrypted, Signature: &thoughtSig, }) } } // Build the choice with message message := &schemas.ChatMessage{ Role: schemas.ChatMessageRoleAssistant, } if len(contentBlocks) == 1 && contentBlocks[0].Type == schemas.ChatContentBlockTypeText { contentStr = contentBlocks[0].Text contentBlocks = nil } message.Content = &schemas.ChatMessageContent{ ContentStr: contentStr, ContentBlocks: contentBlocks, } if len(toolCalls) > 0 || len(reasoningDetails) > 0 { message.ChatAssistantMessage = &schemas.ChatAssistantMessage{ ToolCalls: toolCalls, ReasoningDetails: reasoningDetails, } } // Convert finish reason to Bifrost format. // Gemini uses "STOP" for both normal text completions and tool call responses — // it has no dedicated finish reason for tool calls. Override to "tool_calls" when // tool calls are present so downstream consumers see a uniform signal. finishReason := ConvertGeminiFinishReasonToBifrost(candidate.FinishReason) if len(toolCalls) > 0 && finishReason == "stop" { finishReason = "tool_calls" } bifrostResp.Choices = append(bifrostResp.Choices, schemas.BifrostResponseChoice{ Index: 0, FinishReason: &finishReason, LogProbs: ConvertGeminiLogprobsResultToBifrost(candidate.LogprobsResult), ChatNonStreamResponseChoice: &schemas.ChatNonStreamResponseChoice{ Message: message, }, }) } // Set usage information bifrostResp.Usage = ConvertGeminiUsageMetadataToChatUsage(response.UsageMetadata) return bifrostResp } // GeminiStreamState tracks tool-call index across streaming chunks. type GeminiStreamState struct { nextToolCallIndex int hadToolCalls bool // true if any tool calls were seen in this stream } // NewGeminiStreamState returns initialised stream state for one streaming response. func NewGeminiStreamState() *GeminiStreamState { return &GeminiStreamState{} } // ToBifrostChatCompletionStream converts a Gemini streaming response to a Bifrost Chat Completion Stream response // Returns the response, error (if any), and a boolean indicating if this is the last chunk func (response *GenerateContentResponse) ToBifrostChatCompletionStream(state *GeminiStreamState) (*schemas.BifrostChatResponse, *schemas.BifrostError, bool) { if response == nil { return nil, nil, false } if state == nil { state = NewGeminiStreamState() } // Handle empty candidates (filtered/malformed responses) if len(response.Candidates) == 0 { finishReason := ConvertGeminiFinishReasonToBifrost(FinishReasonMalformedFunctionCall) return createErrorResponse(response, finishReason, true), nil, true } candidate := response.Candidates[0] // Check for filtered finish reasons that indicate errors if isErrorFinishReason(candidate.FinishReason) { finishReason := ConvertGeminiFinishReasonToBifrost(candidate.FinishReason) return createErrorResponse(response, finishReason, true), nil, true } // Determine if this is the last chunk based on finish reason and usage metadata isLastChunk := candidate.FinishReason != "" && response.UsageMetadata != nil // Create the streaming response streamResponse := &schemas.BifrostChatResponse{ ID: response.ResponseID, Model: response.ModelVersion, Object: "chat.completion.chunk", } // Set creation timestamp if available if !response.CreateTime.IsZero() { streamResponse.Created = int(response.CreateTime.Unix()) } // Build delta content delta := &schemas.ChatStreamResponseChoiceDelta{} // Process content parts if candidate.Content != nil && len(candidate.Content.Parts) > 0 { // Set role from the first chunk (Gemini uses "model" for assistant) if candidate.Content.Role != "" { role := candidate.Content.Role if role == string(RoleModel) { role = string(schemas.ChatMessageRoleAssistant) } delta.Role = &role } var textContent string var toolCalls []schemas.ChatAssistantMessageToolCall var reasoningDetails []schemas.ChatReasoningDetails for _, part := range candidate.Content.Parts { switch { case part.Text != "" && part.Thought: // Thought/reasoning content - add to reasoning details reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{ Index: len(reasoningDetails), Type: schemas.BifrostReasoningDetailsTypeText, Text: &part.Text, }) case part.Text != "": // Regular text content textContent += part.Text case part.FunctionCall != nil: // Function call jsonArgs := "" if len(part.FunctionCall.Args) > 0 { jsonArgs = string(part.FunctionCall.Args) } // Use ID if available, otherwise use function name callID := part.FunctionCall.Name if part.FunctionCall.ID != "" { callID = part.FunctionCall.ID } // Embed thought signature into CallID if present if len(part.ThoughtSignature) > 0 && !strings.Contains(callID, thoughtSignatureSeparator) { encoded := base64.RawURLEncoding.EncodeToString(part.ThoughtSignature) callID = fmt.Sprintf("%s%s%s", callID, thoughtSignatureSeparator, encoded) } toolCallIdx := state.nextToolCallIndex state.nextToolCallIndex++ toolCall := schemas.ChatAssistantMessageToolCall{ Index: uint16(toolCallIdx), Type: schemas.Ptr(string(schemas.ChatToolTypeFunction)), ID: &callID, Function: schemas.ChatAssistantMessageToolCallFunction{ Name: &part.FunctionCall.Name, Arguments: jsonArgs, }, } toolCalls = append(toolCalls, toolCall) // Also add thought signature to reasoning details if present if len(part.ThoughtSignature) > 0 { thoughtSig := base64.StdEncoding.EncodeToString(part.ThoughtSignature) // Extract base ID without signature for reasoning detail lookup baseCallID := callID if strings.Contains(callID, thoughtSignatureSeparator) { parts := strings.SplitN(callID, thoughtSignatureSeparator, 2) if len(parts) == 2 { baseCallID = parts[0] } } reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{ Index: len(reasoningDetails), Type: schemas.BifrostReasoningDetailsTypeEncrypted, Signature: &thoughtSig, ID: schemas.Ptr(fmt.Sprintf("tool_call_%s", baseCallID)), }) } case part.FunctionResponse != nil: // Extract the output from the response and add to text content output := extractFunctionResponseOutput(part.FunctionResponse) if output != "" { textContent += output } case part.CodeExecutionResult != nil: output := part.CodeExecutionResult.Output if part.CodeExecutionResult.Outcome != OutcomeOK { output = "Error: " + output } if output != "" { textContent += output } case part.ExecutableCode != nil: codeContent := "```" + part.ExecutableCode.Language + "\n" + part.ExecutableCode.Code + "\n```" textContent += codeContent } // Handle thought signature separately (not part of the switch since it can co-exist with other types) if len(part.ThoughtSignature) > 0 && part.FunctionCall == nil { thoughtSig := base64.StdEncoding.EncodeToString(part.ThoughtSignature) reasoningDetails = append(reasoningDetails, schemas.ChatReasoningDetails{ Index: len(reasoningDetails), Type: schemas.BifrostReasoningDetailsTypeEncrypted, Signature: &thoughtSig, }) } } // Set text content if present if textContent != "" { delta.Content = &textContent } // Set reasoning details if present if len(reasoningDetails) > 0 { delta.ReasoningDetails = reasoningDetails } // Set tool calls if present if len(toolCalls) > 0 { delta.ToolCalls = toolCalls state.hadToolCalls = true } } // Check if delta has any content - if not and it's not the last chunk, skip it hasDeltaContent := delta.Role != nil || delta.Content != nil || len(delta.ToolCalls) > 0 || len(delta.ReasoningDetails) > 0 if !hasDeltaContent && !isLastChunk { return nil, nil, false } // Build the choice var finishReason *string if isLastChunk && candidate.FinishReason != "" { reason := ConvertGeminiFinishReasonToBifrost(candidate.FinishReason) // Gemini uses "STOP" for both text completions and tool call responses. // Override to "tool_calls" when tool calls were seen in this stream for uniformity. if (len(delta.ToolCalls) > 0 || state.hadToolCalls) && reason == "stop" { reason = "tool_calls" } finishReason = &reason } choice := schemas.BifrostResponseChoice{ Index: int(candidate.Index), FinishReason: finishReason, LogProbs: ConvertGeminiLogprobsResultToBifrost(candidate.LogprobsResult), ChatStreamResponseChoice: &schemas.ChatStreamResponseChoice{ Delta: delta, }, } streamResponse.Choices = []schemas.BifrostResponseChoice{choice} // Add usage information if this is the last chunk if isLastChunk && response.UsageMetadata != nil { streamResponse.Usage = ConvertGeminiUsageMetadataToChatUsage(response.UsageMetadata) } return streamResponse, nil, isLastChunk } // isErrorFinishReason checks if a finish reason indicates a filtered or error response func isErrorFinishReason(reason FinishReason) bool { return reason == FinishReasonSafety || reason == FinishReasonRecitation || reason == FinishReasonMalformedFunctionCall || reason == FinishReasonBlocklist || reason == FinishReasonProhibitedContent || reason == FinishReasonSPII || reason == FinishReasonImageSafety || reason == FinishReasonUnexpectedToolCall || reason == FinishReasonMissingThoughtSignature || reason == FinishReasonMalformedResponse || reason == FinishReasonImageProhibitedContent || reason == FinishReasonImageRecitation || reason == FinishReasonTooManyToolCalls || reason == FinishReasonNoImage } // createErrorResponse creates a complete BifrostChatResponse for error cases func createErrorResponse(response *GenerateContentResponse, finishReason string, isStream bool) *schemas.BifrostChatResponse { var choice schemas.BifrostResponseChoice if isStream { choice = schemas.BifrostResponseChoice{ Index: 0, FinishReason: &finishReason, ChatStreamResponseChoice: &schemas.ChatStreamResponseChoice{ Delta: &schemas.ChatStreamResponseChoiceDelta{}, }, } } else { choice = schemas.BifrostResponseChoice{ Index: 0, FinishReason: &finishReason, ChatNonStreamResponseChoice: &schemas.ChatNonStreamResponseChoice{ Message: &schemas.ChatMessage{ Role: schemas.ChatMessageRoleAssistant, Content: &schemas.ChatMessageContent{}, }, }, } } objectType := "chat.completion" if isStream { objectType = "chat.completion.chunk" } errorResp := &schemas.BifrostChatResponse{ ID: response.ResponseID, Model: response.ModelVersion, Object: objectType, Choices: []schemas.BifrostResponseChoice{choice}, Usage: ConvertGeminiUsageMetadataToChatUsage(response.UsageMetadata), } if !response.CreateTime.IsZero() { errorResp.Created = int(response.CreateTime.Unix()) } return errorResp }