Просмотр исходного кода

feat:support $keep_only_declared and deduped $append for header token overrides

Seefs 2 месяцев назад
Родитель
Сommit
d087cc5025

+ 35 - 8
relay/common/override.go

@@ -847,24 +847,30 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
 		return "", false, fmt.Errorf("header value mapping cannot be empty")
 	}
 
-	sourceValue, exists := getHeaderValueFromContext(context, headerName)
-	if !exists {
-		return "", false, nil
+	appendTokens, err := parseHeaderAppendTokens(mapping)
+	if err != nil {
+		return "", false, err
 	}
-	sourceTokens := splitHeaderListValue(sourceValue)
-	if len(sourceTokens) == 0 {
-		return "", false, nil
+	keepOnlyDeclared := parseHeaderKeepOnlyDeclared(mapping)
+
+	sourceValue, exists := getHeaderValueFromContext(context, headerName)
+	sourceTokens := make([]string, 0)
+	if exists {
+		sourceTokens = splitHeaderListValue(sourceValue)
 	}
 
 	wildcardValue, hasWildcard := mapping["*"]
-	resultTokens := make([]string, 0, len(sourceTokens))
+	resultTokens := make([]string, 0, len(sourceTokens)+len(appendTokens))
 	for _, token := range sourceTokens {
 		replacementRaw, hasReplacement := mapping[token]
-		if !hasReplacement && hasWildcard {
+		if !hasReplacement && hasWildcard && !keepOnlyDeclared {
 			replacementRaw = wildcardValue
 			hasReplacement = true
 		}
 		if !hasReplacement {
+			if keepOnlyDeclared {
+				continue
+			}
 			resultTokens = append(resultTokens, token)
 			continue
 		}
@@ -875,6 +881,7 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
 		resultTokens = append(resultTokens, replacementTokens...)
 	}
 
+	resultTokens = append(resultTokens, appendTokens...)
 	resultTokens = lo.Uniq(resultTokens)
 	if len(resultTokens) == 0 {
 		return "", false, nil
@@ -882,6 +889,26 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
 	return strings.Join(resultTokens, ","), true, nil
 }
 
+func parseHeaderAppendTokens(mapping map[string]interface{}) ([]string, error) {
+	appendRaw, ok := mapping["$append"]
+	if !ok {
+		return nil, nil
+	}
+	return parseHeaderReplacementTokens(appendRaw)
+}
+
+func parseHeaderKeepOnlyDeclared(mapping map[string]interface{}) bool {
+	keepOnlyDeclaredRaw, ok := mapping["$keep_only_declared"]
+	if !ok {
+		return false
+	}
+	keepOnlyDeclared, ok := keepOnlyDeclaredRaw.(bool)
+	if !ok {
+		return false
+	}
+	return keepOnlyDeclared
+}
+
 func parseHeaderReplacementTokens(value interface{}) ([]string, error) {
 	switch raw := value.(type) {
 	case nil:

+ 135 - 0
relay/common/override_test.go

@@ -1653,6 +1653,141 @@ func TestApplyParamOverrideSetHeaderMapDeleteWholeHeaderWhenAllTokensCleared(t *
 	}
 }
 
+func TestApplyParamOverrideSetHeaderMapAppendsTokens(t *testing.T) {
+	input := []byte(`{"temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "set_header",
+				"path": "anthropic-beta",
+				"value": map[string]interface{}{
+					"$append": []interface{}{"context-1m-2025-08-07", "computer-use-2025-01-24"},
+				},
+			},
+		},
+	}
+	ctx := map[string]interface{}{
+		"header_override": map[string]interface{}{
+			"anthropic-beta": "computer-use-2025-01-24",
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, ctx)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"temperature":0.7}`, string(out))
+
+	headers, ok := ctx["header_override"].(map[string]interface{})
+	if !ok {
+		t.Fatalf("expected header_override context map")
+	}
+	if headers["anthropic-beta"] != "computer-use-2025-01-24,context-1m-2025-08-07" {
+		t.Fatalf("expected anthropic-beta to append new token without duplicates, got: %v", headers["anthropic-beta"])
+	}
+}
+
+func TestApplyParamOverrideSetHeaderMapAppendsTokensWhenHeaderMissing(t *testing.T) {
+	input := []byte(`{"temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "set_header",
+				"path": "anthropic-beta",
+				"value": map[string]interface{}{
+					"$append": []interface{}{"context-1m-2025-08-07", "computer-use-2025-01-24"},
+				},
+			},
+		},
+	}
+
+	ctx := map[string]interface{}{}
+	out, err := ApplyParamOverride(input, override, ctx)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"temperature":0.7}`, string(out))
+
+	headers, ok := ctx["header_override"].(map[string]interface{})
+	if !ok {
+		t.Fatalf("expected header_override context map")
+	}
+	if headers["anthropic-beta"] != "context-1m-2025-08-07,computer-use-2025-01-24" {
+		t.Fatalf("expected anthropic-beta to be created from appended tokens, got: %v", headers["anthropic-beta"])
+	}
+}
+
+func TestApplyParamOverrideSetHeaderMapKeepOnlyDeclaredDropsUndeclaredTokens(t *testing.T) {
+	input := []byte(`{"temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "set_header",
+				"path": "anthropic-beta",
+				"value": map[string]interface{}{
+					"computer-use-2025-01-24": "computer-use-2025-01-24",
+					"$append":                 []interface{}{"context-1m-2025-08-07"},
+					"$keep_only_declared":     true,
+				},
+			},
+		},
+	}
+	ctx := map[string]interface{}{
+		"header_override": map[string]interface{}{
+			"anthropic-beta": "advanced-tool-use-2025-11-20,computer-use-2025-01-24",
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, ctx)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"temperature":0.7}`, string(out))
+
+	headers, ok := ctx["header_override"].(map[string]interface{})
+	if !ok {
+		t.Fatalf("expected header_override context map")
+	}
+	if headers["anthropic-beta"] != "computer-use-2025-01-24,context-1m-2025-08-07" {
+		t.Fatalf("expected anthropic-beta to keep only declared tokens, got: %v", headers["anthropic-beta"])
+	}
+}
+
+func TestApplyParamOverrideSetHeaderMapKeepOnlyDeclaredDeletesHeaderWhenNothingDeclaredMatches(t *testing.T) {
+	input := []byte(`{"temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "set_header",
+				"path": "anthropic-beta",
+				"value": map[string]interface{}{
+					"computer-use-2025-01-24": "computer-use-2025-01-24",
+					"$keep_only_declared":     true,
+				},
+			},
+		},
+	}
+	ctx := map[string]interface{}{
+		"header_override": map[string]interface{}{
+			"anthropic-beta": "advanced-tool-use-2025-11-20",
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, ctx)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"temperature":0.7}`, string(out))
+
+	headers, ok := ctx["header_override"].(map[string]interface{})
+	if !ok {
+		t.Fatalf("expected header_override context map")
+	}
+	if _, exists := headers["anthropic-beta"]; exists {
+		t.Fatalf("expected anthropic-beta to be deleted when no declared tokens remain, got: %v", headers["anthropic-beta"])
+	}
+}
+
 func TestApplyParamOverrideConditionsObjectShorthand(t *testing.T) {
 	input := []byte(`{"temperature":0.7}`)
 	override := map[string]interface{}{

+ 8 - 2
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx

@@ -163,7 +163,7 @@ const MODE_DESCRIPTIONS = {
   prune_objects: '按条件清理对象中的子项',
   pass_headers: '把指定请求头透传到上游请求',
   sync_fields: '在一个字段有值、另一个缺失时自动补齐',
-  set_header: '设置运行期请求头(支持整值覆盖,或用 JSON 映射按逗号 token 替换/删除)',
+  set_header: '设置运行期请求头(支持整值覆盖,或用 JSON 映射按逗号 token 替换/删除/追加/白名单保留)',
   delete_header: '删除运行期请求头',
   copy_header: '复制请求头',
   move_header: '移动请求头',
@@ -241,6 +241,12 @@ const getModeValuePlaceholder = (mode) => {
       '',
       'JSON map wildcard:',
       '{"*": null, "computer-use-2025-11-24": "computer-use-2025-11-24"}',
+      '',
+      'JSON append example:',
+      '{"$append": ["context-1m-2025-08-07"]}',
+      '',
+      'JSON strict keep example:',
+      '{"computer-use-2025-01-24": "computer-use-2025-01-24", "$append": ["context-1m-2025-08-07"], "$keep_only_declared": true}',
     ].join('\n');
   }
   if (mode === 'pass_headers') return 'Authorization, X-Request-Id';
@@ -260,7 +266,7 @@ const getModeValuePlaceholder = (mode) => {
 
 const getModeValueHelp = (mode) => {
   if (mode !== 'set_header') return '';
-  return '字符串:整条请求头直接覆盖。JSON 映射:按逗号分隔 token 逐项处理,null 表示删除,string/array 表示替换,* 表示兜底规则。';
+  return '字符串:整条请求头直接覆盖。JSON 映射:按逗号分隔 token 逐项处理,null 表示删除,string/array 表示替换,* 表示兜底规则,$append 可在末尾追加新 token,$keep_only_declared=true 时会丢弃未声明 token。';
 };
 
 const SYNC_TARGET_TYPE_OPTIONS = [