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
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
473 additions
and
0 deletions
+473
-0
relay/common/override.go
+0
-0
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
This diff is collapsed.
Click to expand it.
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