fix: expose param override audits for sensitive message fields (#4974)

This commit is contained in:
Seefs 2026-05-19 18:28:03 +08:00 committed by GitHub
parent 146dd77b83
commit 0d4b25795a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 114 additions and 13 deletions

View File

@ -26,13 +26,20 @@ const (
var errSourceHeaderNotFound = errors.New("source header does not exist")
var paramOverrideKeyAuditPaths = map[string]struct{}{
"model": {},
"original_model": {},
"upstream_model": {},
"service_tier": {},
"inference_geo": {},
"speed": {},
var paramOverrideSensitivePathPrefixes = []string{
"model",
"original_model",
"upstream_model",
"service_tier",
"inference_geo",
"speed",
"messages",
"input",
"instructions",
"system",
"contents",
"systemInstruction",
"system_instruction",
}
type paramOverrideAuditRecorder struct {
@ -206,6 +213,7 @@ func shouldEnableParamOverrideAudit(paramOverride map[string]interface{}) bool {
if operations, ok := tryParseOperations(paramOverride); ok {
for _, operation := range operations {
if shouldAuditParamPath(strings.TrimSpace(operation.Path)) ||
shouldAuditParamPath(strings.TrimSpace(operation.From)) ||
shouldAuditParamPath(strings.TrimSpace(operation.To)) {
return true
}
@ -255,15 +263,19 @@ func shouldAuditParamPath(path string) bool {
if common.DebugEnabled {
return true
}
_, ok := paramOverrideKeyAuditPaths[path]
return ok
for _, prefix := range paramOverrideSensitivePathPrefixes {
if path == prefix || strings.HasPrefix(path, prefix+".") {
return true
}
}
return false
}
func shouldAuditOperation(mode, path, from, to string) bool {
if common.DebugEnabled {
return true
}
for _, candidate := range []string{path, to} {
for _, candidate := range []string{path, from, to} {
if shouldAuditParamPath(candidate) {
return true
}

View File

@ -12,6 +12,7 @@ import (
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
)
func TestApplyParamOverrideTrimPrefix(t *testing.T) {
@ -2184,6 +2185,95 @@ func TestApplyParamOverrideWithRelayInfoRecordsOnlyKeyOperationsWhenDebugDisable
}
}
func TestApplyParamOverrideWithRelayInfoRecordsConversationBodyOperationsWhenDebugDisabled(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": "replace",
"path": "messages.0.content",
"from": "hello",
"to": "hi",
},
map[string]interface{}{
"mode": "set",
"path": "input.0.content.0.text",
"value": "rewritten response input",
},
map[string]interface{}{
"mode": "set",
"path": "instructions",
"value": "new instruction",
},
map[string]interface{}{
"mode": "append",
"path": "contents.0.parts",
"value": map[string]interface{}{"text": "new gemini part"},
},
map[string]interface{}{
"mode": "copy",
"from": "system",
"to": "metadata.system_copy",
},
map[string]interface{}{
"mode": "set",
"path": "temperature",
"value": 0.1,
},
},
},
},
}
out, err := ApplyParamOverrideWithRelayInfo([]byte(`{
"messages":[{"role":"user","content":"hello world"}],
"input":[{"role":"user","content":[{"type":"input_text","text":"original response input"}]}],
"instructions":"old instruction",
"system":"old system",
"contents":[{"role":"user","parts":[{"text":"hello gemini"}]}],
"temperature":0.7
}`), info)
require.NoError(t, err)
assertJSONEqual(t, `{
"messages":[{"role":"user","content":"hi world"}],
"input":[{"role":"user","content":[{"type":"input_text","text":"rewritten response input"}]}],
"instructions":"new instruction",
"system":"old system",
"contents":[{"role":"user","parts":[{"text":"hello gemini"},{"text":"new gemini part"}]}],
"temperature":0.1,
"metadata":{"system_copy":"old system"}
}`, string(out))
require.Equal(t, []string{
"replace messages.0.content from hello to hi",
"set input.0.content.0.text = rewritten response input",
"set instructions = new instruction",
"append contents.0.parts with {\"text\":\"new gemini part\"}",
"copy system -> metadata.system_copy",
}, info.ParamOverrideAudit)
}
func TestShouldAuditParamPathUsesFieldBoundaryPrefixMatching(t *testing.T) {
originalDebugEnabled := common2.DebugEnabled
common2.DebugEnabled = false
t.Cleanup(func() {
common2.DebugEnabled = originalDebugEnabled
})
require.True(t, shouldAuditParamPath("messages"))
require.True(t, shouldAuditParamPath("messages.0.content"))
require.True(t, shouldAuditParamPath("systemInstruction.parts.0.text"))
require.False(t, shouldAuditParamPath("model_name"))
require.False(t, shouldAuditParamPath("message"))
}
func assertJSONEqual(t *testing.T, want, got string) {
t.Helper()

View File

@ -985,9 +985,8 @@ export function DetailsDialog(props: DetailsDialogProps) {
</DetailSection>
)}
{/* Param override (admin only) */}
{props.isAdmin &&
other?.po &&
{/* Param override */}
{other?.po &&
Array.isArray(other.po) &&
other.po.length > 0 && (
<DetailSection