From 01da3a55f838cfa21010b05c7451c0a42cf5a173 Mon Sep 17 00:00:00 2001 From: Seefs <40468931+seefs001@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:16:57 +0800 Subject: [PATCH] perf(relay): stop recompiling param override regexes on every request (#8061) --- relay/common/override.go | 34 +++++++++++++++-------- relay/common/override_test.go | 52 +++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/relay/common/override.go b/relay/common/override.go index 0ec6600418..d751e84ba2 100644 --- a/relay/common/override.go +++ b/relay/common/override.go @@ -10,6 +10,7 @@ import ( "sort" "strconv" "strings" + "sync" "github.com/QuantumNous/new-api/common" kitreasoning "github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning" @@ -21,6 +22,22 @@ import ( var negativeIndexRegexp = regexp.MustCompile(`\.(-\d+)`) +// Param override rules are re-parsed for every request, so compiled patterns are +// shared by pattern text instead of being stored on the parsed operations. +var paramOverrideRegexCache sync.Map // map[string]*regexp.Regexp + +func getParamOverrideRegex(pattern string) (*regexp.Regexp, error) { + if cached, ok := paramOverrideRegexCache.Load(pattern); ok { + return cached.(*regexp.Regexp), nil + } + compiled, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + actual, _ := paramOverrideRegexCache.LoadOrStore(pattern, compiled) + return actual.(*regexp.Regexp), nil +} + const ( paramOverrideContextRequestHeaders = "request_headers" paramOverrideContextHeaderOverride = "header_override" @@ -60,10 +77,6 @@ type ConditionOperation struct { Value any `json:"value"` // 匹配的值 Invert bool `json:"invert"` // 反选功能,true表示取反结果 PassMissingKey bool `json:"pass_missing_key"` // 未获取到json key时的行为 - - // Conditions are parsed per request. Reuse the lazily compiled pattern when - // pruning multiple objects; never compile before a matching key exists. - regex *regexp.Regexp } type ParamOperation struct { @@ -729,14 +742,11 @@ func (condition *ConditionOperation) compareGjsonValues(jsonValue, targetValue g if targetValue.Type != gjson.String { return false, fmt.Errorf("regex pattern must be a string") } - if condition.regex == nil { - re, err := regexp.Compile(targetValue.String()) - if err != nil { - return false, fmt.Errorf("invalid regex pattern: %w", err) - } - condition.regex = re + re, err := getParamOverrideRegex(targetValue.String()) + if err != nil { + return false, fmt.Errorf("invalid regex pattern: %w", err) } - return condition.regex.MatchString(jsonValue.String()), nil + return re.MatchString(jsonValue.String()), nil case "gt": return compareNumeric(jsonValue, targetValue, "gt") case "gte": @@ -1914,7 +1924,7 @@ func regexReplaceStringValue(data []byte, path, pattern, replacement string) ([] if pattern == "" { return data, fmt.Errorf("regex pattern is required") } - re, err := regexp.Compile(pattern) + re, err := getParamOverrideRegex(pattern) if err != nil { return data, err } diff --git a/relay/common/override_test.go b/relay/common/override_test.go index 87ab8b227a..3ea22275b7 100644 --- a/relay/common/override_test.go +++ b/relay/common/override_test.go @@ -165,23 +165,45 @@ func TestApplyParamOverrideReplace(t *testing.T) { func TestApplyParamOverrideRegexReplace(t *testing.T) { // regex_replace example: // {"operations":[{"path":"model","mode":"regex_replace","from":"^gpt-","to":"openai/gpt-"}]} - input := []byte(`{"model":"gpt-4o-mini","temperature":0.7}`) - override := map[string]any{ - "operations": []any{ - map[string]any{ - "path": "model", - "mode": "regex_replace", - "from": "^gpt-", - "to": "openai/gpt-", - }, + operation := map[string]any{"mode": "regex_replace"} + override := map[string]any{"operations": []any{operation}} + // Reusing a channel configuration must apply the current pattern on every request. + for _, tt := range []struct { + name string + input string + path string + from string + to string + want string + }{ + { + name: "single path", + input: `{"model":"gpt-4o-mini","temperature":0.7}`, + path: "model", from: "^gpt-", to: "openai/gpt-", + want: `{"model":"openai/gpt-4o-mini","temperature":0.7}`, }, + { + name: "next request uses updated pattern", + input: `{"model":"gpt-4o-mini","temperature":0.7}`, + path: "model", from: "-mini$", to: "", + want: `{"model":"gpt-4o","temperature":0.7}`, + }, + { + name: "wildcard path applies one pattern to every field", + input: `{"messages":[{"content":"foo 1"},{"content":"foo 2"},{"content":"bar"}]}`, + path: "messages.*.content", from: `^foo (\d)$`, to: "baz $1", + want: `{"messages":[{"content":"baz 1"},{"content":"baz 2"},{"content":"bar"}]}`, + }, + } { + t.Run(tt.name, func(t *testing.T) { + operation["path"] = tt.path + operation["from"] = tt.from + operation["to"] = tt.to + out, err := ApplyParamOverride([]byte(tt.input), override, nil) + require.NoError(t, err) + assert.JSONEq(t, tt.want, string(out)) + }) } - - out, err := ApplyParamOverride(input, override, nil) - if err != nil { - t.Fatalf("ApplyParamOverride returned error: %v", err) - } - assertJSONEqual(t, `{"model":"openai/gpt-4o-mini","temperature":0.7}`, string(out)) } func TestApplyParamOverrideReplaceRequiresFrom(t *testing.T) {