304 lines · 7.1 KB · Go
RawDownload
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)}