|
|
@@ -81,7 +81,7 @@ func clampThinkingBudget(modelName string, budget int) int {
|
|
|
return budget
|
|
|
}
|
|
|
|
|
|
-func ThinkingAdaptor(geminiRequest *GeminiChatRequest, info *relaycommon.RelayInfo) {
|
|
|
+func ThinkingAdaptor(geminiRequest *dto.GeminiChatRequest, info *relaycommon.RelayInfo) {
|
|
|
if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
|
|
|
modelName := info.UpstreamModelName
|
|
|
isNew25Pro := strings.HasPrefix(modelName, "gemini-2.5-pro") &&
|
|
|
@@ -93,7 +93,7 @@ func ThinkingAdaptor(geminiRequest *GeminiChatRequest, info *relaycommon.RelayIn
|
|
|
if len(parts) == 2 && parts[1] != "" {
|
|
|
if budgetTokens, err := strconv.Atoi(parts[1]); err == nil {
|
|
|
clampedBudget := clampThinkingBudget(modelName, budgetTokens)
|
|
|
- geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig = &dto.GeminiThinkingConfig{
|
|
|
ThinkingBudget: common.GetPointer(clampedBudget),
|
|
|
IncludeThoughts: true,
|
|
|
}
|
|
|
@@ -113,11 +113,11 @@ func ThinkingAdaptor(geminiRequest *GeminiChatRequest, info *relaycommon.RelayIn
|
|
|
}
|
|
|
|
|
|
if isUnsupported {
|
|
|
- geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig = &dto.GeminiThinkingConfig{
|
|
|
IncludeThoughts: true,
|
|
|
}
|
|
|
} else {
|
|
|
- geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig = &dto.GeminiThinkingConfig{
|
|
|
IncludeThoughts: true,
|
|
|
}
|
|
|
if geminiRequest.GenerationConfig.MaxOutputTokens > 0 {
|
|
|
@@ -128,7 +128,7 @@ func ThinkingAdaptor(geminiRequest *GeminiChatRequest, info *relaycommon.RelayIn
|
|
|
}
|
|
|
} else if strings.HasSuffix(modelName, "-nothinking") {
|
|
|
if !isNew25Pro {
|
|
|
- geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
|
|
+ geminiRequest.GenerationConfig.ThinkingConfig = &dto.GeminiThinkingConfig{
|
|
|
ThinkingBudget: common.GetPointer(0),
|
|
|
}
|
|
|
}
|
|
|
@@ -137,11 +137,11 @@ func ThinkingAdaptor(geminiRequest *GeminiChatRequest, info *relaycommon.RelayIn
|
|
|
}
|
|
|
|
|
|
// 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) (*dto.GeminiChatRequest, error) {
|
|
|
|
|
|
- geminiRequest := GeminiChatRequest{
|
|
|
- Contents: make([]GeminiChatContent, 0, len(textRequest.Messages)),
|
|
|
- GenerationConfig: GeminiChatGenerationConfig{
|
|
|
+ geminiRequest := dto.GeminiChatRequest{
|
|
|
+ Contents: make([]dto.GeminiChatContent, 0, len(textRequest.Messages)),
|
|
|
+ GenerationConfig: dto.GeminiChatGenerationConfig{
|
|
|
Temperature: textRequest.Temperature,
|
|
|
TopP: textRequest.TopP,
|
|
|
MaxOutputTokens: textRequest.MaxTokens,
|
|
|
@@ -158,9 +158,9 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
|
|
|
ThinkingAdaptor(&geminiRequest, info)
|
|
|
|
|
|
- safetySettings := make([]GeminiChatSafetySettings, 0, len(SafetySettingList))
|
|
|
+ safetySettings := make([]dto.GeminiChatSafetySettings, 0, len(SafetySettingList))
|
|
|
for _, category := range SafetySettingList {
|
|
|
- safetySettings = append(safetySettings, GeminiChatSafetySettings{
|
|
|
+ safetySettings = append(safetySettings, dto.GeminiChatSafetySettings{
|
|
|
Category: category,
|
|
|
Threshold: model_setting.GetGeminiSafetySetting(category),
|
|
|
})
|
|
|
@@ -198,17 +198,17 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
functions = append(functions, tool.Function)
|
|
|
}
|
|
|
if codeExecution {
|
|
|
- geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
|
|
+ geminiRequest.Tools = append(geminiRequest.Tools, dto.GeminiChatTool{
|
|
|
CodeExecution: make(map[string]string),
|
|
|
})
|
|
|
}
|
|
|
if googleSearch {
|
|
|
- geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
|
|
+ geminiRequest.Tools = append(geminiRequest.Tools, dto.GeminiChatTool{
|
|
|
GoogleSearch: make(map[string]string),
|
|
|
})
|
|
|
}
|
|
|
if len(functions) > 0 {
|
|
|
- geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
|
|
+ geminiRequest.Tools = append(geminiRequest.Tools, dto.GeminiChatTool{
|
|
|
FunctionDeclarations: functions,
|
|
|
})
|
|
|
}
|
|
|
@@ -238,7 +238,7 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
continue
|
|
|
} else if message.Role == "tool" || message.Role == "function" {
|
|
|
if len(geminiRequest.Contents) == 0 || geminiRequest.Contents[len(geminiRequest.Contents)-1].Role == "model" {
|
|
|
- geminiRequest.Contents = append(geminiRequest.Contents, GeminiChatContent{
|
|
|
+ geminiRequest.Contents = append(geminiRequest.Contents, dto.GeminiChatContent{
|
|
|
Role: "user",
|
|
|
})
|
|
|
}
|
|
|
@@ -265,18 +265,18 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- functionResp := &FunctionResponse{
|
|
|
+ functionResp := &dto.GeminiFunctionResponse{
|
|
|
Name: name,
|
|
|
Response: contentMap,
|
|
|
}
|
|
|
|
|
|
- *parts = append(*parts, GeminiPart{
|
|
|
+ *parts = append(*parts, dto.GeminiPart{
|
|
|
FunctionResponse: functionResp,
|
|
|
})
|
|
|
continue
|
|
|
}
|
|
|
- var parts []GeminiPart
|
|
|
- content := GeminiChatContent{
|
|
|
+ var parts []dto.GeminiPart
|
|
|
+ content := dto.GeminiChatContent{
|
|
|
Role: message.Role,
|
|
|
}
|
|
|
// isToolCall := false
|
|
|
@@ -290,8 +290,8 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
return nil, fmt.Errorf("invalid arguments for function %s, args: %s", call.Function.Name, call.Function.Arguments)
|
|
|
}
|
|
|
}
|
|
|
- toolCall := GeminiPart{
|
|
|
- FunctionCall: &FunctionCall{
|
|
|
+ toolCall := dto.GeminiPart{
|
|
|
+ FunctionCall: &dto.FunctionCall{
|
|
|
FunctionName: call.Function.Name,
|
|
|
Arguments: args,
|
|
|
},
|
|
|
@@ -308,7 +308,7 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
if part.Text == "" {
|
|
|
continue
|
|
|
}
|
|
|
- parts = append(parts, GeminiPart{
|
|
|
+ parts = append(parts, dto.GeminiPart{
|
|
|
Text: part.Text,
|
|
|
})
|
|
|
} else if part.Type == dto.ContentTypeImageURL {
|
|
|
@@ -331,8 +331,8 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
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{
|
|
|
- InlineData: &GeminiInlineData{
|
|
|
+ parts = append(parts, dto.GeminiPart{
|
|
|
+ InlineData: &dto.GeminiInlineData{
|
|
|
MimeType: fileData.MimeType, // 使用原始的 MimeType,因为大小写可能对API有意义
|
|
|
Data: fileData.Base64Data,
|
|
|
},
|
|
|
@@ -342,8 +342,8 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("decode base64 image data failed: %s", err.Error())
|
|
|
}
|
|
|
- parts = append(parts, GeminiPart{
|
|
|
- InlineData: &GeminiInlineData{
|
|
|
+ parts = append(parts, dto.GeminiPart{
|
|
|
+ InlineData: &dto.GeminiInlineData{
|
|
|
MimeType: format,
|
|
|
Data: base64String,
|
|
|
},
|
|
|
@@ -357,8 +357,8 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("decode base64 file data failed: %s", err.Error())
|
|
|
}
|
|
|
- parts = append(parts, GeminiPart{
|
|
|
- InlineData: &GeminiInlineData{
|
|
|
+ parts = append(parts, dto.GeminiPart{
|
|
|
+ InlineData: &dto.GeminiInlineData{
|
|
|
MimeType: format,
|
|
|
Data: base64String,
|
|
|
},
|
|
|
@@ -371,8 +371,8 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("decode base64 audio data failed: %s", err.Error())
|
|
|
}
|
|
|
- parts = append(parts, GeminiPart{
|
|
|
- InlineData: &GeminiInlineData{
|
|
|
+ parts = append(parts, dto.GeminiPart{
|
|
|
+ InlineData: &dto.GeminiInlineData{
|
|
|
MimeType: "audio/" + part.GetInputAudio().Format,
|
|
|
Data: base64String,
|
|
|
},
|
|
|
@@ -392,8 +392,8 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|
|
}
|
|
|
|
|
|
if len(system_content) > 0 {
|
|
|
- geminiRequest.SystemInstructions = &GeminiChatContent{
|
|
|
- Parts: []GeminiPart{
|
|
|
+ geminiRequest.SystemInstructions = &dto.GeminiChatContent{
|
|
|
+ Parts: []dto.GeminiPart{
|
|
|
{
|
|
|
Text: strings.Join(system_content, "\n"),
|
|
|
},
|
|
|
@@ -636,7 +636,7 @@ func unescapeMapOrSlice(data interface{}) interface{} {
|
|
|
return data
|
|
|
}
|
|
|
|
|
|
-func getResponseToolCall(item *GeminiPart) *dto.ToolCallResponse {
|
|
|
+func getResponseToolCall(item *dto.GeminiPart) *dto.ToolCallResponse {
|
|
|
var argsBytes []byte
|
|
|
var err error
|
|
|
if result, ok := item.FunctionCall.Arguments.(map[string]interface{}); ok {
|
|
|
@@ -658,7 +658,7 @@ func getResponseToolCall(item *GeminiPart) *dto.ToolCallResponse {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func responseGeminiChat2OpenAI(c *gin.Context, response *GeminiChatResponse) *dto.OpenAITextResponse {
|
|
|
+func responseGeminiChat2OpenAI(c *gin.Context, response *dto.GeminiChatResponse) *dto.OpenAITextResponse {
|
|
|
fullTextResponse := dto.OpenAITextResponse{
|
|
|
Id: helper.GetResponseID(c),
|
|
|
Object: "chat.completion",
|
|
|
@@ -725,10 +725,9 @@ func responseGeminiChat2OpenAI(c *gin.Context, response *GeminiChatResponse) *dt
|
|
|
return &fullTextResponse
|
|
|
}
|
|
|
|
|
|
-func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool, bool) {
|
|
|
+func streamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool) {
|
|
|
choices := make([]dto.ChatCompletionsStreamResponseChoice, 0, len(geminiResponse.Candidates))
|
|
|
isStop := false
|
|
|
- hasImage := false
|
|
|
for _, candidate := range geminiResponse.Candidates {
|
|
|
if candidate.FinishReason != nil && *candidate.FinishReason == "STOP" {
|
|
|
isStop = true
|
|
|
@@ -759,7 +758,6 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
|
|
|
if strings.HasPrefix(part.InlineData.MimeType, "image") {
|
|
|
imgText := ""
|
|
|
texts = append(texts, imgText)
|
|
|
- hasImage = true
|
|
|
}
|
|
|
} else if part.FunctionCall != nil {
|
|
|
isTools = true
|
|
|
@@ -796,7 +794,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
|
|
|
var response dto.ChatCompletionsStreamResponse
|
|
|
response.Object = "chat.completion.chunk"
|
|
|
response.Choices = choices
|
|
|
- return &response, isStop, hasImage
|
|
|
+ return &response, isStop
|
|
|
}
|
|
|
|
|
|
func handleStream(c *gin.Context, info *relaycommon.RelayInfo, resp *dto.ChatCompletionsStreamResponse) error {
|
|
|
@@ -824,23 +822,31 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
|
|
|
// responseText := ""
|
|
|
id := helper.GetResponseID(c)
|
|
|
createAt := common.GetTimestamp()
|
|
|
+ responseText := strings.Builder{}
|
|
|
var usage = &dto.Usage{}
|
|
|
var imageCount int
|
|
|
|
|
|
- respCount := 0
|
|
|
-
|
|
|
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
|
|
- var geminiResponse GeminiChatResponse
|
|
|
+ var geminiResponse dto.GeminiChatResponse
|
|
|
err := common.UnmarshalJsonStr(data, &geminiResponse)
|
|
|
if err != nil {
|
|
|
common.LogError(c, "error unmarshalling stream response: "+err.Error())
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
- response, isStop, hasImage := streamResponseGeminiChat2OpenAI(&geminiResponse)
|
|
|
- if hasImage {
|
|
|
- imageCount++
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ response, isStop := streamResponseGeminiChat2OpenAI(&geminiResponse)
|
|
|
+
|
|
|
response.Id = id
|
|
|
response.Created = createAt
|
|
|
response.Model = info.UpstreamModelName
|
|
|
@@ -858,7 +864,7 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if respCount == 0 {
|
|
|
+ if info.SendResponseCount == 0 {
|
|
|
// send first response
|
|
|
err = handleStream(c, info, helper.GenerateStartEmptyResponse(id, createAt, info.UpstreamModelName, nil))
|
|
|
if err != nil {
|
|
|
@@ -873,11 +879,10 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
|
|
|
if isStop {
|
|
|
_ = handleStream(c, info, helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop))
|
|
|
}
|
|
|
- respCount++
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
- if respCount == 0 {
|
|
|
+ if info.SendResponseCount == 0 {
|
|
|
// 空补全,报错不计费
|
|
|
// empty response, throw an error
|
|
|
return nil, types.NewOpenAIError(errors.New("no response received from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError)
|
|
|
@@ -892,6 +897,16 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
|
|
|
usage.PromptTokensDetails.TextTokens = usage.PromptTokens
|
|
|
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
|
|
|
|
|
+ if usage.CompletionTokens == 0 {
|
|
|
+ str := responseText.String()
|
|
|
+ if len(str) > 0 {
|
|
|
+ usage = service.ResponseText2Usage(responseText.String(), info.UpstreamModelName, info.PromptTokens)
|
|
|
+ } else {
|
|
|
+ // 空补全,不需要使用量
|
|
|
+ usage = &dto.Usage{}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
response := helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
|
|
|
err := handleFinalStream(c, info, response)
|
|
|
if err != nil {
|
|
|
@@ -913,7 +928,7 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
|
|
|
if common.DebugEnabled {
|
|
|
println(string(responseBody))
|
|
|
}
|
|
|
- var geminiResponse GeminiChatResponse
|
|
|
+ var geminiResponse dto.GeminiChatResponse
|
|
|
err = common.Unmarshal(responseBody, &geminiResponse)
|
|
|
if err != nil {
|
|
|
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
|
|
@@ -959,7 +974,7 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *h
|
|
|
return nil, types.NewOpenAIError(readErr, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
|
|
}
|
|
|
|
|
|
- var geminiResponse GeminiEmbeddingResponse
|
|
|
+ var geminiResponse dto.GeminiEmbeddingResponse
|
|
|
if jsonErr := common.Unmarshal(responseBody, &geminiResponse); jsonErr != nil {
|
|
|
return nil, types.NewOpenAIError(jsonErr, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
|
|
}
|
|
|
@@ -1005,7 +1020,7 @@ func GeminiImageHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.
|
|
|
}
|
|
|
_ = resp.Body.Close()
|
|
|
|
|
|
- var geminiResponse GeminiImageResponse
|
|
|
+ var geminiResponse dto.GeminiImageResponse
|
|
|
if jsonErr := common.Unmarshal(responseBody, &geminiResponse); jsonErr != nil {
|
|
|
return nil, types.NewOpenAIError(jsonErr, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
|
|
}
|