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

feat: add parameter coverage for the operations: copy, trim_prefix, trim_suffix, ensure_prefix, ensure_suffix, trim_space, to_lower, to_upper, replace, and regex_replace

Seefs 1 месяц назад
Родитель
Сommit
817da8d73c
2 измененных файлов с 909 добавлено и 3 удалено
  1. 118 3
      relay/common/override.go
  2. 791 0
      relay/common/override_test.go

+ 118 - 3
relay/common/override.go

@@ -23,7 +23,7 @@ type ConditionOperation struct {
 
 type ParamOperation struct {
 	Path       string               `json:"path"`
-	Mode       string               `json:"mode"` // delete, set, move, prepend, append
+	Mode       string               `json:"mode"` // delete, set, move, copy, prepend, append, trim_prefix, trim_suffix, ensure_prefix, ensure_suffix, trim_space, to_lower, to_upper, replace, regex_replace
 	Value      interface{}          `json:"value"`
 	KeepOrigin bool                 `json:"keep_origin"`
 	From       string               `json:"from,omitempty"`
@@ -330,8 +330,6 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
 		}
 		// 处理路径中的负数索引
 		opPath := processNegativeIndex(result, op.Path)
-		opFrom := processNegativeIndex(result, op.From)
-		opTo := processNegativeIndex(result, op.To)
 
 		switch op.Mode {
 		case "delete":
@@ -342,11 +340,38 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
 			}
 			result, err = sjson.Set(result, opPath, op.Value)
 		case "move":
+			opFrom := processNegativeIndex(result, op.From)
+			opTo := processNegativeIndex(result, op.To)
 			result, err = moveValue(result, opFrom, opTo)
+		case "copy":
+			if op.From == "" || op.To == "" {
+				return "", fmt.Errorf("copy from/to is required")
+			}
+			opFrom := processNegativeIndex(result, op.From)
+			opTo := processNegativeIndex(result, op.To)
+			result, err = copyValue(result, opFrom, opTo)
 		case "prepend":
 			result, err = modifyValue(result, opPath, op.Value, op.KeepOrigin, true)
 		case "append":
 			result, err = modifyValue(result, opPath, op.Value, op.KeepOrigin, false)
+		case "trim_prefix":
+			result, err = trimStringValue(result, opPath, op.Value, true)
+		case "trim_suffix":
+			result, err = trimStringValue(result, opPath, op.Value, false)
+		case "ensure_prefix":
+			result, err = ensureStringAffix(result, opPath, op.Value, true)
+		case "ensure_suffix":
+			result, err = ensureStringAffix(result, opPath, op.Value, false)
+		case "trim_space":
+			result, err = transformStringValue(result, opPath, strings.TrimSpace)
+		case "to_lower":
+			result, err = transformStringValue(result, opPath, strings.ToLower)
+		case "to_upper":
+			result, err = transformStringValue(result, opPath, strings.ToUpper)
+		case "replace":
+			result, err = replaceStringValue(result, opPath, op.From, op.To)
+		case "regex_replace":
+			result, err = regexReplaceStringValue(result, opPath, op.From, op.To)
 		default:
 			return "", fmt.Errorf("unknown operation: %s", op.Mode)
 		}
@@ -369,6 +394,14 @@ func moveValue(jsonStr, fromPath, toPath string) (string, error) {
 	return sjson.Delete(result, fromPath)
 }
 
