mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
Route OpenAI reasoning effort through ThinkingEffortToBudget for Claude translators, preserve "minimal" when translating OpenAI Responses, and treat blank/unknown efforts as no-ops for Gemini thinking configs. Also map budget -1 to "auto" and expand cross-protocol thinking tests.
265 lines
8.7 KiB
Go
265 lines
8.7 KiB
Go
package util
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
const (
|
|
GeminiThinkingBudgetMetadataKey = "gemini_thinking_budget"
|
|
GeminiIncludeThoughtsMetadataKey = "gemini_include_thoughts"
|
|
GeminiOriginalModelMetadataKey = "gemini_original_model"
|
|
)
|
|
|
|
func ApplyGeminiThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte {
|
|
if budget == nil && includeThoughts == nil {
|
|
return body
|
|
}
|
|
updated := body
|
|
if budget != nil {
|
|
valuePath := "generationConfig.thinkingConfig.thinkingBudget"
|
|
rewritten, err := sjson.SetBytes(updated, valuePath, *budget)
|
|
if err == nil {
|
|
updated = rewritten
|
|
}
|
|
}
|
|
// Default to including thoughts when a budget override is present but no explicit include flag is provided.
|
|
incl := includeThoughts
|
|
if incl == nil && budget != nil && *budget != 0 {
|
|
defaultInclude := true
|
|
incl = &defaultInclude
|
|
}
|
|
if incl != nil {
|
|
valuePath := "generationConfig.thinkingConfig.include_thoughts"
|
|
rewritten, err := sjson.SetBytes(updated, valuePath, *incl)
|
|
if err == nil {
|
|
updated = rewritten
|
|
}
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func ApplyGeminiCLIThinkingConfig(body []byte, budget *int, includeThoughts *bool) []byte {
|
|
if budget == nil && includeThoughts == nil {
|
|
return body
|
|
}
|
|
updated := body
|
|
if budget != nil {
|
|
valuePath := "request.generationConfig.thinkingConfig.thinkingBudget"
|
|
rewritten, err := sjson.SetBytes(updated, valuePath, *budget)
|
|
if err == nil {
|
|
updated = rewritten
|
|
}
|
|
}
|
|
// Default to including thoughts when a budget override is present but no explicit include flag is provided.
|
|
incl := includeThoughts
|
|
if incl == nil && budget != nil && *budget != 0 {
|
|
defaultInclude := true
|
|
incl = &defaultInclude
|
|
}
|
|
if incl != nil {
|
|
valuePath := "request.generationConfig.thinkingConfig.include_thoughts"
|
|
rewritten, err := sjson.SetBytes(updated, valuePath, *incl)
|
|
if err == nil {
|
|
updated = rewritten
|
|
}
|
|
}
|
|
return updated
|
|
}
|
|
|
|
// modelsWithDefaultThinking lists models that should have thinking enabled by default
|
|
// when no explicit thinkingConfig is provided.
|
|
var modelsWithDefaultThinking = map[string]bool{
|
|
"gemini-3-pro-preview": true,
|
|
}
|
|
|
|
// ModelHasDefaultThinking returns true if the model should have thinking enabled by default.
|
|
func ModelHasDefaultThinking(model string) bool {
|
|
return modelsWithDefaultThinking[model]
|
|
}
|
|
|
|
// ApplyDefaultThinkingIfNeeded injects default thinkingConfig for models that require it.
|
|
// For standard Gemini API format (generationConfig.thinkingConfig path).
|
|
// Returns the modified body if thinkingConfig was added, otherwise returns the original.
|
|
func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte {
|
|
if !ModelHasDefaultThinking(model) {
|
|
return body
|
|
}
|
|
if gjson.GetBytes(body, "generationConfig.thinkingConfig").Exists() {
|
|
return body
|
|
}
|
|
updated, _ := sjson.SetBytes(body, "generationConfig.thinkingConfig.thinkingBudget", -1)
|
|
updated, _ = sjson.SetBytes(updated, "generationConfig.thinkingConfig.include_thoughts", true)
|
|
return updated
|
|
}
|
|
|
|
// ApplyDefaultThinkingIfNeededCLI injects default thinkingConfig for models that require it.
|
|
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
|
// Returns the modified body if thinkingConfig was added, otherwise returns the original.
|
|
func ApplyDefaultThinkingIfNeededCLI(model string, body []byte) []byte {
|
|
if !ModelHasDefaultThinking(model) {
|
|
return body
|
|
}
|
|
if gjson.GetBytes(body, "request.generationConfig.thinkingConfig").Exists() {
|
|
return body
|
|
}
|
|
updated, _ := sjson.SetBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
|
|
updated, _ = sjson.SetBytes(updated, "request.generationConfig.thinkingConfig.include_thoughts", true)
|
|
return updated
|
|
}
|
|
|
|
// StripThinkingConfigIfUnsupported removes thinkingConfig from the request body
|
|
// when the target model does not advertise Thinking capability. It cleans both
|
|
// standard Gemini and Gemini CLI JSON envelopes. This acts as a final safety net
|
|
// in case upstream injected thinking for an unsupported model.
|
|
func StripThinkingConfigIfUnsupported(model string, body []byte) []byte {
|
|
if ModelSupportsThinking(model) || len(body) == 0 {
|
|
return body
|
|
}
|
|
updated := body
|
|
// Gemini CLI path
|
|
updated, _ = sjson.DeleteBytes(updated, "request.generationConfig.thinkingConfig")
|
|
// Standard Gemini path
|
|
updated, _ = sjson.DeleteBytes(updated, "generationConfig.thinkingConfig")
|
|
return updated
|
|
}
|
|
|
|
// NormalizeGeminiThinkingBudget normalizes the thinkingBudget value in a standard Gemini
|
|
// request body (generationConfig.thinkingConfig.thinkingBudget path).
|
|
func NormalizeGeminiThinkingBudget(model string, body []byte) []byte {
|
|
const budgetPath = "generationConfig.thinkingConfig.thinkingBudget"
|
|
budget := gjson.GetBytes(body, budgetPath)
|
|
if !budget.Exists() {
|
|
return body
|
|
}
|
|
normalized := NormalizeThinkingBudget(model, int(budget.Int()))
|
|
updated, _ := sjson.SetBytes(body, budgetPath, normalized)
|
|
return updated
|
|
}
|
|
|
|
// NormalizeGeminiCLIThinkingBudget normalizes the thinkingBudget value in a Gemini CLI
|
|
// request body (request.generationConfig.thinkingConfig.thinkingBudget path).
|
|
func NormalizeGeminiCLIThinkingBudget(model string, body []byte) []byte {
|
|
const budgetPath = "request.generationConfig.thinkingConfig.thinkingBudget"
|
|
budget := gjson.GetBytes(body, budgetPath)
|
|
if !budget.Exists() {
|
|
return body
|
|
}
|
|
normalized := NormalizeThinkingBudget(model, int(budget.Int()))
|
|
updated, _ := sjson.SetBytes(body, budgetPath, normalized)
|
|
return updated
|
|
}
|
|
|
|
// ReasoningEffortBudgetMapping defines the thinkingBudget values for each reasoning effort level.
|
|
var ReasoningEffortBudgetMapping = map[string]int{
|
|
"none": 0,
|
|
"auto": -1,
|
|
"minimal": 512,
|
|
"low": 1024,
|
|
"medium": 8192,
|
|
"high": 24576,
|
|
"xhigh": 32768,
|
|
}
|
|
|
|
// ApplyReasoningEffortToGemini applies OpenAI reasoning_effort to Gemini thinkingConfig
|
|
// for standard Gemini API format (generationConfig.thinkingConfig path).
|
|
// Returns the modified body with thinkingBudget and include_thoughts set.
|
|
func ApplyReasoningEffortToGemini(body []byte, effort string) []byte {
|
|
normalized := strings.ToLower(strings.TrimSpace(effort))
|
|
if normalized == "" {
|
|
return body
|
|
}
|
|
|
|
budgetPath := "generationConfig.thinkingConfig.thinkingBudget"
|
|
includePath := "generationConfig.thinkingConfig.include_thoughts"
|
|
|
|
if normalized == "none" {
|
|
body, _ = sjson.DeleteBytes(body, "generationConfig.thinkingConfig")
|
|
return body
|
|
}
|
|
|
|
budget, ok := ReasoningEffortBudgetMapping[normalized]
|
|
if !ok {
|
|
return body
|
|
}
|
|
|
|
body, _ = sjson.SetBytes(body, budgetPath, budget)
|
|
body, _ = sjson.SetBytes(body, includePath, true)
|
|
return body
|
|
}
|
|
|
|
// ApplyReasoningEffortToGeminiCLI applies OpenAI reasoning_effort to Gemini CLI thinkingConfig
|
|
// for Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
|
// Returns the modified body with thinkingBudget and include_thoughts set.
|
|
func ApplyReasoningEffortToGeminiCLI(body []byte, effort string) []byte {
|
|
normalized := strings.ToLower(strings.TrimSpace(effort))
|
|
if normalized == "" {
|
|
return body
|
|
}
|
|
|
|
budgetPath := "request.generationConfig.thinkingConfig.thinkingBudget"
|
|
includePath := "request.generationConfig.thinkingConfig.include_thoughts"
|
|
|
|
if normalized == "none" {
|
|
body, _ = sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig")
|
|
return body
|
|
}
|
|
|
|
budget, ok := ReasoningEffortBudgetMapping[normalized]
|
|
if !ok {
|
|
return body
|
|
}
|
|
|
|
body, _ = sjson.SetBytes(body, budgetPath, budget)
|
|
body, _ = sjson.SetBytes(body, includePath, true)
|
|
return body
|
|
}
|
|
|
|
// ConvertThinkingLevelToBudget checks for "generationConfig.thinkingConfig.thinkingLevel"
|
|
// and converts it to "thinkingBudget".
|
|
// "high" -> 32768
|
|
// "low" -> 128
|
|
// It removes "thinkingLevel" after conversion.
|
|
func ConvertThinkingLevelToBudget(body []byte) []byte {
|
|
levelPath := "generationConfig.thinkingConfig.thinkingLevel"
|
|
res := gjson.GetBytes(body, levelPath)
|
|
if !res.Exists() {
|
|
return body
|
|
}
|
|
|
|
level := strings.ToLower(res.String())
|
|
var budget int
|
|
switch level {
|
|
case "high":
|
|
budget = 32768
|
|
case "low":
|
|
budget = 128
|
|
default:
|
|
// If unknown level, we might just leave it or default.
|
|
// User only specified high and low. We'll assume we shouldn't touch it if it's something else,
|
|
// or maybe we should just remove the invalid level?
|
|
// For safety adhering to strict instructions: "If high... if low...".
|
|
// If it's something else, the upstream might fail anyway if we leave it,
|
|
// but let's just delete the level if we processed it.
|
|
// Actually, let's check if we need to do anything for other values.
|
|
// For now, only handle high/low.
|
|
return body
|
|
}
|
|
|
|
// Set budget
|
|
budgetPath := "generationConfig.thinkingConfig.thinkingBudget"
|
|
updated, err := sjson.SetBytes(body, budgetPath, budget)
|
|
if err != nil {
|
|
return body
|
|
}
|
|
|
|
// Remove level
|
|
updated, err = sjson.DeleteBytes(updated, levelPath)
|
|
if err != nil {
|
|
return body
|
|
}
|
|
return updated
|
|
}
|