|
@@ -12,12 +12,72 @@ import (
|
|
|
"one-api/relay/helper"
|
|
"one-api/relay/helper"
|
|
|
"one-api/service"
|
|
"one-api/service"
|
|
|
"one-api/setting/model_setting"
|
|
"one-api/setting/model_setting"
|
|
|
|
|
+ "strconv"
|
|
|
"strings"
|
|
"strings"
|
|
|
"unicode/utf8"
|
|
"unicode/utf8"
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gin-gonic/gin"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+var geminiSupportedMimeTypes = map[string]bool{
|
|
|
|
|
+ "application/pdf": true,
|
|
|
|
|
+ "audio/mpeg": true,
|
|
|
|
|
+ "audio/mp3": true,
|
|
|
|
|
+ "audio/wav": true,
|
|
|
|
|
+ "image/png": true,
|
|
|
|
|
+ "image/jpeg": true,
|
|
|
|
|
+ "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,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Gemini 允许的思考预算范围
|
|
|
|
|
+const (
|
|
|
|
|
+ pro25MinBudget = 128
|
|
|
|
|
+ pro25MaxBudget = 32768
|
|
|
|
|
+ flash25MaxBudget = 24576
|
|
|
|
|
+ flash25LiteMinBudget = 512
|
|
|
|
|
+ flash25LiteMaxBudget = 24576
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// clampThinkingBudget 根据模型名称将预算限制在允许的范围内
|
|
|
|
|
+func clampThinkingBudget(modelName string, budget int) int {
|
|
|
|
|
+ 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")
|
|
|
|
|
+ is25FlashLite := strings.HasPrefix(modelName, "gemini-2.5-flash-lite")
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Setting safety to the lowest possible values since Gemini is already powerless enough
|
|
// Setting safety to the lowest possible values since Gemini is already powerless enough
|
|
|
func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*GeminiChatRequest, error) {
|
|
func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*GeminiChatRequest, error) {
|
|
|
|
|
|
|
@@ -39,18 +99,54 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
|
|
if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
|
|
|
- if strings.HasSuffix(info.OriginModelName, "-thinking") {
|
|
|
|
|
- budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
|
|
|
|
|
- if budgetTokens == 0 || budgetTokens > 24576 {
|
|
|
|
|
- budgetTokens = 24576
|
|
|
|
|
|
|
+ modelName := info.UpstreamModelName
|
|
|
|
|
+ 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)
|
|
|
|
|
+ if len(parts) == 2 && parts[1] != "" {
|
|
|
|
|
+ if budgetTokens, err := strconv.Atoi(parts[1]); err == nil {
|
|
|
|
|
+ clampedBudget := clampThinkingBudget(modelName, budgetTokens)
|
|
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
|
|
+ ThinkingBudget: common.GetPointer(clampedBudget),
|
|
|
|
|
+ IncludeThoughts: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
|
|
- ThinkingBudget: common.GetPointer(int(budgetTokens)),
|
|
|
|
|
- IncludeThoughts: true,
|
|
|
|
|
|
|
+ } else if strings.HasSuffix(modelName, "-thinking") {
|
|
|
|
|
+ unsupportedModels := []string{
|
|
|
|
|
+ "gemini-2.5-pro-preview-05-06",
|
|
|
|
|
+ "gemini-2.5-pro-preview-03-25",
|
|
|
}
|
|
}
|
|
|
- } else if strings.HasSuffix(info.OriginModelName, "-nothinking") {
|
|
|
|
|
- geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
|
|
- ThinkingBudget: common.GetPointer(0),
|
|
|
|
|
|
|
+ isUnsupported := false
|
|
|
|
|
+ for _, unsupportedModel := range unsupportedModels {
|
|
|
|
|
+ if strings.HasPrefix(modelName, unsupportedModel) {
|
|
|
|
|
+ isUnsupported = true
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if isUnsupported {
|
|
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
|
|
+ IncludeThoughts: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
|
|
+ IncludeThoughts: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ if geminiRequest.GenerationConfig.MaxOutputTokens > 0 {
|
|
|
|
|
+ budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
|
|
|
|
|
+ clampedBudget := clampThinkingBudget(modelName, int(budgetTokens))
|
|
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig.ThinkingBudget = common.GetPointer(clampedBudget)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if strings.HasSuffix(modelName, "-nothinking") {
|
|
|
|
|
+ if !isNew25Pro {
|
|
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
|
|
+ ThinkingBudget: common.GetPointer(0),
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -112,12 +208,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
|
|
// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
|
|
|
// json_data, _ := json.Marshal(geminiRequest.Tools)
|
|
// json_data, _ := json.Marshal(geminiRequest.Tools)
|
|
|
// common.SysLog("tools_json: " + string(json_data))
|
|
// common.SysLog("tools_json: " + string(json_data))
|
|
|
- } else if textRequest.Functions != nil {
|
|
|
|
|
- //geminiRequest.Tools = []GeminiChatTool{
|
|
|
|
|
- // {
|
|
|
|
|
- // FunctionDeclarations: textRequest.Functions,
|
|
|
|
|
- // },
|
|
|
|
|
- //}
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") {
|
|
if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") {
|
|
@@ -148,17 +238,27 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
} else if val, exists := tool_call_ids[message.ToolCallId]; exists {
|
|
} else if val, exists := tool_call_ids[message.ToolCallId]; exists {
|
|
|
name = val
|
|
name = val
|
|
|
}
|
|
}
|
|
|
- content := common.StrToMap(message.StringContent())
|
|
|
|
|
- functionResp := &FunctionResponse{
|
|
|
|
|
- Name: name,
|
|
|
|
|
- Response: GeminiFunctionResponseContent{
|
|
|
|
|
- Name: name,
|
|
|
|
|
- Content: content,
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ 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}
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- if content == nil {
|
|
|
|
|
- functionResp.Response.Content = message.StringContent()
|
|
|
|
|
|
|
+
|
|
|
|
|
+ functionResp := &FunctionResponse{
|
|
|
|
|
+ Name: name,
|
|
|
|
|
+ Response: contentMap,
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
*parts = append(*parts, GeminiPart{
|
|
*parts = append(*parts, GeminiPart{
|
|
|
FunctionResponse: functionResp,
|
|
FunctionResponse: functionResp,
|
|
|
})
|
|
})
|
|
@@ -208,14 +308,21 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
}
|
|
}
|
|
|
// 判断是否是url
|
|
// 判断是否是url
|
|
|
if strings.HasPrefix(part.GetImageMedia().Url, "http") {
|
|
if strings.HasPrefix(part.GetImageMedia().Url, "http") {
|
|
|
- // 是url,获取图片的类型和base64编码的数据
|
|
|
|
|
|
|
+ // 是url,获取文件的类型和base64编码的数据
|
|
|
fileData, err := service.GetFileBase64FromUrl(part.GetImageMedia().Url)
|
|
fileData, err := service.GetFileBase64FromUrl(part.GetImageMedia().Url)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("get file base64 from url failed: %s", err.Error())
|
|
|
|
|
|
|
+ return nil, fmt.Errorf("get file base64 from url '%s' failed: %w", part.GetImageMedia().Url, err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 校验 MimeType 是否在 Gemini 支持的白名单中
|
|
|
|
|
+ if _, ok := geminiSupportedMimeTypes[strings.ToLower(fileData.MimeType)]; !ok {
|
|
|
|
|
+ url := part.GetImageMedia().Url
|
|
|
|
|
+ return nil, fmt.Errorf("mime type is not supported by Gemini: '%s', url: '%s', supported types are: %v", fileData.MimeType, url, getSupportedMimeTypesList())
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
parts = append(parts, GeminiPart{
|
|
parts = append(parts, GeminiPart{
|
|
|
InlineData: &GeminiInlineData{
|
|
InlineData: &GeminiInlineData{
|
|
|
- MimeType: fileData.MimeType,
|
|
|
|
|
|
|
+ MimeType: fileData.MimeType, // 使用原始的 MimeType,因为大小写可能对API有意义
|
|
|
Data: fileData.Base64Data,
|
|
Data: fileData.Base64Data,
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
@@ -249,13 +356,13 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
if part.GetInputAudio().Data == "" {
|
|
if part.GetInputAudio().Data == "" {
|
|
|
return nil, fmt.Errorf("only base64 audio is supported in gemini")
|
|
return nil, fmt.Errorf("only base64 audio is supported in gemini")
|
|
|
}
|
|
}
|
|
|
- format, base64String, err := service.DecodeBase64FileData(part.GetInputAudio().Data)
|
|
|
|
|
|
|
+ base64String, err := service.DecodeBase64AudioData(part.GetInputAudio().Data)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("decode base64 audio data failed: %s", err.Error())
|
|
return nil, fmt.Errorf("decode base64 audio data failed: %s", err.Error())
|
|
|
}
|
|
}
|
|
|
parts = append(parts, GeminiPart{
|
|
parts = append(parts, GeminiPart{
|
|
|
InlineData: &GeminiInlineData{
|
|
InlineData: &GeminiInlineData{
|
|
|
- MimeType: format,
|
|
|
|
|
|
|
+ MimeType: "audio/" + part.GetInputAudio().Format,
|
|
|
Data: base64String,
|
|
Data: base64String,
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
@@ -268,7 +375,9 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
if content.Role == "assistant" {
|
|
if content.Role == "assistant" {
|
|
|
content.Role = "model"
|
|
content.Role = "model"
|
|
|
}
|
|
}
|
|
|
- geminiRequest.Contents = append(geminiRequest.Contents, content)
|
|
|
|
|
|
|
+ if len(content.Parts) > 0 {
|
|
|
|
|
+ geminiRequest.Contents = append(geminiRequest.Contents, content)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if len(system_content) > 0 {
|
|
if len(system_content) > 0 {
|
|
@@ -284,100 +393,126 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
return &geminiRequest, nil
|
|
return &geminiRequest, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 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
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// cleanFunctionParameters recursively removes unsupported fields from Gemini function parameters.
|
|
// cleanFunctionParameters recursively removes unsupported fields from Gemini function parameters.
|
|
|
func cleanFunctionParameters(params interface{}) interface{} {
|
|
func cleanFunctionParameters(params interface{}) interface{} {
|
|
|
if params == nil {
|
|
if params == nil {
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- paramMap, ok := params.(map[string]interface{})
|
|
|
|
|
- if !ok {
|
|
|
|
|
- // Not a map, return as is (e.g., could be an array or primitive)
|
|
|
|
|
- return params
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Create a copy to avoid modifying the original
|
|
|
|
|
- cleanedMap := make(map[string]interface{})
|
|
|
|
|
- for k, v := range paramMap {
|
|
|
|
|
- cleanedMap[k] = v
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ switch v := params.(type) {
|
|
|
|
|
+ case map[string]interface{}:
|
|
|
|
|
+ // Create a copy to avoid modifying the original
|
|
|
|
|
+ cleanedMap := make(map[string]interface{})
|
|
|
|
|
+ for k, val := range v {
|
|
|
|
|
+ cleanedMap[k] = val
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Remove unsupported root-level fields
|
|
|
|
|
- delete(cleanedMap, "default")
|
|
|
|
|
- delete(cleanedMap, "exclusiveMaximum")
|
|
|
|
|
- delete(cleanedMap, "exclusiveMinimum")
|
|
|
|
|
- delete(cleanedMap, "$schema")
|
|
|
|
|
- delete(cleanedMap, "additionalProperties")
|
|
|
|
|
|
|
+ // Remove unsupported root-level fields
|
|
|
|
|
+ delete(cleanedMap, "default")
|
|
|
|
|
+ delete(cleanedMap, "exclusiveMaximum")
|
|
|
|
|
+ delete(cleanedMap, "exclusiveMinimum")
|
|
|
|
|
+ delete(cleanedMap, "$schema")
|
|
|
|
|
+ delete(cleanedMap, "additionalProperties")
|
|
|
|
|
|
|
|
- // Clean properties
|
|
|
|
|
- if props, ok := cleanedMap["properties"].(map[string]interface{}); ok && props != nil {
|
|
|
|
|
- cleanedProps := make(map[string]interface{})
|
|
|
|
|
- for propName, propValue := range props {
|
|
|
|
|
- propMap, ok := propValue.(map[string]interface{})
|
|
|
|
|
- if !ok {
|
|
|
|
|
- cleanedProps[propName] = propValue // Keep non-map properties
|
|
|
|
|
- continue
|
|
|
|
|
|
|
+ // Check and clean 'format' for string types
|
|
|
|
|
+ if propType, typeExists := cleanedMap["type"].(string); typeExists && propType == "string" {
|
|
|
|
|
+ if formatValue, formatExists := cleanedMap["format"].(string); formatExists {
|
|
|
|
|
+ if formatValue != "enum" && formatValue != "date-time" {
|
|
|
|
|
+ delete(cleanedMap, "format")
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Create a copy of the property map
|
|
|
|
|
- cleanedPropMap := make(map[string]interface{})
|
|
|
|
|
- for k, v := range propMap {
|
|
|
|
|
- cleanedPropMap[k] = v
|
|
|
|
|
|
|
+ // Clean properties
|
|
|
|
|
+ if props, ok := cleanedMap["properties"].(map[string]interface{}); ok && props != nil {
|
|
|
|
|
+ cleanedProps := make(map[string]interface{})
|
|
|
|
|
+ for propName, propValue := range props {
|
|
|
|
|
+ cleanedProps[propName] = cleanFunctionParameters(propValue)
|
|
|
}
|
|
}
|
|
|
|
|
+ cleanedMap["properties"] = cleanedProps
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Remove unsupported fields
|
|
|
|
|
- delete(cleanedPropMap, "default")
|
|
|
|
|
- delete(cleanedPropMap, "exclusiveMaximum")
|
|
|
|
|
- delete(cleanedPropMap, "exclusiveMinimum")
|
|
|
|
|
- delete(cleanedPropMap, "$schema")
|
|
|
|
|
- delete(cleanedPropMap, "additionalProperties")
|
|
|
|
|
|
|
+ // Recursively clean items in arrays
|
|
|
|
|
+ if items, ok := cleanedMap["items"].(map[string]interface{}); ok && items != nil {
|
|
|
|
|
+ cleanedMap["items"] = cleanFunctionParameters(items)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Also handle items if it's an array of schemas
|
|
|
|
|
+ if itemsArray, ok := cleanedMap["items"].([]interface{}); ok {
|
|
|
|
|
+ cleanedItemsArray := make([]interface{}, len(itemsArray))
|
|
|
|
|
+ for i, item := range itemsArray {
|
|
|
|
|
+ cleanedItemsArray[i] = cleanFunctionParameters(item)
|
|
|
|
|
+ }
|
|
|
|
|
+ cleanedMap["items"] = cleanedItemsArray
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Check and clean 'format' for string types
|
|
|
|
|
- if propType, typeExists := cleanedPropMap["type"].(string); typeExists && propType == "string" {
|
|
|
|
|
- if formatValue, formatExists := cleanedPropMap["format"].(string); formatExists {
|
|
|
|
|
- if formatValue != "enum" && formatValue != "date-time" {
|
|
|
|
|
- delete(cleanedPropMap, "format")
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Recursively clean other schema composition keywords
|
|
|
|
|
+ for _, field := range []string{"allOf", "anyOf", "oneOf"} {
|
|
|
|
|
+ if nested, ok := cleanedMap[field].([]interface{}); ok {
|
|
|
|
|
+ cleanedNested := make([]interface{}, len(nested))
|
|
|
|
|
+ for i, item := range nested {
|
|
|
|
|
+ cleanedNested[i] = cleanFunctionParameters(item)
|
|
|
}
|
|
}
|
|
|
|
|
+ cleanedMap[field] = cleanedNested
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Recursively clean nested properties within this property if it's an object/array
|
|
|
|
|
- // Check the type before recursing
|
|
|
|
|
- if propType, typeExists := cleanedPropMap["type"].(string); typeExists && (propType == "object" || propType == "array") {
|
|
|
|
|
- cleanedProps[propName] = cleanFunctionParameters(cleanedPropMap)
|
|
|
|
|
- } else {
|
|
|
|
|
- cleanedProps[propName] = cleanedPropMap // Assign the cleaned map back if not recursing
|
|
|
|
|
|
|
+ // Recursively clean patternProperties
|
|
|
|
|
+ if patternProps, ok := cleanedMap["patternProperties"].(map[string]interface{}); ok {
|
|
|
|
|
+ cleanedPatternProps := make(map[string]interface{})
|
|
|
|
|
+ for pattern, schema := range patternProps {
|
|
|
|
|
+ cleanedPatternProps[pattern] = cleanFunctionParameters(schema)
|
|
|
}
|
|
}
|
|
|
|
|
+ cleanedMap["patternProperties"] = cleanedPatternProps
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ // Recursively clean definitions
|
|
|
|
|
+ if definitions, ok := cleanedMap["definitions"].(map[string]interface{}); ok {
|
|
|
|
|
+ cleanedDefinitions := make(map[string]interface{})
|
|
|
|
|
+ for defName, defSchema := range definitions {
|
|
|
|
|
+ cleanedDefinitions[defName] = cleanFunctionParameters(defSchema)
|
|
|
|
|
+ }
|
|
|
|
|
+ cleanedMap["definitions"] = cleanedDefinitions
|
|
|
}
|
|
}
|
|
|
- cleanedMap["properties"] = cleanedProps
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // Recursively clean items in arrays if needed (e.g., type: array, items: { ... })
|
|
|
|
|
- if items, ok := cleanedMap["items"].(map[string]interface{}); ok && items != nil {
|
|
|
|
|
- cleanedMap["items"] = cleanFunctionParameters(items)
|
|
|
|
|
- }
|
|
|
|
|
- // Also handle items if it's an array of schemas
|
|
|
|
|
- if itemsArray, ok := cleanedMap["items"].([]interface{}); ok {
|
|
|
|
|
- cleanedItemsArray := make([]interface{}, len(itemsArray))
|
|
|
|
|
- for i, item := range itemsArray {
|
|
|
|
|
- cleanedItemsArray[i] = cleanFunctionParameters(item)
|
|
|
|
|
|
|
+ // Recursively clean $defs (newer JSON Schema draft)
|
|
|
|
|
+ if defs, ok := cleanedMap["$defs"].(map[string]interface{}); ok {
|
|
|
|
|
+ cleanedDefs := make(map[string]interface{})
|
|
|
|
|
+ for defName, defSchema := range defs {
|
|
|
|
|
+ cleanedDefs[defName] = cleanFunctionParameters(defSchema)
|
|
|
|
|
+ }
|
|
|
|
|
+ cleanedMap["$defs"] = cleanedDefs
|
|
|
}
|
|
}
|
|
|
- cleanedMap["items"] = cleanedItemsArray
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // Recursively clean other schema composition keywords if necessary
|
|
|
|
|
- for _, field := range []string{"allOf", "anyOf", "oneOf"} {
|
|
|
|
|
- if nested, ok := cleanedMap[field].([]interface{}); ok {
|
|
|
|
|
- cleanedNested := make([]interface{}, len(nested))
|
|
|
|
|
- for i, item := range nested {
|
|
|
|
|
- cleanedNested[i] = cleanFunctionParameters(item)
|
|
|
|
|
|
|
+ // Clean conditional keywords
|
|
|
|
|
+ for _, field := range []string{"if", "then", "else", "not"} {
|
|
|
|
|
+ if nested, ok := cleanedMap[field]; ok {
|
|
|
|
|
+ cleanedMap[field] = cleanFunctionParameters(nested)
|
|
|
}
|
|
}
|
|
|
- cleanedMap[field] = cleanedNested
|
|
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- return cleanedMap
|
|
|
|
|
|
|
+ return cleanedMap
|
|
|
|
|
+
|
|
|
|
|
+ case []interface{}:
|
|
|
|
|
+ // Handle arrays of schemas
|
|
|
|
|
+ cleanedArray := make([]interface{}, len(v))
|
|
|
|
|
+ for i, item := range v {
|
|
|
|
|
+ cleanedArray[i] = cleanFunctionParameters(item)
|
|
|
|
|
+ }
|
|
|
|
|
+ return cleanedArray
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ // Not a map or array, return as is (e.g., could be a primitive)
|
|
|
|
|
+ return params
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interface{} {
|
|
func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interface{} {
|
|
@@ -512,21 +647,20 @@ func getResponseToolCall(item *GeminiPart) *dto.ToolCallResponse {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResponse {
|
|
|
|
|
|
|
+func responseGeminiChat2OpenAI(c *gin.Context, response *GeminiChatResponse) *dto.OpenAITextResponse {
|
|
|
fullTextResponse := dto.OpenAITextResponse{
|
|
fullTextResponse := dto.OpenAITextResponse{
|
|
|
- Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
|
|
|
|
|
|
|
+ Id: helper.GetResponseID(c),
|
|
|
Object: "chat.completion",
|
|
Object: "chat.completion",
|
|
|
Created: common.GetTimestamp(),
|
|
Created: common.GetTimestamp(),
|
|
|
Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
|
|
Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
|
|
|
}
|
|
}
|
|
|
- content, _ := json.Marshal("")
|
|
|
|
|
isToolCall := false
|
|
isToolCall := false
|
|
|
for _, candidate := range response.Candidates {
|
|
for _, candidate := range response.Candidates {
|
|
|
choice := dto.OpenAITextResponseChoice{
|
|
choice := dto.OpenAITextResponseChoice{
|
|
|
Index: int(candidate.Index),
|
|
Index: int(candidate.Index),
|
|
|
Message: dto.Message{
|
|
Message: dto.Message{
|
|
|
Role: "assistant",
|
|
Role: "assistant",
|
|
|
- Content: content,
|
|
|
|
|
|
|
+ Content: "",
|
|
|
},
|
|
},
|
|
|
FinishReason: constant.FinishReasonStop,
|
|
FinishReason: constant.FinishReasonStop,
|
|
|
}
|
|
}
|
|
@@ -539,6 +673,8 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
|
|
|
if call := getResponseToolCall(&part); call != nil {
|
|
if call := getResponseToolCall(&part); call != nil {
|
|
|
toolCalls = append(toolCalls, *call)
|
|
toolCalls = append(toolCalls, *call)
|
|
|
}
|
|
}
|
|
|
|
|
+ } else if part.Thought {
|
|
|
|
|
+ choice.Message.ReasoningContent = part.Text
|
|
|
} else {
|
|
} else {
|
|
|
if part.ExecutableCode != nil {
|
|
if part.ExecutableCode != nil {
|
|
|
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```")
|
|
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```")
|
|
@@ -556,7 +692,6 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
|
|
|
choice.Message.SetToolCalls(toolCalls)
|
|
choice.Message.SetToolCalls(toolCalls)
|
|
|
isToolCall = true
|
|
isToolCall = true
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
choice.Message.SetStringContent(strings.Join(texts, "\n"))
|
|
choice.Message.SetStringContent(strings.Join(texts, "\n"))
|
|
|
|
|
|
|
|
}
|
|
}
|
|
@@ -596,6 +731,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
|
|
|
}
|
|
}
|
|
|
var texts []string
|
|
var texts []string
|
|
|
isTools := false
|
|
isTools := false
|
|
|
|
|
+ isThought := false
|
|
|
if candidate.FinishReason != nil {
|
|
if candidate.FinishReason != nil {
|
|
|
// p := GeminiConvertFinishReason(*candidate.FinishReason)
|
|
// p := GeminiConvertFinishReason(*candidate.FinishReason)
|
|
|
switch *candidate.FinishReason {
|
|
switch *candidate.FinishReason {
|
|
@@ -620,6 +756,9 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
|
|
|
call.SetIndex(len(choice.Delta.ToolCalls))
|
|
call.SetIndex(len(choice.Delta.ToolCalls))
|
|
|
choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, *call)
|
|
choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, *call)
|
|
|
}
|
|
}
|
|
|
|
|
+ } else if part.Thought {
|
|
|
|
|
+ isThought = true
|
|
|
|
|
+ texts = append(texts, part.Text)
|
|
|
} else {
|
|
} else {
|
|
|
if part.ExecutableCode != nil {
|
|
if part.ExecutableCode != nil {
|
|
|
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```\n")
|
|
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```\n")
|
|
@@ -632,7 +771,11 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- choice.Delta.SetContentString(strings.Join(texts, "\n"))
|
|
|
|
|
|
|
+ if isThought {
|
|
|
|
|
+ choice.Delta.SetReasoningContent(strings.Join(texts, "\n"))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ choice.Delta.SetContentString(strings.Join(texts, "\n"))
|
|
|
|
|
+ }
|
|
|
if isTools {
|
|
if isTools {
|
|
|
choice.FinishReason = &constant.FinishReasonToolCalls
|
|
choice.FinishReason = &constant.FinishReasonToolCalls
|
|
|
}
|
|
}
|
|
@@ -647,7 +790,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
|
|
|
|
|
|
|
|
func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
|
func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
|
|
// responseText := ""
|
|
// responseText := ""
|
|
|
- id := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
|
|
|
|
|
|
|
+ id := helper.GetResponseID(c)
|
|
|
createAt := common.GetTimestamp()
|
|
createAt := common.GetTimestamp()
|
|
|
var usage = &dto.Usage{}
|
|
var usage = &dto.Usage{}
|
|
|
var imageCount int
|
|
var imageCount int
|
|
@@ -672,6 +815,13 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
|
|
|
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount
|
|
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount
|
|
|
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
|
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
|
|
usage.TotalTokens = geminiResponse.UsageMetadata.TotalTokenCount
|
|
usage.TotalTokens = geminiResponse.UsageMetadata.TotalTokenCount
|
|
|
|
|
+ for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
|
|
|
|
+ if detail.Modality == "AUDIO" {
|
|
|
|
|
+ usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
|
|
|
|
+ } else if detail.Modality == "TEXT" {
|
|
|
|
|
+ usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
err = helper.ObjectData(c, response)
|
|
err = helper.ObjectData(c, response)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -716,8 +866,11 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
|
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
|
|
}
|
|
}
|
|
|
|
|
+ if common.DebugEnabled {
|
|
|
|
|
+ println(string(responseBody))
|
|
|
|
|
+ }
|
|
|
var geminiResponse GeminiChatResponse
|
|
var geminiResponse GeminiChatResponse
|
|
|
- err = json.Unmarshal(responseBody, &geminiResponse)
|
|
|
|
|
|
|
+ err = common.DecodeJson(responseBody, &geminiResponse)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
|
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
|
|
}
|
|
}
|
|
@@ -732,7 +885,7 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
|
|
|
StatusCode: resp.StatusCode,
|
|
StatusCode: resp.StatusCode,
|
|
|
}, nil
|
|
}, nil
|
|
|
}
|
|
}
|
|
|
- fullTextResponse := responseGeminiChat2OpenAI(&geminiResponse)
|
|
|
|
|
|
|
+ fullTextResponse := responseGeminiChat2OpenAI(c, &geminiResponse)
|
|
|
fullTextResponse.Model = info.UpstreamModelName
|
|
fullTextResponse.Model = info.UpstreamModelName
|
|
|
usage := dto.Usage{
|
|
usage := dto.Usage{
|
|
|
PromptTokens: geminiResponse.UsageMetadata.PromptTokenCount,
|
|
PromptTokens: geminiResponse.UsageMetadata.PromptTokenCount,
|
|
@@ -743,6 +896,14 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
|
|
|
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
|
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
|
|
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
|
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
|
|
|
|
|
|
|
|
|
+ for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
|
|
|
|
+ if detail.Modality == "AUDIO" {
|
|
|
|
|
+ usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
|
|
|
|
+ } else if detail.Modality == "TEXT" {
|
|
|
|
|
+ usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fullTextResponse.Usage = usage
|
|
fullTextResponse.Usage = usage
|
|
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
|
jsonResponse, err := json.Marshal(fullTextResponse)
|
|
|
if err != nil {
|
|
if err != nil {
|