diff --git a/relay/channel/gemini/constant.go b/relay/channel/gemini/constant.go index 2c972e37dca9..9063fe73813d 100644 --- a/relay/channel/gemini/constant.go +++ b/relay/channel/gemini/constant.go @@ -26,6 +26,19 @@ var ModelList = []string{ "embedding-001", } +// Abilities and limitations +// See: https://ai.google.dev/gemini-api/docs/thinking#supported-models +var ModelsWithThoughtsSummarySupport = []string{ + "gemini-2.5-pro", + "gemini-2.5-flash", + "gemini-2.5-flash-lite", +} + +// Models with minimum thinking budget limits (thinking cannot be completely disabled due to the model's design) +var ModelsWithMinimumThinkingBudgetLimits = []string{ + "gemini-2.5-pro", +} + var SafetySettingList = []string{ "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH", diff --git a/relay/channel/gemini/helper.go b/relay/channel/gemini/helper.go new file mode 100644 index 000000000000..b24a7736c49c --- /dev/null +++ b/relay/channel/gemini/helper.go @@ -0,0 +1,25 @@ +package gemini + +import ( + "strings" +) + +func IsModelSupportThoughtsSummary(modelName string) bool { + for _, supportedModel := range ModelsWithThoughtsSummarySupport { + if strings.HasPrefix(modelName, supportedModel) { + return true + } + } + + return false +} + +func IsModelWithMinimumThinkingBudgetLimits(modelName string) bool { + for _, specialModel := range ModelsWithMinimumThinkingBudgetLimits { + if strings.HasPrefix(modelName, specialModel) { + return true + } + } + + return false +} diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index a65216ad0c27..f2e7ebd2e73a 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -79,56 +79,74 @@ func clampThinkingBudget(modelName string, budget int) int { } func ThinkingAdaptor(geminiRequest *GeminiChatRequest, info *relaycommon.RelayInfo) { - if model_setting.GetGeminiSettings().ThinkingAdapterEnabled { - 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, - } + if !model_setting.GetGeminiSettings().ThinkingAdapterEnabled { + return + } + + modelName := info.UpstreamModelName + + // 1. Process thinking budget settings + 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), } } - } else if strings.HasSuffix(modelName, "-thinking") { - unsupportedModels := []string{ - "gemini-2.5-pro-preview-05-06", - "gemini-2.5-pro-preview-03-25", - } - isUnsupported := false - for _, unsupportedModel := range unsupportedModels { - if strings.HasPrefix(modelName, unsupportedModel) { - isUnsupported = true - break - } + } + } else if strings.HasSuffix(modelName, "-thinking") { + unsupportedModels := []string{ + "gemini-2.5-pro-preview-05-06", + "gemini-2.5-pro-preview-03-25", + } + isSupported := true + for _, unsupportedModel := range unsupportedModels { + if strings.HasPrefix(modelName, unsupportedModel) { + isSupported = false + 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) - } + if isSupported && geminiRequest.GenerationConfig.MaxOutputTokens > 0 { + budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens) + clampedBudget := clampThinkingBudget(modelName, int(budgetTokens)) + geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{ + ThinkingBudget: common.GetPointer(clampedBudget), } - } else if strings.HasSuffix(modelName, "-nothinking") { - if !isNew25Pro { - geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{ - ThinkingBudget: common.GetPointer(0), - } + } + } else if strings.HasSuffix(modelName, "-nothinking") { + thinkingBudget := 0 + + // Reset to minimum allowed value if the model has minimum thinking budget limits + if IsModelWithMinimumThinkingBudgetLimits(modelName) { + thinkingBudget = clampThinkingBudget(modelName, 0) + } + + geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{ + ThinkingBudget: common.GetPointer(thinkingBudget), + } + } + + // 2. Process thoughts summary parameter settings + if model_setting.GetGeminiSettings().IncludeThoughtsSummaryEnabled && IsModelSupportThoughtsSummary(modelName) { + applyIncludeThoughts := true + allowZeroThinkingBudget := !IsModelWithMinimumThinkingBudgetLimits(modelName) + + // Skip only if the model has -nothinking suffix and no minimum thinking budget limits. + // Keep applying if the model has minimum thinking budget limits since there still a little pieces + // of thoughts content in the response + if strings.HasSuffix(modelName, "-nothinking") && allowZeroThinkingBudget { + applyIncludeThoughts = false + } + + if applyIncludeThoughts { + if geminiRequest.GenerationConfig.ThinkingConfig == nil { + geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{} } + + geminiRequest.GenerationConfig.ThinkingConfig.IncludeThoughts = applyIncludeThoughts } } } diff --git a/setting/model_setting/gemini.go b/setting/model_setting/gemini.go index f132fec88162..dcd0fc79378c 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"` + IncludeThoughtsSummaryEnabled bool `json:"include_thoughts_summary_enabled"` } // 默认配置 @@ -29,6 +30,7 @@ var defaultGeminiSettings = GeminiSettings{ }, ThinkingAdapterEnabled: false, ThinkingAdapterBudgetTokensPercentage: 0.6, + IncludeThoughtsSummaryEnabled: true, } // 全局实例 diff --git a/web/src/components/settings/ModelSetting.js b/web/src/components/settings/ModelSetting.js index 1d58363443e0..11b70f49bcd1 100644 --- a/web/src/components/settings/ModelSetting.js +++ b/web/src/components/settings/ModelSetting.js @@ -22,6 +22,7 @@ const ModelSetting = () => { 'general_setting.ping_interval_seconds': 60, 'gemini.thinking_adapter_enabled': false, 'gemini.thinking_adapter_budget_tokens_percentage': 0.6, + 'gemini.include_thoughts_summary_enabled': true, }); let [loading, setLoading] = useState(false); diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 6b982b51e101..a3d1f677c2e6 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1691,6 +1691,8 @@ "请先选择同步渠道": "Please select the synchronization channel first", "与本地相同": "Same as local", "未找到匹配的模型": "No matching model found", + "包含思考摘要": "Include thinking summaries", + "开启思考过程摘要并将收到的摘要包含在回复中。仅在模型支持和和思考预算不为0时生效。": "Enable thinking summaries and include the received summaries in the reply. Only effective when the model supports it and thinking budget is not 0.", "暴露倍率接口": "Expose ratio API", "支付设置": "Payment Settings", "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)", diff --git a/web/src/pages/Setting/Model/SettingGeminiModel.js b/web/src/pages/Setting/Model/SettingGeminiModel.js index a5daace63bbe..dcae865ae72b 100644 --- a/web/src/pages/Setting/Model/SettingGeminiModel.js +++ b/web/src/pages/Setting/Model/SettingGeminiModel.js @@ -30,6 +30,7 @@ export default function SettingGeminiModel(props) { 'gemini.supported_imagine_models': '', 'gemini.thinking_adapter_enabled': false, 'gemini.thinking_adapter_budget_tokens_percentage': 0.6, + 'gemini.include_thoughts_summary_enabled': true, }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); @@ -221,6 +222,21 @@ export default function SettingGeminiModel(props) { /> + + + + setInputs({ + ...inputs, + 'gemini.include_thoughts_summary_enabled': value, + }) + } + /> + +