mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
- Added support for parsing and normalizing dynamic thinking model suffixes. - Centralized budget resolution across executors and payload helpers. - Retired legacy Gemini-specific thinking handlers in favor of unified logic. - Updated executors to use metadata-based thinking configuration. - Added `ResolveOriginalModel` utility for resolving normalized upstream models using request metadata. - Updated executors (Gemini, Codex, iFlow, OpenAI, Qwen) to incorporate upstream model resolution and substitute model values in payloads and request URLs. - Ensured fallbacks handle cases with missing or malformed metadata to derive models robustly. - Refactored upstream model resolution to dynamically incorporate metadata for selecting and normalizing models. - Improved handling of thinking configurations and model overrides in executors. - Removed hardcoded thinking model entries and migrated logic to metadata-based resolution. - Updated payload mutations to always include the resolved model.
188 lines
6.3 KiB
Go
188 lines
6.3 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
|
|
}
|
|
}
|
|
if includeThoughts != nil {
|
|
valuePath := "generationConfig.thinkingConfig.include_thoughts"
|
|
rewritten, err := sjson.SetBytes(updated, valuePath, *includeThoughts)
|
|
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
|
|
}
|
|
}
|
|
if includeThoughts != nil {
|
|
valuePath := "request.generationConfig.thinkingConfig.include_thoughts"
|
|
rewritten, err := sjson.SetBytes(updated, valuePath, *includeThoughts)
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|