1020 lines · 26.3 KB · Go
RawDownload
package helps import (	"encoding/json"	"net/http"	"reflect"	"strconv"	"strings" 	"github.com/router-for-me/CLIProxyAPI/v7/internal/config"	"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"	cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"	"github.com/tidwall/gjson"	"github.com/tidwall/sjson") // ApplyPayloadConfigWithRoot behaves like applyPayloadConfig but treats all parameter// paths as relative to the provided root path and restricts matches to the given// protocol when supplied. Defaults are checked// against the original payload when provided. requestedModel carries the client-visible// model name before alias resolution so payload rules can target aliases precisely.// requestPath is the inbound HTTP request path (when available) used for endpoint-scoped gates.func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string, requestPath string) []byte {	return ApplyPayloadConfigWithRequest(cfg, model, protocol, "", root, payload, original, requestedModel, requestPath, nil)} // ApplyPayloadConfigWithRequest applies payload config using source protocol and request header gates.func ApplyPayloadConfigWithRequest(cfg *config.Config, model, protocol, fromProtocol, root string, payload, original []byte, requestedModel string, requestPath string, headers http.Header) []byte {	out, _ := ApplyPayloadConfigWithRequestTracked(cfg, model, protocol, fromProtocol, root, payload, original, requestedModel, requestPath, headers, "")	return out} // ApplyPayloadConfigWithRequestTracked applies payload config and reports whether// an applied rule targeted trackedPath or one of its descendants.// ApplyPayloadConfigWithTrackedPaths applies payload config and reports which// tracked paths (or their descendants) were targeted by an applied rule.func ApplyPayloadConfigWithTrackedPaths(cfg *config.Config, model, protocol, fromProtocol, root string, payload, original []byte, requestedModel string, requestPath string, headers http.Header, trackedPaths ...string) ([]byte, map[string]bool) {	touched := make(map[string]bool)	if cfg == nil || len(payload) == 0 {		return payload, touched	}	out := payload 	markTouched := func(resolvedPath string) {		for _, tp := range trackedPaths {			tp = strings.TrimSpace(tp)			if tp != "" && payloadRuleTargetsPath(resolvedPath, tp) {				touched[tp] = true			}		}	} 	// Apply disable-image-generation filtering before payload rules so config payload	// overrides can explicitly re-enable image_generation when desired.	if shouldStripImageGeneration(cfg.DisableImageGeneration, requestPath) {		out = removeToolTypeFromPayloadWithRoot(out, root, "image_generation")		out = removeToolChoiceFromPayloadWithRoot(out, root, "image_generation")	} 	rules := cfg.Payload	hasPayloadRules := len(rules.Default) != 0 || len(rules.DefaultRaw) != 0 || len(rules.Override) != 0 || len(rules.OverrideRaw) != 0 || len(rules.Filter) != 0	if hasPayloadRules {		model = strings.TrimSpace(model)		requestedModel = strings.TrimSpace(requestedModel)		if model != "" || requestedModel != "" {			candidates := payloadModelCandidates(model, requestedModel)			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 !payloadModelRulesMatch(rule.Models, protocol, fromProtocol, headers, out, root, candidates) {					continue				}				for path, value := range rule.Params {					fullPath := buildPayloadPath(root, path)					if fullPath == "" {						continue					}					for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {						if gjson.GetBytes(source, resolvedPath).Exists() {							continue						}						if _, ok := appliedDefaults[resolvedPath]; ok {							continue						}						updated, errSet := sjson.SetBytes(out, resolvedPath, value)						if errSet != nil {							continue						}						out = updated						appliedDefaults[resolvedPath] = struct{}{}						markTouched(resolvedPath)					}				}			}			// Apply default raw rules: first write wins per field across all matching rules.			for i := range rules.DefaultRaw {				rule := &rules.DefaultRaw[i]				if !payloadModelRulesMatch(rule.Models, protocol, fromProtocol, headers, out, root, candidates) {					continue				}				for path, value := range rule.Params {					fullPath := buildPayloadPath(root, path)					if fullPath == "" {						continue					}					for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {						if gjson.GetBytes(source, resolvedPath).Exists() {							continue						}						if _, ok := appliedDefaults[resolvedPath]; ok {							continue						}						rawValue, ok := payloadRawValue(value)						if !ok {							continue						}						updated, errSet := sjson.SetRawBytes(out, resolvedPath, rawValue)						if errSet != nil {							continue						}						out = updated						appliedDefaults[resolvedPath] = struct{}{}						markTouched(resolvedPath)					}				}			}			// Apply override rules: last write wins per field across all matching rules.			for i := range rules.Override {				rule := &rules.Override[i]				if !payloadModelRulesMatch(rule.Models, protocol, fromProtocol, headers, out, root, candidates) {					continue				}				for path, value := range rule.Params {					fullPath := buildPayloadPath(root, path)					if fullPath == "" {						continue					}					for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {						var applied bool						out, applied = setPayloadValueIfDifferentTracked(out, resolvedPath, value)						if applied {							markTouched(resolvedPath)						}					}				}			}			// Apply override raw rules: last write wins per field across all matching rules.			for i := range rules.OverrideRaw {				rule := &rules.OverrideRaw[i]				if !payloadModelRulesMatch(rule.Models, protocol, fromProtocol, headers, out, root, candidates) {					continue				}				for path, value := range rule.Params {					fullPath := buildPayloadPath(root, path)					if fullPath == "" {						continue					}					rawValue, ok := payloadRawValue(value)					if !ok {						continue					}					for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {						var applied bool						out, applied = setPayloadRawValueIfDifferentTracked(out, resolvedPath, rawValue)						if applied {							markTouched(resolvedPath)						}					}				}			}			// Apply filter rules: remove matching paths from payload.			for i := range rules.Filter {				rule := &rules.Filter[i]				if !payloadModelRulesMatch(rule.Models, protocol, fromProtocol, headers, out, root, candidates) {					continue				}				for _, path := range rule.Params {					fullPath := buildPayloadPath(root, path)					if fullPath == "" {						continue					}					resolvedPaths := resolvePayloadRulePaths(out, fullPath)					for i := len(resolvedPaths) - 1; i >= 0; i-- {						resolvedPath := resolvedPaths[i]						updated, errDel := sjson.DeleteBytes(out, resolvedPath)						if errDel != nil {							continue						}						out = updated						markTouched(resolvedPath)					}				}			}		}	}	return out, touched} // ApplyPayloadConfigWithRequestTracked applies payload config and reports whether// an applied rule targeted trackedPath or one of its descendants.func ApplyPayloadConfigWithRequestTracked(cfg *config.Config, model, protocol, fromProtocol, root string, payload, original []byte, requestedModel string, requestPath string, headers http.Header, trackedPath string) ([]byte, bool) {	out, touched := ApplyPayloadConfigWithTrackedPaths(cfg, model, protocol, fromProtocol, root, payload, original, requestedModel, requestPath, headers, trackedPath)	return out, touched[trackedPath]} func isImagesEndpointRequestPath(path string) bool {	path = strings.TrimSpace(path)	if path == "" {		return false	}	if path == "/v1/images/generations" || path == "/v1/images/edits" {		return true	}	// Be tolerant of prefix routers that may report a longer matched route.	if strings.HasSuffix(path, "/v1/images/generations") || strings.HasSuffix(path, "/v1/images/edits") {		return true	}	if strings.HasSuffix(path, "/images/generations") || strings.HasSuffix(path, "/images/edits") {		return true	}	return false} // shouldStripImageGeneration reports whether the built-in image_generation tool must be// removed from the outbound payload for the given mode and request path.//   - All: strip on every endpoint.//   - Chat: strip only on non-images endpoints; keep it on /v1/images/* endpoints.//   - Off / Passthrough: never strip. Off injects the tool elsewhere; Passthrough forwards//     the client payload untouched.func shouldStripImageGeneration(mode config.DisableImageGenerationMode, requestPath string) bool {	switch mode {	case config.DisableImageGenerationAll:		return true	case config.DisableImageGenerationChat:		return !isImagesEndpointRequestPath(requestPath)	default:		return false	}} func payloadModelRulesMatch(rules []config.PayloadModelRule, protocol string, fromProtocol string, headers http.Header, payload []byte, root string, models []string) bool {	if len(rules) == 0 || len(models) == 0 {		return false	}	for _, model := range models {		for _, entry := range rules {			name := strings.TrimSpace(entry.Name)			if name == "" {				continue			}			if ep := strings.TrimSpace(entry.Protocol); ep != "" && protocol != "" && !strings.EqualFold(ep, protocol) {				continue			}			if !payloadFromProtocolMatches(entry.FromProtocol, fromProtocol) {				continue			}			if !payloadHeadersMatch(headers, entry.Headers) {				continue			}			if !matchModelPattern(name, model) {				continue			}			if payloadModelRuleConditionsMatch(payload, root, entry) {				return true			}		}	}	return false} func payloadModelRuleConditionsMatch(payload []byte, root string, rule config.PayloadModelRule) bool {	if !payloadMatchConditionsMatch(payload, root, rule.Match) {		return false	}	if !payloadNotMatchConditionsMatch(payload, root, rule.NotMatch) {		return false	}	if !payloadExistConditionsMatch(payload, root, rule.Exist) {		return false	}	if !payloadNotExistConditionsMatch(payload, root, rule.NotExist) {		return false	}	return true} func payloadMatchConditionsMatch(payload []byte, root string, conditions []map[string]any) bool {	for _, condition := range conditions {		for path, value := range condition {			if strings.TrimSpace(path) == "" {				continue			}			if !payloadPathMatchesValue(payload, buildPayloadPath(root, path), value) {				return false			}		}	}	return true} func payloadNotMatchConditionsMatch(payload []byte, root string, conditions []map[string]any) bool {	for _, condition := range conditions {		for path, value := range condition {			if strings.TrimSpace(path) == "" {				continue			}			if payloadPathMatchesValue(payload, buildPayloadPath(root, path), value) {				return false			}		}	}	return true} func payloadExistConditionsMatch(payload []byte, root string, paths []string) bool {	for _, path := range paths {		if strings.TrimSpace(path) == "" {			continue		}		if !payloadPathExists(payload, buildPayloadPath(root, path)) {			return false		}	}	return true} func payloadNotExistConditionsMatch(payload []byte, root string, paths []string) bool {	for _, path := range paths {		if strings.TrimSpace(path) == "" {			continue		}		if payloadPathExists(payload, buildPayloadPath(root, path)) {			return false		}	}	return true} func payloadPathMatchesValue(payload []byte, path string, value any) bool {	for _, resolvedPath := range resolvePayloadRulePaths(payload, path) {		result := gjson.GetBytes(payload, resolvedPath)		if !result.Exists() {			continue		}		if payloadResultEquals(result, value) {			return true		}	}	return false} func payloadPathExists(payload []byte, path string) bool {	for _, resolvedPath := range resolvePayloadRulePaths(payload, path) {		result := gjson.GetBytes(payload, resolvedPath)		if result.Exists() && result.Type != gjson.Null {			return true		}	}	return false} func payloadResultEquals(result gjson.Result, value any) bool {	actual, ok := normalizedPayloadResult(result)	if !ok {		return false	}	expected, ok := normalizedPayloadValue(value)	if !ok {		return false	}	return reflect.DeepEqual(actual, expected)} func normalizedPayloadResult(result gjson.Result) (any, bool) {	if !result.Exists() {		return nil, false	}	raw := strings.TrimSpace(result.Raw)	if raw == "" {		encoded, errMarshal := json.Marshal(result.Value())		if errMarshal != nil {			return nil, false		}		raw = string(encoded)	}	return normalizedPayloadJSON([]byte(raw))} func normalizedPayloadValue(value any) (any, bool) {	encoded, errMarshal := json.Marshal(value)	if errMarshal != nil {		return nil, false	}	return normalizedPayloadJSON(encoded)} func normalizedPayloadJSON(data []byte) (any, bool) {	if len(strings.TrimSpace(string(data))) == 0 {		return nil, false	}	var out any	if errUnmarshal := json.Unmarshal(data, &out); errUnmarshal != nil {		return nil, false	}	return out, true} func payloadFromProtocolMatches(pattern, fromProtocol string) bool {	pattern = normalizePayloadFromProtocol(pattern)	if pattern == "" {		return true	}	fromProtocol = normalizePayloadFromProtocol(fromProtocol)	if fromProtocol == "" {		return false	}	return strings.EqualFold(pattern, fromProtocol)} func normalizePayloadFromProtocol(protocol string) string {	protocol = strings.ToLower(strings.TrimSpace(protocol))	switch protocol {	case "openai-response", "openai-responses", "response":		return "responses"	default:		return protocol	}} func payloadHeadersMatch(headers http.Header, rules map[string]string) bool {	if len(rules) == 0 {		return true	}	for key, pattern := range rules {		key = strings.TrimSpace(key)		if key == "" {			continue		}		values := payloadHeaderValues(headers, key)		if len(values) == 0 {			return false		}		matched := false		for _, value := range values {			if matchModelPattern(pattern, value) {				matched = true				break			}		}		if !matched {			return false		}	}	return true} func payloadHeaderValues(headers http.Header, key string) []string {	if headers == nil {		return nil	}	var values []string	for headerKey, headerValues := range headers {		if strings.EqualFold(headerKey, key) {			values = append(values, headerValues...)		}	}	return values} 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)

Lines 1–500 of 1020.