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