package executor import ( "encoding/json" "strings" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/tidwall/gjson" "github.com/tidwall/sjson") // 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. Defaults are checked// against the original payload when provided.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.DefaultRaw) == 0 && len(rules.Override) == 0 && len(rules.OverrideRaw) == 0 { return payload } model = strings.TrimSpace(model) if model == "" { return payload } candidates := payloadModelCandidates(cfg, model, protocol) out := payload source := original if len(source) == 0 { source = payload } appliedDefaults := make(map[string]struct{}) // Apply default rules: first write wins per field across all matching rules. 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{}{} } } // Apply default raw rules: first write wins per field across all matching rules. 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{}{} } } // Apply override rules: last write wins per field across all matching rules. 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 } } // Apply override raw rules: last write wins per field across all matching rules. 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(cfg *config.Config, model, protocol string) []string { model = strings.TrimSpace(model) if model == "" { return nil } candidates := []string{model} if cfg == nil { return candidates } aliases := payloadModelAliases(cfg, model, protocol) if len(aliases) == 0 { return candidates } seen := map[string]struct{}{strings.ToLower(model): struct{}{}} for _, alias := range aliases { alias = strings.TrimSpace(alias) if alias == "" { continue } key := strings.ToLower(alias) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} candidates = append(candidates, alias) } return candidates} func payloadModelAliases(cfg *config.Config, model, protocol string) []string { if cfg == nil { return nil } model = strings.TrimSpace(model) if model == "" { return nil } channel := strings.ToLower(strings.TrimSpace(protocol)) if channel == "" { return nil } entries := cfg.OAuthModelAlias[channel] if len(entries) == 0 { return nil } aliases := make([]string, 0, 2) for _, entry := range entries { if !strings.EqualFold(strings.TrimSpace(entry.Name), model) { continue } alias := strings.TrimSpace(entry.Alias) if alias == "" { continue } aliases = append(aliases, alias) } return aliases} // 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} 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 }} // 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)}
package executor
import (
"encoding/json"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// 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. Defaults are checked
// against the original payload when provided.
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.DefaultRaw) == 0 && len(rules.Override) == 0 && len(rules.OverrideRaw) == 0 {
return payload
}
model = strings.TrimSpace(model)
if model == "" {
return payload
}
candidates := payloadModelCandidates(cfg, model, protocol)
out := payload
source := original
if len(source) == 0 {
source = payload
}
appliedDefaults := make(map[string]struct{})
// Apply default rules: first write wins per field across all matching rules.
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{}{}
}
}
// Apply default raw rules: first write wins per field across all matching rules.
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{}{}
}
}
// Apply override rules: last write wins per field across all matching rules.
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
}
}
// Apply override raw rules: last write wins per field across all matching rules.
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(cfg *config.Config, model, protocol string) []string {
model = strings.TrimSpace(model)
if model == "" {
return nil
}
candidates := []string{model}
if cfg == nil {
return candidates
}
aliases := payloadModelAliases(cfg, model, protocol)
if len(aliases) == 0 {
return candidates
}
seen := map[string]struct{}{strings.ToLower(model): struct{}{}}
for _, alias := range aliases {
alias = strings.TrimSpace(alias)
if alias == "" {
continue
}
key := strings.ToLower(alias)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
candidates = append(candidates, alias)
}
return candidates
}
func payloadModelAliases(cfg *config.Config, model, protocol string) []string {
if cfg == nil {
return nil
}
model = strings.TrimSpace(model)
if model == "" {
return nil
}
channel := strings.ToLower(strings.TrimSpace(protocol))
if channel == "" {
return nil
}
entries := cfg.OAuthModelAlias[channel]
if len(entries) == 0 {
return nil
}
aliases := make([]string, 0, 2)
for _, entry := range entries {
if !strings.EqualFold(strings.TrimSpace(entry.Name), model) {
continue
}
alias := strings.TrimSpace(entry.Alias)
if alias == "" {
continue
}
aliases = append(aliases, alias)
}
return aliases
}
// 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
}
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
}
}
// 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)
}