| package executor |
|
|
| import ( |
| "encoding/json" |
| "strings" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" |
| cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" |
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| |
| |
| |
| |
| |
| func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string) []byte { |
| if cfg == nil || len(payload) == 0 { |
| return payload |
| } |
| rules := cfg.Payload |
| if len(rules.Default) == 0 && len(rules.DefaultRaw) == 0 && len(rules.Override) == 0 && len(rules.OverrideRaw) == 0 { |
| return payload |
| } |
| model = strings.TrimSpace(model) |
| requestedModel = strings.TrimSpace(requestedModel) |
| if model == "" && requestedModel == "" { |
| return payload |
| } |
| candidates := payloadModelCandidates(model, requestedModel) |
| 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 !payloadRuleMatchesModels(rule, protocol, candidates) { |
| 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.DefaultRaw { |
| rule := &rules.DefaultRaw[i] |
| if !payloadRuleMatchesModels(rule, protocol, candidates) { |
| 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 |
| } |
| rawValue, ok := payloadRawValue(value) |
| if !ok { |
| continue |
| } |
| updated, errSet := sjson.SetRawBytes(out, fullPath, rawValue) |
| if errSet != nil { |
| continue |
| } |
| out = updated |
| appliedDefaults[fullPath] = struct{}{} |
| } |
| } |
| |
| for i := range rules.Override { |
| rule := &rules.Override[i] |
| if !payloadRuleMatchesModels(rule, protocol, candidates) { |
| 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 |
| } |
| } |
| |
| for i := range rules.OverrideRaw { |
| rule := &rules.OverrideRaw[i] |
| if !payloadRuleMatchesModels(rule, protocol, candidates) { |
| continue |
| } |
| for path, value := range rule.Params { |
| fullPath := buildPayloadPath(root, path) |
| if fullPath == "" { |
| continue |
| } |
| rawValue, ok := payloadRawValue(value) |
| if !ok { |
| continue |
| } |
| updated, errSet := sjson.SetRawBytes(out, fullPath, rawValue) |
| if errSet != nil { |
| continue |
| } |
| out = updated |
| } |
| } |
| return out |
| } |
|
|
| func payloadRuleMatchesModels(rule *config.PayloadRule, protocol string, models []string) bool { |
| if rule == nil || len(models) == 0 { |
| return false |
| } |
| for _, model := range models { |
| if payloadRuleMatchesModel(rule, model, protocol) { |
| return true |
| } |
| } |
| return false |
| } |
|
|
| 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 payloadModelCandidates(model, requestedModel string) []string { |
| model = strings.TrimSpace(model) |
| requestedModel = strings.TrimSpace(requestedModel) |
| if model == "" && requestedModel == "" { |
| return nil |
| } |
| candidates := make([]string, 0, 3) |
| seen := make(map[string]struct{}, 3) |
| addCandidate := func(value string) { |
| value = strings.TrimSpace(value) |
| if value == "" { |
| return |
| } |
| key := strings.ToLower(value) |
| if _, ok := seen[key]; ok { |
| return |
| } |
| seen[key] = struct{}{} |
| candidates = append(candidates, value) |
| } |
| if model != "" { |
| addCandidate(model) |
| } |
| if requestedModel != "" { |
| parsed := thinking.ParseSuffix(requestedModel) |
| base := strings.TrimSpace(parsed.ModelName) |
| if base != "" { |
| addCandidate(base) |
| } |
| if parsed.HasSuffix { |
| addCandidate(requestedModel) |
| } |
| } |
| return candidates |
| } |
|
|
| |
| |
| |
| 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 payloadRawValue(value any) ([]byte, bool) { |
| if value == nil { |
| return nil, false |
| } |
| switch typed := value.(type) { |
| case string: |
| return []byte(typed), true |
| case []byte: |
| return typed, true |
| default: |
| raw, errMarshal := json.Marshal(typed) |
| if errMarshal != nil { |
| return nil, false |
| } |
| return raw, true |
| } |
| } |
|
|
| func payloadRequestedModel(opts cliproxyexecutor.Options, fallback string) string { |
| fallback = strings.TrimSpace(fallback) |
| if len(opts.Metadata) == 0 { |
| return fallback |
| } |
| raw, ok := opts.Metadata[cliproxyexecutor.RequestedModelMetadataKey] |
| if !ok || raw == nil { |
| return fallback |
| } |
| switch v := raw.(type) { |
| case string: |
| if strings.TrimSpace(v) == "" { |
| return fallback |
| } |
| return strings.TrimSpace(v) |
| case []byte: |
| if len(v) == 0 { |
| return fallback |
| } |
| trimmed := strings.TrimSpace(string(v)) |
| if trimmed == "" { |
| return fallback |
| } |
| return trimmed |
| default: |
| return fallback |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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) |
| } |
|
|