From 50c04a62f95d035aa60084215519e46f9c54cec0 Mon Sep 17 00:00:00 2001 From: Seefs Date: Thu, 20 Nov 2025 15:54:33 +0800 Subject: [PATCH] feat: Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format --- relay/channel/gemini/adaptor.go | 2 +- relay/channel/gemini/relay-gemini.go | 36 ++++++++++++++++- relay/channel/vertex/adaptor.go | 2 +- setting/model_setting/gemini.go | 2 + web/bun.lock | 1 + web/src/i18n/locales/en.json | 2 + web/src/i18n/locales/fr.json | 2 + web/src/i18n/locales/ja.json | 2 + web/src/i18n/locales/ru.json | 2 + web/src/i18n/locales/zh.json | 2 + .../Setting/Model/SettingGeminiModel.jsx | 40 ++++++++++++++----- 11 files changed, 80 insertions(+), 13 deletions(-) diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index b1067bc20..b522ca1be 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -177,7 +177,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn return nil, errors.New("request is nil") } - geminiRequest, err := CovertGemini2OpenAI(c, *request, info) + geminiRequest, err := CovertOpenAI2Gemini(c, *request, info) if err != nil { return nil, err } diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 51a0d615d..7ecf40fa9 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -44,6 +44,8 @@ var geminiSupportedMimeTypes = map[string]bool{ "video/flv": true, } +const thoughtSignatureBypassValue = "context_engineering_is_the_way_to_go" + // Gemini 允许的思考预算范围 const ( pro25MinBudget = 128 @@ -181,7 +183,7 @@ func ThinkingAdaptor(geminiRequest *dto.GeminiChatRequest, info *relaycommon.Rel } // Setting safety to the lowest possible values since Gemini is already powerless enough -func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) { +func CovertOpenAI2Gemini(c *gin.Context, textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) { geminiRequest := dto.GeminiChatRequest{ Contents: make([]dto.GeminiChatContent, 0, len(textRequest.Messages)), @@ -193,6 +195,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i }, } + attachThoughtSignature := (info.ChannelType == constant.ChannelTypeGemini || + info.ChannelType == constant.ChannelTypeVertexAi) && + model_setting.GetGeminiSettings().FunctionCallThoughtSignatureEnabled + if model_setting.IsGeminiModelSupportImagine(info.UpstreamModelName) { geminiRequest.GenerationConfig.ResponseModalities = []string{ "TEXT", @@ -371,6 +377,8 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i content := dto.GeminiChatContent{ Role: message.Role, } + shouldAttachThoughtSignature := attachThoughtSignature && (message.Role == "assistant" || message.Role == "model") + signatureAttached := false // isToolCall := false if message.ToolCalls != nil { // message.Role = "model" @@ -388,6 +396,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i Arguments: args, }, } + if shouldAttachThoughtSignature && !signatureAttached && hasFunctionCallContent(toolCall.FunctionCall) && len(toolCall.ThoughtSignature) == 0 { + toolCall.ThoughtSignature = json.RawMessage(strconv.Quote(thoughtSignatureBypassValue)) + signatureAttached = true + } parts = append(parts, toolCall) tool_call_ids[call.ID] = call.Function.Name } @@ -496,6 +508,28 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i return &geminiRequest, nil } +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 + } +} + // Helper function to get a list of supported MIME types for error messages func getSupportedMimeTypesList() []string { keys := make([]string, 0, len(geminiSupportedMimeTypes)) diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index a4b303e26..d117ef70e 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -296,7 +296,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn info.UpstreamModelName = claudeReq.Model return vertexClaudeReq, nil } else if a.RequestMode == RequestModeGemini { - geminiRequest, err := gemini.CovertGemini2OpenAI(c, *request, info) + geminiRequest, err := gemini.CovertOpenAI2Gemini(c, *request, info) if err != nil { return nil, err } diff --git a/setting/model_setting/gemini.go b/setting/model_setting/gemini.go index 4856482ed..6b68e6b20 100644 --- a/setting/model_setting/gemini.go +++ b/setting/model_setting/gemini.go @@ -11,6 +11,7 @@ type GeminiSettings struct { SupportedImagineModels []string `json:"supported_imagine_models"` ThinkingAdapterEnabled bool `json:"thinking_adapter_enabled"` ThinkingAdapterBudgetTokensPercentage float64 `json:"thinking_adapter_budget_tokens_percentage"` + FunctionCallThoughtSignatureEnabled bool `json:"function_call_thought_signature_enabled"` } // 默认配置 @@ -29,6 +30,7 @@ var defaultGeminiSettings = GeminiSettings{ }, ThinkingAdapterEnabled: false, ThinkingAdapterBudgetTokensPercentage: 0.6, + FunctionCallThoughtSignatureEnabled: true, } // 全局实例 diff --git a/web/bun.lock b/web/bun.lock index e349685c2..fdec073ec 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "react-template", diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 0d4614ecf..7295914f4 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -69,6 +69,8 @@ "Gemini思考适配设置": "Gemini thinking adaptation settings", "Gemini版本设置": "Gemini version settings", "Gemini设置": "Gemini settings", + "启用FunctionCall思维签名填充": "Enable FunctionCall thoughtSignature fill", + "仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format", "GitHub": "GitHub", "GitHub Client ID": "GitHub Client ID", "GitHub Client Secret": "GitHub Client Secret", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index fd310d259..ded4de6d0 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -71,6 +71,8 @@ "Gemini思考适配设置": "Paramètres d'adaptation de la pensée Gemini", "Gemini版本设置": "Paramètres de version Gemini", "Gemini设置": "Paramètres Gemini", + "启用FunctionCall思维签名填充": "Activer le remplissage de thoughtSignature pour FunctionCall", + "仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "Remplit thoughtSignature uniquement pour les canaux Gemini/Vertex utilisant le format OpenAI", "GitHub": "GitHub", "GitHub Client ID": "ID client GitHub", "GitHub Client Secret": "Secret client GitHub", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 28590bb62..a9ea3d0d8 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -69,6 +69,8 @@ "Gemini思考适配设置": "Gemini思考モード設定", "Gemini版本设置": "Geminiバージョン設定", "Gemini设置": "Gemini設定", + "启用FunctionCall思维签名填充": "FunctionCall用のthoughtSignature自動付与を有効化", + "仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "OpenAI形式を利用するGemini/VertexチャネルにのみthoughtSignatureを付与します", "GitHub": "GitHub", "GitHub Client ID": "GitHub Client ID", "GitHub Client Secret": "GitHub Client Secret", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 67d4663f8..df9e52a38 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -73,6 +73,8 @@ "Gemini思考适配设置": "Настройки адаптации мышления Gemini", "Gemini版本设置": "Настройки версии Gemini", "Gemini设置": "Настройки Gemini", + "启用FunctionCall思维签名填充": "Включить автозаполнение thoughtSignature для FunctionCall", + "仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "Заполнять thoughtSignature только для каналов Gemini/Vertex, использующих формат OpenAI", "GitHub": "GitHub", "GitHub Client ID": "ID клиента GitHub", "GitHub Client Secret": "Секрет клиента GitHub", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 63772d8d7..29c1c7f40 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -67,6 +67,8 @@ "Gemini思考适配设置": "Gemini思考适配设置", "Gemini版本设置": "Gemini版本设置", "Gemini设置": "Gemini设置", + "启用FunctionCall思维签名填充": "启用FunctionCall思维签名填充", + "仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature": "仅为使用OpenAI格式的Gemini/Vertex渠道填充thoughtSignature", "GitHub": "GitHub", "GitHub Client ID": "GitHub Client ID", "GitHub Client Secret": "GitHub Client Secret", diff --git a/web/src/pages/Setting/Model/SettingGeminiModel.jsx b/web/src/pages/Setting/Model/SettingGeminiModel.jsx index 0ba7e2927..e75a4ca91 100644 --- a/web/src/pages/Setting/Model/SettingGeminiModel.jsx +++ b/web/src/pages/Setting/Model/SettingGeminiModel.jsx @@ -39,19 +39,22 @@ const GEMINI_VERSION_EXAMPLE = { default: 'v1beta', }; +const DEFAULT_GEMINI_INPUTS = { + 'gemini.safety_settings': '', + 'gemini.version_settings': '', + 'gemini.supported_imagine_models': '', + 'gemini.thinking_adapter_enabled': false, + 'gemini.thinking_adapter_budget_tokens_percentage': 0.6, + 'gemini.function_call_thought_signature_enabled': true, +}; + export default function SettingGeminiModel(props) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [inputs, setInputs] = useState({ - 'gemini.safety_settings': '', - 'gemini.version_settings': '', - 'gemini.supported_imagine_models': '', - 'gemini.thinking_adapter_enabled': false, - 'gemini.thinking_adapter_budget_tokens_percentage': 0.6, - }); + const [inputs, setInputs] = useState(DEFAULT_GEMINI_INPUTS); const refForm = useRef(); - const [inputsRow, setInputsRow] = useState(inputs); + const [inputsRow, setInputsRow] = useState(DEFAULT_GEMINI_INPUTS); async function onSubmit() { await refForm.current @@ -92,9 +95,9 @@ export default function SettingGeminiModel(props) { } useEffect(() => { - const currentInputs = {}; + const currentInputs = { ...DEFAULT_GEMINI_INPUTS }; for (let key in props.options) { - if (Object.keys(inputs).includes(key)) { + if (Object.prototype.hasOwnProperty.call(DEFAULT_GEMINI_INPUTS, key)) { currentInputs[key] = props.options[key]; } } @@ -166,6 +169,23 @@ export default function SettingGeminiModel(props) { /> + + + + setInputs({ + ...inputs, + 'gemini.function_call_thought_signature_enabled': value, + }) + } + /> + +