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) }