Explorar o código

feat: simplify param override audit UI and operation labels

Seefs hai 1 mes
pai
achega
bc80477b1a

+ 136 - 103
relay/common/override.go

@@ -230,28 +230,14 @@ func getParamOverrideAuditRecorder(context map[string]interface{}) *paramOverrid
 	return recorder
 	return recorder
 }
 }
 
 
-func (r *paramOverrideAuditRecorder) record(path string, beforeExists bool, beforeValue interface{}, afterExists bool, afterValue interface{}) {
+func (r *paramOverrideAuditRecorder) recordOperation(mode, path, from, to string, value interface{}) {
 	if r == nil {
 	if r == nil {
 		return
 		return
 	}
 	}
-	path = strings.TrimSpace(path)
-	if path == "" {
-		return
-	}
-	if !shouldAuditParamPath(path) {
+	line := buildParamOverrideAuditLine(mode, path, from, to, value)
+	if line == "" {
 		return
 		return
 	}
 	}
-
-	beforeText := "<empty>"
-	if beforeExists {
-		beforeText = formatParamOverrideAuditValue(beforeValue)
-	}
-	afterText := "<deleted>"
-	if afterExists {
-		afterText = formatParamOverrideAuditValue(afterValue)
-	}
-
-	line := fmt.Sprintf("%s: %s -> %s", path, beforeText, afterText)
 	if lo.Contains(r.lines, line) {
 	if lo.Contains(r.lines, line) {
 		return
 		return
 	}
 	}
@@ -270,23 +256,16 @@ func shouldAuditParamPath(path string) bool {
 	return ok
 	return ok
 }
 }
 
 
-func applyAuditedPathMutation(result, path string, auditRecorder *paramOverrideAuditRecorder, mutate func(string) (string, error)) (string, error) {
-	needAudit := auditRecorder != nil && shouldAuditParamPath(path)
-	var beforeResult gjson.Result
-	if needAudit {
-		beforeResult = gjson.Get(result, path)
-	}
-
-	next, err := mutate(result)
-	if err != nil {
-		return next, err
+func shouldAuditOperation(mode, path, from, to string) bool {
+	if common.DebugEnabled {
+		return true
 	}
 	}
-
-	if needAudit {
-		afterResult := gjson.Get(next, path)
-		auditRecorder.record(path, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
+	for _, candidate := range []string{path, to} {
+		if shouldAuditParamPath(candidate) {
+			return true
+		}
 	}
 	}
-	return next, nil
+	return false
 }
 }
 
 
 func formatParamOverrideAuditValue(value interface{}) string {
 func formatParamOverrideAuditValue(value interface{}) string {
@@ -300,6 +279,94 @@ func formatParamOverrideAuditValue(value interface{}) string {
 	}
 	}
 }
 }
 
 
+func buildParamOverrideAuditLine(mode, path, from, to string, value interface{}) string {
+	mode = strings.TrimSpace(mode)
+	path = strings.TrimSpace(path)
+	from = strings.TrimSpace(from)
+	to = strings.TrimSpace(to)
+
+	if !shouldAuditOperation(mode, path, from, to) {
+		return ""
+	}
+
+	switch mode {
+	case "set":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("set %s = %s", path, formatParamOverrideAuditValue(value))
+	case "delete":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("delete %s", path)
+	case "copy":
+		if from == "" || to == "" {
+			return ""
+		}
+		return fmt.Sprintf("copy %s -> %s", from, to)
+	case "move":
+		if from == "" || to == "" {
+			return ""
+		}
+		return fmt.Sprintf("move %s -> %s", from, to)
+	case "prepend":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("prepend %s with %s", path, formatParamOverrideAuditValue(value))
+	case "append":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("append %s with %s", path, formatParamOverrideAuditValue(value))
+	case "trim_prefix", "trim_suffix", "ensure_prefix", "ensure_suffix":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("%s %s with %s", mode, path, formatParamOverrideAuditValue(value))
+	case "trim_space", "to_lower", "to_upper":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("%s %s", mode, path)
+	case "replace", "regex_replace":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("%s %s from %s to %s", mode, path, from, to)
+	case "set_header":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("set_header %s = %s", path, formatParamOverrideAuditValue(value))
+	case "delete_header":
+		if path == "" {
+			return ""
+		}
+		return fmt.Sprintf("delete_header %s", path)
+	case "copy_header", "move_header":
+		if from == "" || to == "" {
+			return ""
+		}
+		return fmt.Sprintf("%s %s -> %s", mode, from, to)
+	case "pass_headers":
+		return fmt.Sprintf("pass_headers %s", formatParamOverrideAuditValue(value))
+	case "sync_fields":
+		if from == "" || to == "" {
+			return ""
+		}
+		return fmt.Sprintf("sync_fields %s -> %s", from, to)
+	case "return_error":
+		return fmt.Sprintf("return_error %s", formatParamOverrideAuditValue(value))
+	default:
+		if path == "" {
+			return mode
+		}
+		return fmt.Sprintf("%s %s", mode, path)
+	}
+}
+
 func getParamOverrideMap(info *RelayInfo) map[string]interface{} {
 func getParamOverrideMap(info *RelayInfo) map[string]interface{} {
 	if info == nil || info.ChannelMeta == nil {
 	if info == nil || info.ChannelMeta == nil {
 		return nil
 		return nil
@@ -594,9 +661,8 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
 	}
 	}
 
 
 	for key, value := range paramOverride {
 	for key, value := range paramOverride {
-		beforeValue, beforeExists := reqMap[key]
 		reqMap[key] = value
 		reqMap[key] = value
-		auditRecorder.record(key, beforeExists, beforeValue, true, value)
+		auditRecorder.recordOperation("set", key, "", "", value)
 	}
 	}
 
 
 	return common.Marshal(reqMap)
 	return common.Marshal(reqMap)
@@ -636,47 +702,29 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
 		switch op.Mode {
 		switch op.Mode {
 		case "delete":
 		case "delete":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return deleteValue(current, path)
-				})
+				result, err = deleteValue(result, path)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("delete", path, "", "", nil)
 			}
 			}
 		case "set":
 		case "set":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
 				if op.KeepOrigin && gjson.Get(result, path).Exists() {
 				if op.KeepOrigin && gjson.Get(result, path).Exists() {
 					continue
 					continue
 				}
 				}
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return sjson.Set(current, path, op.Value)
-				})
+				result, err = sjson.Set(result, path, op.Value)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("set", path, "", "", op.Value)
 			}
 			}
 		case "move":
 		case "move":
 			opFrom := processNegativeIndex(result, op.From)
 			opFrom := processNegativeIndex(result, op.From)
 			opTo := processNegativeIndex(result, op.To)
 			opTo := processNegativeIndex(result, op.To)
