feat: sync field

This commit is contained in:
Seefs
2026-02-22 01:27:58 +08:00
parent 81d9173027
commit 285d7233a3
3 changed files with 378 additions and 3 deletions

View File

@@ -34,7 +34,7 @@ type ConditionOperation struct {
type ParamOperation struct {
Path string `json:"path"`
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, return_error, prune_objects, set_header, delete_header, copy_header, move_header
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, return_error, prune_objects, set_header, delete_header, copy_header, move_header, sync_fields
Value interface{} `json:"value"`
KeepOrigin bool `json:"keep_origin"`
From string `json:"from,omitempty"`
@@ -494,6 +494,11 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if err == nil {
contextJSON, err = marshalContextJSON(context)
}
case "sync_fields":
result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
if err == nil {
contextJSON, err = marshalContextJSON(context)
}
default:
return "", fmt.Errorf("unknown operation: %s", op.Mode)
}
@@ -673,6 +678,119 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
return nil
}
type syncTarget struct {
kind string
key string
}
func parseSyncTarget(spec string) (syncTarget, error) {
raw := strings.TrimSpace(spec)
if raw == "" {
return syncTarget{}, fmt.Errorf("sync_fields target is required")
}
idx := strings.Index(raw, ":")
if idx < 0 {
// Backward compatibility: treat bare value as JSON path.
return syncTarget{
kind: "json",
key: raw,
}, nil
}
kind := strings.ToLower(strings.TrimSpace(raw[:idx]))
key := strings.TrimSpace(raw[idx+1:])
if key == "" {
return syncTarget{}, fmt.Errorf("sync_fields target key is required: %s", raw)
}
switch kind {
case "json", "body":
return syncTarget{
kind: "json",
key: key,
}, nil
case "header":
return syncTarget{
kind: "header",
key: key,
}, nil
default:
return syncTarget{}, fmt.Errorf("sync_fields target prefix is invalid: %s", raw)
}
}
func readSyncTargetValue(jsonStr string, context map[string]interface{}, target syncTarget) (interface{}, bool, error) {
switch target.kind {
case "json":
path := processNegativeIndex(jsonStr, target.key)
value := gjson.Get(jsonStr, path)
if !value.Exists() || value.Type == gjson.Null {
return nil, false, nil
}
if value.Type == gjson.String && strings.TrimSpace(value.String()) == "" {
return nil, false, nil
}
return value.Value(), true, nil
case "header":
value, ok := getHeaderValueFromContext(context, target.key)
if !ok || strings.TrimSpace(value) == "" {
return nil, false, nil
}
return value, true, nil
default:
return nil, false, fmt.Errorf("unsupported sync_fields target kind: %s", target.kind)
}
}
func writeSyncTargetValue(jsonStr string, context map[string]interface{}, target syncTarget, value interface{}) (string, error) {
switch target.kind {
case "json":
path := processNegativeIndex(jsonStr, target.key)
nextJSON, err := sjson.Set(jsonStr, path, value)
if err != nil {
return "", err
}
return nextJSON, nil
case "header":
if err := setHeaderOverrideInContext(context, target.key, value, false); err != nil {
return "", err
}
return jsonStr, nil
default:
return "", fmt.Errorf("unsupported sync_fields target kind: %s", target.kind)
}
}
func syncFieldsBetweenTargets(jsonStr string, context map[string]interface{}, fromSpec string, toSpec string) (string, error) {
fromTarget, err := parseSyncTarget(fromSpec)
if err != nil {
return "", err
}
toTarget, err := parseSyncTarget(toSpec)
if err != nil {
return "", err
}
fromValue, fromExists, err := readSyncTargetValue(jsonStr, context, fromTarget)
if err != nil {
return "", err
}
toValue, toExists, err := readSyncTargetValue(jsonStr, context, toTarget)
if err != nil {
return "", err
}
// If one side exists and the other side is missing, sync the missing side.
if fromExists && !toExists {
return writeSyncTargetValue(jsonStr, context, toTarget, fromValue)
}
if toExists && !fromExists {
return writeSyncTargetValue(jsonStr, context, fromTarget, toValue)
}
return jsonStr, nil
}
func ensureMapKeyInContext(context map[string]interface{}, key string) map[string]interface{} {
if context == nil {
return map[string]interface{}{}

View File

@@ -1057,6 +1057,113 @@ func TestApplyParamOverrideCopyHeaderFromRequestHeaders(t *testing.T) {
assertJSONEqual(t, `{"temperature":0.1}`, string(out))
}
func TestApplyParamOverrideSyncFieldsHeaderToJSON(t *testing.T) {
input := []byte(`{"model":"gpt-4"}`)
override := map[string]interface{}{
"operations": []interface{}{
map[string]interface{}{
"mode": "sync_fields",
"from": "header:session_id",
"to": "json:prompt_cache_key",
},
},
}
ctx := map[string]interface{}{
"request_headers_raw": map[string]interface{}{
"session_id": "sess-123",
},
"request_headers": map[string]interface{}{
"session_id": "sess-123",
},
}
out, err := ApplyParamOverride(input, override, ctx)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"model":"gpt-4","prompt_cache_key":"sess-123"}`, string(out))
}
func TestApplyParamOverrideSyncFieldsJSONToHeader(t *testing.T) {
input := []byte(`{"model":"gpt-4","prompt_cache_key":"cache-abc"}`)
override := map[string]interface{}{
"operations": []interface{}{
map[string]interface{}{
"mode": "sync_fields",
"from": "header:session_id",
"to": "json:prompt_cache_key",
},
},
}
ctx := map[string]interface{}{}
out, err := ApplyParamOverride(input, override, ctx)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"model":"gpt-4","prompt_cache_key":"cache-abc"}`, string(out))
headers, ok := ctx["header_override"].(map[string]interface{})
if !ok {
t.Fatalf("expected header_override context map")
}
if headers["session_id"] != "cache-abc" {
t.Fatalf("expected session_id to be synced from prompt_cache_key, got: %v", headers["session_id"])
}
}
func TestApplyParamOverrideSyncFieldsNoChangeWhenBothExist(t *testing.T) {
input := []byte(`{"model":"gpt-4","prompt_cache_key":"cache-body"}`)
override := map[string]interface{}{
"operations": []interface{}{
map[string]interface{}{
"mode": "sync_fields",
"from": "header:session_id",
"to": "json:prompt_cache_key",
},
},
}
ctx := map[string]interface{}{
"request_headers_raw": map[string]interface{}{
"session_id": "cache-header",
},
"request_headers": map[string]interface{}{
"session_id": "cache-header",
},
}
out, err := ApplyParamOverride(input, override, ctx)
if err != nil {
t.Fatalf("ApplyParamOverride returned error: %v", err)
}
assertJSONEqual(t, `{"model":"gpt-4","prompt_cache_key":"cache-body"}`, string(out))
headers, _ := ctx["header_override"].(map[string]interface{})
if headers != nil {
if _, exists := headers["session_id"]; exists {
t.Fatalf("expected no override when both sides already have value")
}
}
}
func TestApplyParamOverrideSyncFieldsInvalidTarget(t *testing.T) {
input := []byte(`{"model":"gpt-4"}`)
override := map[string]interface{}{
"operations": []interface{}{
map[string]interface{}{
"mode": "sync_fields",
"from": "foo:session_id",
"to": "json:prompt_cache_key",
},
},
}
_, err := ApplyParamOverride(input, override, nil)
if err == nil {
t.Fatalf("expected error, got nil")
}
}
func TestApplyParamOverrideSetHeaderKeepOrigin(t *testing.T) {
input := []byte(`{"temperature":0.7}`)
override := map[string]interface{}{