Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
69551ab2
authored
Mar 17, 2026
by
Calcium-Ion
Committed by
GitHub
Mar 17, 2026
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3285 from seefs001/feature/param-override-log
feat: params override log
parents
a4fd2246
8aa8b81e
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
704 additions
and
3 deletions
+704
-3
relay/common/override.go
+231
-3
relay/common/override_test.go
+100
-0
relay/common/relay_info.go
+1
-0
service/log_info_generate.go
+8
-0
web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx
+54
-0
web/src/components/table/usage-logs/index.jsx
+2
-0
web/src/components/table/usage-logs/modals/ParamOverrideModal.jsx
+272
-0
web/src/hooks/usage-logs/useUsageLogsData.jsx
+36
-0
No files found.
relay/common/override.go
View file @
69551ab2
...
...
@@ -21,10 +21,23 @@ var negativeIndexRegexp = regexp.MustCompile(`\.(-\d+)`)
const
(
paramOverrideContextRequestHeaders
=
"request_headers"
paramOverrideContextHeaderOverride
=
"header_override"
paramOverrideContextAuditRecorder
=
"__param_override_audit_recorder"
)
var
errSourceHeaderNotFound
=
errors
.
New
(
"source header does not exist"
)
var
paramOverrideKeyAuditPaths
=
map
[
string
]
struct
{}{
"model"
:
{},
"original_model"
:
{},
"upstream_model"
:
{},
"service_tier"
:
{},
"inference_geo"
:
{},
}
type
paramOverrideAuditRecorder
struct
{
lines
[]
string
}
type
ConditionOperation
struct
{
Path
string
`json:"path"`
// JSON路径
Mode
string
`json:"mode"`
// full, prefix, suffix, contains, gt, gte, lt, lte
...
...
@@ -118,6 +131,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c
if
len
(
paramOverride
)
==
0
{
return
jsonData
,
nil
}
auditRecorder
:=
getParamOverrideAuditRecorder
(
conditionContext
)
// 尝试断言为操作格式
if
operations
,
ok
:=
tryParseOperations
(
paramOverride
);
ok
{
...
...
@@ -125,7 +139,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c
workingJSON
:=
jsonData
var
err
error
if
len
(
legacyOverride
)
>
0
{
workingJSON
,
err
=
applyOperationsLegacy
(
workingJSON
,
legacyOverride
)
workingJSON
,
err
=
applyOperationsLegacy
(
workingJSON
,
legacyOverride
,
auditRecorder
)
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -137,7 +151,7 @@ func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, c
}
// 直接使用旧方法
return
applyOperationsLegacy
(
jsonData
,
paramOverride
)
return
applyOperationsLegacy
(
jsonData
,
paramOverride
,
auditRecorder
)
}
func
buildLegacyParamOverride
(
paramOverride
map
[
string
]
interface
{})
map
[
string
]
interface
{}
{
...
...
@@ -161,14 +175,200 @@ func ApplyParamOverrideWithRelayInfo(jsonData []byte, info *RelayInfo) ([]byte,
}
overrideCtx
:=
BuildParamOverrideContext
(
info
)
var
recorder
*
paramOverrideAuditRecorder
if
shouldEnableParamOverrideAudit
(
paramOverride
)
{
recorder
=
&
paramOverrideAuditRecorder
{}
overrideCtx
[
paramOverrideContextAuditRecorder
]
=
recorder
}
result
,
err
:=
ApplyParamOverride
(
jsonData
,
paramOverride
,
overrideCtx
)
if
err
!=
nil
{
return
nil
,
err
}
syncRuntimeHeaderOverrideFromContext
(
info
,
overrideCtx
)
if
info
!=
nil
{
if
recorder
!=
nil
{
info
.
ParamOverrideAudit
=
recorder
.
lines
}
else
{
info
.
ParamOverrideAudit
=
nil
}
}
return
result
,
nil
}
func
shouldEnableParamOverrideAudit
(
paramOverride
map
[
string
]
interface
{})
bool
{
if
common
.
DebugEnabled
{
return
true
}
if
len
(
paramOverride
)
==
0
{
return
false
}
if
operations
,
ok
:=
tryParseOperations
(
paramOverride
);
ok
{
for
_
,
operation
:=
range
operations
{
if
shouldAuditParamPath
(
strings
.
TrimSpace
(
operation
.
Path
))
||
shouldAuditParamPath
(
strings
.
TrimSpace
(
operation
.
To
))
{
return
true
}
}
for
key
:=
range
buildLegacyParamOverride
(
paramOverride
)
{
if
shouldAuditParamPath
(
strings
.
TrimSpace
(
key
))
{
return
true
}
}
return
false
}
for
key
:=
range
paramOverride
{
if
shouldAuditParamPath
(
strings
.
TrimSpace
(
key
))
{
return
true
}
}
return
false
}
func
getParamOverrideAuditRecorder
(
context
map
[
string
]
interface
{})
*
paramOverrideAuditRecorder
{
if
context
==
nil
{
return
nil
}
recorder
,
_
:=
context
[
paramOverrideContextAuditRecorder
]
.
(
*
paramOverrideAuditRecorder
)
return
recorder
}
func
(
r
*
paramOverrideAuditRecorder
)
recordOperation
(
mode
,
path
,
from
,
to
string
,
value
interface
{})
{
if
r
==
nil
{
return
}
line
:=
buildParamOverrideAuditLine
(
mode
,
path
,
from
,
to
,
value
)
if
line
==
""
{
return
}
if
lo
.
Contains
(
r
.
lines
,
line
)
{
return
}
r
.
lines
=
append
(
r
.
lines
,
line
)
}
func
shouldAuditParamPath
(
path
string
)
bool
{
path
=
strings
.
TrimSpace
(
path
)
if
path
==
""
{
return
false
}
if
common
.
DebugEnabled
{
return
true
}
_
,
ok
:=
paramOverrideKeyAuditPaths
[
path
]
return
ok
}
func
shouldAuditOperation
(
mode
,
path
,
from
,
to
string
)
bool
{
if
common
.
DebugEnabled
{
return
true
}
for
_
,
candidate
:=
range
[]
string
{
path
,
to
}
{
if
shouldAuditParamPath
(
candidate
)
{
return
true
}
}
return
false
}
func
formatParamOverrideAuditValue
(
value
interface
{})
string
{
switch
typed
:=
value
.
(
type
)
{
case
nil
:
return
"<empty>"
case
string
:
return
typed
default
:
return
common
.
GetJsonString
(
typed
)
}
}
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
{}
{
if
info
==
nil
||
info
.
ChannelMeta
==
nil
{
return
nil
...
...
@@ -455,7 +655,7 @@ func compareNumeric(jsonValue, targetValue gjson.Result, operator string) (bool,
}
// applyOperationsLegacy 原参数覆盖方法
func
applyOperationsLegacy
(
jsonData
[]
byte
,
paramOverride
map
[
string
]
interface
{})
([]
byte
,
error
)
{
func
applyOperationsLegacy
(
jsonData
[]
byte
,
paramOverride
map
[
string
]
interface
{}
,
auditRecorder
*
paramOverrideAuditRecorder
)
([]
byte
,
error
)
{
reqMap
:=
make
(
map
[
string
]
interface
{})
err
:=
common
.
Unmarshal
(
jsonData
,
&
reqMap
)
if
err
!=
nil
{
...
...
@@ -464,6 +664,7 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
for
key
,
value
:=
range
paramOverride
{
reqMap
[
key
]
=
value
auditRecorder
.
recordOperation
(
"set"
,
key
,
""
,
""
,
value
)
}
return
common
.
Marshal
(
reqMap
)
...
...
@@ -471,6 +672,7 @@ func applyOperationsLegacy(jsonData []byte, paramOverride map[string]interface{}
func
applyOperations
(
jsonStr
string
,
operations
[]
ParamOperation
,
conditionContext
map
[
string
]
interface
{})
(
string
,
error
)
{
context
:=
ensureContextMap
(
conditionContext
)
auditRecorder
:=
getParamOverrideAuditRecorder
(
context
)
contextJSON
,
err
:=
marshalContextJSON
(
context
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"failed to marshal condition context: %v"
,
err
)
...
...
@@ -506,6 +708,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"delete"
,
path
,
""
,
""
,
nil
)
}
case
"set"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -516,11 +719,15 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"set"
,
path
,
""
,
""
,
op
.
Value
)
}
case
"move"
:
opFrom
:=
processNegativeIndex
(
result
,
op
.
From
)
opTo
:=
processNegativeIndex
(
result
,
op
.
To
)
result
,
err
=
moveValue
(
result
,
opFrom
,
opTo
)
if
err
==
nil
{
auditRecorder
.
recordOperation
(
"move"
,
""
,
opFrom
,
opTo
,
nil
)
}
case
"copy"
:
if
op
.
From
==
""
||
op
.
To
==
""
{
return
""
,
fmt
.
Errorf
(
"copy from/to is required"
)
...
...
@@ -528,12 +735,16 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
opFrom
:=
processNegativeIndex
(
result
,
op
.
From
)
opTo
:=
processNegativeIndex
(
result
,
op
.
To
)
result
,
err
=
copyValue
(
result
,
opFrom
,
opTo
)
if
err
==
nil
{
auditRecorder
.
recordOperation
(
"copy"
,
""
,
opFrom
,
opTo
,
nil
)
}
case
"prepend"
:
for
_
,
path
:=
range
opPaths
{
result
,
err
=
modifyValue
(
result
,
path
,
op
.
Value
,
op
.
KeepOrigin
,
true
)
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"prepend"
,
path
,
""
,
""
,
op
.
Value
)
}
case
"append"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -541,6 +752,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"append"
,
path
,
""
,
""
,
op
.
Value
)
}
case
"trim_prefix"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -548,6 +760,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"trim_prefix"
,
path
,
""
,
""
,
op
.
Value
)
}
case
"trim_suffix"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -555,6 +768,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"trim_suffix"
,
path
,
""
,
""
,
op
.
Value
)
}
case
"ensure_prefix"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -562,6 +776,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"ensure_prefix"
,
path
,
""
,
""
,
op
.
Value
)
}
case
"ensure_suffix"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -569,6 +784,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"ensure_suffix"
,
path
,
""
,
""
,
op
.
Value
)
}
case
"trim_space"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -576,6 +792,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"trim_space"
,
path
,
""
,
""
,
nil
)
}
case
"to_lower"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -583,6 +800,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"to_lower"
,
path
,
""
,
""
,
nil
)
}
case
"to_upper"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -590,6 +808,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"to_upper"
,
path
,
""
,
""
,
nil
)
}
case
"replace"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -597,6 +816,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"replace"
,
path
,
op
.
From
,
op
.
To
,
nil
)
}
case
"regex_replace"
:
for
_
,
path
:=
range
opPaths
{
...
...
@@ -604,8 +824,10 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
!=
nil
{
break
}
auditRecorder
.
recordOperation
(
"regex_replace"
,
path
,
op
.
From
,
op
.
To
,
nil
)
}
case
"return_error"
:
auditRecorder
.
recordOperation
(
"return_error"
,
op
.
Path
,
""
,
""
,
op
.
Value
)
returnErr
,
parseErr
:=
parseParamOverrideReturnError
(
op
.
Value
)
if
parseErr
!=
nil
{
return
""
,
parseErr
...
...
@@ -621,11 +843,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
case
"set_header"
:
err
=
setHeaderOverrideInContext
(
context
,
op
.
Path
,
op
.
Value
,
op
.
KeepOrigin
)
if
err
==
nil
{
auditRecorder
.
recordOperation
(
"set_header"
,
op
.
Path
,
""
,
""
,
op
.
Value
)
contextJSON
,
err
=
marshalContextJSON
(
context
)
}
case
"delete_header"
:
err
=
deleteHeaderOverrideInContext
(
context
,
op
.
Path
)
if
err
==
nil
{
auditRecorder
.
recordOperation
(
"delete_header"
,
op
.
Path
,
""
,
""
,
nil
)
contextJSON
,
err
=
marshalContextJSON
(
context
)
}
case
"copy_header"
:
...
...
@@ -642,6 +866,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
err
=
nil
}
if
err
==
nil
{
auditRecorder
.
recordOperation
(
"copy_header"
,
""
,
sourceHeader
,
targetHeader
,
nil
)
contextJSON
,
err
=
marshalContextJSON
(
context
)
}
case
"move_header"
:
...
...
@@ -658,6 +883,7 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
err
=
nil
}
if
err
==
nil
{
auditRecorder
.
recordOperation
(
"move_header"
,
""
,
sourceHeader
,
targetHeader
,
nil
)
contextJSON
,
err
=
marshalContextJSON
(
context
)
}
case
"pass_headers"
:
...
...
@@ -675,11 +901,13 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
}
}
if
err
==
nil
{
auditRecorder
.
recordOperation
(
"pass_headers"
,
""
,
""
,
""
,
headerNames
)
contextJSON
,
err
=
marshalContextJSON
(
context
)
}
case
"sync_fields"
:
result
,
err
=
syncFieldsBetweenTargets
(
result
,
context
,
op
.
From
,
op
.
To
)
if
err
==
nil
{
auditRecorder
.
recordOperation
(
"sync_fields"
,
""
,
op
.
From
,
op
.
To
,
nil
)
contextJSON
,
err
=
marshalContextJSON
(
context
)
}
default
:
...
...
relay/common/override_test.go
View file @
69551ab2
...
...
@@ -6,6 +6,7 @@ import (
"reflect"
"testing"
common2
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/types"
"github.com/QuantumNous/new-api/dto"
...
...
@@ -2066,6 +2067,105 @@ func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) {
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
)
{
t
.
Helper
()
...
...
relay/common/relay_info.go
View file @
69551ab2
...
...
@@ -149,6 +149,7 @@ type RelayInfo struct {
LastError
*
types
.
NewAPIError
RuntimeHeadersOverride
map
[
string
]
interface
{}
UseRuntimeHeadersOverride
bool
ParamOverrideAudit
[]
string
PriceData
types
.
PriceData
...
...
service/log_info_generate.go
View file @
69551ab2
...
...
@@ -74,9 +74,17 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
appendRequestPath
(
ctx
,
relayInfo
,
other
)
appendRequestConversionChain
(
relayInfo
,
other
)
appendBillingInfo
(
relayInfo
,
other
)
appendParamOverrideInfo
(
relayInfo
,
other
)
return
other
}
func
appendParamOverrideInfo
(
relayInfo
*
relaycommon
.
RelayInfo
,
other
map
[
string
]
interface
{})
{
if
relayInfo
==
nil
||
other
==
nil
||
len
(
relayInfo
.
ParamOverrideAudit
)
==
0
{
return
}
other
[
"po"
]
=
relayInfo
.
ParamOverrideAudit
}
func
appendBillingInfo
(
relayInfo
*
relaycommon
.
RelayInfo
,
other
map
[
string
]
interface
{})
{
if
relayInfo
==
nil
||
other
==
nil
{
return
...
...
web/src/components/table/usage-logs/components/ParamOverrideEntry.jsx
0 → 100644
View file @
69551ab2
/*
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
);
web/src/components/table/usage-logs/index.jsx
View file @
69551ab2
...
...
@@ -25,6 +25,7 @@ import LogsFilters from './UsageLogsFilters';
import
ColumnSelectorModal
from
'./modals/ColumnSelectorModal'
;
import
UserInfoModal
from
'./modals/UserInfoModal'
;
import
ChannelAffinityUsageCacheModal
from
'./modals/ChannelAffinityUsageCacheModal'
;
import
ParamOverrideModal
from
'./modals/ParamOverrideModal'
;
import
{
useLogsData
}
from
'../../../hooks/usage-logs/useUsageLogsData'
;
import
{
useIsMobile
}
from
'../../../hooks/common/useIsMobile'
;
import
{
createCardProPagination
}
from
'../../../helpers/utils'
;
...
...
@@ -39,6 +40,7 @@ const LogsPage = () => {
<
ColumnSelectorModal
{
...
logsData
}
/>
<
UserInfoModal
{
...
logsData
}
/>
<
ChannelAffinityUsageCacheModal
{
...
logsData
}
/>
<
ParamOverrideModal
{
...
logsData
}
/>
{
/* Main Content */
}
<
CardPro
...
...
web/src/components/table/usage-logs/modals/ParamOverrideModal.jsx
0 → 100644
View file @
69551ab2
/*
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
,
{
useMemo
}
from
'react'
;
import
{
Modal
,
Button
,
Empty
,
Divider
,
Typography
,
}
from
'@douyinfe/semi-ui'
;
import
{
IconCopy
}
from
'@douyinfe/semi-icons'
;
import
{
copy
,
showError
,
showSuccess
}
from
'../../../../helpers'
;
const
{
Text
}
=
Typography
;
const
parseAuditLine
=
(
line
)
=>
{
if
(
typeof
line
!==
'string'
)
{
return
null
;
}
const
firstSpaceIndex
=
line
.
indexOf
(
' '
);
if
(
firstSpaceIndex
<=
0
)
{
return
{
action
:
line
,
content
:
line
};
}
return
{
action
:
line
.
slice
(
0
,
firstSpaceIndex
),
content
:
line
.
slice
(
firstSpaceIndex
+
1
),
};
};
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
=
({
showParamOverrideModal
,
setShowParamOverrideModal
,
paramOverrideTarget
,
t
,
})
=>
{
const
lines
=
Array
.
isArray
(
paramOverrideTarget
?.
lines
)
?
paramOverrideTarget
.
lines
:
[];
const
parsedLines
=
useMemo
(()
=>
{
return
lines
.
map
(
parseAuditLine
);
},
[
lines
]);
const
copyAll
=
async
()
=>
{
const
content
=
lines
.
join
(
'\n'
);
if
(
!
content
)
{
return
;
}
if
(
await
copy
(
content
))
{
showSuccess
(
t
(
'参数覆盖已复制'
));
return
;
}
showError
(
t
(
'无法复制到剪贴板,请手动复制'
));
};
return
(
<
Modal
title=
{
t
(
'参数覆盖详情'
)
}
visible=
{
showParamOverrideModal
}
onCancel=
{
()
=>
setShowParamOverrideModal
(
false
)
}
footer=
{
null
}
centered
closable
maskClosable
width=
{
640
}
>
<
div
style=
{
{
padding
:
'8px 20px 20px'
}
}
>
<
div
style=
{
{
display
:
'flex'
,
justifyContent
:
'space-between'
,
alignItems
:
'flex-start'
,
gap
:
12
,
marginBottom
:
10
,
}
}
>
<
div
style=
{
{
minWidth
:
0
}
}
>
<
div
style=
{
{
marginBottom
:
4
}
}
>
<
Text
style=
{
{
fontWeight
:
600
}
}
>
{
t
(
'
{{
count
}}
项操作'
,
{
count
:
lines
.
length
})
}
</
Text
>
</
div
>
<
div
style=
{
{
display
:
'flex'
,
flexWrap
:
'wrap'
,
gap
:
8
,
fontSize
:
12
,
color
:
'var(--semi-color-text-2)'
,
}
}
>
{
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
>
<
Button
icon=
{
<
IconCopy
/>
}
theme=
'borderless'
type=
'tertiary'
size=
'small'
onClick=
{
copyAll
}
disabled=
{
lines
.
length
===
0
}
>
{
t
(
'复制'
)
}
</
Button
>
</
div
>
<
Divider
margin=
'12px'
/>
{
lines
.
length
===
0
?
(
<
Empty
description=
{
t
(
'暂无参数覆盖记录'
)
}
style=
{
{
padding
:
'24px 0 8px'
}
}
/>
)
:
(
<
div
style=
{
{
display
:
'flex'
,
flexDirection
:
'column'
,
gap
:
8
,
maxHeight
:
'56vh'
,
overflowY
:
'auto'
,
paddingRight
:
2
,
}
}
>
{
parsedLines
.
map
((
item
,
index
)
=>
{
if
(
!
item
)
{
return
null
;
}
return
(
<
div
key=
{
`${item.action}-${index}`
}
style=
{
{
padding
:
'10px 12px'
,
borderRadius
:
10
,
border
:
'1px solid var(--semi-color-border)'
,
background
:
'var(--semi-color-fill-0)'
,
display
:
'flex'
,
gap
:
12
,
alignItems
:
'flex-start'
,
}
}
>
<
div
style=
{
{
flex
:
'0 0 auto'
,
minWidth
:
74
,
}
}
>
<
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
>
<
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
>
)
}
</
div
>
</
Modal
>
);
};
export
default
ParamOverrideModal
;
web/src/hooks/usage-logs/useUsageLogsData.jsx
View file @
69551ab2
...
...
@@ -39,6 +39,7 @@ import {
}
from
'../../helpers'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
useTableCompactMode
}
from
'../common/useTableCompactMode'
;
import
ParamOverrideEntry
from
'../../components/table/usage-logs/components/ParamOverrideEntry'
;
export
const
useLogsData
=
()
=>
{
const
{
t
}
=
useTranslation
();
...
...
@@ -181,6 +182,8 @@ export const useLogsData = () => {
]
=
useState
(
false
);
const
[
channelAffinityUsageCacheTarget
,
setChannelAffinityUsageCacheTarget
]
=
useState
(
null
);
const
[
showParamOverrideModal
,
setShowParamOverrideModal
]
=
useState
(
false
);
const
[
paramOverrideTarget
,
setParamOverrideTarget
]
=
useState
(
null
);
// Initialize default column visibility
const
initDefaultColumns
=
()
=>
{
...
...
@@ -345,6 +348,20 @@ export const useLogsData = () => {
setShowChannelAffinityUsageCacheModal
(
true
);
};
const
openParamOverrideModal
=
(
log
,
other
)
=>
{
const
lines
=
Array
.
isArray
(
other
?.
po
)
?
other
.
po
.
filter
(
Boolean
)
:
[];
if
(
lines
.
length
===
0
)
{
return
;
}
setParamOverrideTarget
({
lines
,
modelName
:
log
?.
model_name
||
''
,
requestId
:
log
?.
request_id
||
''
,
requestPath
:
other
?.
request_path
||
''
,
});
setShowParamOverrideModal
(
true
);
};
// Format logs data
const
setLogsFormat
=
(
logs
)
=>
{
const
requestConversionDisplayValue
=
(
conversionChain
)
=>
{
...
...
@@ -584,6 +601,21 @@ export const useLogsData = () => {
value
:
other
.
request_path
,
});
}
if
(
Array
.
isArray
(
other
?.
po
)
&&
other
.
po
.
length
>
0
)
{
expandDataLocal
.
push
({
key
:
t
(
'参数覆盖'
),
value
:
(
<
ParamOverrideEntry
count=
{
other
.
po
.
length
}
t=
{
t
}
onOpen=
{
(
event
)
=>
{
event
.
stopPropagation
();
openParamOverrideModal
(
logs
[
i
],
other
);
}
}
/>
),
});
}
if
(
other
?.
billing_source
===
'subscription'
)
{
const
planId
=
other
?.
subscription_plan_id
;
const
planTitle
=
other
?.
subscription_plan_title
||
''
;
...
...
@@ -811,6 +843,9 @@ export const useLogsData = () => {
setShowChannelAffinityUsageCacheModal
,
channelAffinityUsageCacheTarget
,
openChannelAffinityUsageCacheModal
,
showParamOverrideModal
,
setShowParamOverrideModal
,
paramOverrideTarget
,
// Functions
loadLogs
,
...
...
@@ -822,6 +857,7 @@ export const useLogsData = () => {
setLogsFormat
,
hasExpandableRows
,
setLogType
,
openParamOverrideModal
,
// Translation
t
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment