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. Load more
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)