Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
8321e4a6
Unverified
Commit
8321e4a6
authored
Mar 15, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 15, 2026
Browse files
Merge pull request #1023 from YanzheL/fix/claude-output-effort-logging
fix: extract and log Claude output_config.effort in usage records
parents
3084330d
1bff2292
Changes
7
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/dto/types.go
View file @
8321e4a6
...
@@ -334,8 +334,8 @@ type UsageLog struct {
...
@@ -334,8 +334,8 @@ type UsageLog struct {
Model
string
`json:"model"`
Model
string
`json:"model"`
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
ServiceTier
*
string
`json:"service_tier,omitempty"`
ServiceTier
*
string
`json:"service_tier,omitempty"`
// ReasoningEffort is the request's reasoning effort level
(OpenAI Responses API)
.
// ReasoningEffort is the request's reasoning effort level.
//
nil means not provided / not applicable
.
//
OpenAI: "low"/"medium"/"high"/"xhigh"; Claude: "low"/"medium"/"high"/"max"
.
ReasoningEffort
*
string
`json:"reasoning_effort,omitempty"`
ReasoningEffort
*
string
`json:"reasoning_effort,omitempty"`
// InboundEndpoint is the client-facing API endpoint path, e.g. /v1/chat/completions.
// InboundEndpoint is the client-facing API endpoint path, e.g. /v1/chat/completions.
InboundEndpoint
*
string
`json:"inbound_endpoint,omitempty"`
InboundEndpoint
*
string
`json:"inbound_endpoint,omitempty"`
...
...
backend/internal/handler/gateway_handler.go
View file @
8321e4a6
...
@@ -443,6 +443,10 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
...
@@ -443,6 +443,10 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
clientIP
:=
ip
.
GetClientIP
(
c
)
clientIP
:=
ip
.
GetClientIP
(
c
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
if
result
.
ReasoningEffort
==
nil
{
result
.
ReasoningEffort
=
service
.
NormalizeClaudeOutputEffort
(
parsedReq
.
OutputEffort
)
}
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
...
@@ -754,6 +758,10 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
...
@@ -754,6 +758,10 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
clientIP
:=
ip
.
GetClientIP
(
c
)
clientIP
:=
ip
.
GetClientIP
(
c
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
if
result
.
ReasoningEffort
==
nil
{
result
.
ReasoningEffort
=
service
.
NormalizeClaudeOutputEffort
(
parsedReq
.
OutputEffort
)
}
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
...
...
backend/internal/service/gateway_record_usage_test.go
View file @
8321e4a6
...
@@ -369,3 +369,54 @@ func TestGatewayServiceRecordUsage_BillingErrorSkipsUsageLogWrite(t *testing.T)
...
@@ -369,3 +369,54 @@ func TestGatewayServiceRecordUsage_BillingErrorSkipsUsageLogWrite(t *testing.T)
require
.
Equal
(
t
,
1
,
billingRepo
.
calls
)
require
.
Equal
(
t
,
1
,
billingRepo
.
calls
)
require
.
Equal
(
t
,
0
,
usageRepo
.
calls
)
require
.
Equal
(
t
,
0
,
usageRepo
.
calls
)
}
}
func
TestGatewayServiceRecordUsage_ReasoningEffortPersisted
(
t
*
testing
.
T
)
{
usageRepo
:=
&
openAIRecordUsageBestEffortLogRepoStub
{}
svc
:=
newGatewayRecordUsageServiceForTest
(
usageRepo
,
&
openAIRecordUsageUserRepoStub
{},
&
openAIRecordUsageSubRepoStub
{})
effort
:=
"max"
err
:=
svc
.
RecordUsage
(
context
.
Background
(),
&
RecordUsageInput
{
Result
:
&
ForwardResult
{
RequestID
:
"effort_test"
,
Usage
:
ClaudeUsage
{
InputTokens
:
10
,
OutputTokens
:
5
,
},
Model
:
"claude-opus-4-6"
,
Duration
:
time
.
Second
,
ReasoningEffort
:
&
effort
,
},
APIKey
:
&
APIKey
{
ID
:
1
},
User
:
&
User
{
ID
:
1
},
Account
:
&
Account
{
ID
:
1
},
})
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
usageRepo
.
lastLog
)
require
.
NotNil
(
t
,
usageRepo
.
lastLog
.
ReasoningEffort
)
require
.
Equal
(
t
,
"max"
,
*
usageRepo
.
lastLog
.
ReasoningEffort
)
}
func
TestGatewayServiceRecordUsage_ReasoningEffortNil
(
t
*
testing
.
T
)
{
usageRepo
:=
&
openAIRecordUsageBestEffortLogRepoStub
{}
svc
:=
newGatewayRecordUsageServiceForTest
(
usageRepo
,
&
openAIRecordUsageUserRepoStub
{},
&
openAIRecordUsageSubRepoStub
{})
err
:=
svc
.
RecordUsage
(
context
.
Background
(),
&
RecordUsageInput
{
Result
:
&
ForwardResult
{
RequestID
:
"no_effort_test"
,
Usage
:
ClaudeUsage
{
InputTokens
:
10
,
OutputTokens
:
5
,
},
Model
:
"claude-sonnet-4"
,
Duration
:
time
.
Second
,
},
APIKey
:
&
APIKey
{
ID
:
1
},
User
:
&
User
{
ID
:
1
},
Account
:
&
Account
{
ID
:
1
},
})
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
usageRepo
.
lastLog
)
require
.
Nil
(
t
,
usageRepo
.
lastLog
.
ReasoningEffort
)
}
backend/internal/service/gateway_request.go
View file @
8321e4a6
...
@@ -60,6 +60,7 @@ type ParsedRequest struct {
...
@@ -60,6 +60,7 @@ type ParsedRequest struct {
Messages
[]
any
// messages 数组
Messages
[]
any
// messages 数组
HasSystem
bool
// 是否包含 system 字段(包含 null 也视为显式传入)
HasSystem
bool
// 是否包含 system 字段(包含 null 也视为显式传入)
ThinkingEnabled
bool
// 是否开启 thinking(部分平台会影响最终模型名)
ThinkingEnabled
bool
// 是否开启 thinking(部分平台会影响最终模型名)
OutputEffort
string
// output_config.effort(Claude API 的推理强度控制)
MaxTokens
int
// max_tokens 值(用于探测请求拦截)
MaxTokens
int
// max_tokens 值(用于探测请求拦截)
SessionContext
*
SessionContext
// 可选:请求上下文区分因子(nil 时行为不变)
SessionContext
*
SessionContext
// 可选:请求上下文区分因子(nil 时行为不变)
...
@@ -116,6 +117,9 @@ func ParseGatewayRequest(body []byte, protocol string) (*ParsedRequest, error) {
...
@@ -116,6 +117,9 @@ func ParseGatewayRequest(body []byte, protocol string) (*ParsedRequest, error) {
parsed
.
ThinkingEnabled
=
true
parsed
.
ThinkingEnabled
=
true
}
}
// output_config.effort: Claude API 的推理强度控制参数
parsed
.
OutputEffort
=
strings
.
TrimSpace
(
gjson
.
Get
(
jsonStr
,
"output_config.effort"
)
.
String
())
// max_tokens: 仅接受整数值
// max_tokens: 仅接受整数值
maxTokensResult
:=
gjson
.
Get
(
jsonStr
,
"max_tokens"
)
maxTokensResult
:=
gjson
.
Get
(
jsonStr
,
"max_tokens"
)
if
maxTokensResult
.
Exists
()
&&
maxTokensResult
.
Type
==
gjson
.
Number
{
if
maxTokensResult
.
Exists
()
&&
maxTokensResult
.
Type
==
gjson
.
Number
{
...
@@ -747,6 +751,21 @@ func filterThinkingBlocksInternal(body []byte, _ bool) []byte {
...
@@ -747,6 +751,21 @@ func filterThinkingBlocksInternal(body []byte, _ bool) []byte {
return
newBody
return
newBody
}
}
// NormalizeClaudeOutputEffort normalizes Claude's output_config.effort value.
// Returns nil for empty or unrecognized values.
func
NormalizeClaudeOutputEffort
(
raw
string
)
*
string
{
value
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
raw
))
if
value
==
""
{
return
nil
}
switch
value
{
case
"low"
,
"medium"
,
"high"
,
"max"
:
return
&
value
default
:
return
nil
}
}
// =========================
// =========================
// Thinking Budget Rectifier
// Thinking Budget Rectifier
// =========================
// =========================
...
...
backend/internal/service/gateway_request_test.go
View file @
8321e4a6
...
@@ -972,6 +972,76 @@ func BenchmarkParseGatewayRequest_Old_Large(b *testing.B) {
...
@@ -972,6 +972,76 @@ func BenchmarkParseGatewayRequest_Old_Large(b *testing.B) {
}
}
}
}
func
TestParseGatewayRequest_OutputEffort
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
body
string
wantEffort
string
}{
{
name
:
"output_config.effort present"
,
body
:
`{"model":"claude-opus-4-6","output_config":{"effort":"medium"},"messages":[]}`
,
wantEffort
:
"medium"
,
},
{
name
:
"output_config.effort max"
,
body
:
`{"model":"claude-opus-4-6","output_config":{"effort":"max"},"messages":[]}`
,
wantEffort
:
"max"
,
},
{
name
:
"output_config without effort"
,
body
:
`{"model":"claude-opus-4-6","output_config":{},"messages":[]}`
,
wantEffort
:
""
,
},
{
name
:
"no output_config"
,
body
:
`{"model":"claude-opus-4-6","messages":[]}`
,
wantEffort
:
""
,
},
{
name
:
"effort with whitespace trimmed"
,
body
:
`{"model":"claude-opus-4-6","output_config":{"effort":" high "},"messages":[]}`
,
wantEffort
:
"high"
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
parsed
,
err
:=
ParseGatewayRequest
([]
byte
(
tt
.
body
),
""
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
tt
.
wantEffort
,
parsed
.
OutputEffort
)
})
}
}
func
TestNormalizeClaudeOutputEffort
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
input
string
want
*
string
}{
{
"low"
,
strPtr
(
"low"
)},
{
"medium"
,
strPtr
(
"medium"
)},
{
"high"
,
strPtr
(
"high"
)},
{
"max"
,
strPtr
(
"max"
)},
{
"LOW"
,
strPtr
(
"low"
)},
{
"Max"
,
strPtr
(
"max"
)},
{
" medium "
,
strPtr
(
"medium"
)},
{
""
,
nil
},
{
"unknown"
,
nil
},
{
"xhigh"
,
nil
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
input
,
func
(
t
*
testing
.
T
)
{
got
:=
NormalizeClaudeOutputEffort
(
tt
.
input
)
if
tt
.
want
==
nil
{
require
.
Nil
(
t
,
got
)
}
else
{
require
.
NotNil
(
t
,
got
)
require
.
Equal
(
t
,
*
tt
.
want
,
*
got
)
}
})
}
}
func
BenchmarkParseGatewayRequest_New_Large
(
b
*
testing
.
B
)
{
func
BenchmarkParseGatewayRequest_New_Large
(
b
*
testing
.
B
)
{
data
:=
buildLargeJSON
()
data
:=
buildLargeJSON
()
b
.
SetBytes
(
int64
(
len
(
data
)))
b
.
SetBytes
(
int64
(
len
(
data
)))
...
...
backend/internal/service/gateway_service.go
View file @
8321e4a6
...
@@ -492,6 +492,7 @@ type ForwardResult struct {
...
@@ -492,6 +492,7 @@ type ForwardResult struct {
Duration
time
.
Duration
Duration
time
.
Duration
FirstTokenMs
*
int
// 首字时间(流式请求)
FirstTokenMs
*
int
// 首字时间(流式请求)
ClientDisconnect
bool
// 客户端是否在流式传输过程中断开
ClientDisconnect
bool
// 客户端是否在流式传输过程中断开
ReasoningEffort
*
string
// 图片生成计费字段(图片生成模型使用)
// 图片生成计费字段(图片生成模型使用)
ImageCount
int
// 生成的图片数量
ImageCount
int
// 生成的图片数量
...
@@ -7523,6 +7524,7 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
...
@@ -7523,6 +7524,7 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
AccountID
:
account
.
ID
,
AccountID
:
account
.
ID
,
RequestID
:
requestID
,
RequestID
:
requestID
,
Model
:
result
.
Model
,
Model
:
result
.
Model
,
ReasoningEffort
:
result
.
ReasoningEffort
,
InputTokens
:
result
.
Usage
.
InputTokens
,
InputTokens
:
result
.
Usage
.
InputTokens
,
OutputTokens
:
result
.
Usage
.
OutputTokens
,
OutputTokens
:
result
.
Usage
.
OutputTokens
,
CacheCreationTokens
:
result
.
Usage
.
CacheCreationInputTokens
,
CacheCreationTokens
:
result
.
Usage
.
CacheCreationInputTokens
,
...
@@ -7699,6 +7701,7 @@ func (s *GatewayService) RecordUsageWithLongContext(ctx context.Context, input *
...
@@ -7699,6 +7701,7 @@ func (s *GatewayService) RecordUsageWithLongContext(ctx context.Context, input *
AccountID
:
account
.
ID
,
AccountID
:
account
.
ID
,
RequestID
:
requestID
,
RequestID
:
requestID
,
Model
:
result
.
Model
,
Model
:
result
.
Model
,
ReasoningEffort
:
result
.
ReasoningEffort
,
InputTokens
:
result
.
Usage
.
InputTokens
,
InputTokens
:
result
.
Usage
.
InputTokens
,
OutputTokens
:
result
.
Usage
.
OutputTokens
,
OutputTokens
:
result
.
Usage
.
OutputTokens
,
CacheCreationTokens
:
result
.
Usage
.
CacheCreationInputTokens
,
CacheCreationTokens
:
result
.
Usage
.
CacheCreationInputTokens
,
...
...
backend/internal/service/usage_log.go
View file @
8321e4a6
...
@@ -100,8 +100,9 @@ type UsageLog struct {
...
@@ -100,8 +100,9 @@ type UsageLog struct {
Model
string
Model
string
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
ServiceTier
*
string
ServiceTier
*
string
// ReasoningEffort is the request's reasoning effort level (OpenAI Responses API),
// ReasoningEffort is the request's reasoning effort level.
// e.g. "low" / "medium" / "high" / "xhigh". Nil means not provided / not applicable.
// OpenAI: "low" / "medium" / "high" / "xhigh"; Claude: "low" / "medium" / "high" / "max".
// Nil means not provided / not applicable.
ReasoningEffort
*
string
ReasoningEffort
*
string
// InboundEndpoint is the client-facing API endpoint path, e.g. /v1/chat/completions.
// InboundEndpoint is the client-facing API endpoint path, e.g. /v1/chat/completions.
InboundEndpoint
*
string
InboundEndpoint
*
string
...
...
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