-			needAuditTo := auditRecorder != nil && shouldAuditParamPath(opTo)
-			needAuditFrom := auditRecorder != nil && shouldAuditParamPath(opFrom)
-			var beforeResult gjson.Result
-			var fromResult gjson.Result
-			if needAuditTo {
-				beforeResult = gjson.Get(result, opTo)
-			}
-			if needAuditFrom {
-				fromResult = gjson.Get(result, opFrom)
-			}
 			result, err = moveValue(result, opFrom, opTo)
 			result, err = moveValue(result, opFrom, opTo)
 			if err == nil {
 			if err == nil {
-				if needAuditTo {
-					afterResult := gjson.Get(result, opTo)
-					auditRecorder.record(opTo, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
-				}
-				if needAuditFrom && common.DebugEnabled {
-					auditRecorder.record(opFrom, fromResult.Exists(), fromResult.Value(), false, nil)
-				}
+				auditRecorder.recordOperation("move", "", opFrom, opTo, nil)
 			}
 			}
 		case "copy":
 		case "copy":
 			if op.From == "" || op.To == "" {
 			if op.From == "" || op.To == "" {
@@ -684,116 +732,100 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
 			}
 			}
 			opFrom := processNegativeIndex(result, op.From)
 			opFrom := processNegativeIndex(result, op.From)
 			opTo := processNegativeIndex(result, op.To)
 			opTo := processNegativeIndex(result, op.To)
-			needAudit := auditRecorder != nil && shouldAuditParamPath(opTo)
-			var beforeResult gjson.Result
-			if needAudit {
-				beforeResult = gjson.Get(result, opTo)
-			}
 			result, err = copyValue(result, opFrom, opTo)
 			result, err = copyValue(result, opFrom, opTo)
-			if err == nil && needAudit {
-				afterResult := gjson.Get(result, opTo)
-				auditRecorder.record(opTo, beforeResult.Exists(), beforeResult.Value(), afterResult.Exists(), afterResult.Value())
+			if err == nil {
+				auditRecorder.recordOperation("copy", "", opFrom, opTo, nil)
 			}
 			}
 		case "prepend":
 		case "prepend":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return modifyValue(current, path, op.Value, op.KeepOrigin, true)
-				})
+				result, err = modifyValue(result, path, op.Value, op.KeepOrigin, true)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("prepend", path, "", "", op.Value)
 			}
 			}
 		case "append":
 		case "append":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return modifyValue(current, path, op.Value, op.KeepOrigin, false)