+func copyValue(jsonStr, fromPath, toPath string) (string, error) {
+	sourceValue := gjson.Get(jsonStr, fromPath)
+	if !sourceValue.Exists() {
+		return jsonStr, fmt.Errorf("source path does not exist: %s", fromPath)
+	}
+	return sjson.Set(jsonStr, toPath, sourceValue.Value())
+}
+
 func modifyValue(jsonStr, path string, value interface{}, keepOrigin, isPrepend bool) (string, error) {
 	current := gjson.Get(jsonStr, path)
 	switch {
@@ -422,6 +455,88 @@ func modifyString(jsonStr, path string, value interface{}, isPrepend bool) (stri
 	return sjson.Set(jsonStr, path, newStr)
 }
 
+func trimStringValue(jsonStr, path string, value interface{}, isPrefix bool) (string, error) {
+	current := gjson.Get(jsonStr, path)
+	if current.Type != gjson.String {
+		return jsonStr, fmt.Errorf("operation not supported for type: %v", current.Type)
+	}
+
+	if value == nil {
+		return jsonStr, fmt.Errorf("trim value is required")
+	}
+	valueStr := fmt.Sprintf("%v", value)
+
+	var newStr string
+	if isPrefix {
+		newStr = strings.TrimPrefix(current.String(), valueStr)
+	} else {
+		newStr = strings.TrimSuffix(current.String(), valueStr)
+	}
+	return sjson.Set(jsonStr, path, newStr)
+}
+
+func ensureStringAffix(jsonStr, path string, value interface{}, isPrefix bool) (string, error) {
+	current := gjson.Get(jsonStr, path)
+	if current.Type != gjson.String {
+		return jsonStr, fmt.Errorf("operation not supported for type: %v", current.Type)
+	}
+
+	if value == nil {
+		return jsonStr, fmt.Errorf("ensure value is required")
+	}
+	valueStr := fmt.Sprintf("%v", value)
+	if valueStr == "" {
+		return jsonStr, fmt.Errorf("ensure value is required")
+	}
+
+	currentStr := current.String()
+	if isPrefix {
+		if strings.HasPrefix(currentStr, valueStr) {
+			return jsonStr, nil
+		}
+		return sjson.Set(jsonStr, path, valueStr+currentStr)
+	}
+
+	if strings.HasSuffix(currentStr, valueStr) {
+		return jsonStr, nil
+	}
+	return sjson.Set(jsonStr, path, currentStr+valueStr)
+}
+
+func transformStringValue(jsonStr, path string, transform func(string) string) (string, error) {
+	current := gjson.Get(jsonStr, path)
+	if current.Type != gjson.String {
+		return jsonStr, fmt.Errorf("operation not supported for type: %v", current.Type)
+	}
+	return sjson.Set(jsonStr, path, transform(current.String()))
+}
+
+func replaceStringValue(jsonStr, path, from, to string) (string, error) {
+	current := gjson.Get(jsonStr, path)
+	if current.Type != gjson.String {
+		return jsonStr, fmt.Errorf("operation not supported for type: %v", current.Type)
+	}
+	if from == "" {
+		return jsonStr, fmt.Errorf("replace from is required")
+	}
+	return sjson.Set(jsonStr, path, strings.ReplaceAll(current.String(), from, to))
+}
+
+func regexReplaceStringValue(jsonStr, path, pattern, replacement string) (string, error) {
+	current := gjson.Get(jsonStr, path)
+	if current.Type != gjson.String {
+		return jsonStr, fmt.Errorf("operation not supported for type: %v", current.Type)
+	}
+	if pattern == "" {
+		return jsonStr, fmt.Errorf("regex pattern is required")
+	}
+	re, err := regexp.Compile(pattern)
+	if err != nil {
+		return jsonStr, err
+	}
+	return sjson.Set(jsonStr, path, re.ReplaceAllString(current.String(), replacement))
+}
+
 func mergeObjects(jsonStr, path string, value interface{}, keepOrigin bool) (string, error) {
 	current := gjson.Get(jsonStr, path)
 	var currentMap, newMap map[string]interface{}

+ 791 - 0
relay/common/override_test.go

@@ -0,0 +1,791 @@
+package common
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+)
+
+func TestApplyParamOverrideTrimPrefix(t *testing.T) {
+	// trim_prefix example:
+	// {"operations":[{"path":"model","mode":"trim_prefix","value":"openai/"}]}
+	input := []byte(`{"model":"openai/gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "trim_prefix",
+				"value": "openai/",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","temperature":0.7}`, string(out))
+}
+
+func TestApplyParamOverrideTrimSuffix(t *testing.T) {
+	// trim_suffix example:
+	// {"operations":[{"path":"model","mode":"trim_suffix","value":"-latest"}]}
+	input := []byte(`{"model":"gpt-4-latest","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "trim_suffix",
+				"value": "-latest",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","temperature":0.7}`, string(out))
+}
+
+func TestApplyParamOverrideTrimNoop(t *testing.T) {
+	// trim_prefix no-op example:
+	// {"operations":[{"path":"model","mode":"trim_prefix","value":"openai/"}]}
+	input := []byte(`{"model":"gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "trim_prefix",
+				"value": "openai/",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","temperature":0.7}`, string(out))
+}
+
+func TestApplyParamOverrideTrimRequiresValue(t *testing.T) {
+	// trim_prefix requires value example:
+	// {"operations":[{"path":"model","mode":"trim_prefix"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "trim_prefix",
+			},
+		},
+	}
+
+	_, err := ApplyParamOverride(input, override, nil)
+	if err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
+
+func TestApplyParamOverrideReplace(t *testing.T) {
+	// replace example:
+	// {"operations":[{"path":"model","mode":"replace","from":"openai/","to":""}]}
+	input := []byte(`{"model":"openai/gpt-4o-mini","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "replace",
+				"from": "openai/",
+				"to":   "",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4o-mini","temperature":0.7}`, string(out))
+}
+
+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]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "regex_replace",
+				"from": "^gpt-",
+				"to":   "openai/gpt-",
+			},
+		},
+	}
+
+	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) {
+	// replace requires from example:
+	// {"operations":[{"path":"model","mode":"replace"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "replace",
+			},
+		},
+	}
+
+	_, err := ApplyParamOverride(input, override, nil)
+	if err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
+
+func TestApplyParamOverrideRegexReplaceRequiresPattern(t *testing.T) {
+	// regex_replace requires from(pattern) example:
+	// {"operations":[{"path":"model","mode":"regex_replace"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "regex_replace",
+			},
+		},
+	}
+
+	_, err := ApplyParamOverride(input, override, nil)
+	if err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
+
+func TestApplyParamOverrideDelete(t *testing.T) {
+	input := []byte(`{"model":"gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "temperature",
+				"mode": "delete",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+
+	var got map[string]interface{}
+	if err := json.Unmarshal(out, &got); err != nil {
+		t.Fatalf("failed to unmarshal output JSON: %v", err)
+	}
+	if _, exists := got["temperature"]; exists {
+		t.Fatalf("expected temperature to be deleted")
+	}
+}
+
+func TestApplyParamOverrideSet(t *testing.T) {
+	input := []byte(`{"model":"gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "temperature",
+				"mode":  "set",
+				"value": 0.1,
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","temperature":0.1}`, string(out))
+}
+
+func TestApplyParamOverrideSetKeepOrigin(t *testing.T) {
+	input := []byte(`{"model":"gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":        "temperature",
+				"mode":        "set",
+				"value":       0.1,
+				"keep_origin": true,
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","temperature":0.7}`, string(out))
+}
+
+func TestApplyParamOverrideMove(t *testing.T) {
+	input := []byte(`{"model":"gpt-4","meta":{"x":1}}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "move",
+				"from": "model",
+				"to":   "meta.model",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"meta":{"x":1,"model":"gpt-4"}}`, string(out))
+}
+
+func TestApplyParamOverrideMoveMissingSource(t *testing.T) {
+	input := []byte(`{"meta":{"x":1}}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "move",
+				"from": "model",
+				"to":   "meta.model",
+			},
+		},
+	}
+
+	_, err := ApplyParamOverride(input, override, nil)
+	if err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
+
+func TestApplyParamOverridePrependAppendString(t *testing.T) {
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "prepend",
+				"value": "openai/",
+			},
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "append",
+				"value": "-latest",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"openai/gpt-4-latest"}`, string(out))
+}
+
+func TestApplyParamOverridePrependAppendArray(t *testing.T) {
+	input := []byte(`{"arr":[1,2]}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "arr",
+				"mode":  "prepend",
+				"value": 0,
+			},
+			map[string]interface{}{
+				"path":  "arr",
+				"mode":  "append",
+				"value": []interface{}{3, 4},
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"arr":[0,1,2,3,4]}`, string(out))
+}
+
+func TestApplyParamOverrideAppendObjectMergeKeepOrigin(t *testing.T) {
+	input := []byte(`{"obj":{"a":1}}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":        "obj",
+				"mode":        "append",
+				"keep_origin": true,
+				"value": map[string]interface{}{
+					"a": 2,
+					"b": 3,
+				},
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"obj":{"a":1,"b":3}}`, string(out))
+}
+
+func TestApplyParamOverrideAppendObjectMergeOverride(t *testing.T) {
+	input := []byte(`{"obj":{"a":1}}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "obj",
+				"mode": "append",
+				"value": map[string]interface{}{
+					"a": 2,
+					"b": 3,
+				},
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"obj":{"a":2,"b":3}}`, string(out))
+}
+
+func TestApplyParamOverrideConditionORDefault(t *testing.T) {
+	input := []byte(`{"model":"gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "temperature",
+				"mode":  "set",
+				"value": 0.1,
+				"conditions": []interface{}{
+					map[string]interface{}{
+						"path":  "model",
+						"mode":  "prefix",
+						"value": "gpt",
+					},
+					map[string]interface{}{
+						"path":  "model",
+						"mode":  "prefix",
+						"value": "claude",
+					},
+				},
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","temperature":0.1}`, string(out))
+}
+
+func TestApplyParamOverrideConditionAND(t *testing.T) {
+	input := []byte(`{"model":"gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "temperature",
+				"mode":  "set",
+				"value": 0.1,
+				"logic": "AND",
+				"conditions": []interface{}{
+					map[string]interface{}{
+						"path":  "model",
+						"mode":  "prefix",
+						"value": "gpt",
+					},
+					map[string]interface{}{
+						"path":  "temperature",
+						"mode":  "gt",
+						"value": 0.5,
+					},
+				},
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","temperature":0.1}`, string(out))
+}
+
+func TestApplyParamOverrideConditionInvert(t *testing.T) {
+	input := []byte(`{"model":"gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "temperature",
+				"mode":  "set",
+				"value": 0.1,
+				"conditions": []interface{}{
+					map[string]interface{}{
+						"path":   "model",
+						"mode":   "prefix",
+						"value":  "gpt",
+						"invert": true,
+					},
+				},
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","temperature":0.7}`, string(out))
+}
+
+func TestApplyParamOverrideConditionPassMissingKey(t *testing.T) {
+	input := []byte(`{"temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "temperature",
+				"mode":  "set",
+				"value": 0.1,
+				"conditions": []interface{}{
+					map[string]interface{}{
+						"path":             "model",
+						"mode":             "prefix",
+						"value":            "gpt",
+						"pass_missing_key": true,
+					},
+				},
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"temperature":0.1}`, string(out))
+}
+
+func TestApplyParamOverrideConditionFromContext(t *testing.T) {
+	input := []byte(`{"temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "temperature",
+				"mode":  "set",
+				"value": 0.1,
+				"conditions": []interface{}{
+					map[string]interface{}{
+						"path":  "model",
+						"mode":  "prefix",
+						"value": "gpt",
+					},
+				},
+			},
+		},
+	}
+	ctx := map[string]interface{}{
+		"model": "gpt-4",
+	}
+
+	out, err := ApplyParamOverride(input, override, ctx)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"temperature":0.1}`, string(out))
+}
+
+func TestApplyParamOverrideNegativeIndexPath(t *testing.T) {
+	input := []byte(`{"arr":[{"model":"a"},{"model":"b"}]}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "arr.-1.model",
+				"mode":  "set",
+				"value": "c",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"arr":[{"model":"a"},{"model":"c"}]}`, string(out))
+}
+
+func TestApplyParamOverrideRegexReplaceInvalidPattern(t *testing.T) {
+	// regex_replace invalid pattern example:
+	// {"operations":[{"path":"model","mode":"regex_replace","from":"(","to":"x"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "regex_replace",
+				"from": "(",
+				"to":   "x",
+			},
+		},
+	}
+
+	_, err := ApplyParamOverride(input, override, nil)
+	if err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
+
+func TestApplyParamOverrideCopy(t *testing.T) {
+	// copy example:
+	// {"operations":[{"mode":"copy","from":"model","to":"original_model"}]}
+	input := []byte(`{"model":"gpt-4","temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "copy",
+				"from": "model",
+				"to":   "original_model",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4","original_model":"gpt-4","temperature":0.7}`, string(out))
+}
+
+func TestApplyParamOverrideCopyMissingSource(t *testing.T) {
+	// copy missing source example:
+	// {"operations":[{"mode":"copy","from":"model","to":"original_model"}]}
+	input := []byte(`{"temperature":0.7}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "copy",
+				"from": "model",
+				"to":   "original_model",
+			},
+		},
+	}
+
+	_, err := ApplyParamOverride(input, override, nil)
+	if err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
+
+func TestApplyParamOverrideCopyRequiresFromTo(t *testing.T) {
+	// copy requires from/to example:
+	// {"operations":[{"mode":"copy"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"mode": "copy",
+			},
+		},
+	}
+
+	_, err := ApplyParamOverride(input, override, nil)
+	if err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
+
+func TestApplyParamOverrideEnsurePrefix(t *testing.T) {
+	// ensure_prefix example:
+	// {"operations":[{"path":"model","mode":"ensure_prefix","value":"openai/"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "ensure_prefix",
+				"value": "openai/",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"openai/gpt-4"}`, string(out))
+}
+
+func TestApplyParamOverrideEnsurePrefixNoop(t *testing.T) {
+	// ensure_prefix no-op example:
+	// {"operations":[{"path":"model","mode":"ensure_prefix","value":"openai/"}]}
+	input := []byte(`{"model":"openai/gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "ensure_prefix",
+				"value": "openai/",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"openai/gpt-4"}`, string(out))
+}
+
+func TestApplyParamOverrideEnsureSuffix(t *testing.T) {
+	// ensure_suffix example:
+	// {"operations":[{"path":"model","mode":"ensure_suffix","value":"-latest"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "ensure_suffix",
+				"value": "-latest",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4-latest"}`, string(out))
+}
+
+func TestApplyParamOverrideEnsureSuffixNoop(t *testing.T) {
+	// ensure_suffix no-op example:
+	// {"operations":[{"path":"model","mode":"ensure_suffix","value":"-latest"}]}
+	input := []byte(`{"model":"gpt-4-latest"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path":  "model",
+				"mode":  "ensure_suffix",
+				"value": "-latest",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4-latest"}`, string(out))
+}
+
+func TestApplyParamOverrideEnsureRequiresValue(t *testing.T) {
+	// ensure_prefix requires value example:
+	// {"operations":[{"path":"model","mode":"ensure_prefix"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "ensure_prefix",
+			},
+		},
+	}
+
+	_, err := ApplyParamOverride(input, override, nil)
+	if err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
+
+func TestApplyParamOverrideTrimSpace(t *testing.T) {
+	// trim_space example:
+	// {"operations":[{"path":"model","mode":"trim_space"}]}
+	input := []byte("{\"model\":\"  gpt-4 \\n\"}")
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "trim_space",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4"}`, string(out))
+}
+
+func TestApplyParamOverrideToLower(t *testing.T) {
+	// to_lower example:
+	// {"operations":[{"path":"model","mode":"to_lower"}]}
+	input := []byte(`{"model":"GPT-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "to_lower",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"gpt-4"}`, string(out))
+}
+
+func TestApplyParamOverrideToUpper(t *testing.T) {
+	// to_upper example:
+	// {"operations":[{"path":"model","mode":"to_upper"}]}
+	input := []byte(`{"model":"gpt-4"}`)
+	override := map[string]interface{}{
+		"operations": []interface{}{
+			map[string]interface{}{
+				"path": "model",
+				"mode": "to_upper",
+			},
+		},
+	}
+
+	out, err := ApplyParamOverride(input, override, nil)
+	if err != nil {
+		t.Fatalf("ApplyParamOverride returned error: %v", err)
+	}
+	assertJSONEqual(t, `{"model":"GPT-4"}`, string(out))
+}
+
+func assertJSONEqual(t *testing.T, want, got string) {
+	t.Helper()
+
+	var wantObj interface{}
+	var gotObj interface{}
+
+	if err := json.Unmarshal([]byte(want), &wantObj); err != nil {
+		t.Fatalf("failed to unmarshal want JSON: %v", err)
+	}
+	if err := json.Unmarshal([]byte(got), &gotObj); err != nil {
+		t.Fatalf("failed to unmarshal got JSON: %v", err)
+	}
+
+	if !reflect.DeepEqual(wantObj, gotObj) {
+		t.Fatalf("json not equal\nwant: %s\ngot:  %s", want, got)
+	}
+}