From 817da8d73c1fbd527bc8405cdbd6b9ac6a338221 Mon Sep 17 00:00:00 2001 From: Seefs Date: Sat, 3 Jan 2026 10:27:16 +0800 Subject: [PATCH] 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 --- relay/common/override.go | 121 +++++- relay/common/override_test.go | 791 ++++++++++++++++++++++++++++++++++ 2 files changed, 909 insertions(+), 3 deletions(-) create mode 100644 relay/common/override_test.go diff --git a/relay/common/override.go b/relay/common/override.go index 3850218c3..872c960ff 100644 --- a/relay/common/override.go +++ b/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{} diff --git a/relay/common/override_test.go b/relay/common/override_test.go new file mode 100644 index 000000000..021df3f60 --- /dev/null +++ b/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) + } +}