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
eab18a83
authored
Aug 10, 2026
by
Seefs
Committed by
GitHub
Aug 10, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: record reasoning effort consistently in usage logs (#6641)
parent
7dd1000a
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
332 additions
and
13 deletions
+332
-13
relay/channel/deepseek/adaptor.go
+3
-3
relay/channel/openai/adaptor.go
+2
-2
relay/channel/xai/adaptor.go
+1
-1
relay/claude_handler.go
+5
-0
relay/common/override.go
+51
-0
relay/common/override_test.go
+111
-0
relay/common/relay_info.go
+38
-1
relay/common/relay_info_test.go
+98
-0
web/src/features/usage-logs/components/dialogs/details-dialog.tsx
+4
-6
web/src/features/usage-logs/lib/format.ts
+19
-0
No files found.
relay/channel/deepseek/adaptor.go
View file @
eab18a83
...
...
@@ -115,7 +115,7 @@ func applyDeepSeekV4OpenAIThinkingSuffix(info *relaycommon.RelayInfo, request *d
if
info
.
ChannelMeta
!=
nil
{
info
.
UpstreamModelName
=
baseModel
}
info
.
ReasoningEffort
=
effort
info
.
SetReasoningEffort
(
effort
)
}
return
nil
}
...
...
@@ -146,7 +146,7 @@ func applyDeepSeekV4ClaudeThinkingSuffix(info *relaycommon.RelayInfo, request *d
if
info
.
ChannelMeta
!=
nil
{
info
.
UpstreamModelName
=
baseModel
}
info
.
ReasoningEffort
=
effort
info
.
SetReasoningEffort
(
effort
)
}
return
nil
}
...
...
@@ -185,7 +185,7 @@ func applyDeepSeekV4ResponsesThinkingSuffix(info *relaycommon.RelayInfo, request
}
}
if
info
!=
nil
&&
request
.
Reasoning
!=
nil
{
info
.
ReasoningEffort
=
request
.
Reasoning
.
Effort
info
.
SetReasoningEffort
(
request
.
Reasoning
.
Effort
)
}
}
...
...
relay/channel/openai/adaptor.go
View file @
eab18a83
...
...
@@ -352,7 +352,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
request
.
Model
=
originModel
}
info
.
ReasoningEffort
=
request
.
ReasoningEffort
info
.
SetReasoningEffort
(
request
.
ReasoningEffort
)
// o系列模型developer适配(o1-mini除外)
if
!
strings
.
HasPrefix
(
info
.
UpstreamModelName
,
"o1-mini"
)
&&
!
strings
.
HasPrefix
(
info
.
UpstreamModelName
,
"o1-preview"
)
{
...
...
@@ -615,7 +615,7 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
request
.
Model
=
originModel
}
if
info
!=
nil
&&
request
.
Reasoning
!=
nil
&&
request
.
Reasoning
.
Effort
!=
""
{
info
.
ReasoningEffort
=
request
.
Reasoning
.
Effort
info
.
SetReasoningEffort
(
request
.
Reasoning
.
Effort
)
}
return
request
,
nil
}
...
...
relay/channel/xai/adaptor.go
View file @
eab18a83
...
...
@@ -85,7 +85,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
request
.
ReasoningEffort
=
"low"
request
.
Model
=
strings
.
TrimSuffix
(
request
.
Model
,
"-low"
)
}
info
.
ReasoningEffort
=
request
.
ReasoningEffort
info
.
SetReasoningEffort
(
request
.
ReasoningEffort
)
info
.
UpstreamModelName
=
request
.
Model
}
return
request
,
nil
...
...
relay/claude_handler.go
View file @
eab18a83
...
...
@@ -106,6 +106,11 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
}
info
.
UpstreamModelName
=
request
.
Model
}
if
!
model_setting
.
GetGlobalSettings
()
.
PassThroughRequestEnabled
&&
!
info
.
ChannelSetting
.
PassThroughBodyEnabled
{
if
effort
:=
request
.
GetEfforts
();
effort
!=
""
{
info
.
SetReasoningEffort
(
effort
)
}
}
if
info
.
ChannelSetting
.
SystemPrompt
!=
""
{
if
request
.
System
==
nil
{
...
...
relay/common/override.go
View file @
eab18a83
...
...
@@ -30,6 +30,11 @@ var paramOverrideSensitivePathPrefixes = []string{
"model"
,
"original_model"
,
"upstream_model"
,
"reasoning"
,
"reasoning_effort"
,
"output_config"
,
"generationConfig.thinkingConfig"
,
"generation_config.thinking_config"
,
"service_tier"
,
"inference_geo"
,
"speed"
,
...
...
@@ -191,6 +196,7 @@ func ApplyParamOverrideWithRelayInfo(jsonData []byte, info *RelayInfo) ([]byte,
if
err
!=
nil
{
return
nil
,
err
}
syncReasoningEffortAfterParamOverride
(
info
,
jsonData
,
result
)
syncRuntimeHeaderOverrideFromContext
(
info
,
overrideCtx
)
if
info
!=
nil
{
if
recorder
!=
nil
{
...
...
@@ -202,6 +208,51 @@ func ApplyParamOverrideWithRelayInfo(jsonData []byte, info *RelayInfo) ([]byte,
return
result
,
nil
}
func
syncReasoningEffortAfterParamOverride
(
info
*
RelayInfo
,
before
,
after
[]
byte
)
{
if
info
==
nil
{
return
}
_
,
existedBefore
:=
extractReasoningEffortFromJSON
(
info
.
GetFinalRequestRelayFormat
(),
before
)
effort
,
existsAfter
:=
extractReasoningEffortFromJSON
(
info
.
GetFinalRequestRelayFormat
(),
after
)
if
existsAfter
{
info
.
SetReasoningEffort
(
effort
)
return
}
if
existedBefore
{
info
.
SetReasoningEffort
(
""
)
}
}
func
extractReasoningEffortFromJSON
(
format
types
.
RelayFormat
,
data
[]
byte
)
(
string
,
bool
)
{
var
paths
[]
string
switch
format
{
case
types
.
RelayFormatOpenAI
:
paths
=
[]
string
{
"reasoning_effort"
,
"reasoning.effort"
}
case
types
.
RelayFormatOpenAIResponses
:
paths
=
[]
string
{
"reasoning.effort"
}
case
types
.
RelayFormatClaude
:
paths
=
[]
string
{
"output_config.effort"
}
case
types
.
RelayFormatGemini
:
paths
=
[]
string
{
"generationConfig.thinkingConfig.thinkingLevel"
,
"generation_config.thinking_config.thinking_level"
,
}
default
:
return
""
,
false
}
for
_
,
path
:=
range
paths
{
value
:=
gjson
.
GetBytes
(
data
,
path
)
if
!
value
.
Exists
()
{
continue
}
if
value
.
Type
!=
gjson
.
String
{
return
""
,
true
}
return
strings
.
TrimSpace
(
value
.
String
()),
true
}
return
""
,
false
}
func
shouldEnableParamOverrideAudit
(
paramOverride
map
[
string
]
interface
{})
bool
{
if
common
.
DebugEnabled
{
return
true
...
...
relay/common/override_test.go
View file @
eab18a83
...
...
@@ -12,6 +12,7 @@ import (
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
...
...
@@ -2302,3 +2303,113 @@ func assertJSONEqual(t *testing.T, want, got string) {
t
.
Fatalf
(
"json not equal
\n
want: %s
\n
got: %s"
,
want
,
got
)
}
}
func
TestApplyParamOverrideWithRelayInfoSynchronizesReasoningEffort
(
t
*
testing
.
T
)
{
originalDebugEnabled
:=
common2
.
DebugEnabled
common2
.
DebugEnabled
=
false
t
.
Cleanup
(
func
()
{
common2
.
DebugEnabled
=
originalDebugEnabled
})
tests
:=
[]
struct
{
name
string
relayFormat
types
.
RelayFormat
initialEffort
string
input
string
operation
map
[
string
]
interface
{}
expected
string
}{
{
name
:
"Responses set"
,
relayFormat
:
types
.
RelayFormatOpenAIResponses
,
initialEffort
:
"high"
,
input
:
`{"reasoning":{"effort":"high"}}`
,
operation
:
map
[
string
]
interface
{}{
"mode"
:
"set"
,
"path"
:
"reasoning.effort"
,
"value"
:
"max"
},
expected
:
"max"
,
},
{
name
:
"chat delete"
,
relayFormat
:
types
.
RelayFormatOpenAI
,
initialEffort
:
"high"
,
input
:
`{"reasoning_effort":"high"}`
,
operation
:
map
[
string
]
interface
{}{
"mode"
:
"delete"
,
"path"
:
"reasoning_effort"
},
expected
:
""
,
},
{
name
:
"OpenRouter nested set"
,
relayFormat
:
types
.
RelayFormatOpenAI
,
initialEffort
:
"medium"
,
input
:
`{"reasoning":{"effort":"medium"}}`
,
operation
:
map
[
string
]
interface
{}{
"mode"
:
"set"
,
"path"
:
"reasoning.effort"
,
"value"
:
"xhigh"
},
expected
:
"xhigh"
,
},
{
name
:
"Claude output config set"
,
relayFormat
:
types
.
RelayFormatClaude
,
initialEffort
:
"high"
,
input
:
`{"output_config":{"effort":"high"}}`
,
operation
:
map
[
string
]
interface
{}{
"mode"
:
"set"
,
"path"
:
"output_config.effort"
,
"value"
:
"max"
},
expected
:
"max"
,
},
{
name
:
"Gemini thinking level set"
,
relayFormat
:
types
.
RelayFormatGemini
,
initialEffort
:
"medium"
,
input
:
`{"generationConfig":{"thinkingConfig":{"thinkingLevel":"medium"}}}`
,
operation
:
map
[
string
]
interface
{}{
"mode"
:
"set"
,
"path"
:
"generationConfig.thinkingConfig.thinkingLevel"
,
"value"
:
"high"
},
expected
:
"high"
,
},
{
name
:
"non-string value clears effort"
,
relayFormat
:
types
.
RelayFormatOpenAIResponses
,
initialEffort
:
"high"
,
input
:
`{"reasoning":{"effort":"high"}}`
,
operation
:
map
[
string
]
interface
{}{
"mode"
:
"set"
,
"path"
:
"reasoning.effort"
,
"value"
:
42
},
expected
:
""
,
},
{
name
:
"unrelated override preserves converter-derived effort"
,
relayFormat
:
types
.
RelayFormatClaude
,
initialEffort
:
"high"
,
input
:
`{"thinking":{"type":"adaptive"},"max_tokens":4096}`
,
operation
:
map
[
string
]
interface
{}{
"mode"
:
"set"
,
"path"
:
"max_tokens"
,
"value"
:
8192
},
expected
:
"high"
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
info
:=
&
RelayInfo
{
RelayFormat
:
tt
.
relayFormat
,
ReasoningEffort
:
tt
.
initialEffort
,
ChannelMeta
:
&
ChannelMeta
{
ParamOverride
:
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
tt
.
operation
},
}},
}
_
,
err
:=
ApplyParamOverrideWithRelayInfo
([]
byte
(
tt
.
input
),
info
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
tt
.
expected
,
info
.
ReasoningEffort
)
})
}
}
func
TestReasoningEffortOverrideIsAuditedWithoutDebugMode
(
t
*
testing
.
T
)
{
originalDebugEnabled
:=
common2
.
DebugEnabled
common2
.
DebugEnabled
=
false
t
.
Cleanup
(
func
()
{
common2
.
DebugEnabled
=
originalDebugEnabled
})
info
:=
&
RelayInfo
{
RelayFormat
:
types
.
RelayFormatOpenAIResponses
,
ChannelMeta
:
&
ChannelMeta
{
ParamOverride
:
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"mode"
:
"set"
,
"path"
:
"reasoning.effort"
,
"value"
:
"max"
},
},
}},
}
_
,
err
:=
ApplyParamOverrideWithRelayInfo
([]
byte
(
`{"reasoning":{"effort":"high"}}`
),
info
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
[]
string
{
"set reasoning.effort = max"
},
info
.
ParamOverrideAudit
)
}
relay/common/relay_info.go
View file @
eab18a83
...
...
@@ -234,6 +234,11 @@ func (info *RelayInfo) InitChannelMeta(c *gin.Context) {
// Channel identity feeds the converter options snapshot (e.g.
// OpenRouterDialect); drop the cache so a cross-channel retry rebuilds it.
info
.
convOptions
=
nil
if
model_setting
.
GetGlobalSettings
()
.
PassThroughRequestEnabled
||
channelMeta
.
ChannelSetting
.
PassThroughBodyEnabled
{
info
.
ReasoningEffort
=
""
}
else
{
info
.
ReasoningEffort
=
reasoningEffortFromRequest
(
info
.
Request
)
}
// reset some fields based on channel meta
// 重置某些字段,例如模型名称等
...
...
@@ -435,6 +440,36 @@ func GenRelayInfoOpenAI(c *gin.Context, request dto.Request) *RelayInfo {
return
info
}
func
reasoningEffortFromRequest
(
request
dto
.
Request
)
string
{
var
effort
string
switch
req
:=
request
.
(
type
)
{
case
*
dto
.
GeneralOpenAIRequest
:
if
req
==
nil
{
return
""
}
effort
=
req
.
ReasoningEffort
if
strings
.
TrimSpace
(
effort
)
==
""
&&
len
(
req
.
Reasoning
)
>
0
{
value
:=
gjson
.
GetBytes
(
req
.
Reasoning
,
"effort"
)
if
value
.
Type
==
gjson
.
String
{
effort
=
value
.
String
()
}
}
case
*
dto
.
OpenAIResponsesRequest
:
if
req
!=
nil
&&
req
.
Reasoning
!=
nil
{
effort
=
req
.
Reasoning
.
Effort
}
case
*
dto
.
ClaudeRequest
:
if
req
!=
nil
{
effort
=
req
.
GetEfforts
()
}
case
*
dto
.
GeminiChatRequest
:
if
req
!=
nil
&&
req
.
GenerationConfig
.
ThinkingConfig
!=
nil
{
effort
=
req
.
GenerationConfig
.
ThinkingConfig
.
ThinkingLevel
}
}
return
strings
.
TrimSpace
(
effort
)
}
func
genBaseRelayInfo
(
c
*
gin
.
Context
,
request
dto
.
Request
)
*
RelayInfo
{
//channelType := common.GetContextKeyInt(c, constant.ContextKeyChannelType)
...
...
@@ -465,8 +500,10 @@ func genBaseRelayInfo(c *gin.Context, request dto.Request) *RelayInfo {
if
reqId
==
""
{
reqId
=
common
.
NewRequestId
()
}
reasoningEffort
:=
reasoningEffortFromRequest
(
request
)
info
:=
&
RelayInfo
{
Request
:
request
,
ReasoningEffort
:
reasoningEffort
,
RequestId
:
reqId
,
UserId
:
common
.
GetContextKeyInt
(
c
,
constant
.
ContextKeyUserId
),
...
...
@@ -740,7 +777,7 @@ func (info *RelayInfo) SetReasoningEffort(effort string) {
if
info
==
nil
{
return
}
info
.
ReasoningEffort
=
effort
info
.
ReasoningEffort
=
strings
.
TrimSpace
(
effort
)
}
func
(
info
*
RelayInfo
)
EnsureClaudeConvertInfo
()
*
convmeta
.
ClaudeConvertInfo
{
...
...
relay/common/relay_info_test.go
View file @
eab18a83
package
common
import
(
"encoding/json"
"net/http/httptest"
"testing"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
...
...
@@ -78,3 +82,97 @@ func TestRelayInfoMetaTypedNilReceiver(t *testing.T) {
assert
.
NotNil
(
t
,
firstOptions
.
Gemini
.
SafetySetting
)
assert
.
NotNil
(
t
,
firstOptions
.
PreserveThinkingSuffix
)
}
func
TestGenRelayInfoCapturesRequestReasoningEffort
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
tests
:=
[]
struct
{
name
string
path
string
relayFormat
types
.
RelayFormat
request
dto
.
Request
expected
string
}{
{
name
:
"OpenAI chat top-level effort"
,
path
:
"/v1/chat/completions"
,
relayFormat
:
types
.
RelayFormatOpenAI
,
request
:
&
dto
.
GeneralOpenAIRequest
{
Model
:
"gpt-5.6-sol"
,
ReasoningEffort
:
" high "
},
expected
:
"high"
,
},
{
name
:
"OpenRouter nested chat effort"
,
path
:
"/v1/chat/completions"
,
relayFormat
:
types
.
RelayFormatOpenAI
,
request
:
&
dto
.
GeneralOpenAIRequest
{
Model
:
"anthropic/claude"
,
Reasoning
:
json
.
RawMessage
(
`{"effort":"xhigh"}`
)},
expected
:
"xhigh"
,
},
{
name
:
"OpenAI Responses effort"
,
path
:
"/v1/responses"
,
relayFormat
:
types
.
RelayFormatOpenAIResponses
,
request
:
&
dto
.
OpenAIResponsesRequest
{
Model
:
"gpt-5.6-sol"
,
Reasoning
:
&
dto
.
Reasoning
{
Effort
:
"max"
}},
expected
:
"max"
,
},
{
name
:
"explicit none is preserved"
,
path
:
"/v1/responses"
,
relayFormat
:
types
.
RelayFormatOpenAIResponses
,
request
:
&
dto
.
OpenAIResponsesRequest
{
Model
:
"gpt-5.6-sol"
,
Reasoning
:
&
dto
.
Reasoning
{
Effort
:
"none"
}},
expected
:
"none"
,
},
{
name
:
"non-string nested effort is ignored"
,
path
:
"/v1/chat/completions"
,
relayFormat
:
types
.
RelayFormatOpenAI
,
request
:
&
dto
.
GeneralOpenAIRequest
{
Model
:
"anthropic/claude"
,
Reasoning
:
json
.
RawMessage
(
`{"effort":42}`
)},
expected
:
""
,
},
{
name
:
"Claude output config effort"
,
path
:
"/v1/messages"
,
relayFormat
:
types
.
RelayFormatClaude
,
request
:
&
dto
.
ClaudeRequest
{
Model
:
"claude-opus-4-7"
,
OutputConfig
:
json
.
RawMessage
(
`{"effort":"medium"}`
)},
expected
:
"medium"
,
},
{
name
:
"Gemini thinking level"
,
path
:
"/v1beta/models/gemini-3-pro:generateContent"
,
relayFormat
:
types
.
RelayFormatGemini
,
request
:
&
dto
.
GeminiChatRequest
{
GenerationConfig
:
dto
.
GeminiChatGenerationConfig
{
ThinkingConfig
:
&
dto
.
GeminiThinkingConfig
{
ThinkingLevel
:
"low"
},
}},
expected
:
"low"
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
ctx
,
_
:=
gin
.
CreateTestContext
(
httptest
.
NewRecorder
())
ctx
.
Request
=
httptest
.
NewRequest
(
"POST"
,
tt
.
path
,
nil
)
info
,
err
:=
GenRelayInfo
(
ctx
,
tt
.
relayFormat
,
tt
.
request
,
nil
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
tt
.
expected
,
info
.
ReasoningEffort
)
})
}
}
func
TestInitChannelMetaRestoresRequestReasoningEffortForRetry
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
_
:=
gin
.
CreateTestContext
(
httptest
.
NewRecorder
())
ctx
.
Request
=
httptest
.
NewRequest
(
"POST"
,
"/v1/responses"
,
nil
)
request
:=
&
dto
.
OpenAIResponsesRequest
{
Model
:
"gpt-5.6-sol"
,
Reasoning
:
&
dto
.
Reasoning
{
Effort
:
"max"
},
}
info
,
err
:=
GenRelayInfo
(
ctx
,
types
.
RelayFormatOpenAIResponses
,
request
,
nil
)
require
.
NoError
(
t
,
err
)
info
.
SetReasoningEffort
(
"high"
)
info
.
InitChannelMeta
(
ctx
)
assert
.
Equal
(
t
,
"max"
,
info
.
ReasoningEffort
)
info
.
SetReasoningEffort
(
"low"
)
info
.
InitChannelMeta
(
ctx
)
assert
.
Equal
(
t
,
"max"
,
info
.
ReasoningEffort
)
}
web/src/features/usage-logs/components/dialogs/details-dialog.tsx
View file @
eab18a83
...
...
@@ -74,6 +74,7 @@ import {
isViolationFeeLog
,
getFirstResponseTimeColor
,
getResponseTimeColor
,
getReasoningEffortVariant
,
renderAuditContent
,
}
from
'../../lib/format'
import
{
...
...
@@ -604,12 +605,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
const
useChannel
=
other
?.
admin_info
?.
use_channel
const
channelChain
=
useChannel
&&
useChannel
.
length
>
0
?
useChannel
.
join
(
' → '
)
:
undefined
let
reasoningEffortVariant
:
StatusBadgeProps
[
'variant'
]
=
'green'
if
(
other
?.
reasoning_effort
===
'high'
)
{
reasoningEffortVariant
=
'orange'
}
else
if
(
other
?.
reasoning_effort
===
'medium'
)
{
reasoningEffortVariant
=
'yellow'
}
const
reasoningEffortVariant
=
getReasoningEffortVariant
(
other
?.
reasoning_effort
)
return
(
<
Dialog
...
...
web/src/features/usage-logs/lib/format.ts
View file @
eab18a83
...
...
@@ -167,6 +167,25 @@ export function parseLogOther(other: string): LogOtherData | null {
}
}
export
function
getReasoningEffortVariant
(
effort
:
string
|
undefined
):
StatusBadgeProps
[
'variant'
]
{
switch
(
effort
?.
trim
().
toLowerCase
())
{
case
'max'
:
case
'xhigh'
:
case
'high'
:
return
'orange'
case
'medium'
:
return
'yellow'
case
'low'
:
case
'minimal'
:
return
'green'
case
'none'
:
default
:
return
'grey'
}
}
/**
* Get time color based on duration (in seconds)
*/
...
...
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