-				})
+				result, err = modifyValue(result, path, op.Value, op.KeepOrigin, false)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("append", path, "", "", op.Value)
 			}
 			}
 		case "trim_prefix":
 		case "trim_prefix":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return trimStringValue(current, path, op.Value, true)
-				})
+				result, err = trimStringValue(result, path, op.Value, true)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("trim_prefix", path, "", "", op.Value)
 			}
 			}
 		case "trim_suffix":
 		case "trim_suffix":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return trimStringValue(current, path, op.Value, false)
-				})
+				result, err = trimStringValue(result, path, op.Value, false)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("trim_suffix", path, "", "", op.Value)
 			}
 			}
 		case "ensure_prefix":
 		case "ensure_prefix":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return ensureStringAffix(current, path, op.Value, true)
-				})
+				result, err = ensureStringAffix(result, path, op.Value, true)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("ensure_prefix", path, "", "", op.Value)
 			}
 			}
 		case "ensure_suffix":
 		case "ensure_suffix":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return ensureStringAffix(current, path, op.Value, false)
-				})
+				result, err = ensureStringAffix(result, path, op.Value, false)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("ensure_suffix", path, "", "", op.Value)
 			}
 			}
 		case "trim_space":
 		case "trim_space":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return transformStringValue(current, path, strings.TrimSpace)
-				})
+				result, err = transformStringValue(result, path, strings.TrimSpace)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("trim_space", path, "", "", nil)
 			}
 			}
 		case "to_lower":
 		case "to_lower":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return transformStringValue(current, path, strings.ToLower)
-				})
+				result, err = transformStringValue(result, path, strings.ToLower)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("to_lower", path, "", "", nil)
 			}
 			}
 		case "to_upper":
 		case "to_upper":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return transformStringValue(current, path, strings.ToUpper)
-				})
+				result, err = transformStringValue(result, path, strings.ToUpper)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("to_upper", path, "", "", nil)
 			}
 			}
 		case "replace":
 		case "replace":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return replaceStringValue(current, path, op.From, op.To)
-				})
+				result, err = replaceStringValue(result, path, op.From, op.To)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("replace", path, op.From, op.To, nil)
 			}
 			}
 		case "regex_replace":
 		case "regex_replace":
 			for _, path := range opPaths {
 			for _, path := range opPaths {
-				result, err = applyAuditedPathMutation(result, path, auditRecorder, func(current string) (string, error) {
-					return regexReplaceStringValue(current, path, op.From, op.To)
-				})
+				result, err = regexReplaceStringValue(result, path, op.From, op.To)
 				if err != nil {
 				if err != nil {
 					break
 					break
 				}
 				}
+				auditRecorder.recordOperation("regex_replace", path, op.From, op.To, nil)
 			}
 			}
 		case "return_error":
 		case "return_error":
+			auditRecorder.recordOperation("return_error", op.Path, "", "", op.Value)
 			returnErr, parseErr := parseParamOverrideReturnError(op.Value)
 			returnErr, parseErr := parseParamOverrideReturnError(op.Value)
 			if parseErr != nil {
 			if parseErr != nil {
 				return "", parseErr
 				return "", parseErr
@@ -809,11 +841,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
 		case "set_header":
 		case "set_header":
 			err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin)
 			err = setHeaderOverrideInContext(context, op.Path, op.Value, op.KeepOrigin)
 			if err == nil {
 			if err == nil {
+				auditRecorder.recordOperation("set_header", op.Path, "", "", op.Value)
 				contextJSON, err = marshalContextJSON(context)
 				contextJSON, err = marshalContextJSON(context)
 			}
 			}
 		case "delete_header":
 		case "delete_header":
 			err = deleteHeaderOverrideInContext(context, op.Path)
 			err = deleteHeaderOverrideInContext(context, op.Path)
 			if err == nil {
 			if err == nil {
+				auditRecorder.recordOperation("delete_header", op.Path, "", "", nil)
 				contextJSON, err = marshalContextJSON(context)
 				contextJSON, err = marshalContextJSON(context)
 			}
 			}
 		case "copy_header":
 		case "copy_header":
@@ -830,6 +864,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
 				err = nil
 				err = nil
 			}
 			}
 			if err == nil {
 			if err == nil {
+				auditRecorder.recordOperation("copy_header", "", sourceHeader, targetHeader, nil)
 				contextJSON, err = marshalContextJSON(context)
 				contextJSON, err = marshalContextJSON(context)
 			}
 			}
 		case "move_header":
 		case "move_header":
