| package executor |
|
|
| import ( |
| "fmt" |
| "net/http" |
| "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" |
| ) |
|
|
| |
| |
| func ApplyThinkingMetadata(payload []byte, metadata map[string]any, model string) []byte { |
| |
| |
| lookupModel := util.ResolveOriginalModel(model, metadata) |
|
|
| |
| |
| thinkingModel := lookupModel |
| if !util.ModelSupportsThinking(lookupModel) && util.ModelSupportsThinking(model) { |
| thinkingModel = model |
| } |
|
|
| budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(thinkingModel, metadata) |
| if !ok || (budgetOverride == nil && includeOverride == nil) { |
| return payload |
| } |
| if !util.ModelSupportsThinking(thinkingModel) { |
| return payload |
| } |
| if budgetOverride != nil { |
| norm := util.NormalizeThinkingBudget(thinkingModel, *budgetOverride) |
| budgetOverride = &norm |
| } |
| return util.ApplyGeminiThinkingConfig(payload, budgetOverride, includeOverride) |
| } |
|
|
| |
| |
| func ApplyThinkingMetadataCLI(payload []byte, metadata map[string]any, model string) []byte { |
| |
| |
| lookupModel := util.ResolveOriginalModel(model, metadata) |
|
|
| |
| |
| thinkingModel := lookupModel |
| if !util.ModelSupportsThinking(lookupModel) && util.ModelSupportsThinking(model) { |
| thinkingModel = model |
| } |
|
|
| budgetOverride, includeOverride, ok := util.ResolveThinkingConfigFromMetadata(thinkingModel, metadata) |
| if !ok || (budgetOverride == nil && includeOverride == nil) { |
| return payload |
| } |
| if !util.ModelSupportsThinking(thinkingModel) { |
| return payload |
| } |
| if budgetOverride != nil { |
| norm := util.NormalizeThinkingBudget(thinkingModel, *budgetOverride) |
| budgetOverride = &norm |
| } |
| return util.ApplyGeminiCLIThinkingConfig(payload, budgetOverride, includeOverride) |
| } |
|
|
| |
| |
| |
| func ApplyReasoningEffortMetadata(payload []byte, metadata map[string]any, model, field string, allowCompat bool) []byte { |
| if len(metadata) == 0 { |
| return payload |
| } |
| if field == "" { |
| return payload |
| } |
| baseModel := util.ResolveOriginalModel(model, metadata) |
| if baseModel == "" { |
| baseModel = model |
| } |
| if !util.ModelSupportsThinking(baseModel) && !allowCompat { |
| return payload |
| } |
| if effort, ok := util.ReasoningEffortFromMetadata(metadata); ok && effort != "" { |
| if util.ModelUsesThinkingLevels(baseModel) || allowCompat { |
| if updated, err := sjson.SetBytes(payload, field, effort); err == nil { |
| return updated |
| } |
| } |
| } |
| |
| if util.ModelUsesThinkingLevels(baseModel) || allowCompat { |
| if budget, _, _, matched := util.ThinkingFromMetadata(metadata); matched && budget != nil { |
| if effort, ok := util.ThinkingBudgetToEffort(baseModel, *budget); ok && effort != "" { |
| if updated, err := sjson.SetBytes(payload, field, effort); err == nil { |
| return updated |
| } |
| } |
| } |
| } |
| return payload |
| } |
|
|
| |
| |
| |
| |
| func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []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 |
| source := original |
| if len(source) == 0 { |
| source = payload |
| } |
| appliedDefaults := make(map[string]struct{}) |
| |
| 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(source, fullPath).Exists() { |
| continue |
| } |
| if _, ok := appliedDefaults[fullPath]; ok { |
| continue |
| } |
| updated, errSet := sjson.SetBytes(out, fullPath, value) |
| if errSet != nil { |
| continue |
| } |
| out = updated |
| appliedDefaults[fullPath] = struct{}{} |
| } |
| } |
| |
| 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 |
| } |
|
|
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func matchModelPattern(pattern, model string) bool { |
| pattern = strings.TrimSpace(pattern) |
| model = strings.TrimSpace(model) |
| if pattern == "" { |
| return false |
| } |
| if pattern == "*" { |
| return true |
| } |
| |
| 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) |
| } |
|
|
| |
| |
| |
| |
| |
| func NormalizeThinkingConfig(payload []byte, model string, allowCompat bool) []byte { |
| if len(payload) == 0 || model == "" { |
| return payload |
| } |
|
|
| if !util.ModelSupportsThinking(model) { |
| if allowCompat { |
| return payload |
| } |
| return StripThinkingFields(payload, false) |
| } |
|
|
| if util.ModelUsesThinkingLevels(model) { |
| return NormalizeReasoningEffortLevel(payload, model) |
| } |
|
|
| |
| |
| return StripThinkingFields(payload, true) |
| } |
|
|
| |
| |
| |
| func StripThinkingFields(payload []byte, effortOnly bool) []byte { |
| fieldsToRemove := []string{ |
| "reasoning_effort", |
| "reasoning.effort", |
| } |
| if !effortOnly { |
| fieldsToRemove = append([]string{"reasoning", "thinking"}, fieldsToRemove...) |
| } |
| out := payload |
| for _, field := range fieldsToRemove { |
| if gjson.GetBytes(out, field).Exists() { |
| out, _ = sjson.DeleteBytes(out, field) |
| } |
| } |
| return out |
| } |
|
|
| |
| |
| func NormalizeReasoningEffortLevel(payload []byte, model string) []byte { |
| out := payload |
|
|
| if effort := gjson.GetBytes(out, "reasoning_effort"); effort.Exists() { |
| if normalized, ok := util.NormalizeReasoningEffortLevel(model, effort.String()); ok { |
| out, _ = sjson.SetBytes(out, "reasoning_effort", normalized) |
| } |
| } |
|
|
| if effort := gjson.GetBytes(out, "reasoning.effort"); effort.Exists() { |
| if normalized, ok := util.NormalizeReasoningEffortLevel(model, effort.String()); ok { |
| out, _ = sjson.SetBytes(out, "reasoning.effort", normalized) |
| } |
| } |
|
|
| return out |
| } |
|
|
| |
| |
| |
| func ValidateThinkingConfig(payload []byte, model string) error { |
| if len(payload) == 0 || model == "" { |
| return nil |
| } |
| if !util.ModelSupportsThinking(model) || !util.ModelUsesThinkingLevels(model) { |
| return nil |
| } |
|
|
| levels := util.GetModelThinkingLevels(model) |
| checkField := func(path string) error { |
| if effort := gjson.GetBytes(payload, path); effort.Exists() { |
| if _, ok := util.NormalizeReasoningEffortLevel(model, effort.String()); !ok { |
| return statusErr{ |
| code: http.StatusBadRequest, |
| msg: fmt.Sprintf("unsupported reasoning effort level %q for model %s (supported: %s)", effort.String(), model, strings.Join(levels, ", ")), |
| } |
| } |
| } |
| return nil |
| } |
|
|
| if err := checkField("reasoning_effort"); err != nil { |
| return err |
| } |
| if err := checkField("reasoning.effort"); err != nil { |
| return err |
| } |
| return nil |
| } |
|
|