Files
CLIProxyAPI/internal/runtime/executor/payload_helpers.go
auroraflux 1c6f4be8ae refactor(executor): dedupe thinking metadata helpers across Gemini executors
Extract applyThinkingMetadata and applyThinkingMetadataCLI helpers to
payload_helpers.go and use them across all four Gemini-based executors:
- gemini_executor.go (Execute, ExecuteStream, CountTokens)
- gemini_cli_executor.go (Execute, ExecuteStream, CountTokens)
- aistudio_executor.go (translateRequest)
- antigravity_executor.go (Execute, ExecuteStream)

This eliminates code duplication introduced in the -reasoning suffix PR
and centralizes the thinking config application logic.

Net reduction: 28 lines of code.
2025-11-30 15:20:15 -08:00

192 lines
5.3 KiB
Go

package executor
import (
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// applyThinkingMetadata applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N)
// for standard Gemini format payloads. It normalizes the budget when the model supports thinking.
func applyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte {
budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata)
if !ok {
return payload
}
if !util.ModelSupportsThinking(model) {
return payload
}
if budgetOverride != nil {
norm := util.NormalizeThinkingBudget(model, *budgetOverride)
budgetOverride = &norm
}
return util.ApplyGeminiThinkingConfig(payload, budgetOverride, includeOverride)
}
// applyThinkingMetadataCLI applies thinking config from model suffix metadata (e.g., -reasoning, -thinking-N)
// for Gemini CLI format payloads (nested under "request"). It normalizes the budget when the model supports thinking.
func applyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte {
budgetOverride, includeOverride, ok := util.GeminiThinkingFromMetadata(metadata)
if !ok {
return payload
}
if budgetOverride != nil && util.ModelSupportsThinking(model) {
norm := util.NormalizeThinkingBudget(model, *budgetOverride)
budgetOverride = &norm
}
return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride)
}
// applyPayloadConfig applies payload default and override rules from configuration
// to the given JSON payload for the specified model.
// Defaults only fill missing fields, while overrides always overwrite existing values.
func applyPayloadConfig(cfg *config.Config, model string, payload []byte) []byte {
return applyPayloadConfigWithRoot(cfg, model, "", "", payload)
}
// applyPayloadConfigWithRoot behaves like applyPayloadConfig but treats all parameter
// paths as relative to the provided root path (for example, "request" for Gemini CLI)
// and restricts matches to the given protocol when supplied.
func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload []byte) []byte {
if cfg == nil || len(payload) == 0 {
return payload
}
rules := cfg.Payload
if len(rules.Default) == 0 && len(rules.Override) == 0 {
return payload
}
model = strings.TrimSpace(model)
if model == "" {
return payload
}
out := payload
// Apply default rules: first write wins per field across all matching rules.
for i := range rules.Default {
rule := &rules.Default[i]
if !payloadRuleMatchesModel(rule, model, protocol) {
continue
}
for path, value := range rule.Params {
fullPath := buildPayloadPath(root, path)
if fullPath == "" {
continue
}
if gjson.GetBytes(out, fullPath).Exists() {
continue
}
updated, errSet := sjson.SetBytes(out, fullPath, value)
if errSet != nil {
continue
}
out = updated
}
}
// Apply override rules: last write wins per field across all matching rules.
for i := range rules.Override {
rule := &rules.Override[i]
if !payloadRuleMatchesModel(rule, model, protocol) {
continue
}
for path, value := range rule.Params {
fullPath := buildPayloadPath(root, path)
if fullPath == "" {
continue
}
updated, errSet := sjson.SetBytes(out, fullPath, value)
if errSet != nil {
continue
}
out = updated
}
}
return out
}
func payloadRuleMatchesModel(rule *config.PayloadRule, model, protocol string) bool {
if rule == nil {
return false
}
if len(rule.Models) == 0 {
return false
}
for _, entry := range rule.Models {
name := strings.TrimSpace(entry.Name)
if name == "" {
continue
}
if ep := strings.TrimSpace(entry.Protocol); ep != "" && protocol != "" && !strings.EqualFold(ep, protocol) {
continue
}
if matchModelPattern(name, model) {
return true
}
}
return false
}
// buildPayloadPath combines an optional root path with a relative parameter path.
// When root is empty, the parameter path is used as-is. When root is non-empty,
// the parameter path is treated as relative to root.
func buildPayloadPath(root, path string) string {
r := strings.TrimSpace(root)
p := strings.TrimSpace(path)
if r == "" {
return p
}
if p == "" {
return r
}
if strings.HasPrefix(p, ".") {
p = p[1:]
}
return r + "." + p
}
// matchModelPattern performs simple wildcard matching where '*' matches zero or more characters.
// Examples:
//
// "*-5" matches "gpt-5"
// "gpt-*" matches "gpt-5" and "gpt-4"
// "gemini-*-pro" matches "gemini-2.5-pro" and "gemini-3-pro".
func matchModelPattern(pattern, model string) bool {
pattern = strings.TrimSpace(pattern)
model = strings.TrimSpace(model)
if pattern == "" {
return false
}
if pattern == "*" {
return true
}
// Iterative glob-style matcher supporting only '*' wildcard.
pi, si := 0, 0
starIdx := -1
matchIdx := 0
for si < len(model) {
if pi < len(pattern) && (pattern[pi] == model[si]) {
pi++
si++
continue
}
if pi < len(pattern) && pattern[pi] == '*' {
starIdx = pi
matchIdx = si
pi++
continue
}
if starIdx != -1 {
pi = starIdx + 1
matchIdx++
si = matchIdx
continue
}
return false
}
for pi < len(pattern) && pattern[pi] == '*' {
pi++
}
return pi == len(pattern)
}