@@ -846,6 +881,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
 				err = nil
 				err = nil
 			}
 			}
 			if err == nil {
 			if err == nil {
+				auditRecorder.recordOperation("move_header", "", sourceHeader, targetHeader, nil)
 				contextJSON, err = marshalContextJSON(context)
 				contextJSON, err = marshalContextJSON(context)
 			}
 			}
 		case "pass_headers":
 		case "pass_headers":
@@ -863,11 +899,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
 				}
 				}
 			}
 			}
 			if err == nil {
 			if err == nil {
+				auditRecorder.recordOperation("pass_headers", "", "", "", headerNames)
 				contextJSON, err = marshalContextJSON(context)
 				contextJSON, err = marshalContextJSON(context)
 			}
 			}
 		case "sync_fields":
 		case "sync_fields":
 			result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
 			result, err = syncFieldsBetweenTargets(result, context, op.From, op.To)
 			if err == nil {
 			if err == nil {
+				auditRecorder.recordOperation("sync_fields", "", op.From, op.To, nil)
 				contextJSON, err = marshalContextJSON(context)
 				contextJSON, err = marshalContextJSON(context)
 			}
 			}
 		default:
 		default:
@@ -985,7 +1023,6 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
 	}
 	}
 
 
 	rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
 	rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
-	beforeRaw, beforeExists := rawHeaders[headerName]
 	if keepOrigin {
 	if keepOrigin {
 		if existing, ok := rawHeaders[headerName]; ok {
 		if existing, ok := rawHeaders[headerName]; ok {
 			existingValue := strings.TrimSpace(fmt.Sprintf("%v", existing))
 			existingValue := strings.TrimSpace(fmt.Sprintf("%v", existing))
@@ -1001,12 +1038,10 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
 	}
 	}
 	if !hasValue {
 	if !hasValue {
 		delete(rawHeaders, headerName)
 		delete(rawHeaders, headerName)
-		getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil)
 		return nil
 		return nil
 	}
 	}
 
 
 	rawHeaders[headerName] = headerValue
 	rawHeaders[headerName] = headerValue
-	getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, true, headerValue)
 	return nil
 	return nil
 }
 }
 
 
@@ -1178,9 +1213,7 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
 		return fmt.Errorf("header name is required")
 		return fmt.Errorf("header name is required")
 	}
 	}
 	rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
 	rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
-	beforeRaw, beforeExists := rawHeaders[headerName]
 	delete(rawHeaders, headerName)
 	delete(rawHeaders, headerName)
-	getParamOverrideAuditRecorder(context).record("header."+headerName, beforeExists, beforeRaw, false, nil)
 	return nil
 	return nil
 }
 }
 
 

+ 100 - 0
relay/common/override_test.go

@@ -6,6 +6,7 @@ import (
 	"reflect"
 	"reflect"
 	"testing"
 	"testing"
 
 
+	common2 "github.com/QuantumNous/new-api/common"
 	"github.com/QuantumNous/new-api/types"
 	"github.com/QuantumNous/new-api/types"
 
 
 	"github.com/QuantumNous/new-api/dto"
 	"github.com/QuantumNous/new-api/dto"
@@ -2066,6 +2067,105 @@ func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) {
 	assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out))
 	assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out))
 }
 }
 
 
