first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,558 @@
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
}