2024-02-29 01:08:18 +08:00
package gemini
2023-12-18 23:45:08 +08:00
import (
2026-01-09 18:00:40 +08:00
"context"
2023-12-18 23:45:08 +08:00
"encoding/json"
2025-07-10 15:02:40 +08:00
"errors"
2023-12-18 23:45:08 +08:00
"fmt"
"io"
"net/http"
2025-06-15 21:12:56 +08:00
"strconv"
2023-12-18 23:45:08 +08:00
"strings"
2026-01-09 18:00:40 +08:00
"time"
2024-12-29 03:58:21 +08:00
"unicode/utf8"
2024-12-20 13:20:07 +08:00
2025-10-11 15:30:09 +08:00
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/relay/channel/openai"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/model_setting"
2025-12-01 16:40:46 +08:00
"github.com/QuantumNous/new-api/setting/reasoning"
2025-10-11 15:30:09 +08:00
"github.com/QuantumNous/new-api/types"
2024-12-20 13:20:07 +08:00
"github.com/gin-gonic/gin"
2026-03-01 15:47:03 +08:00
"github.com/samber/lo"
2023-12-18 23:45:08 +08:00
)
2025-09-15 01:01:48 +08:00
// https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference?hl=zh-cn#blob
2025-06-02 19:00:55 +08:00
var geminiSupportedMimeTypes = map [ string ] bool {
"application/pdf" : true ,
"audio/mpeg" : true ,
"audio/mp3" : true ,
"audio/wav" : true ,
"image/png" : true ,
"image/jpeg" : true ,
2026-01-04 22:09:03 +08:00
"image/jpg" : true , // support old image/jpeg
2025-09-15 01:01:48 +08:00
"image/webp" : true ,
2026-04-02 16:40:45 +08:00
"image/heic" : true ,
"image/heif" : true ,
2025-06-02 19:00:55 +08:00
"text/plain" : true ,
"video/mov" : true ,
"video/mpeg" : true ,
"video/mp4" : true ,
"video/mpg" : true ,
"video/avi" : true ,
"video/wmv" : true ,
"video/mpegps" : true ,
"video/flv" : true ,
}
2025-11-20 15:54:33 +08:00
const thoughtSignatureBypassValue = "context_engineering_is_the_way_to_go"
2025-06-15 21:12:56 +08:00
// Gemini 允许的思考预算范围
const (
2025-06-18 00:49:35 +08:00
pro25MinBudget = 128
pro25MaxBudget = 32768
flash25MaxBudget = 24576
flash25LiteMinBudget = 512
flash25LiteMaxBudget = 24576
2025-06-15 21:12:56 +08:00
)
2025-08-06 16:20:38 +08:00
func isNew25ProModel ( modelName string ) bool {
return strings . HasPrefix ( modelName , "gemini-2.5-pro" ) &&
2025-06-18 00:49:35 +08:00
! strings . HasPrefix ( modelName , "gemini-2.5-pro-preview-05-06" ) &&
! strings . HasPrefix ( modelName , "gemini-2.5-pro-preview-03-25" )
2025-08-06 16:20:38 +08:00
}
func is25FlashLiteModel ( modelName string ) bool {
return strings . HasPrefix ( modelName , "gemini-2.5-flash-lite" )
}
// clampThinkingBudget 根据模型名称将预算限制在允许的范围内
func clampThinkingBudget ( modelName string , budget int ) int {
isNew25Pro := isNew25ProModel ( modelName )
is25FlashLite := is25FlashLiteModel ( modelName )
2025-06-18 00:49:35 +08:00
if is25FlashLite {
if budget < flash25LiteMinBudget {
return flash25LiteMinBudget
}
if budget > flash25LiteMaxBudget {
return flash25LiteMaxBudget
}
} else if isNew25Pro {
if budget < pro25MinBudget {
return pro25MinBudget
}
if budget > pro25MaxBudget {
return pro25MaxBudget
}
} else { // 其他模型
if budget < 0 {
return 0
}
if budget > flash25MaxBudget {
return flash25MaxBudget
}
}
return budget
}
2025-08-06 16:20:38 +08:00
// "effort": "high" - Allocates a large portion of tokens for reasoning (approximately 80% of max_tokens)
// "effort": "medium" - Allocates a moderate portion of tokens (approximately 50% of max_tokens)
// "effort": "low" - Allocates a smaller portion of tokens (approximately 20% of max_tokens)
2025-12-18 08:10:46 +08:00
// "effort": "minimal" - Allocates a minimal portion of tokens (approximately 5% of max_tokens)
2025-08-06 16:20:38 +08:00
func clampThinkingBudgetByEffort ( modelName string , effort string ) int {
isNew25Pro := isNew25ProModel ( modelName )
is25FlashLite := is25FlashLiteModel ( modelName )
maxBudget := 0
if is25FlashLite {
maxBudget = flash25LiteMaxBudget
}
if isNew25Pro {
maxBudget = pro25MaxBudget
} else {
maxBudget = flash25MaxBudget
}
switch effort {
case "high" :
2025-08-06 16:25:48 +08:00
maxBudget = maxBudget * 80 / 100
2025-08-06 16:20:38 +08:00
case "medium" :
2025-08-06 16:25:48 +08:00
maxBudget = maxBudget * 50 / 100
2025-08-06 16:20:38 +08:00
case "low" :
2025-08-06 16:25:48 +08:00
maxBudget = maxBudget * 20 / 100
2025-12-18 08:10:46 +08:00
case "minimal" :
maxBudget = maxBudget * 5 / 100
2025-08-06 16:20:38 +08:00
}
2025-08-06 16:25:48 +08:00
return clampThinkingBudget ( modelName , maxBudget )
2025-08-06 16:20:38 +08:00
}
func ThinkingAdaptor ( geminiRequest * dto . GeminiChatRequest , info * relaycommon . RelayInfo , oaiRequest ... dto . GeneralOpenAIRequest ) {
2025-04-18 19:36:18 +08:00
if model_setting . GetGeminiSettings ( ) . ThinkingAdapterEnabled {
2025-06-20 16:02:23 +08:00
modelName := info . UpstreamModelName
2025-06-18 00:49:35 +08:00
isNew25Pro := strings . HasPrefix ( modelName , "gemini-2.5-pro" ) &&
! strings . HasPrefix ( modelName , "gemini-2.5-pro-preview-05-06" ) &&
! strings . HasPrefix ( modelName , "gemini-2.5-pro-preview-03-25" )
if strings . Contains ( modelName , "-thinking-" ) {
parts := strings . SplitN ( modelName , "-thinking-" , 2 )
2025-06-15 21:12:56 +08:00
if len ( parts ) == 2 && parts [ 1 ] != "" {
if budgetTokens , err := strconv . Atoi ( parts [ 1 ] ) ; err == nil {
2025-06-18 00:49:35 +08:00
clampedBudget := clampThinkingBudget ( modelName , budgetTokens )
2025-08-01 22:23:35 +08:00
geminiRequest . GenerationConfig . ThinkingConfig = & dto . GeminiThinkingConfig {
2025-06-18 00:49:35 +08:00
ThinkingBudget : common . GetPointer ( clampedBudget ) ,
2025-06-15 21:12:56 +08:00
IncludeThoughts : true ,
}
}
}
2025-06-18 00:49:35 +08:00
} else if strings . HasSuffix ( modelName , "-thinking" ) {
2025-06-06 01:29:06 +08:00
unsupportedModels := [ ] string {
"gemini-2.5-pro-preview-05-06" ,
"gemini-2.5-pro-preview-03-25" ,
}
isUnsupported := false
for _ , unsupportedModel := range unsupportedModels {
2025-06-18 00:49:35 +08:00
if strings . HasPrefix ( modelName , unsupportedModel ) {
2025-06-06 01:29:06 +08:00
isUnsupported = true
break
}
}
if isUnsupported {
2025-08-01 22:23:35 +08:00
geminiRequest . GenerationConfig . ThinkingConfig = & dto . GeminiThinkingConfig {
2025-06-06 00:56:38 +08:00
IncludeThoughts : true ,
}
} else {
2025-08-01 22:23:35 +08:00
geminiRequest . GenerationConfig . ThinkingConfig = & dto . GeminiThinkingConfig {
2025-06-06 00:56:38 +08:00
IncludeThoughts : true ,
}
2026-03-01 15:47:03 +08:00
if geminiRequest . GenerationConfig . MaxOutputTokens != nil && * geminiRequest . GenerationConfig . MaxOutputTokens > 0 {
budgetTokens := model_setting . GetGeminiSettings ( ) . ThinkingAdapterBudgetTokensPercentage * float64 ( * geminiRequest . GenerationConfig . MaxOutputTokens )
2025-06-21 17:51:13 +08:00
clampedBudget := clampThinkingBudget ( modelName , int ( budgetTokens ) )
geminiRequest . GenerationConfig . ThinkingConfig . ThinkingBudget = common . GetPointer ( clampedBudget )
2025-08-06 16:20:38 +08:00
} else {
if len ( oaiRequest ) > 0 {
// 如果有reasoningEffort参数, 则根据其值设置思考预算
geminiRequest . GenerationConfig . ThinkingConfig . ThinkingBudget = common . GetPointer ( clampThinkingBudgetByEffort ( modelName , oaiRequest [ 0 ] . ReasoningEffort ) )
}
2025-06-21 17:51:13 +08:00
}
2025-06-06 00:56:38 +08:00
}
2025-06-18 00:49:35 +08:00
} else if strings . HasSuffix ( modelName , "-nothinking" ) {
2025-06-18 03:25:59 +08:00
if ! isNew25Pro {
2025-08-01 22:23:35 +08:00
geminiRequest . GenerationConfig . ThinkingConfig = & dto . GeminiThinkingConfig {
2025-06-06 01:58:02 +08:00
ThinkingBudget : common . GetPointer ( 0 ) ,
}
2025-04-18 23:13:28 +08:00
}
2025-12-18 08:10:46 +08:00
} else if _ , level , ok := reasoning . TrimEffortSuffix ( info . UpstreamModelName ) ; ok && level != "" {
2025-12-01 16:40:46 +08:00
geminiRequest . GenerationConfig . ThinkingConfig = & dto . GeminiThinkingConfig {
IncludeThoughts : true ,
ThinkingLevel : level ,
}
info . ReasoningEffort = level
2025-04-18 19:36:18 +08:00
}
}
2025-06-21 21:50:03 +08:00
}
// Setting safety to the lowest possible values since Gemini is already powerless enough
2025-11-30 18:45:54 +08:00
func CovertOpenAI2Gemini ( c * gin . Context , textRequest dto . GeneralOpenAIRequest , info * relaycommon . RelayInfo ) ( * dto . GeminiChatRequest , error ) {
2025-06-21 21:50:03 +08:00
2025-08-01 22:23:35 +08:00
geminiRequest := dto . GeminiChatRequest {
Contents : make ( [ ] dto . GeminiChatContent , 0 , len ( textRequest . Messages ) ) ,
GenerationConfig : dto . GeminiChatGenerationConfig {
2026-03-01 15:47:03 +08:00
Temperature : textRequest . Temperature ,
2025-06-21 21:50:03 +08:00
} ,
}
2026-03-01 15:47:03 +08:00
if textRequest . TopP != nil && * textRequest . TopP > 0 {
geminiRequest . GenerationConfig . TopP = common . GetPointer ( * textRequest . TopP )
}
if maxTokens := textRequest . GetMaxTokens ( ) ; maxTokens > 0 {
geminiRequest . GenerationConfig . MaxOutputTokens = common . GetPointer ( maxTokens )
}
if textRequest . Seed != nil && * textRequest . Seed != 0 {
geminiSeed := int64 ( lo . FromPtr ( textRequest . Seed ) )
geminiRequest . GenerationConfig . Seed = common . GetPointer ( geminiSeed )
}
2025-11-20 15:54:33 +08:00
attachThoughtSignature := ( info . ChannelType == constant . ChannelTypeGemini ||
info . ChannelType == constant . ChannelTypeVertexAi ) &&
model_setting . GetGeminiSettings ( ) . FunctionCallThoughtSignatureEnabled
2025-06-21 21:50:03 +08:00
if model_setting . IsGeminiModelSupportImagine ( info . UpstreamModelName ) {
geminiRequest . GenerationConfig . ResponseModalities = [ ] string {
"TEXT" ,
"IMAGE" ,
}
}
2026-01-29 21:30:27 +08:00
if stopSequences := parseStopSequences ( textRequest . Stop ) ; len ( stopSequences ) > 0 {
// Gemini supports up to 5 stop sequences
if len ( stopSequences ) > 5 {
stopSequences = stopSequences [ : 5 ]
}
geminiRequest . GenerationConfig . StopSequences = stopSequences
}
2025-06-21 21:50:03 +08:00
2025-08-06 16:20:38 +08:00
adaptorWithExtraBody := false
2025-11-30 19:31:08 +08:00
// patch extra_body
2025-08-06 16:20:38 +08:00
if len ( textRequest . ExtraBody ) > 0 {
2026-02-11 22:14:25 +08:00
var extraBody map [ string ] interface { }
if err := common . Unmarshal ( textRequest . ExtraBody , & extraBody ) ; err != nil {
return nil , fmt . Errorf ( "invalid extra body: %w" , err )
}
// eg. {"google":{"thinking_config":{"thinking_budget":5324,"include_thoughts":true}}}
if googleBody , ok := extraBody [ "google" ] . ( map [ string ] interface { } ) ; ok {
if ! strings . HasSuffix ( info . UpstreamModelName , "-nothinking" ) {
2025-08-06 16:20:38 +08:00
adaptorWithExtraBody = true
2025-10-12 22:21:45 +08:00
// check error param name like thinkingConfig, should be thinking_config
if _ , hasErrorParam := googleBody [ "thinkingConfig" ] ; hasErrorParam {
return nil , errors . New ( "extra_body.google.thinkingConfig is not supported, use extra_body.google.thinking_config instead" )
}
2025-08-06 16:20:38 +08:00
if thinkingConfig , ok := googleBody [ "thinking_config" ] . ( map [ string ] interface { } ) ; ok {
2025-10-12 22:21:45 +08:00
// check error param name like thinkingBudget, should be thinking_budget
if _ , hasErrorParam := thinkingConfig [ "thinkingBudget" ] ; hasErrorParam {
return nil , errors . New ( "extra_body.google.thinking_config.thinkingBudget is not supported, use extra_body.google.thinking_config.thinking_budget instead" )
}
2026-02-11 22:14:25 +08:00
var hasThinkingConfig bool
var tempThinkingConfig dto . GeminiThinkingConfig
if thinkingBudget , exists := thinkingConfig [ "thinking_budget" ] ; exists {
switch v := thinkingBudget . ( type ) {
case float64 :
budgetInt := int ( v )
tempThinkingConfig . ThinkingBudget = common . GetPointer ( budgetInt )
if budgetInt > 0 {
// 有正数预算
tempThinkingConfig . IncludeThoughts = true
} else {
// 存在但为0或负数, 禁用思考
tempThinkingConfig . IncludeThoughts = false
}
hasThinkingConfig = true
default :
return nil , errors . New ( "extra_body.google.thinking_config.thinking_budget must be an integer" )
2025-08-06 16:20:38 +08:00
}
}
2025-11-30 19:31:08 +08:00
2026-02-11 22:14:25 +08:00
if includeThoughts , exists := thinkingConfig [ "include_thoughts" ] ; exists {
if v , ok := includeThoughts . ( bool ) ; ok {
tempThinkingConfig . IncludeThoughts = v
hasThinkingConfig = true
} else {
return nil , errors . New ( "extra_body.google.thinking_config.include_thoughts must be a boolean" )
}
2025-11-30 19:31:08 +08:00
}
2026-02-11 22:14:25 +08:00
if thinkingLevel , exists := thinkingConfig [ "thinking_level" ] ; exists {
if v , ok := thinkingLevel . ( string ) ; ok {
tempThinkingConfig . ThinkingLevel = v
hasThinkingConfig = true
} else {
return nil , errors . New ( "extra_body.google.thinking_config.thinking_level must be a string" )
}
2025-11-30 19:31:08 +08:00
}
2026-02-11 22:14:25 +08:00
if hasThinkingConfig {
// 避免 panic: 仅在获得配置时分配,防止后续赋值时空指针
if geminiRequest . GenerationConfig . ThinkingConfig == nil {
geminiRequest . GenerationConfig . ThinkingConfig = & tempThinkingConfig
} else {
// 如果已分配,则合并内容
if tempThinkingConfig . ThinkingBudget != nil {
geminiRequest . GenerationConfig . ThinkingConfig . ThinkingBudget = tempThinkingConfig . ThinkingBudget
}
geminiRequest . GenerationConfig . ThinkingConfig . IncludeThoughts = tempThinkingConfig . IncludeThoughts
if tempThinkingConfig . ThinkingLevel != "" {
geminiRequest . GenerationConfig . ThinkingConfig . ThinkingLevel = tempThinkingConfig . ThinkingLevel
}
}
2025-11-30 19:31:08 +08:00
}
2026-02-11 22:14:25 +08:00
}
}
2025-11-30 19:31:08 +08:00
2026-02-11 22:14:25 +08:00
// check error param name like imageConfig, should be image_config
if _ , hasErrorParam := googleBody [ "imageConfig" ] ; hasErrorParam {
return nil , errors . New ( "extra_body.google.imageConfig is not supported, use extra_body.google.image_config instead" )
}
if imageConfig , ok := googleBody [ "image_config" ] . ( map [ string ] interface { } ) ; ok {
// check error param name like aspectRatio, should be aspect_ratio
if _ , hasErrorParam := imageConfig [ "aspectRatio" ] ; hasErrorParam {
return nil , errors . New ( "extra_body.google.image_config.aspectRatio is not supported, use extra_body.google.image_config.aspect_ratio instead" )
}
// check error param name like imageSize, should be image_size
if _ , hasErrorParam := imageConfig [ "imageSize" ] ; hasErrorParam {
return nil , errors . New ( "extra_body.google.image_config.imageSize is not supported, use extra_body.google.image_config.image_size instead" )
}
// convert snake_case to camelCase for Gemini API
geminiImageConfig := make ( map [ string ] interface { } )
if aspectRatio , ok := imageConfig [ "aspect_ratio" ] ; ok {
geminiImageConfig [ "aspectRatio" ] = aspectRatio
}
if imageSize , ok := imageConfig [ "image_size" ] ; ok {
geminiImageConfig [ "imageSize" ] = imageSize
}
if len ( geminiImageConfig ) > 0 {
imageConfigBytes , err := common . Marshal ( geminiImageConfig )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal image_config: %w" , err )
2025-11-30 19:31:08 +08:00
}
2026-02-11 22:14:25 +08:00
geminiRequest . GenerationConfig . ImageConfig = imageConfigBytes
2025-11-30 19:31:08 +08:00
}
2025-08-06 16:20:38 +08:00
}
}
}
if ! adaptorWithExtraBody {
ThinkingAdaptor ( & geminiRequest , info , textRequest )
}
2025-04-18 19:36:18 +08:00
2025-08-01 22:23:35 +08:00
safetySettings := make ( [ ] dto . GeminiChatSafetySettings , 0 , len ( SafetySettingList ) )
2025-02-26 16:54:43 +08:00
for _ , category := range SafetySettingList {
2025-08-01 22:23:35 +08:00
safetySettings = append ( safetySettings , dto . GeminiChatSafetySettings {
2025-02-26 16:54:43 +08:00
Category : category ,
2025-02-26 18:19:09 +08:00
Threshold : model_setting . GetGeminiSafetySetting ( category ) ,
2025-02-26 16:54:43 +08:00
} )
}
geminiRequest . SafetySettings = safetySettings
2024-12-24 20:46:02 +08:00
// openaiContent.FuncToToolCalls()
2024-07-18 20:28:47 +08:00
if textRequest . Tools != nil {
2025-02-26 23:56:10 +08:00
functions := make ( [ ] dto . FunctionRequest , 0 , len ( textRequest . Tools ) )
2024-12-12 17:58:25 +08:00
googleSearch := false
2024-12-24 20:46:02 +08:00
codeExecution := false
2025-09-27 16:14:09 +08:00
urlContext := false
2024-07-18 20:28:47 +08:00
for _ , tool := range textRequest . Tools {
2024-12-12 17:58:25 +08:00
if tool . Function . Name == "googleSearch" {
googleSearch = true
continue
}
2024-12-24 20:46:02 +08:00
if tool . Function . Name == "codeExecution" {
codeExecution = true
continue
}
2025-09-27 16:14:09 +08:00
if tool . Function . Name == "urlContext" {
urlContext = true
continue
}
2024-12-22 14:29:14 +08:00
if tool . Function . Parameters != nil {
2025-04-10 22:35:03 +08:00
2024-12-22 14:29:14 +08:00
params , ok := tool . Function . Parameters . ( map [ string ] interface { } )
if ok {
if props , hasProps := params [ "properties" ] . ( map [ string ] interface { } ) ; hasProps {
if len ( props ) == 0 {
2024-12-22 14:35:21 +08:00
tool . Function . Parameters = nil
2024-12-22 14:29:14 +08:00
}
}
}
}
2025-04-10 22:35:03 +08:00
// Clean the parameters before appending
cleanedParams := cleanFunctionParameters ( tool . Function . Parameters )
tool . Function . Parameters = cleanedParams
2024-07-18 20:28:47 +08:00
functions = append ( functions , tool . Function )
}
2025-08-11 19:48:04 +08:00
geminiTools := geminiRequest . GetTools ( )
2024-12-24 20:46:02 +08:00
if codeExecution {
2025-08-11 19:48:04 +08:00
geminiTools = append ( geminiTools , dto . GeminiChatTool {
2024-12-24 20:46:02 +08:00
CodeExecution : make ( map [ string ] string ) ,
} )
2024-12-12 17:58:25 +08:00
}
if googleSearch {
2025-08-11 19:48:04 +08:00
geminiTools = append ( geminiTools , dto . GeminiChatTool {
2024-12-12 17:58:25 +08:00
GoogleSearch : make ( map [ string ] string ) ,
} )
2024-07-18 20:28:47 +08:00
}
2025-09-27 16:14:09 +08:00
if urlContext {
geminiTools = append ( geminiTools , dto . GeminiChatTool {
URLContext : make ( map [ string ] string ) ,
} )
}
2024-12-24 20:46:02 +08:00
if len ( functions ) > 0 {
2025-08-11 19:48:04 +08:00
geminiTools = append ( geminiTools , dto . GeminiChatTool {
2024-12-24 20:46:02 +08:00
FunctionDeclarations : functions ,
} )
}
2025-08-11 19:48:04 +08:00
geminiRequest . SetTools ( geminiTools )
2025-12-27 18:22:30 +08:00
// [NEW] Convert OpenAI tool_choice to Gemini toolConfig.functionCallingConfig
// Mapping: "auto" -> "AUTO", "none" -> "NONE", "required" -> "ANY"
// Object format: {"type": "function", "function": {"name": "xxx"}} -> "ANY" + allowedFunctionNames
if textRequest . ToolChoice != nil {
geminiRequest . ToolConfig = convertToolChoiceToGeminiConfig ( textRequest . ToolChoice )
}
2023-12-18 23:45:08 +08:00
}
2024-12-24 20:46:02 +08:00
2024-12-21 16:01:17 +08:00
if textRequest . ResponseFormat != nil && ( textRequest . ResponseFormat . Type == "json_schema" || textRequest . ResponseFormat . Type == "json_object" ) {
geminiRequest . GenerationConfig . ResponseMimeType = "application/json"
2025-07-26 12:11:20 +08:00
if len ( textRequest . ResponseFormat . JsonSchema ) > 0 {
// 先将json.RawMessage解析
var jsonSchema dto . FormatJsonSchema
if err := common . Unmarshal ( textRequest . ResponseFormat . JsonSchema , & jsonSchema ) ; err == nil {
cleanedSchema := removeAdditionalPropertiesWithDepth ( jsonSchema . Schema , 0 )
geminiRequest . GenerationConfig . ResponseSchema = cleanedSchema
}
2024-12-21 16:01:17 +08:00
}
}
2024-12-23 01:26:14 +08:00
tool_call_ids := make ( map [ string ] string )
2024-12-24 20:46:02 +08:00
var system_content [ ] string
2024-12-16 20:19:29 +08:00
//shouldAddDummyModelMessage := false
2023-12-18 23:45:08 +08:00
for _ , message := range textRequest . Messages {
2025-12-27 02:52:33 +08:00
if message . Role == "system" || message . Role == "developer" {
2024-12-24 20:46:02 +08:00
system_content = append ( system_content , message . StringContent ( ) )
2024-12-16 20:19:29 +08:00
continue
2024-12-24 20:46:02 +08:00
} else if message . Role == "tool" || message . Role == "function" {
if len ( geminiRequest . Contents ) == 0 || geminiRequest . Contents [ len ( geminiRequest . Contents ) - 1 ] . Role == "model" {
2025-08-01 22:23:35 +08:00
geminiRequest . Contents = append ( geminiRequest . Contents , dto . GeminiChatContent {
2024-12-23 01:26:14 +08:00
Role : "user" ,
} )
}
var parts = & geminiRequest . Contents [ len ( geminiRequest . Contents ) - 1 ] . Parts
name := ""
if message . Name != nil {
name = * message . Name
} else if val , exists := tool_call_ids [ message . ToolCallId ] ; exists {
name = val
}
2025-06-08 14:35:56 +08:00
var contentMap map [ string ] interface { }
contentStr := message . StringContent ( )
// 1. 尝试解析为 JSON 对象
if err := json . Unmarshal ( [ ] byte ( contentStr ) , & contentMap ) ; err != nil {
// 2. 如果失败,尝试解析为 JSON 数组
var contentSlice [ ] interface { }
if err := json . Unmarshal ( [ ] byte ( contentStr ) , & contentSlice ) ; err == nil {
// 如果是数组,包装成对象
contentMap = map [ string ] interface { } { "result" : contentSlice }
} else {
// 3. 如果再次失败,作为纯文本处理
contentMap = map [ string ] interface { } { "content" : contentStr }
}
}
2025-08-01 22:23:35 +08:00
functionResp := & dto . GeminiFunctionResponse {
2025-06-06 00:56:38 +08:00
Name : name ,
Response : contentMap ,
2024-12-23 01:26:14 +08:00
}
2025-06-06 00:56:38 +08:00
2025-08-01 22:23:35 +08:00
* parts = append ( * parts , dto . GeminiPart {
2024-12-23 01:26:14 +08:00
FunctionResponse : functionResp ,
} )
continue
2024-12-16 20:19:29 +08:00
}
2025-08-01 22:23:35 +08:00
var parts [ ] dto . GeminiPart
content := dto . GeminiChatContent {
2023-12-18 23:45:08 +08:00
Role : message . Role ,
}
2025-11-20 15:54:33 +08:00
shouldAttachThoughtSignature := attachThoughtSignature && ( message . Role == "assistant" || message . Role == "model" )
signatureAttached := false
2024-12-24 20:46:02 +08:00
// isToolCall := false
2024-12-22 16:20:30 +08:00
if message . ToolCalls != nil {
2024-12-24 20:46:02 +08:00
// message.Role = "model"
// isToolCall = true
2024-12-22 16:20:30 +08:00
for _ , call := range message . ParseToolCalls ( ) {
2024-12-24 20:46:02 +08:00
args := map [ string ] interface { } { }
if call . Function . Arguments != "" {
if json . Unmarshal ( [ ] byte ( call . Function . Arguments ) , & args ) != nil {
return nil , fmt . Errorf ( "invalid arguments for function %s, args: %s" , call . Function . Name , call . Function . Arguments )
}
}
2025-08-01 22:23:35 +08:00
toolCall := dto . GeminiPart {
FunctionCall : & dto . FunctionCall {
2024-12-22 16:20:30 +08:00
FunctionName : call . Function . Name ,
2024-12-24 20:46:02 +08:00
Arguments : args ,
2024-12-22 16:20:30 +08:00
} ,
2024-12-20 21:36:23 +08:00
}
2025-11-20 15:54:33 +08:00
if shouldAttachThoughtSignature && ! signatureAttached && hasFunctionCallContent ( toolCall . FunctionCall ) && len ( toolCall . ThoughtSignature ) == 0 {
toolCall . ThoughtSignature = json . RawMessage ( strconv . Quote ( thoughtSignatureBypassValue ) )
signatureAttached = true
}
2024-12-22 16:20:30 +08:00
parts = append ( parts , toolCall )
2024-12-23 01:26:14 +08:00
tool_call_ids [ call . ID ] = call . Function . Name
2024-12-22 16:20:30 +08:00
}
}
2024-12-24 20:46:02 +08:00
openaiContent := message . ParseContent ( )
for _ , part := range openaiContent {
if part . Type == dto . ContentTypeText {
if part . Text == "" {
continue
}
2025-12-01 17:54:41 +08:00
// check markdown image 
// 使用字符串查找而非正则,避免大文本性能问题
text := part . Text
hasMarkdownImage := false
for {
// 快速检查是否包含 markdown 图片标记
startIdx := strings . Index ( text , "
if bracketIdx == - 1 {
break
}
bracketIdx += startIdx
// 找到闭合的 )
closeIdx := strings . Index ( text [ bracketIdx + 2 : ] , ")" )
if closeIdx == - 1 {
break
}
closeIdx += bracketIdx + 2
hasMarkdownImage = true
// 添加图片前的文本
if startIdx > 0 {
textBefore := text [ : startIdx ]
if textBefore != "" {
parts = append ( parts , dto . GeminiPart {
Text : textBefore ,
} )
}
}
// 提取 data URL (从 "](" 后面开始,到 ")" 之前)
dataUrl := text [ bracketIdx + 2 : closeIdx ]
format , base64String , err := service . DecodeBase64FileData ( dataUrl )
if err != nil {
return nil , fmt . Errorf ( "decode markdown base64 image data failed: %s" , err . Error ( ) )
}
imgPart := dto . GeminiPart {
InlineData : & dto . GeminiInlineData {
MimeType : format ,
Data : base64String ,
} ,
}
if shouldAttachThoughtSignature {
imgPart . ThoughtSignature = json . RawMessage ( strconv . Quote ( thoughtSignatureBypassValue ) )
}
parts = append ( parts , imgPart )
// 继续处理剩余文本
text = text [ closeIdx + 1 : ]
}
// 添加剩余文本或原始文本(如果没有找到 markdown 图片)
if ! hasMarkdownImage {
parts = append ( parts , dto . GeminiPart {
Text : part . Text ,
} )
}
2026-04-06 15:54:42 +08:00
} else {
source := part . ToFileSource ( )
if source == nil {
continue
2024-07-22 21:20:23 +08:00
}
2026-02-04 17:15:24 +08:00
base64Data , mimeType , err := service . GetBase64Data ( c , source , "formatting image for Gemini" )
if err != nil {
return nil , fmt . Errorf ( "get file data from '%s' failed: %w" , source . GetIdentifier ( ) , err )
}
// 校验 MimeType 是否在 Gemini 支持的白名单中
if _ , ok := geminiSupportedMimeTypes [ strings . ToLower ( mimeType ) ] ; ! ok {
return nil , fmt . Errorf ( "mime type is not supported by Gemini: '%s', url: '%s', supported types are: %v" , mimeType , source . GetIdentifier ( ) , getSupportedMimeTypesList ( ) )
}
2025-08-01 22:23:35 +08:00
parts = append ( parts , dto . GeminiPart {
InlineData : & dto . GeminiInlineData {
2026-02-04 17:15:24 +08:00
MimeType : mimeType ,
Data : base64Data ,
2025-04-11 16:23:54 +08:00
} ,
} )
2023-12-27 16:32:54 +08:00
}
}
2024-12-23 01:26:14 +08:00
2025-11-24 00:31:20 +00:00
// 如果需要附加签名但还没有附加(没有 tool_calls 或 tool_calls 为空),
// 则在第一个文本 part 上附加 thoughtSignature
if shouldAttachThoughtSignature && ! signatureAttached && len ( parts ) > 0 {
for i := range parts {
if parts [ i ] . Text != "" {
parts [ i ] . ThoughtSignature = json . RawMessage ( strconv . Quote ( thoughtSignatureBypassValue ) )
break
}
}
}
2023-12-27 16:32:54 +08:00
content . Parts = parts
2023-12-18 23:45:08 +08:00
// there's no assistant role in gemini and API shall vomit if Role is not user or model
if content . Role == "assistant" {
content . Role = "model"
}
2025-06-19 11:25:59 +08:00
if len ( content . Parts ) > 0 {
geminiRequest . Contents = append ( geminiRequest . Contents , content )
}
2023-12-18 23:45:08 +08:00
}
2024-12-24 20:46:02 +08:00
if len ( system_content ) > 0 {
2025-08-01 22:23:35 +08:00
geminiRequest . SystemInstructions = & dto . GeminiChatContent {
Parts : [ ] dto . GeminiPart {
2024-12-24 20:46:02 +08:00
{
Text : strings . Join ( system_content , "\n" ) ,
} ,
} ,
}
}
2024-12-20 21:50:58 +08:00
return & geminiRequest , nil
2023-12-18 23:45:08 +08:00
}
2026-01-29 21:30:27 +08:00
// parseStopSequences 解析停止序列,支持字符串或字符串数组
func parseStopSequences ( stop any ) [ ] string {
if stop == nil {
return nil
}
switch v := stop . ( type ) {
case string :
if v != "" {
return [ ] string { v }
}
case [ ] string :
return v
case [ ] interface { } :
sequences := make ( [ ] string , 0 , len ( v ) )
for _ , item := range v {
if str , ok := item . ( string ) ; ok && str != "" {
sequences = append ( sequences , str )
}
}
return sequences
}
return nil
}
2025-11-20 15:54:33 +08:00
func hasFunctionCallContent ( call * dto . FunctionCall ) bool {
if call == nil {
return false
}
if strings . TrimSpace ( call . FunctionName ) != "" {
return true
}
switch v := call . Arguments . ( type ) {
case nil :
return false
case string :
return strings . TrimSpace ( v ) != ""
case map [ string ] interface { } :
return len ( v ) > 0
case [ ] interface { } :
return len ( v ) > 0
default :
return true
}
}
2025-06-02 19:00:55 +08:00
// Helper function to get a list of supported MIME types for error messages
func getSupportedMimeTypesList ( ) [ ] string {
keys := make ( [ ] string , 0 , len ( geminiSupportedMimeTypes ) )
for k := range geminiSupportedMimeTypes {
keys = append ( keys , k )
}
return keys
}
2026-01-15 13:25:17 +08:00
var geminiOpenAPISchemaAllowedFields = map [ string ] struct { } {
"anyOf" : { } ,
"default" : { } ,
"description" : { } ,
"enum" : { } ,
"example" : { } ,
"format" : { } ,
"items" : { } ,
"maxItems" : { } ,
"maxLength" : { } ,
"maxProperties" : { } ,
"maximum" : { } ,
"minItems" : { } ,
"minLength" : { } ,
"minProperties" : { } ,
"minimum" : { } ,
"nullable" : { } ,
"pattern" : { } ,
"properties" : { } ,
"propertyOrdering" : { } ,
"required" : { } ,
"title" : { } ,
"type" : { } ,
}
const geminiFunctionSchemaMaxDepth = 64
2025-04-10 22:35:03 +08:00
// cleanFunctionParameters recursively removes unsupported fields from Gemini function parameters.
func cleanFunctionParameters ( params interface { } ) interface { } {
2026-01-15 13:25:17 +08:00
return cleanFunctionParametersWithDepth ( params , 0 )
}
func cleanFunctionParametersWithDepth ( params interface { } , depth int ) interface { } {
2025-04-10 22:35:03 +08:00
if params == nil {
return nil
}
2026-01-15 13:25:17 +08:00
if depth >= geminiFunctionSchemaMaxDepth {
return cleanFunctionParametersShallow ( params )
}
2025-05-23 20:02:50 +08:00
switch v := params . ( type ) {
case map [ string ] interface { } :
2026-01-15 13:25:17 +08:00
// Keep only Gemini-supported OpenAPI schema subset fields (per official SDK Schema).
cleanedMap := make ( map [ string ] interface { } , len ( v ) )
2025-05-23 20:02:50 +08:00
for k , val := range v {
2026-01-15 13:25:17 +08:00
if _ , ok := geminiOpenAPISchemaAllowedFields [ k ] ; ok {
cleanedMap [ k ] = val
2025-05-23 20:02:50 +08:00
}
}
2025-04-10 22:35:03 +08:00
2026-01-15 13:25:17 +08:00
normalizeGeminiSchemaTypeAndNullable ( cleanedMap )
2025-05-23 20:02:50 +08:00
// Clean properties
if props , ok := cleanedMap [ "properties" ] . ( map [ string ] interface { } ) ; ok && props != nil {
cleanedProps := make ( map [ string ] interface { } )
for propName , propValue := range props {
2026-01-15 13:25:17 +08:00
cleanedProps [ propName ] = cleanFunctionParametersWithDepth ( propValue , depth + 1 )
2025-04-10 22:35:03 +08:00
}
2025-05-23 20:02:50 +08:00
cleanedMap [ "properties" ] = cleanedProps
}
2025-04-10 22:35:03 +08:00
2025-05-23 20:02:50 +08:00
// Recursively clean items in arrays
if items , ok := cleanedMap [ "items" ] . ( map [ string ] interface { } ) ; ok && items != nil {
2026-01-15 13:25:17 +08:00
cleanedMap [ "items" ] = cleanFunctionParametersWithDepth ( items , depth + 1 )
2025-05-23 20:02:50 +08:00
}
2026-01-15 13:25:17 +08:00
// OpenAPI tuple-style items is not supported by Gemini SDK Schema; keep first to avoid API rejection.
if itemsArray , ok := cleanedMap [ "items" ] . ( [ ] interface { } ) ; ok && len ( itemsArray ) > 0 {
cleanedMap [ "items" ] = cleanFunctionParametersWithDepth ( itemsArray [ 0 ] , depth + 1 )
2025-04-10 22:35:03 +08:00
}
2026-01-15 13:25:17 +08:00
// Recursively clean anyOf
if nested , ok := cleanedMap [ "anyOf" ] . ( [ ] interface { } ) ; ok && nested != nil {
cleanedNested := make ( [ ] interface { } , len ( nested ) )
for i , item := range nested {
cleanedNested [ i ] = cleanFunctionParametersWithDepth ( item , depth + 1 )
2025-04-10 22:35:03 +08:00
}
2026-01-15 13:25:17 +08:00
cleanedMap [ "anyOf" ] = cleanedNested
2025-04-10 22:35:03 +08:00
}
2025-05-23 20:02:50 +08:00
return cleanedMap
case [ ] interface { } :
// Handle arrays of schemas
cleanedArray := make ( [ ] interface { } , len ( v ) )
for i , item := range v {
2026-01-15 13:25:17 +08:00
cleanedArray [ i ] = cleanFunctionParametersWithDepth ( item , depth + 1 )
2025-05-23 20:02:50 +08:00
}
return cleanedArray
default :
// Not a map or array, return as is (e.g., could be a primitive)
return params
}
2025-04-10 22:35:03 +08:00
}
2026-01-15 13:25:17 +08:00
func cleanFunctionParametersShallow ( params interface { } ) interface { } {
switch v := params . ( type ) {
case map [ string ] interface { } :
cleanedMap := make ( map [ string ] interface { } , len ( v ) )
for k , val := range v {
if _ , ok := geminiOpenAPISchemaAllowedFields [ k ] ; ok {
cleanedMap [ k ] = val
}
}
normalizeGeminiSchemaTypeAndNullable ( cleanedMap )
// Stop recursion and avoid retaining huge nested structures.
delete ( cleanedMap , "properties" )
delete ( cleanedMap , "items" )
delete ( cleanedMap , "anyOf" )
return cleanedMap
case [ ] interface { } :
// Prefer an empty list over deep recursion on attacker-controlled inputs.
return [ ] interface { } { }
default :
return params
}
}
func normalizeGeminiSchemaTypeAndNullable ( schema map [ string ] interface { } ) {
rawType , ok := schema [ "type" ]
if ! ok || rawType == nil {
return
}
normalize := func ( t string ) ( string , bool ) {
switch strings . ToLower ( strings . TrimSpace ( t ) ) {
case "object" :
return "OBJECT" , false
case "array" :
return "ARRAY" , false
case "string" :
return "STRING" , false
case "integer" :
return "INTEGER" , false
case "number" :
return "NUMBER" , false
case "boolean" :
return "BOOLEAN" , false
case "null" :
return "" , true
default :
return t , false
}
}
switch t := rawType . ( type ) {
case string :
normalized , isNull := normalize ( t )
if isNull {
schema [ "nullable" ] = true
delete ( schema , "type" )
return
}
schema [ "type" ] = normalized
case [ ] interface { } :
nullable := false
var chosen string
for _ , item := range t {
if s , ok := item . ( string ) ; ok {
normalized , isNull := normalize ( s )
if isNull {
nullable = true
continue
}
if chosen == "" {
chosen = normalized
}
}
}
if nullable {
schema [ "nullable" ] = true
}
if chosen != "" {
schema [ "type" ] = chosen
} else {
delete ( schema , "type" )
}
}
}
2024-12-21 16:01:17 +08:00
func removeAdditionalPropertiesWithDepth ( schema interface { } , depth int ) interface { } {
if depth >= 5 {
return schema
}
v , ok := schema . ( map [ string ] interface { } )
if ! ok || len ( v ) == 0 {
return schema
}
2024-12-26 00:24:45 +08:00
// 删除所有的title字段
delete ( v , "title" )
2025-05-07 18:08:56 +08:00
delete ( v , "$schema" )
2024-12-21 16:01:17 +08:00
// 如果type不为object和array, 则直接返回
if typeVal , exists := v [ "type" ] ; ! exists || ( typeVal != "object" && typeVal != "array" ) {
return schema
}
switch v [ "type" ] {
case "object" :
delete ( v , "additionalProperties" )
// 处理 properties
if properties , ok := v [ "properties" ] . ( map [ string ] interface { } ) ; ok {
for key , value := range properties {
properties [ key ] = removeAdditionalPropertiesWithDepth ( value , depth + 1 )
}
}
for _ , field := range [ ] string { "allOf" , "anyOf" , "oneOf" } {
if nested , ok := v [ field ] . ( [ ] interface { } ) ; ok {
for i , item := range nested {
nested [ i ] = removeAdditionalPropertiesWithDepth ( item , depth + 1 )
}
}
}
case "array" :
if items , ok := v [ "items" ] . ( map [ string ] interface { } ) ; ok {
v [ "items" ] = removeAdditionalPropertiesWithDepth ( items , depth + 1 )
}
}
return v
}
2024-12-29 03:58:21 +08:00
func unescapeString ( s string ) ( string , error ) {
var result [ ] rune
escaped := false
i := 0
for i < len ( s ) {
r , size := utf8 . DecodeRuneInString ( s [ i : ] ) // 正确解码UTF-8字符
if r == utf8 . RuneError {
return "" , fmt . Errorf ( "invalid UTF-8 encoding" )
}
if escaped {
// 如果是转义符后的字符,检查其类型
switch r {
case '"' :
result = append ( result , '"' )
case '\\' :
result = append ( result , '\\' )
case '/' :
result = append ( result , '/' )
case 'b' :
result = append ( result , '\b' )
case 'f' :
result = append ( result , '\f' )
case 'n' :
result = append ( result , '\n' )
case 'r' :
result = append ( result , '\r' )
case 't' :
result = append ( result , '\t' )
case '\'' :
result = append ( result , '\'' )
default :
// 如果遇到一个非法的转义字符,直接按原样输出
result = append ( result , '\\' , r )
}
escaped = false
} else {
if r == '\\' {
escaped = true // 记录反斜杠作为转义符
} else {
result = append ( result , r )
}
}
i += size // 移动到下一个字符
}
return string ( result ) , nil
}
func unescapeMapOrSlice ( data interface { } ) interface { } {
switch v := data . ( type ) {
case map [ string ] interface { } :
for k , val := range v {
v [ k ] = unescapeMapOrSlice ( val )
}
case [ ] interface { } :
for i , val := range v {
v [ i ] = unescapeMapOrSlice ( val )
}
case string :
if unescaped , err := unescapeString ( v ) ; err != nil {
return v
} else {
return unescaped
}
}
return data
}
2023-12-18 23:45:08 +08:00
2025-08-01 22:23:35 +08:00
func getResponseToolCall ( item * dto . GeminiPart ) * dto . ToolCallResponse {
2024-12-29 03:58:21 +08:00
var argsBytes [ ] byte
var err error
2026-02-03 11:47:45 +08:00
// 移除 unescapeMapOrSlice 调用,直接使用 json.Marshal
// JSON 序列化/反序列化已经正确处理了转义字符
argsBytes , err = json . Marshal ( item . FunctionCall . Arguments )
2024-12-29 03:58:21 +08:00
2024-07-18 20:28:47 +08:00
if err != nil {
2024-12-23 01:26:14 +08:00
return nil
2024-07-18 20:28:47 +08:00
}
2025-02-26 23:56:10 +08:00
return & dto . ToolCallResponse {
2024-07-18 20:28:47 +08:00
ID : fmt . Sprintf ( "call_%s" , common . GetUUID ( ) ) ,
Type : "function" ,
2025-02-26 23:56:10 +08:00
Function : dto . FunctionResponse {
2024-12-29 03:58:21 +08:00
Arguments : string ( argsBytes ) ,
2024-07-18 20:28:47 +08:00
Name : item . FunctionCall . FunctionName ,
} ,
}
}
2026-02-17 15:45:14 +08:00
func buildUsageFromGeminiMetadata ( metadata dto . GeminiUsageMetadata , fallbackPromptTokens int ) dto . Usage {
promptTokens := metadata . PromptTokenCount + metadata . ToolUsePromptTokenCount
if promptTokens <= 0 && fallbackPromptTokens > 0 {
promptTokens = fallbackPromptTokens
}
usage := dto . Usage {
PromptTokens : promptTokens ,
CompletionTokens : metadata . CandidatesTokenCount + metadata . ThoughtsTokenCount ,
TotalTokens : metadata . TotalTokenCount ,
}
usage . CompletionTokenDetails . ReasoningTokens = metadata . ThoughtsTokenCount
usage . PromptTokensDetails . CachedTokens = metadata . CachedContentTokenCount
for _ , detail := range metadata . PromptTokensDetails {
if detail . Modality == "AUDIO" {
usage . PromptTokensDetails . AudioTokens += detail . TokenCount
} else if detail . Modality == "TEXT" {
usage . PromptTokensDetails . TextTokens += detail . TokenCount
}
}
for _ , detail := range metadata . ToolUsePromptTokensDetails {
if detail . Modality == "AUDIO" {
usage . PromptTokensDetails . AudioTokens += detail . TokenCount
} else if detail . Modality == "TEXT" {
usage . PromptTokensDetails . TextTokens += detail . TokenCount
}
}
2026-03-17 15:29:43 +08:00
for _ , detail := range metadata . CandidatesTokensDetails {
switch detail . Modality {
case "IMAGE" :
usage . CompletionTokenDetails . ImageTokens += detail . TokenCount
case "AUDIO" :
usage . CompletionTokenDetails . AudioTokens += detail . TokenCount
}
}
2026-02-17 15:45:14 +08:00
if usage . TotalTokens > 0 && usage . CompletionTokens <= 0 {
usage . CompletionTokens = usage . TotalTokens - usage . PromptTokens
}
if usage . PromptTokens > 0 && usage . PromptTokensDetails . TextTokens == 0 && usage . PromptTokensDetails . AudioTokens == 0 {
usage . PromptTokensDetails . TextTokens = usage . PromptTokens
}
return usage
}
2025-08-01 22:23:35 +08:00
func responseGeminiChat2OpenAI ( c * gin . Context , response * dto . GeminiChatResponse ) * dto . OpenAITextResponse {
2024-02-29 01:08:18 +08:00
fullTextResponse := dto . OpenAITextResponse {
2025-06-16 21:02:27 +08:00
Id : helper . GetResponseID ( c ) ,
2023-12-18 23:45:08 +08:00
Object : "chat.completion" ,
Created : common . GetTimestamp ( ) ,
2024-02-29 01:08:18 +08:00
Choices : make ( [ ] dto . OpenAITextResponseChoice , 0 , len ( response . Candidates ) ) ,
2023-12-18 23:45:08 +08:00
}
2025-02-26 23:40:16 +08:00
isToolCall := false
2024-12-24 20:46:02 +08:00
for _ , candidate := range response . Candidates {
2024-02-29 01:08:18 +08:00
choice := dto . OpenAITextResponseChoice {
2024-12-24 20:46:02 +08:00
Index : int ( candidate . Index ) ,
2024-02-29 01:08:18 +08:00
Message : dto . Message {
2023-12-18 23:45:08 +08:00
Role : "assistant" ,
2025-06-07 23:05:01 +08:00
Content : "" ,
2023-12-18 23:45:08 +08:00
} ,
2024-12-06 14:31:27 +08:00
FinishReason : constant . FinishReasonStop ,
2023-12-18 23:45:08 +08:00
}
if len ( candidate . Content . Parts ) > 0 {
2024-12-23 01:26:14 +08:00
var texts [ ] string
2025-02-26 23:56:10 +08:00
var toolCalls [ ] dto . ToolCallResponse
2024-12-23 01:26:14 +08:00
for _ , part := range candidate . Content . Parts {
2025-08-27 12:14:50 +08:00
if part . InlineData != nil {
// 媒体内容
if strings . HasPrefix ( part . InlineData . MimeType , "image" ) {
imgText := ""
texts = append ( texts , imgText )
} else {
// 其他媒体类型,直接显示链接
texts = append ( texts , fmt . Sprintf ( "[media](data:%s;base64,%s)" , part . InlineData . MimeType , part . InlineData . Data ) )
}
} else if part . FunctionCall != nil {
2024-12-23 01:26:14 +08:00
choice . FinishReason = constant . FinishReasonToolCalls
2025-02-26 23:56:10 +08:00
if call := getResponseToolCall ( & part ) ; call != nil {
2025-02-26 23:40:16 +08:00
toolCalls = append ( toolCalls , * call )
2024-12-23 01:26:14 +08:00
}
2025-05-22 16:11:50 +08:00
} else if part . Thought {
choice . Message . ReasoningContent = part . Text
2024-12-23 01:26:14 +08:00
} else {
2024-12-24 20:46:02 +08:00
if part . ExecutableCode != nil {
texts = append ( texts , "```" + part . ExecutableCode . Language + "\n" + part . ExecutableCode . Code + "\n```" )
} else if part . CodeExecutionResult != nil {
texts = append ( texts , "```output\n" + part . CodeExecutionResult . Output + "\n```" )
} else {
// 过滤掉空行
if part . Text != "\n" {
texts = append ( texts , part . Text )
}
}
2024-12-20 13:20:07 +08:00
}
2024-07-18 20:28:47 +08:00
}
2025-02-26 23:40:16 +08:00
if len ( toolCalls ) > 0 {
choice . Message . SetToolCalls ( toolCalls )
isToolCall = true
2024-12-24 20:46:02 +08:00
}
2024-12-23 01:26:14 +08:00
choice . Message . SetStringContent ( strings . Join ( texts , "\n" ) )
2024-12-24 20:46:02 +08:00
2023-12-18 23:45:08 +08:00
}
2024-12-24 20:46:02 +08:00
if candidate . FinishReason != nil {
switch * candidate . FinishReason {
case "STOP" :
choice . FinishReason = constant . FinishReasonStop
case "MAX_TOKENS" :
choice . FinishReason = constant . FinishReasonLength
2025-12-27 18:22:30 +08:00
case "SAFETY" :
// Safety filter triggered
choice . FinishReason = constant . FinishReasonContentFilter
case "RECITATION" :
// Recitation (citation) detected
choice . FinishReason = constant . FinishReasonContentFilter
case "BLOCKLIST" :
// Blocklist triggered
choice . FinishReason = constant . FinishReasonContentFilter
case "PROHIBITED_CONTENT" :
// Prohibited content detected
choice . FinishReason = constant . FinishReasonContentFilter
case "SPII" :
// Sensitive personally identifiable information
choice . FinishReason = constant . FinishReasonContentFilter
case "OTHER" :
// Other reasons
choice . FinishReason = constant . FinishReasonContentFilter
2024-12-24 20:46:02 +08:00
default :
choice . FinishReason = constant . FinishReasonContentFilter
}
}
2025-02-26 23:40:16 +08:00
if isToolCall {
2024-12-24 20:46:02 +08:00
choice . FinishReason = constant . FinishReasonToolCalls
}
2023-12-18 23:45:08 +08:00
fullTextResponse . Choices = append ( fullTextResponse . Choices , choice )
}
return & fullTextResponse
}
2025-08-01 22:42:48 +08:00
func streamResponseGeminiChat2OpenAI ( geminiResponse * dto . GeminiChatResponse ) ( * dto . ChatCompletionsStreamResponse , bool ) {
2024-12-24 20:46:02 +08:00
choices := make ( [ ] dto . ChatCompletionsStreamResponseChoice , 0 , len ( geminiResponse . Candidates ) )
2025-03-05 19:47:41 +08:00
isStop := false
2024-12-24 20:46:02 +08:00
for _ , candidate := range geminiResponse . Candidates {
if candidate . FinishReason != nil && * candidate . FinishReason == "STOP" {
2025-03-05 19:47:41 +08:00
isStop = true
2024-12-24 20:46:02 +08:00
candidate . FinishReason = nil
}
choice := dto . ChatCompletionsStreamResponseChoice {
Index : int ( candidate . Index ) ,
Delta : dto . ChatCompletionsStreamResponseChoiceDelta {
2025-07-26 13:31:33 +08:00
//Role: "assistant",
2024-12-24 20:46:02 +08:00
} ,
}
2024-12-23 01:26:14 +08:00
var texts [ ] string
2024-12-24 20:46:02 +08:00
isTools := false
2025-05-22 15:52:23 +08:00
isThought := false
2024-12-24 20:46:02 +08:00
if candidate . FinishReason != nil {
2025-12-27 18:22:30 +08:00
// Map Gemini FinishReason to OpenAI finish_reason
2024-12-24 20:46:02 +08:00
switch * candidate . FinishReason {
case "STOP" :
2025-12-27 18:22:30 +08:00
// Normal completion
2024-12-24 20:46:02 +08:00
choice . FinishReason = & constant . FinishReasonStop
case "MAX_TOKENS" :
2025-12-27 18:22:30 +08:00
// Reached maximum token limit
2024-12-24 20:46:02 +08:00
choice . FinishReason = & constant . FinishReasonLength
2025-12-27 18:22:30 +08:00
case "SAFETY" :
// Safety filter triggered
choice . FinishReason = & constant . FinishReasonContentFilter
case "RECITATION" :
// Recitation (citation) detected
choice . FinishReason = & constant . FinishReasonContentFilter
case "BLOCKLIST" :
// Blocklist triggered
choice . FinishReason = & constant . FinishReasonContentFilter
case "PROHIBITED_CONTENT" :
// Prohibited content detected
choice . FinishReason = & constant . FinishReasonContentFilter
case "SPII" :
// Sensitive personally identifiable information
choice . FinishReason = & constant . FinishReasonContentFilter
case "OTHER" :
// Other reasons
choice . FinishReason = & constant . FinishReasonContentFilter
2024-12-24 20:46:02 +08:00
default :
2025-12-27 18:22:30 +08:00
// Unknown reason, treat as content filter
2024-12-24 20:46:02 +08:00
choice . FinishReason = & constant . FinishReasonContentFilter
}
}
for _ , part := range candidate . Content . Parts {
2025-04-15 02:32:51 +08:00
if part . InlineData != nil {
if strings . HasPrefix ( part . InlineData . MimeType , "image" ) {
imgText := ""
texts = append ( texts , imgText )
}
} else if part . FunctionCall != nil {
2024-12-24 20:46:02 +08:00
isTools = true
2025-02-26 23:56:10 +08:00
if call := getResponseToolCall ( & part ) ; call != nil {
2024-12-28 17:46:56 +08:00
call . SetIndex ( len ( choice . Delta . ToolCalls ) )
2024-12-24 20:46:02 +08:00
choice . Delta . ToolCalls = append ( choice . Delta . ToolCalls , * call )
2024-12-23 01:26:14 +08:00
}
2025-08-08 16:45:37 +08:00
2025-05-22 15:52:23 +08:00
} else if part . Thought {
isThought = true
texts = append ( texts , part . Text )
2024-12-23 01:26:14 +08:00
} else {
2024-12-24 20:46:02 +08:00
if part . ExecutableCode != nil {
texts = append ( texts , "```" + part . ExecutableCode . Language + "\n" + part . ExecutableCode . Code + "\n```\n" )
} else if part . CodeExecutionResult != nil {
texts = append ( texts , "```output\n" + part . CodeExecutionResult . Output + "\n```\n" )
} else {
if part . Text != "\n" {
texts = append ( texts , part . Text )
}
}
2024-12-20 20:24:49 +08:00
}
2024-12-23 01:26:14 +08:00
}
2025-05-22 15:52:23 +08:00
if isThought {
choice . Delta . SetReasoningContent ( strings . Join ( texts , "\n" ) )
} else {
choice . Delta . SetContentString ( strings . Join ( texts , "\n" ) )
}
2024-12-24 20:46:02 +08:00
if isTools {
choice . FinishReason = & constant . FinishReasonToolCalls
2024-12-23 01:26:14 +08:00
}
2024-12-24 20:46:02 +08:00
choices = append ( choices , choice )
2024-07-18 20:28:47 +08:00
}
2024-12-24 20:46:02 +08:00
2024-02-29 01:08:18 +08:00
var response dto . ChatCompletionsStreamResponse
2023-12-18 23:45:08 +08:00
response . Object = "chat.completion.chunk"
2024-12-24 20:46:02 +08:00
response . Choices = choices
2025-08-01 17:58:21 +08:00
return & response , isStop
2023-12-18 23:45:08 +08:00
}
2025-07-26 13:31:33 +08:00
func handleStream ( c * gin . Context , info * relaycommon . RelayInfo , resp * dto . ChatCompletionsStreamResponse ) error {
streamData , err := common . Marshal ( resp )
if err != nil {
return fmt . Errorf ( "failed to marshal stream response: %w" , err )
}
err = openai . HandleStreamFormat ( c , info , string ( streamData ) , info . ChannelSetting . ForceFormat , info . ChannelSetting . ThinkingToContent )
if err != nil {
return fmt . Errorf ( "failed to handle stream format: %w" , err )
}
return nil
}
func handleFinalStream ( c * gin . Context , info * relaycommon . RelayInfo , resp * dto . ChatCompletionsStreamResponse ) error {
streamData , err := common . Marshal ( resp )
if err != nil {
return fmt . Errorf ( "failed to marshal stream response: %w" , err )
}
2025-08-04 09:06:57 +08:00
openai . HandleFinalResponse ( c , info , string ( streamData ) , resp . Id , resp . Created , resp . Model , resp . GetSystemFingerprint ( ) , resp . Usage , false )
2025-07-26 13:31:33 +08:00
return nil
}
2025-11-21 18:16:40 +08:00
func geminiStreamHandler ( c * gin . Context , info * relaycommon . RelayInfo , resp * http . Response , callback func ( data string , geminiResponse * dto . GeminiChatResponse ) bool ) ( * dto . Usage , * types . NewAPIError ) {
2024-07-10 16:01:09 +08:00
var usage = & dto . Usage { }
2025-04-15 02:32:51 +08:00
var imageCount int
2025-11-21 18:16:40 +08:00
responseText := strings . Builder { }
2025-03-05 19:47:41 +08:00
2026-03-31 16:50:24 +08:00
helper . StreamScannerHandler ( c , resp , info , func ( data string , sr * helper . StreamResult ) {
2025-08-01 22:23:35 +08:00
var geminiResponse dto . GeminiChatResponse
2026-03-31 16:50:24 +08:00
if err := common . UnmarshalJsonStr ( data , & geminiResponse ) ; err != nil {
sr . Stop ( fmt . Errorf ( "unmarshal: %w" , err ) )
return
2023-12-18 23:45:08 +08:00
}
2024-07-18 20:28:47 +08:00
2026-01-25 14:52:18 +08:00
if len ( geminiResponse . Candidates ) == 0 && geminiResponse . PromptFeedback != nil && geminiResponse . PromptFeedback . BlockReason != nil {
common . SetContextKey ( c , constant . ContextKeyAdminRejectReason , fmt . Sprintf ( "gemini_block_reason=%s" , * geminiResponse . PromptFeedback . BlockReason ) )
}
2025-11-21 18:16:40 +08:00
// 统计图片数量
2025-08-01 17:58:21 +08:00
for _ , candidate := range geminiResponse . Candidates {
for _ , part := range candidate . Content . Parts {
if part . InlineData != nil && part . InlineData . MimeType != "" {
imageCount ++
}
if part . Text != "" {
responseText . WriteString ( part . Text )
}
}
2025-04-15 02:32:51 +08:00
}
2025-08-01 17:58:21 +08:00
2025-11-21 18:16:40 +08:00
// 更新使用量统计
2024-07-18 20:28:47 +08:00
if geminiResponse . UsageMetadata . TotalTokenCount != 0 {
2026-02-17 15:45:14 +08:00
mappedUsage := buildUsageFromGeminiMetadata ( geminiResponse . UsageMetadata , info . GetEstimatePromptTokens ( ) )
* usage = mappedUsage
2023-12-18 23:45:08 +08:00
}
2025-11-21 18:16:40 +08:00
2026-03-31 16:50:24 +08:00
if ! callback ( data , & geminiResponse ) {
sr . Stop ( fmt . Errorf ( "gemini callback stopped" ) )
}
2025-11-21 18:16:40 +08:00
} )
if imageCount != 0 {
if usage . CompletionTokens == 0 {
usage . CompletionTokens = imageCount * 1400
}
}
if usage . CompletionTokens <= 0 {
2026-02-05 15:57:17 +08:00
if info . ReceivedResponseCount > 0 {
2025-12-02 21:34:39 +08:00
usage = service . ResponseText2Usage ( c , responseText . String ( ) , info . UpstreamModelName , info . GetEstimatePromptTokens ( ) )
2025-11-21 18:16:40 +08:00
} else {
usage = & dto . Usage { }
}
}
return usage , nil
}
func GeminiChatStreamHandler ( c * gin . Context , info * relaycommon . RelayInfo , resp * http . Response ) ( * dto . Usage , * types . NewAPIError ) {
id := helper . GetResponseID ( c )
createAt := common . GetTimestamp ( )
finishReason := constant . FinishReasonStop
2026-01-20 22:03:19 +08:00
toolCallIndexByChoice := make ( map [ int ] map [ string ] int )
nextToolCallIndexByChoice := make ( map [ int ] int )
2025-11-21 18:16:40 +08:00
usage , err := geminiStreamHandler ( c , info , resp , func ( data string , geminiResponse * dto . GeminiChatResponse ) bool {
response , isStop := streamResponseGeminiChat2OpenAI ( geminiResponse )
response . Id = id
response . Created = createAt
response . Model = info . UpstreamModelName
2026-01-20 22:03:19 +08:00
for choiceIdx := range response . Choices {
choiceKey := response . Choices [ choiceIdx ] . Index
for toolIdx := range response . Choices [ choiceIdx ] . Delta . ToolCalls {
tool := & response . Choices [ choiceIdx ] . Delta . ToolCalls [ toolIdx ]
if tool . ID == "" {
continue
}
m := toolCallIndexByChoice [ choiceKey ]
if m == nil {
m = make ( map [ string ] int )
toolCallIndexByChoice [ choiceKey ] = m
}
if idx , ok := m [ tool . ID ] ; ok {
tool . SetIndex ( idx )
continue
}
idx := nextToolCallIndexByChoice [ choiceKey ]
nextToolCallIndexByChoice [ choiceKey ] = idx + 1
m [ tool . ID ] = idx
tool . SetIndex ( idx )
}
}
2025-11-21 18:16:40 +08:00
2025-08-17 19:08:06 +08:00
logger . LogDebug ( c , fmt . Sprintf ( "info.SendResponseCount = %d" , info . SendResponseCount ) )
2025-08-01 17:04:16 +08:00
if info . SendResponseCount == 0 {
2025-07-26 13:31:33 +08:00
// send first response
2025-08-08 16:45:37 +08:00
emptyResponse := helper . GenerateStartEmptyResponse ( id , createAt , info . UpstreamModelName , nil )
if response . IsToolCall ( ) {
2025-10-04 13:02:35 +08:00
if len ( emptyResponse . Choices ) > 0 && len ( response . Choices ) > 0 {
toolCalls := response . Choices [ 0 ] . Delta . ToolCalls
copiedToolCalls := make ( [ ] dto . ToolCallResponse , len ( toolCalls ) )
for idx := range toolCalls {
copiedToolCalls [ idx ] = toolCalls [ idx ]
copiedToolCalls [ idx ] . Function . Arguments = ""
}
emptyResponse . Choices [ 0 ] . Delta . ToolCalls = copiedToolCalls
}
2025-08-08 16:45:37 +08:00
finishReason = constant . FinishReasonToolCalls
2025-11-21 18:16:40 +08:00
err := handleStream ( c , info , emptyResponse )
2025-08-08 16:45:37 +08:00
if err != nil {
2025-08-14 20:05:06 +08:00
logger . LogError ( c , err . Error ( ) )
2025-08-08 16:45:37 +08:00
}
response . ClearToolCalls ( )
if response . IsFinished ( ) {
response . Choices [ 0 ] . FinishReason = nil
}
2025-08-17 19:08:06 +08:00
} else {
2025-11-21 18:16:40 +08:00
err := handleStream ( c , info , emptyResponse )
2025-08-17 19:08:06 +08:00
if err != nil {
logger . LogError ( c , err . Error ( ) )
}
2025-07-26 13:31:33 +08:00
}
}
2025-11-21 18:16:40 +08:00
err := handleStream ( c , info , response )
2024-07-18 20:28:47 +08:00
if err != nil {
2025-08-14 20:05:06 +08:00
logger . LogError ( c , err . Error ( ) )
2024-07-10 16:01:09 +08:00
}
2025-03-05 19:47:41 +08:00
if isStop {
2025-08-08 16:45:37 +08:00
_ = handleStream ( c , info , helper . GenerateStopResponse ( id , createAt , info . UpstreamModelName , finishReason ) )
2024-12-24 20:46:02 +08:00
}
2025-03-05 19:47:41 +08:00
return true
} )
2024-07-19 17:16:20 +08:00
2025-11-21 18:16:40 +08:00
if err != nil {
return usage , err
2025-08-01 17:58:21 +08:00
}
2025-07-26 13:31:33 +08:00
response := helper . GenerateFinalUsageResponse ( id , createAt , info . UpstreamModelName , * usage )
2025-11-21 18:16:40 +08:00
handleErr := handleFinalStream ( c , info , response )
if handleErr != nil {
common . SysLog ( "send final response failed: " + handleErr . Error ( ) )
2024-07-10 16:01:09 +08:00
}
2025-07-10 15:02:40 +08:00
return usage , nil
2023-12-18 23:45:08 +08:00
}
2025-07-10 15:02:40 +08:00
func GeminiChatHandler ( c * gin . Context , info * relaycommon . RelayInfo , resp * http . Response ) ( * dto . Usage , * types . NewAPIError ) {
2023-12-18 23:45:08 +08:00
responseBody , err := io . ReadAll ( resp . Body )
if err != nil {
2025-07-30 18:39:19 +08:00
return nil , types . NewOpenAIError ( err , types . ErrorCodeBadResponseBody , http . StatusInternalServerError )
2023-12-18 23:45:08 +08:00
}
2025-08-14 20:05:06 +08:00
service . CloseResponseBodyGracefully ( resp )
2025-05-22 16:11:50 +08:00
if common . DebugEnabled {
println ( string ( responseBody ) )
}
2025-08-01 22:23:35 +08:00
var geminiResponse dto . GeminiChatResponse
2025-07-10 15:02:40 +08:00
err = common . Unmarshal ( responseBody , & geminiResponse )
2023-12-18 23:45:08 +08:00
if err != nil {
2025-07-30 18:39:19 +08:00
return nil , types . NewOpenAIError ( err , types . ErrorCodeBadResponseBody , http . StatusInternalServerError )
2023-12-18 23:45:08 +08:00
}
if len ( geminiResponse . Candidates ) == 0 {
2026-02-17 15:45:14 +08:00
usage := buildUsageFromGeminiMetadata ( geminiResponse . UsageMetadata , info . GetEstimatePromptTokens ( ) )
2026-01-25 14:32:51 +08:00
var newAPIError * types . NewAPIError
2025-12-27 18:22:30 +08:00
if geminiResponse . PromptFeedback != nil && geminiResponse . PromptFeedback . BlockReason != nil {
2026-01-25 14:52:18 +08:00
common . SetContextKey ( c , constant . ContextKeyAdminRejectReason , fmt . Sprintf ( "gemini_block_reason=%s" , * geminiResponse . PromptFeedback . BlockReason ) )
2026-01-25 14:32:51 +08:00
newAPIError = types . NewOpenAIError (
2025-12-27 18:22:30 +08:00
errors . New ( "request blocked by Gemini API: " + * geminiResponse . PromptFeedback . BlockReason ) ,
types . ErrorCodePromptBlocked ,
http . StatusBadRequest ,
)
} else {
2026-01-25 14:52:18 +08:00
common . SetContextKey ( c , constant . ContextKeyAdminRejectReason , "gemini_empty_candidates" )
2026-01-25 14:32:51 +08:00
newAPIError = types . NewOpenAIError (
2025-12-27 18:22:30 +08:00
errors . New ( "empty response from Gemini API" ) ,
types . ErrorCodeEmptyResponse ,
http . StatusInternalServerError ,
)
}
2026-01-25 14:32:51 +08:00
service . ResetStatusCode ( newAPIError , c . GetString ( "status_code_mapping" ) )
switch info . RelayFormat {
case types . RelayFormatClaude :
c . JSON ( newAPIError . StatusCode , gin . H {
"type" : "error" ,
"error" : newAPIError . ToClaudeError ( ) ,
} )
default :
c . JSON ( newAPIError . StatusCode , gin . H {
"error" : newAPIError . ToOpenAIError ( ) ,
} )
}
return & usage , nil
2023-12-18 23:45:08 +08:00
}
2025-06-16 21:02:27 +08:00
fullTextResponse := responseGeminiChat2OpenAI ( c , & geminiResponse )
2024-12-23 00:02:15 +08:00
fullTextResponse . Model = info . UpstreamModelName
2026-02-17 15:45:14 +08:00
usage := buildUsageFromGeminiMetadata ( geminiResponse . UsageMetadata , info . GetEstimatePromptTokens ( ) )
2025-06-07 12:26:23 +08:00
2023-12-18 23:45:08 +08:00
fullTextResponse . Usage = usage
2025-08-08 16:45:37 +08:00
switch info . RelayFormat {
2025-08-14 21:10:04 +08:00
case types . RelayFormatOpenAI :
2025-08-08 16:45:37 +08:00
responseBody , err = common . Marshal ( fullTextResponse )
if err != nil {
return nil , types . NewError ( err , types . ErrorCodeBadResponseBody )
}
2025-08-14 21:10:04 +08:00
case types . RelayFormatClaude :
2025-08-08 16:45:37 +08:00
claudeResp := service . ResponseOpenAI2Claude ( fullTextResponse , info )
claudeRespStr , err := common . Marshal ( claudeResp )
if err != nil {
return nil , types . NewError ( err , types . ErrorCodeBadResponseBody )
}
responseBody = claudeRespStr
2025-08-14 21:10:04 +08:00
case types . RelayFormatGemini :
2025-08-08 16:45:37 +08:00
break
2023-12-18 23:45:08 +08:00
}
2025-08-08 16:45:37 +08:00
2025-08-14 20:05:06 +08:00
service . IOCopyBytesGracefully ( c , resp , responseBody )
2025-08-08 16:45:37 +08:00
2025-07-10 15:02:40 +08:00
return & usage , nil
2023-12-18 23:45:08 +08:00
}
2025-03-10 23:32:06 +08:00
2025-07-10 15:02:40 +08:00
func GeminiEmbeddingHandler ( c * gin . Context , info * relaycommon . RelayInfo , resp * http . Response ) ( * dto . Usage , * types . NewAPIError ) {
2025-08-14 20:05:06 +08:00
defer service . CloseResponseBodyGracefully ( resp )
2025-06-28 00:02:07 +08:00
2025-03-10 23:32:06 +08:00
responseBody , readErr := io . ReadAll ( resp . Body )
if readErr != nil {
2025-07-30 18:39:19 +08:00
return nil , types . NewOpenAIError ( readErr , types . ErrorCodeBadResponseBody , http . StatusInternalServerError )
2025-03-10 23:32:06 +08:00
}
2025-08-04 13:02:57 +00:00
var geminiResponse dto . GeminiBatchEmbeddingResponse
2025-07-10 15:02:40 +08:00
if jsonErr := common . Unmarshal ( responseBody , & geminiResponse ) ; jsonErr != nil {
2025-07-30 18:39:19 +08:00
return nil , types . NewOpenAIError ( jsonErr , types . ErrorCodeBadResponseBody , http . StatusInternalServerError )
2025-03-10 23:32:06 +08:00
}
// convert to openai format response
openAIResponse := dto . OpenAIEmbeddingResponse {
Object : "list" ,
2025-08-04 13:02:57 +00:00
Data : make ( [ ] dto . OpenAIEmbeddingResponseItem , 0 , len ( geminiResponse . Embeddings ) ) ,
Model : info . UpstreamModelName ,
}
for i , embedding := range geminiResponse . Embeddings {
openAIResponse . Data = append ( openAIResponse . Data , dto . OpenAIEmbeddingResponseItem {
Object : "embedding" ,
Embedding : embedding . Values ,
Index : i ,
} )
2025-03-10 23:32:06 +08:00
}
// calculate usage
// https://ai.google.dev/gemini-api/docs/pricing?hl=zh-cn#text-embedding-004
// Google has not yet clarified how embedding models will be billed
// refer to openai billing method to use input tokens billing
// https://platform.openai.com/docs/guides/embeddings#what-are-embeddings
2025-12-02 21:34:39 +08:00
usage := service . ResponseText2Usage ( c , "" , info . UpstreamModelName , info . GetEstimatePromptTokens ( ) )
2025-07-10 15:02:40 +08:00
openAIResponse . Usage = * usage
2025-03-10 23:32:06 +08:00
2025-07-10 15:02:40 +08:00
jsonResponse , jsonErr := common . Marshal ( openAIResponse )
2025-03-10 23:32:06 +08:00
if jsonErr != nil {
2025-07-30 18:39:19 +08:00
return nil , types . NewOpenAIError ( jsonErr , types . ErrorCodeBadResponseBody , http . StatusInternalServerError )
2025-03-10 23:32:06 +08:00
}
2025-08-14 20:05:06 +08:00
service . IOCopyBytesGracefully ( c , resp , jsonResponse )
2025-03-10 23:32:06 +08:00
return usage , nil
}
2025-07-30 18:39:19 +08:00
func GeminiImageHandler ( c * gin . Context , info * relaycommon . RelayInfo , resp * http . Response ) ( * dto . Usage , * types . NewAPIError ) {
responseBody , readErr := io . ReadAll ( resp . Body )
if readErr != nil {
return nil , types . NewOpenAIError ( readErr , types . ErrorCodeBadResponseBody , http . StatusInternalServerError )
}
_ = resp . Body . Close ( )
2025-08-01 22:23:35 +08:00
var geminiResponse dto . GeminiImageResponse
2025-07-30 18:39:19 +08:00
if jsonErr := common . Unmarshal ( responseBody , & geminiResponse ) ; jsonErr != nil {
return nil , types . NewOpenAIError ( jsonErr , types . ErrorCodeBadResponseBody , http . StatusInternalServerError )
}
if len ( geminiResponse . Predictions ) == 0 {
return nil , types . NewOpenAIError ( errors . New ( "no images generated" ) , types . ErrorCodeBadResponseBody , http . StatusInternalServerError )
}
// convert to openai format response
openAIResponse := dto . ImageResponse {
Created : common . GetTimestamp ( ) ,
Data : make ( [ ] dto . ImageData , 0 , len ( geminiResponse . Predictions ) ) ,
}
for _ , prediction := range geminiResponse . Predictions {
if prediction . RaiFilteredReason != "" {
continue // skip filtered image
}
openAIResponse . Data = append ( openAIResponse . Data , dto . ImageData {
B64Json : prediction . BytesBase64Encoded ,
} )
}
jsonResponse , jsonErr := json . Marshal ( openAIResponse )
if jsonErr != nil {
return nil , types . NewError ( jsonErr , types . ErrorCodeBadResponseBody )
}
c . Writer . Header ( ) . Set ( "Content-Type" , "application/json" )
c . Writer . WriteHeader ( resp . StatusCode )
_ , _ = c . Writer . Write ( jsonResponse )
// https://github.com/google-gemini/cookbook/blob/719a27d752aac33f39de18a8d3cb42a70874917e/quickstarts/Counting_Tokens.ipynb
// each image has fixed 258 tokens
const imageTokens = 258
generatedImages := len ( openAIResponse . Data )
usage := & dto . Usage {
PromptTokens : imageTokens * generatedImages , // each generated image has fixed 258 tokens
CompletionTokens : 0 , // image generation does not calculate completion tokens
TotalTokens : imageTokens * generatedImages ,
}
return usage , nil
}
2025-12-27 18:22:30 +08:00
2026-01-09 18:00:40 +08:00
type GeminiModelsResponse struct {
2026-01-09 18:08:11 +08:00
Models [ ] dto . GeminiModel ` json:"models" `
2026-01-09 18:00:40 +08:00
NextPageToken string ` json:"nextPageToken" `
}
func FetchGeminiModels ( baseURL , apiKey , proxyURL string ) ( [ ] string , error ) {
client , err := service . GetHttpClientWithProxy ( proxyURL )
if err != nil {
return nil , fmt . Errorf ( "创建HTTP客户端失败: %v" , err )
}
allModels := make ( [ ] string , 0 )
nextPageToken := ""
maxPages := 100 // Safety limit to prevent infinite loops
for page := 0 ; page < maxPages ; page ++ {
url := fmt . Sprintf ( "%s/v1beta/models" , baseURL )
if nextPageToken != "" {
url = fmt . Sprintf ( "%s?pageToken=%s" , url , nextPageToken )
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
request , err := http . NewRequestWithContext ( ctx , "GET" , url , nil )
if err != nil {
cancel ( )
return nil , fmt . Errorf ( "创建请求失败: %v" , err )
}
request . Header . Set ( "x-goog-api-key" , apiKey )
response , err := client . Do ( request )
if err != nil {
cancel ( )
return nil , fmt . Errorf ( "请求失败: %v" , err )
}
if response . StatusCode != http . StatusOK {
body , _ := io . ReadAll ( response . Body )
response . Body . Close ( )
cancel ( )
return nil , fmt . Errorf ( "服务器返回错误 %d: %s" , response . StatusCode , string ( body ) )
}
body , err := io . ReadAll ( response . Body )
response . Body . Close ( )
cancel ( )
if err != nil {
return nil , fmt . Errorf ( "读取响应失败: %v" , err )
}
var modelsResponse GeminiModelsResponse
if err = common . Unmarshal ( body , & modelsResponse ) ; err != nil {
return nil , fmt . Errorf ( "解析响应失败: %v" , err )
}
for _ , model := range modelsResponse . Models {
2026-01-09 18:08:11 +08:00
modelNameValue , ok := model . Name . ( string )
if ! ok {
continue
}
modelName := strings . TrimPrefix ( modelNameValue , "models/" )
2026-01-09 18:00:40 +08:00
allModels = append ( allModels , modelName )
}
nextPageToken = modelsResponse . NextPageToken
if nextPageToken == "" {
break
}
}
return allModels , nil
}
2026-01-25 14:14:05 +08:00
2025-12-27 18:22:30 +08:00
// convertToolChoiceToGeminiConfig converts OpenAI tool_choice to Gemini toolConfig
// OpenAI tool_choice values:
// - "auto": Let the model decide (default)
// - "none": Don't call any tools
// - "required": Must call at least one tool
// - {"type": "function", "function": {"name": "xxx"}}: Call specific function
//
// Gemini functionCallingConfig.mode values:
// - "AUTO": Model decides whether to call functions
// - "NONE": Model won't call functions
// - "ANY": Model must call at least one function
func convertToolChoiceToGeminiConfig ( toolChoice any ) * dto . ToolConfig {
if toolChoice == nil {
return nil
}
// Handle string values: "auto", "none", "required"
if toolChoiceStr , ok := toolChoice . ( string ) ; ok {
config := & dto . ToolConfig {
FunctionCallingConfig : & dto . FunctionCallingConfig { } ,
}
switch toolChoiceStr {
case "auto" :
config . FunctionCallingConfig . Mode = "AUTO"
case "none" :
config . FunctionCallingConfig . Mode = "NONE"
case "required" :
config . FunctionCallingConfig . Mode = "ANY"
default :
// Unknown string value, default to AUTO
config . FunctionCallingConfig . Mode = "AUTO"
}
return config
}
// Handle object value: {"type": "function", "function": {"name": "xxx"}}
if toolChoiceMap , ok := toolChoice . ( map [ string ] interface { } ) ; ok {
if toolChoiceMap [ "type" ] == "function" {
config := & dto . ToolConfig {
FunctionCallingConfig : & dto . FunctionCallingConfig {
Mode : "ANY" ,
} ,
}
// Extract function name if specified
if function , ok := toolChoiceMap [ "function" ] . ( map [ string ] interface { } ) ; ok {
if name , ok := function [ "name" ] . ( string ) ; ok && name != "" {
config . FunctionCallingConfig . AllowedFunctionNames = [ ] string { name }
}
}
return config
}
// Unsupported map structure (type is not "function"), return nil
return nil
}
// Unsupported type, return nil
return nil
}