+func TestApplyParamOverrideWithRelayInfoRecordsOperationAuditInDebugMode(t *testing.T) {
+	originalDebugEnabled := common2.DebugEnabled
+	common2.DebugEnabled = true
+	t.Cleanup(func() {
+		common2.DebugEnabled = originalDebugEnabled
+	})
+
+	info := &RelayInfo{
+		ChannelMeta: &ChannelMeta{
+			ParamOverride: map[string]interface{}{
+				"operations": []interface{}{
+					map[string]interface{}{
+						"mode": "copy",
+						"from": "metadata.target_model",
+						"to":   "model",
+					},
+					map[string]interface{}{
+						"mode":  "set",
+						"path":  "service_tier",
+						"value": "flex",
+					},
+					map[string]interface{}{
+						"mode":  "set",
+						"path":  "temperature",
+						"value": 0.1,
+					},
+				},
+			},
+		},
+	}
+
+	out, err := ApplyParamOverrideWithRelayInfo([]byte(`{
+		"model":"gpt-4.1",
+		"temperature":0.7,
+		"metadata":{"target_model":"gpt-4.1-mini"}
+	}`), info)
+	if err != nil {
+		t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err)
+	}
+	assertJSONEqual(t, `{
+		"model":"gpt-4.1-mini",
+		"temperature":0.1,
+		"service_tier":"flex",
+		"metadata":{"target_model":"gpt-4.1-mini"}
+	}`, string(out))
+
+	expected := []string{
+		"copy metadata.target_model -> model",
+		"set service_tier = flex",
+		"set temperature = 0.1",
+	}
+	if !reflect.DeepEqual(info.ParamOverrideAudit, expected) {
+		t.Fatalf("unexpected param override audit, got %#v", info.ParamOverrideAudit)
+	}
+}
+
+func TestApplyParamOverrideWithRelayInfoRecordsOnlyKeyOperationsWhenDebugDisabled(t *testing.T) {
+	originalDebugEnabled := common2.DebugEnabled
+	common2.DebugEnabled = false
+	t.Cleanup(func() {
+		common2.DebugEnabled = originalDebugEnabled
+	})
+
+	info := &RelayInfo{
+		ChannelMeta: &ChannelMeta{
+			ParamOverride: map[string]interface{}{
+				"operations": []interface{}{
+					map[string]interface{}{
+						"mode": "copy",
+						"from": "metadata.target_model",
+						"to":   "model",
+					},
+					map[string]interface{}{
+						"mode":  "set",
+						"path":  "temperature",
+						"value": 0.1,
+					},
+				},
+			},
+		},
+	}
+
+	_, err := ApplyParamOverrideWithRelayInfo([]byte(`{
+		"model":"gpt-4.1",
+		"temperature":0.7,
+		"metadata":{"target_model":"gpt-4.1-mini"}
+	}`), info)
+	if err != nil {
+		t.Fatalf("ApplyParamOverrideWithRelayInfo returned error: %v", err)
+	}
+
+	expected := []string{
+		"copy metadata.target_model -> model",
+	}
+	if !reflect.DeepEqual(info.ParamOverrideAudit, expected) {
+		t.Fatalf("unexpected param override audit, got %#v", info.ParamOverrideAudit)
+	}
+}
+
 func assertJSONEqual(t *testing.T, want, got string) {
 func assertJSONEqual(t *testing.T, want, got string) {
 	t.Helper()
 	t.Helper()
 
 

+ 54 - 0
web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx

@@ -0,0 +1,54 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React from 'react';
+import { Typography } from '@douyinfe/semi-ui';
+
+const { Text } = Typography;
+
+const ParamOverrideEntry = ({ count, onOpen, t }) => {
+  return (
+    <div
+      style={{
+        display: 'flex',
+        alignItems: 'center',
+        gap: 10,
+        flexWrap: 'wrap',
+      }}
+    >
+      <Text
+        type='tertiary'
+        size='small'
+        style={{ fontVariantNumeric: 'tabular-nums' }}
+      >
+        {t('{{count}} 项操作', { count })}
+      </Text>
+      <Text
+        link
+        size='small'
+        style={{ fontWeight: 600 }}
+        onClick={onOpen}
+      >
+        {t('查看详情')}
+      </Text>
+    </div>
+  );
+};
+
+export default React.memo(ParamOverrideEntry);

+ 147 - 153
web/src/components/table/usage-logs/modals/ParamOverrideModal.jsx

@@ -22,8 +22,7 @@ import {
   Modal,
   Modal,
   Button,
   Button,
   Empty,
   Empty,
-  Space,
-  Tag,
+  Divider,
   Typography,
   Typography,
 } from '@douyinfe/semi-ui';
 } from '@douyinfe/semi-ui';
 import { IconCopy } from '@douyinfe/semi-icons';
 import { IconCopy } from '@douyinfe/semi-icons';
@@ -35,59 +34,66 @@ const parseAuditLine = (line) => {
   if (typeof line !== 'string') {
   if (typeof line !== 'string') {
     return null;
     return null;
   }
   }
-  const colonIndex = line.indexOf(': ');
-  const arrowIndex = line.indexOf(' -> ', colonIndex + 2);
-  if (colonIndex <= 0 || arrowIndex <= colonIndex) {
-    return null;
+  const firstSpaceIndex = line.indexOf(' ');
+  if (firstSpaceIndex <= 0) {
+    return { action: line, content: line };
   }
   }
-
   return {
   return {
-    field: line.slice(0, colonIndex),
-    before: line.slice(colonIndex + 2, arrowIndex),
-    after: line.slice(arrowIndex + 4),
-    raw: line,
+    action: line.slice(0, firstSpaceIndex),
+    content: line.slice(firstSpaceIndex + 1),
   };
   };
 };
 };
 
 
-const ValuePanel = ({ label, value, tone }) => (
-  <div
-    style={{
-      flex: 1,
-      minWidth: 0,
-      padding: 12,
-      borderRadius: 12,
-      border: '1px solid var(--semi-color-border)',
-      background:
-        tone === 'after'
-          ? 'rgba(var(--semi-blue-5), 0.08)'
-          : 'var(--semi-color-fill-0)',
-    }}
-  >
-    <div
-      style={{
-        marginBottom: 6,
-        fontSize: 12,
-        fontWeight: 600,
-        color: 'var(--semi-color-text-2)',
-      }}
-    >
-      {label}
-    </div>
-    <Text
-      style={{
-        display: 'block',
-        fontFamily:
-          'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
-        fontSize: 13,
-        lineHeight: 1.65,
-        whiteSpace: 'pre-wrap',
-        wordBreak: 'break-word',
-      }}
-    >
-      {value}
-    </Text>
-  </div>
-);
+const getActionLabel = (action, t) => {
+  switch ((action || '').toLowerCase()) {
+    case 'set':
+      return t('设置');
+    case 'delete':
+      return t('删除');
+    case 'copy':
+      return t('复制');
+    case 'move':
+      return t('移动');
+    case 'append':
+      return t('追加');
+    case 'prepend':
+      return t('前置');
+    case 'trim_prefix':
+      return t('去前缀');
+    case 'trim_suffix':
+      return t('去后缀');
+    case 'ensure_prefix':
+      return t('保前缀');
+    case 'ensure_suffix':
+      return t('保后缀');
+    case 'trim_space':
+      return t('去空格');
+    case 'to_lower':
+      return t('转小写');
+    case 'to_upper':
+      return t('转大写');
+    case 'replace':
+      return t('替换');
+    case 'regex_replace':
+      return t('正则替换');
+    case 'set_header':
+      return t('设请求头');
+    case 'delete_header':
+      return t('删请求头');
+    case 'copy_header':
+      return t('复制请求头');
+    case 'move_header':
+      return t('移动请求头');
+    case 'pass_headers':
+      return t('透传请求头');
+    case 'sync_fields':
+      return t('同步字段');
+    case 'return_error':
+      return t('返回错误');
+    default:
+      return action;
+  }
+};
 
 
 const ParamOverrideModal = ({
 const ParamOverrideModal = ({
   showParamOverrideModal,
   showParamOverrideModal,
@@ -124,147 +130,135 @@ const ParamOverrideModal = ({
       centered
       centered
       closable
       closable
       maskClosable
       maskClosable
-      width={760}
+      width={640}
     >
     >
-      <div style={{ padding: 20 }}>
+      <div style={{ padding: '8px 20px 20px' }}>
         <div
         <div
           style={{
           style={{
-            marginBottom: 16,
-            padding: 16,
-            borderRadius: 16,
-            background:
-              'linear-gradient(135deg, rgba(var(--semi-blue-5), 0.08), rgba(var(--semi-teal-5), 0.12))',
-            border: '1px solid rgba(var(--semi-blue-5), 0.16)',
+            display: 'flex',
+            justifyContent: 'space-between',
+            alignItems: 'flex-start',
+            gap: 12,
+            marginBottom: 10,
           }}
           }}
         >
         >
-          <div
-            style={{
-              display: 'flex',
-              justifyContent: 'space-between',
-              gap: 12,
-              flexWrap: 'wrap',
-              alignItems: 'flex-start',
-            }}
-          >
-            <div>
-              <div
-                style={{
-                  fontSize: 18,
-                  fontWeight: 700,
-                  color: 'var(--semi-color-text-0)',
-                  marginBottom: 8,
-                }}
-              >
-                {t('已应用参数覆盖')}
-              </div>
-              <Space wrap spacing={8}>
-                <Tag color='blue' size='large'>
-                  {t('{{count}} 项变更', { count: lines.length })}
-                </Tag>
-                {paramOverrideTarget?.modelName ? (
-                  <Tag color='cyan' size='large'>
-                    {paramOverrideTarget.modelName}
-                  </Tag>
-                ) : null}
-                {paramOverrideTarget?.requestId ? (
-                  <Tag color='grey' size='large'>
-                    {t('Request ID')}: {paramOverrideTarget.requestId}
-                  </Tag>
-                ) : null}
-              </Space>
+          <div style={{ minWidth: 0 }}>
+            <div style={{ marginBottom: 4 }}>
+              <Text style={{ fontWeight: 600 }}>
+                {t('{{count}} 项操作', { count: lines.length })}
+              </Text>
             </div>
             </div>
-
-            <Button
-              icon={<IconCopy />}
-              theme='solid'
-              type='tertiary'
-              onClick={copyAll}
-              disabled={lines.length === 0}
+            <div
+              style={{
+                display: 'flex',
+                flexWrap: 'wrap',
+                gap: 8,
+                fontSize: 12,
+                color: 'var(--semi-color-text-2)',
+              }}
             >
             >
-              {t('复制全部')}
-            </Button>
+              {paramOverrideTarget?.modelName ? (
+                <Text type='tertiary' size='small'>
+                  {paramOverrideTarget.modelName}
+                </Text>
+              ) : null}
+              {paramOverrideTarget?.requestId ? (
+                <Text type='tertiary' size='small'>
+                  {t('Request ID')}: {paramOverrideTarget.requestId}
+                </Text>
+              ) : null}
+              {paramOverrideTarget?.requestPath ? (
+                <Text type='tertiary' size='small'>
+                  {t('请求路径')}: {paramOverrideTarget.requestPath}
+                </Text>
+              ) : null}
+            </div>
           </div>
           </div>
 
 
-          {paramOverrideTarget?.requestPath ? (
-            <div style={{ marginTop: 12 }}>
-              <Text type='tertiary' size='small'>
-                {t('请求路径')}: {paramOverrideTarget.requestPath}
-              </Text>
-            </div>
-          ) : null}
+          <Button
+            icon={<IconCopy />}
+            theme='borderless'
+            type='tertiary'
+            size='small'
+            onClick={copyAll}
+            disabled={lines.length === 0}
+          >
+            {t('复制')}
+          </Button>
         </div>
         </div>
 
 
+        <Divider margin='12px' />
+
         {lines.length === 0 ? (
         {lines.length === 0 ? (
           <Empty
           <Empty
             description={t('暂无参数覆盖记录')}
             description={t('暂无参数覆盖记录')}
-            style={{ padding: '32px 0 12px' }}
+            style={{ padding: '24px 0 8px' }}
           />
           />
         ) : (
         ) : (
           <div
           <div
             style={{
             style={{
               display: 'flex',
               display: 'flex',
               flexDirection: 'column',
               flexDirection: 'column',
-              gap: 12,
-              maxHeight: '60vh',
+              gap: 8,
+              maxHeight: '56vh',
               overflowY: 'auto',
               overflowY: 'auto',
-              paddingRight: 4,
+              paddingRight: 2,
             }}
             }}
           >
           >
             {parsedLines.map((item, index) => {
             {parsedLines.map((item, index) => {
               if (!item) {
               if (!item) {
-                return (
-                  <div
-                    key={`raw-${index}`}
-                    style={{
-                      padding: 14,
-                      borderRadius: 14,
-                      border: '1px solid var(--semi-color-border)',
-                      background: 'var(--semi-color-fill-0)',
-                    }}
-                  >
-                    <Text
-                      style={{
-                        fontFamily:
-                          'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
-                        fontSize: 13,
-                        lineHeight: 1.65,
-                        whiteSpace: 'pre-wrap',
-                        wordBreak: 'break-word',
-                      }}
-                    >
-                      {lines[index]}
-                    </Text>
-                  </div>
-                );
+                return null;
               }
               }
 
 
               return (
               return (
                 <div
                 <div
-                  key={`${item.field}-${index}`}
+                  key={`${item.action}-${index}`}
                   style={{
                   style={{
-                    padding: 14,
-                    borderRadius: 16,
+                    padding: '10px 12px',
+                    borderRadius: 10,
                     border: '1px solid var(--semi-color-border)',
                     border: '1px solid var(--semi-color-border)',
-                    background: 'var(--semi-color-bg-0)',
-                    boxShadow: '0 8px 24px rgba(15, 23, 42, 0.04)',
+                    background: 'var(--semi-color-fill-0)',
+                    display: 'flex',
+                    gap: 12,
+                    alignItems: 'flex-start',
                   }}
                   }}
                 >
                 >
-                  <div style={{ marginBottom: 12 }}>
-                    <Tag color='blue' shape='circle' size='large'>
-                      {item.field}
-                    </Tag>
-                  </div>
                   <div
                   <div
                     style={{
                     style={{
-                      display: 'flex',
-                      gap: 12,
-                      flexWrap: 'wrap',
-                      alignItems: 'stretch',
+                      flex: '0 0 auto',
+                      minWidth: 74,
                     }}
                     }}
                   >
                   >
-                    <ValuePanel label={t('变更前')} value={item.before} />
-                    <ValuePanel label={t('变更后')} value={item.after} tone='after' />
+                    <Text
+                      style={{
+                        display: 'inline-block',
+                        fontSize: 11,
+                        fontWeight: 700,
+                        lineHeight: '20px',
+                        padding: '0 8px',
+                        borderRadius: 999,
+                        background: 'rgba(var(--semi-blue-5), 0.12)',
+                        color: 'var(--semi-color-primary)',
+                      }}
+                    >
+                      {getActionLabel(item.action, t)}
+                    </Text>
                   </div>
                   </div>
+                  <Text
+                    style={{
+                      flex: 1,
+                      minWidth: 0,
+                      fontFamily:
+                        'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
+                      fontSize: 12,
+                      lineHeight: 1.6,
+                      whiteSpace: 'pre-wrap',
+                      wordBreak: 'break-word',
+                      color: 'var(--semi-color-text-0)',
+                    }}
+                  >
+                    {item.content}
+                  </Text>
                 </div>
                 </div>
               );
               );
             })}
             })}

+ 9 - 24
web/src/hooks/usage-logs/useUsageLogsData.jsx

@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
 
 
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { Modal, Button, Tag } from '@douyinfe/semi-ui';
+import { Modal } from '@douyinfe/semi-ui';
 import {
 import {
   API,
   API,
   getTodayStartTimestamp,
   getTodayStartTimestamp,
@@ -39,6 +39,7 @@ import {
 } from '../../helpers';
 } from '../../helpers';
 import { ITEMS_PER_PAGE } from '../../constants';
 import { ITEMS_PER_PAGE } from '../../constants';
 import { useTableCompactMode } from '../common/useTableCompactMode';
 import { useTableCompactMode } from '../common/useTableCompactMode';
+import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry';
 
 
 export const useLogsData = () => {
 export const useLogsData = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -604,30 +605,14 @@ export const useLogsData = () => {
         expandDataLocal.push({
         expandDataLocal.push({
           key: t('参数覆盖'),
           key: t('参数覆盖'),
           value: (
           value: (
-            <div
-              style={{
-                display: 'flex',
-                alignItems: 'center',
-                gap: 10,
-                flexWrap: 'wrap',
+            <ParamOverrideEntry
+              count={other.po.length}
+              t={t}
+              onOpen={(event) => {
+                event.stopPropagation();
+                openParamOverrideModal(logs[i], other);
               }}
               }}
-            >
-              <Tag color='blue' shape='circle'>
-                {t('{{count}} 项变更', { count: other.po.length })}
-              </Tag>
-              <Button
-                theme='borderless'
-                type='primary'
-                size='small'
-                style={{ paddingLeft: 0 }}
-                onClick={(event) => {
-                  event.stopPropagation();
-                  openParamOverrideModal(logs[i], other);
-                }}
-              >
-                {t('查看详情')}
-              </Button>
-            </div>
+            />
           ),
           ),
         });
         });
       }
       }