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
4e894bac
Commit
4e894bac
authored
Feb 13, 2026
by
yangjianbo
Browse files
Merge branch 'test' into release
parents
ed681d08
f96acf6e
Changes
8
Hide whitespace changes
Inline
Side-by-side
backend/internal/server/middleware/logger.go
View file @
4e894bac
...
@@ -58,36 +58,6 @@ func Logger() gin.HandlerFunc {
...
@@ -58,36 +58,6 @@ func Logger() gin.HandlerFunc {
l
:=
logger
.
FromContext
(
c
.
Request
.
Context
())
.
With
(
fields
...
)
l
:=
logger
.
FromContext
(
c
.
Request
.
Context
())
.
With
(
fields
...
)
l
.
Info
(
"http request completed"
,
zap
.
Time
(
"completed_at"
,
endTime
))
l
.
Info
(
"http request completed"
,
zap
.
Time
(
"completed_at"
,
endTime
))
// 当全局日志级别高于 info(如 warn/error)时,access info 不会进入 zap core,
// 这里补写一次 sink,保证 ops 系统日志仍可索引关键访问轨迹。
if
!
logger
.
L
()
.
Core
()
.
Enabled
(
logger
.
LevelInfo
)
{
sinkFields
:=
map
[
string
]
any
{
"component"
:
"http.access"
,
"status_code"
:
statusCode
,
"latency_ms"
:
latency
.
Milliseconds
(),
"client_ip"
:
clientIP
,
"protocol"
:
protocol
,
"method"
:
method
,
"path"
:
path
,
"completed_at"
:
endTime
,
}
if
requestID
,
ok
:=
c
.
Request
.
Context
()
.
Value
(
ctxkey
.
RequestID
)
.
(
string
);
ok
&&
requestID
!=
""
{
sinkFields
[
"request_id"
]
=
requestID
}
if
clientRequestID
,
ok
:=
c
.
Request
.
Context
()
.
Value
(
ctxkey
.
ClientRequestID
)
.
(
string
);
ok
&&
clientRequestID
!=
""
{
sinkFields
[
"client_request_id"
]
=
clientRequestID
}
if
hasAccountID
&&
accountID
>
0
{
sinkFields
[
"account_id"
]
=
accountID
}
if
platform
!=
""
{
sinkFields
[
"platform"
]
=
platform
}
if
model
!=
""
{
sinkFields
[
"model"
]
=
model
}
logger
.
WriteSinkEvent
(
"info"
,
"http.access"
,
"http request completed"
,
sinkFields
)
}
if
len
(
c
.
Errors
)
>
0
{
if
len
(
c
.
Errors
)
>
0
{
l
.
Warn
(
"http request contains gin errors"
,
zap
.
String
(
"errors"
,
c
.
Errors
.
String
()))
l
.
Warn
(
"http request contains gin errors"
,
zap
.
String
(
"errors"
,
c
.
Errors
.
String
()))
...
...
backend/internal/server/middleware/request_access_logger_test.go
View file @
4e894bac
...
@@ -201,7 +201,7 @@ func TestLogger_HealthPathSkipped(t *testing.T) {
...
@@ -201,7 +201,7 @@ func TestLogger_HealthPathSkipped(t *testing.T) {
}
}
}
}
func
TestLogger_AccessLog
StillIndex
edWhenLevelWarn
(
t
*
testing
.
T
)
{
func
TestLogger_AccessLog
Dropp
edWhenLevelWarn
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
gin
.
SetMode
(
gin
.
TestMode
)
sink
:=
initMiddlewareTestLoggerWithLevel
(
t
,
"warn"
)
sink
:=
initMiddlewareTestLoggerWithLevel
(
t
,
"warn"
)
...
@@ -220,30 +220,9 @@ func TestLogger_AccessLogStillIndexedWhenLevelWarn(t *testing.T) {
...
@@ -220,30 +220,9 @@ func TestLogger_AccessLogStillIndexedWhenLevelWarn(t *testing.T) {
}
}
events
:=
sink
.
list
()
events
:=
sink
.
list
()
if
len
(
events
)
==
0
{
t
.
Fatalf
(
"expected access log event to be indexed when level=warn"
)
}
found
:=
false
for
_
,
event
:=
range
events
{
for
_
,
event
:=
range
events
{
if
event
==
nil
||
event
.
Message
!=
"http request completed"
{
if
event
!=
nil
&&
event
.
Message
==
"http request completed"
{
continue
t
.
Fatalf
(
"access log should not be indexed when level=warn: %+v"
,
event
)
}
found
=
true
if
event
.
Level
!=
"info"
{
t
.
Fatalf
(
"event level=%q, want info"
,
event
.
Level
)
}
if
event
.
Component
!=
"http.access"
&&
event
.
Fields
[
"component"
]
!=
"http.access"
{
t
.
Fatalf
(
"event component mismatch: component=%q fields=%v"
,
event
.
Component
,
event
.
Fields
[
"component"
])
}
if
_
,
ok
:=
event
.
Fields
[
"status_code"
];
!
ok
{
t
.
Fatalf
(
"status_code field missing: %+v"
,
event
.
Fields
)
}
if
_
,
ok
:=
event
.
Fields
[
"request_id"
];
!
ok
{
t
.
Fatalf
(
"request_id field missing: %+v"
,
event
.
Fields
)
}
}
}
}
if
!
found
{
t
.
Fatalf
(
"access log event not found"
)
}
}
}
backend/internal/service/error_passthrough_runtime_test.go
View file @
4e894bac
...
@@ -76,7 +76,7 @@ func TestOpenAIHandleErrorResponse_NoRuleKeepsDefault(t *testing.T) {
...
@@ -76,7 +76,7 @@ func TestOpenAIHandleErrorResponse_NoRuleKeepsDefault(t *testing.T) {
}
}
account
:=
&
Account
{
ID
:
12
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeAPIKey
}
account
:=
&
Account
{
ID
:
12
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeAPIKey
}
_
,
err
:=
svc
.
handleErrorResponse
(
context
.
Background
(),
resp
,
c
,
account
)
_
,
err
:=
svc
.
handleErrorResponse
(
context
.
Background
(),
resp
,
c
,
account
,
nil
)
require
.
Error
(
t
,
err
)
require
.
Error
(
t
,
err
)
assert
.
Equal
(
t
,
http
.
StatusBadGateway
,
rec
.
Code
)
assert
.
Equal
(
t
,
http
.
StatusBadGateway
,
rec
.
Code
)
...
@@ -157,7 +157,7 @@ func TestOpenAIHandleErrorResponse_AppliesRuleFor422(t *testing.T) {
...
@@ -157,7 +157,7 @@ func TestOpenAIHandleErrorResponse_AppliesRuleFor422(t *testing.T) {
}
}
account
:=
&
Account
{
ID
:
2
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeAPIKey
}
account
:=
&
Account
{
ID
:
2
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeAPIKey
}
_
,
err
:=
svc
.
handleErrorResponse
(
context
.
Background
(),
resp
,
c
,
account
)
_
,
err
:=
svc
.
handleErrorResponse
(
context
.
Background
(),
resp
,
c
,
account
,
nil
)
require
.
Error
(
t
,
err
)
require
.
Error
(
t
,
err
)
assert
.
Equal
(
t
,
http
.
StatusTeapot
,
rec
.
Code
)
assert
.
Equal
(
t
,
http
.
StatusTeapot
,
rec
.
Code
)
...
...
backend/internal/service/openai_gateway_service.go
View file @
4e894bac
...
@@ -35,6 +35,8 @@ const (
...
@@ -35,6 +35,8 @@ const (
openaiPlatformAPIURL
=
"https://api.openai.com/v1/responses"
openaiPlatformAPIURL
=
"https://api.openai.com/v1/responses"
openaiStickySessionTTL
=
time
.
Hour
// 粘性会话TTL
openaiStickySessionTTL
=
time
.
Hour
// 粘性会话TTL
codexCLIUserAgent
=
"codex_cli_rs/0.98.0"
codexCLIUserAgent
=
"codex_cli_rs/0.98.0"
// codex_cli_only 拒绝时单个请求头日志长度上限(字符)
codexCLIOnlyHeaderValueMaxBytes
=
256
// OpenAIParsedRequestBodyKey 缓存 handler 侧已解析的请求体,避免重复解析。
// OpenAIParsedRequestBodyKey 缓存 handler 侧已解析的请求体,避免重复解析。
OpenAIParsedRequestBodyKey
=
"openai_parsed_request_body"
OpenAIParsedRequestBodyKey
=
"openai_parsed_request_body"
...
@@ -63,6 +65,22 @@ var openaiPassthroughAllowedHeaders = map[string]bool{
...
@@ -63,6 +65,22 @@ var openaiPassthroughAllowedHeaders = map[string]bool{
"session_id"
:
true
,
"session_id"
:
true
,
}
}
// codex_cli_only 拒绝时记录的请求头白名单(仅用于诊断日志,不参与上游透传)
var
codexCLIOnlyDebugHeaderWhitelist
=
[]
string
{
"User-Agent"
,
"Content-Type"
,
"Accept"
,
"Accept-Language"
,
"OpenAI-Beta"
,
"Originator"
,
"Session_ID"
,
"Conversation_ID"
,
"X-Request-ID"
,
"X-Client-Request-ID"
,
"X-Forwarded-For"
,
"X-Real-IP"
,
}
// OpenAICodexUsageSnapshot represents Codex API usage limits from response headers
// OpenAICodexUsageSnapshot represents Codex API usage limits from response headers
type
OpenAICodexUsageSnapshot
struct
{
type
OpenAICodexUsageSnapshot
struct
{
PrimaryUsedPercent
*
float64
`json:"primary_used_percent,omitempty"`
PrimaryUsedPercent
*
float64
`json:"primary_used_percent,omitempty"`
...
@@ -269,7 +287,7 @@ func getAPIKeyIDFromContext(c *gin.Context) int64 {
...
@@ -269,7 +287,7 @@ func getAPIKeyIDFromContext(c *gin.Context) int64 {
return
apiKey
.
ID
return
apiKey
.
ID
}
}
func
logCodexCLIOnlyDetection
(
ctx
context
.
Context
,
account
*
Account
,
apiKeyID
int64
,
result
CodexClientRestrictionDetectionResult
)
{
func
logCodexCLIOnlyDetection
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
account
*
Account
,
apiKeyID
int64
,
result
CodexClientRestrictionDetectionResult
,
body
[]
byte
)
{
if
!
result
.
Enabled
{
if
!
result
.
Enabled
{
return
return
}
}
...
@@ -290,14 +308,171 @@ func logCodexCLIOnlyDetection(ctx context.Context, account *Account, apiKeyID in
...
@@ -290,14 +308,171 @@ func logCodexCLIOnlyDetection(ctx context.Context, account *Account, apiKeyID in
if
apiKeyID
>
0
{
if
apiKeyID
>
0
{
fields
=
append
(
fields
,
zap
.
Int64
(
"api_key_id"
,
apiKeyID
))
fields
=
append
(
fields
,
zap
.
Int64
(
"api_key_id"
,
apiKeyID
))
}
}
if
!
result
.
Matched
{
fields
=
appendCodexCLIOnlyRejectedRequestFields
(
fields
,
c
,
body
)
}
log
:=
logger
.
FromContext
(
ctx
)
.
With
(
fields
...
)
log
:=
logger
.
FromContext
(
ctx
)
.
With
(
fields
...
)
if
result
.
Matched
{
if
result
.
Matched
{
log
.
Info
(
"OpenAI codex_cli_only
检测通过
"
)
log
.
Warn
(
"OpenAI codex_cli_only
允许官方客户端请求
"
)
return
return
}
}
log
.
Warn
(
"OpenAI codex_cli_only 拒绝非官方客户端请求"
)
log
.
Warn
(
"OpenAI codex_cli_only 拒绝非官方客户端请求"
)
}
}
func
appendCodexCLIOnlyRejectedRequestFields
(
fields
[]
zap
.
Field
,
c
*
gin
.
Context
,
body
[]
byte
)
[]
zap
.
Field
{
if
c
==
nil
||
c
.
Request
==
nil
{
return
fields
}
req
:=
c
.
Request
requestModel
,
requestStream
,
promptCacheKey
:=
extractOpenAIRequestMetaFromBody
(
body
)
fields
=
append
(
fields
,
zap
.
String
(
"request_method"
,
strings
.
TrimSpace
(
req
.
Method
)),
zap
.
String
(
"request_path"
,
strings
.
TrimSpace
(
req
.
URL
.
Path
)),
zap
.
String
(
"request_query"
,
strings
.
TrimSpace
(
req
.
URL
.
RawQuery
)),
zap
.
String
(
"request_host"
,
strings
.
TrimSpace
(
req
.
Host
)),
zap
.
String
(
"request_client_ip"
,
strings
.
TrimSpace
(
c
.
ClientIP
())),
zap
.
String
(
"request_remote_addr"
,
strings
.
TrimSpace
(
req
.
RemoteAddr
)),
zap
.
String
(
"request_user_agent"
,
strings
.
TrimSpace
(
req
.
Header
.
Get
(
"User-Agent"
))),
zap
.
String
(
"request_content_type"
,
strings
.
TrimSpace
(
req
.
Header
.
Get
(
"Content-Type"
))),
zap
.
Int64
(
"request_content_length"
,
req
.
ContentLength
),
zap
.
Bool
(
"request_stream"
,
requestStream
),
)
if
requestModel
!=
""
{
fields
=
append
(
fields
,
zap
.
String
(
"request_model"
,
requestModel
))
}
if
promptCacheKey
!=
""
{
fields
=
append
(
fields
,
zap
.
String
(
"request_prompt_cache_key_sha256"
,
hashSensitiveValueForLog
(
promptCacheKey
)))
}
if
headers
:=
snapshotCodexCLIOnlyHeaders
(
req
.
Header
);
len
(
headers
)
>
0
{
fields
=
append
(
fields
,
zap
.
Any
(
"request_headers"
,
headers
))
}
fields
=
append
(
fields
,
zap
.
Int
(
"request_body_size"
,
len
(
body
)))
return
fields
}
func
snapshotCodexCLIOnlyHeaders
(
header
http
.
Header
)
map
[
string
]
string
{
if
len
(
header
)
==
0
{
return
nil
}
result
:=
make
(
map
[
string
]
string
,
len
(
codexCLIOnlyDebugHeaderWhitelist
))
for
_
,
key
:=
range
codexCLIOnlyDebugHeaderWhitelist
{
value
:=
strings
.
TrimSpace
(
header
.
Get
(
key
))
if
value
==
""
{
continue
}
result
[
strings
.
ToLower
(
key
)]
=
truncateString
(
value
,
codexCLIOnlyHeaderValueMaxBytes
)
}
return
result
}
func
hashSensitiveValueForLog
(
raw
string
)
string
{
value
:=
strings
.
TrimSpace
(
raw
)
if
value
==
""
{
return
""
}
sum
:=
sha256
.
Sum256
([]
byte
(
value
))
return
hex
.
EncodeToString
(
sum
[
:
8
])
}
func
logOpenAIInstructionsRequiredDebug
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
account
*
Account
,
upstreamStatusCode
int
,
upstreamMsg
string
,
requestBody
[]
byte
,
upstreamBody
[]
byte
,
)
{
msg
:=
strings
.
TrimSpace
(
upstreamMsg
)
if
!
isOpenAIInstructionsRequiredError
(
upstreamStatusCode
,
msg
,
upstreamBody
)
{
return
}
if
ctx
==
nil
{
ctx
=
context
.
Background
()
}
accountID
:=
int64
(
0
)
accountName
:=
""
if
account
!=
nil
{
accountID
=
account
.
ID
accountName
=
strings
.
TrimSpace
(
account
.
Name
)
}
userAgent
:=
""
if
c
!=
nil
{
userAgent
=
strings
.
TrimSpace
(
c
.
GetHeader
(
"User-Agent"
))
}
fields
:=
[]
zap
.
Field
{
zap
.
String
(
"component"
,
"service.openai_gateway"
),
zap
.
Int64
(
"account_id"
,
accountID
),
zap
.
String
(
"account_name"
,
accountName
),
zap
.
Int
(
"upstream_status_code"
,
upstreamStatusCode
),
zap
.
String
(
"upstream_error_message"
,
msg
),
zap
.
String
(
"request_user_agent"
,
userAgent
),
zap
.
Bool
(
"codex_official_client_match"
,
openai
.
IsCodexCLIRequest
(
userAgent
)),
}
fields
=
appendCodexCLIOnlyRejectedRequestFields
(
fields
,
c
,
requestBody
)
logger
.
FromContext
(
ctx
)
.
With
(
fields
...
)
.
Warn
(
"OpenAI 上游返回 Instructions are required,已记录请求详情用于排查"
)
}
func
isOpenAIInstructionsRequiredError
(
upstreamStatusCode
int
,
upstreamMsg
string
,
upstreamBody
[]
byte
)
bool
{
if
upstreamStatusCode
!=
http
.
StatusBadRequest
{
return
false
}
hasInstructionRequired
:=
func
(
text
string
)
bool
{
lower
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
text
))
if
lower
==
""
{
return
false
}
if
strings
.
Contains
(
lower
,
"instructions are required"
)
{
return
true
}
if
strings
.
Contains
(
lower
,
"required parameter: 'instructions'"
)
{
return
true
}
if
strings
.
Contains
(
lower
,
"required parameter: instructions"
)
{
return
true
}
if
strings
.
Contains
(
lower
,
"missing required parameter"
)
&&
strings
.
Contains
(
lower
,
"instructions"
)
{
return
true
}
return
strings
.
Contains
(
lower
,
"instruction"
)
&&
strings
.
Contains
(
lower
,
"required"
)
}
if
hasInstructionRequired
(
upstreamMsg
)
{
return
true
}
if
len
(
upstreamBody
)
==
0
{
return
false
}
errMsg
:=
gjson
.
GetBytes
(
upstreamBody
,
"error.message"
)
.
String
()
errMsgLower
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
errMsg
))
errCode
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
gjson
.
GetBytes
(
upstreamBody
,
"error.code"
)
.
String
()))
errParam
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
gjson
.
GetBytes
(
upstreamBody
,
"error.param"
)
.
String
()))
errType
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
gjson
.
GetBytes
(
upstreamBody
,
"error.type"
)
.
String
()))
if
errParam
==
"instructions"
{
return
true
}
if
hasInstructionRequired
(
errMsg
)
{
return
true
}
if
strings
.
Contains
(
errCode
,
"missing_required_parameter"
)
&&
strings
.
Contains
(
errMsgLower
,
"instructions"
)
{
return
true
}
if
strings
.
Contains
(
errType
,
"invalid_request"
)
&&
strings
.
Contains
(
errMsgLower
,
"instructions"
)
&&
strings
.
Contains
(
errMsgLower
,
"required"
)
{
return
true
}
return
false
}
// GenerateSessionHash generates a sticky-session hash for OpenAI requests.
// GenerateSessionHash generates a sticky-session hash for OpenAI requests.
//
//
// Priority:
// Priority:
...
@@ -820,7 +995,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
...
@@ -820,7 +995,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
restrictionResult
:=
s
.
detectCodexClientRestriction
(
c
,
account
)
restrictionResult
:=
s
.
detectCodexClientRestriction
(
c
,
account
)
apiKeyID
:=
getAPIKeyIDFromContext
(
c
)
apiKeyID
:=
getAPIKeyIDFromContext
(
c
)
logCodexCLIOnlyDetection
(
ctx
,
account
,
apiKeyID
,
restrictionResult
)
logCodexCLIOnlyDetection
(
ctx
,
c
,
account
,
apiKeyID
,
restrictionResult
,
body
)
if
restrictionResult
.
Enabled
&&
!
restrictionResult
.
Matched
{
if
restrictionResult
.
Enabled
&&
!
restrictionResult
.
Matched
{
c
.
JSON
(
http
.
StatusForbidden
,
gin
.
H
{
c
.
JSON
(
http
.
StatusForbidden
,
gin
.
H
{
"error"
:
gin
.
H
{
"error"
:
gin
.
H
{
...
@@ -1047,7 +1222,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
...
@@ -1047,7 +1222,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
s
.
handleFailoverSideEffects
(
ctx
,
resp
,
account
)
s
.
handleFailoverSideEffects
(
ctx
,
resp
,
account
)
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
,
ResponseBody
:
respBody
}
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
,
ResponseBody
:
respBody
}
}
}
return
s
.
handleErrorResponse
(
ctx
,
resp
,
c
,
account
)
return
s
.
handleErrorResponse
(
ctx
,
resp
,
c
,
account
,
body
)
}
}
// Handle normal response
// Handle normal response
...
@@ -1183,7 +1358,7 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
...
@@ -1183,7 +1358,7 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
if
resp
.
StatusCode
>=
400
{
if
resp
.
StatusCode
>=
400
{
// 透传模式不做 failover(避免改变原始上游语义),按上游原样返回错误响应。
// 透传模式不做 failover(避免改变原始上游语义),按上游原样返回错误响应。
return
nil
,
s
.
handleErrorResponsePassthrough
(
ctx
,
resp
,
c
,
account
)
return
nil
,
s
.
handleErrorResponsePassthrough
(
ctx
,
resp
,
c
,
account
,
body
)
}
}
var
usage
*
OpenAIUsage
var
usage
*
OpenAIUsage
...
@@ -1314,7 +1489,13 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
...
@@ -1314,7 +1489,13 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
return
req
,
nil
return
req
,
nil
}
}
func
(
s
*
OpenAIGatewayService
)
handleErrorResponsePassthrough
(
ctx
context
.
Context
,
resp
*
http
.
Response
,
c
*
gin
.
Context
,
account
*
Account
)
error
{
func
(
s
*
OpenAIGatewayService
)
handleErrorResponsePassthrough
(
ctx
context
.
Context
,
resp
*
http
.
Response
,
c
*
gin
.
Context
,
account
*
Account
,
requestBody
[]
byte
,
)
error
{
body
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
resp
.
Body
,
2
<<
20
))
body
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
resp
.
Body
,
2
<<
20
))
upstreamMsg
:=
strings
.
TrimSpace
(
extractUpstreamErrorMessage
(
body
))
upstreamMsg
:=
strings
.
TrimSpace
(
extractUpstreamErrorMessage
(
body
))
...
@@ -1328,6 +1509,7 @@ func (s *OpenAIGatewayService) handleErrorResponsePassthrough(ctx context.Contex
...
@@ -1328,6 +1509,7 @@ func (s *OpenAIGatewayService) handleErrorResponsePassthrough(ctx context.Contex
upstreamDetail
=
truncateString
(
string
(
body
),
maxBytes
)
upstreamDetail
=
truncateString
(
string
(
body
),
maxBytes
)
}
}
setOpsUpstreamError
(
c
,
resp
.
StatusCode
,
upstreamMsg
,
upstreamDetail
)
setOpsUpstreamError
(
c
,
resp
.
StatusCode
,
upstreamMsg
,
upstreamDetail
)
logOpenAIInstructionsRequiredDebug
(
ctx
,
c
,
account
,
resp
.
StatusCode
,
upstreamMsg
,
requestBody
,
body
)
appendOpsUpstreamError
(
c
,
OpsUpstreamErrorEvent
{
appendOpsUpstreamError
(
c
,
OpsUpstreamErrorEvent
{
Platform
:
account
.
Platform
,
Platform
:
account
.
Platform
,
AccountID
:
account
.
ID
,
AccountID
:
account
.
ID
,
...
@@ -1470,7 +1652,7 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
...
@@ -1470,7 +1652,7 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
}
}
if
errors
.
Is
(
err
,
context
.
Canceled
)
||
errors
.
Is
(
err
,
context
.
DeadlineExceeded
)
{
if
errors
.
Is
(
err
,
context
.
Canceled
)
||
errors
.
Is
(
err
,
context
.
DeadlineExceeded
)
{
logger
.
LegacyPrintf
(
"service.openai_gateway"
,
logger
.
LegacyPrintf
(
"service.openai_gateway"
,
"
[WARN]
[OpenAI passthrough] 流读取被取消,可能发生断流: account=%d request_id=%s err=%v ctx_err=%v"
,
"[OpenAI passthrough] 流读取被取消,可能发生断流: account=%d request_id=%s err=%v ctx_err=%v"
,
account
.
ID
,
account
.
ID
,
upstreamRequestID
,
upstreamRequestID
,
err
,
err
,
...
@@ -1483,7 +1665,7 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
...
@@ -1483,7 +1665,7 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
return
&
openaiStreamingResultPassthrough
{
usage
:
usage
,
firstTokenMs
:
firstTokenMs
},
err
return
&
openaiStreamingResultPassthrough
{
usage
:
usage
,
firstTokenMs
:
firstTokenMs
},
err
}
}
logger
.
LegacyPrintf
(
"service.openai_gateway"
,
logger
.
LegacyPrintf
(
"service.openai_gateway"
,
"
[WARN]
[OpenAI passthrough] 流读取异常中断: account=%d request_id=%s err=%v"
,
"[OpenAI passthrough] 流读取异常中断: account=%d request_id=%s err=%v"
,
account
.
ID
,
account
.
ID
,
upstreamRequestID
,
upstreamRequestID
,
err
,
err
,
...
@@ -1495,7 +1677,7 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
...
@@ -1495,7 +1677,7 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
zap
.
String
(
"component"
,
"service.openai_gateway"
),
zap
.
String
(
"component"
,
"service.openai_gateway"
),
zap
.
Int64
(
"account_id"
,
account
.
ID
),
zap
.
Int64
(
"account_id"
,
account
.
ID
),
zap
.
String
(
"upstream_request_id"
,
upstreamRequestID
),
zap
.
String
(
"upstream_request_id"
,
upstreamRequestID
),
)
.
Warn
(
"OpenAI passthrough 上游流在未收到 [DONE] 时结束,疑似断流"
)
)
.
Info
(
"OpenAI passthrough 上游流在未收到 [DONE] 时结束,疑似断流"
)
}
}
return
&
openaiStreamingResultPassthrough
{
usage
:
usage
,
firstTokenMs
:
firstTokenMs
},
nil
return
&
openaiStreamingResultPassthrough
{
usage
:
usage
,
firstTokenMs
:
firstTokenMs
},
nil
...
@@ -1678,7 +1860,13 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
...
@@ -1678,7 +1860,13 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
return
req
,
nil
return
req
,
nil
}
}
func
(
s
*
OpenAIGatewayService
)
handleErrorResponse
(
ctx
context
.
Context
,
resp
*
http
.
Response
,
c
*
gin
.
Context
,
account
*
Account
)
(
*
OpenAIForwardResult
,
error
)
{
func
(
s
*
OpenAIGatewayService
)
handleErrorResponse
(
ctx
context
.
Context
,
resp
*
http
.
Response
,
c
*
gin
.
Context
,
account
*
Account
,
requestBody
[]
byte
,
)
(
*
OpenAIForwardResult
,
error
)
{
body
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
resp
.
Body
,
2
<<
20
))
body
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
resp
.
Body
,
2
<<
20
))
upstreamMsg
:=
strings
.
TrimSpace
(
extractUpstreamErrorMessage
(
body
))
upstreamMsg
:=
strings
.
TrimSpace
(
extractUpstreamErrorMessage
(
body
))
...
@@ -1692,6 +1880,7 @@ func (s *OpenAIGatewayService) handleErrorResponse(ctx context.Context, resp *ht
...
@@ -1692,6 +1880,7 @@ func (s *OpenAIGatewayService) handleErrorResponse(ctx context.Context, resp *ht
upstreamDetail
=
truncateString
(
string
(
body
),
maxBytes
)
upstreamDetail
=
truncateString
(
string
(
body
),
maxBytes
)
}
}
setOpsUpstreamError
(
c
,
resp
.
StatusCode
,
upstreamMsg
,
upstreamDetail
)
setOpsUpstreamError
(
c
,
resp
.
StatusCode
,
upstreamMsg
,
upstreamDetail
)
logOpenAIInstructionsRequiredDebug
(
ctx
,
c
,
account
,
resp
.
StatusCode
,
upstreamMsg
,
requestBody
,
body
)
if
s
.
cfg
!=
nil
&&
s
.
cfg
.
Gateway
.
LogUpstreamErrorBody
{
if
s
.
cfg
!=
nil
&&
s
.
cfg
.
Gateway
.
LogUpstreamErrorBody
{
logger
.
LegacyPrintf
(
"service.openai_gateway"
,
logger
.
LegacyPrintf
(
"service.openai_gateway"
,
...
...
backend/internal/service/openai_gateway_service_codex_cli_only_test.go
View file @
4e894bac
package
service
package
service
import
(
import
(
"bytes"
"context"
"context"
"io"
"net/http"
"net/http"
"net/http/httptest"
"net/http/httptest"
"strings"
"testing"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/config"
...
@@ -95,7 +98,169 @@ func TestGetAPIKeyIDFromContext(t *testing.T) {
...
@@ -95,7 +98,169 @@ func TestGetAPIKeyIDFromContext(t *testing.T) {
func
TestLogCodexCLIOnlyDetection_NilSafety
(
t
*
testing
.
T
)
{
func
TestLogCodexCLIOnlyDetection_NilSafety
(
t
*
testing
.
T
)
{
// 不校验日志内容,仅保证在 nil 入参下不会 panic。
// 不校验日志内容,仅保证在 nil 入参下不会 panic。
require
.
NotPanics
(
t
,
func
()
{
require
.
NotPanics
(
t
,
func
()
{
logCodexCLIOnlyDetection
(
context
.
TODO
(),
nil
,
0
,
CodexClientRestrictionDetectionResult
{
Enabled
:
true
,
Matched
:
false
,
Reason
:
"test"
})
logCodexCLIOnlyDetection
(
context
.
TODO
(),
nil
,
nil
,
0
,
CodexClientRestrictionDetectionResult
{
Enabled
:
true
,
Matched
:
false
,
Reason
:
"test"
}
,
nil
)
logCodexCLIOnlyDetection
(
context
.
Background
(),
nil
,
0
,
CodexClientRestrictionDetectionResult
{
Enabled
:
false
,
Matched
:
false
,
Reason
:
"disabled"
})
logCodexCLIOnlyDetection
(
context
.
Background
(),
nil
,
nil
,
0
,
CodexClientRestrictionDetectionResult
{
Enabled
:
false
,
Matched
:
false
,
Reason
:
"disabled"
}
,
nil
)
})
})
}
}
func
TestLogCodexCLIOnlyDetection_LogsBothMatchedAndRejected
(
t
*
testing
.
T
)
{
logSink
,
restore
:=
captureStructuredLog
(
t
)
defer
restore
()
account
:=
&
Account
{
ID
:
1001
}
logCodexCLIOnlyDetection
(
context
.
Background
(),
nil
,
account
,
2002
,
CodexClientRestrictionDetectionResult
{
Enabled
:
true
,
Matched
:
true
,
Reason
:
CodexClientRestrictionReasonMatchedUA
,
},
nil
)
logCodexCLIOnlyDetection
(
context
.
Background
(),
nil
,
account
,
2002
,
CodexClientRestrictionDetectionResult
{
Enabled
:
true
,
Matched
:
false
,
Reason
:
CodexClientRestrictionReasonNotMatchedUA
,
},
nil
)
require
.
True
(
t
,
logSink
.
ContainsMessage
(
"OpenAI codex_cli_only 允许官方客户端请求"
))
require
.
True
(
t
,
logSink
.
ContainsMessage
(
"OpenAI codex_cli_only 拒绝非官方客户端请求"
))
}
func
TestLogCodexCLIOnlyDetection_RejectedIncludesRequestDetails
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
logSink
,
restore
:=
captureStructuredLog
(
t
)
defer
restore
()
rec
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
rec
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/v1/responses?trace=1"
,
bytes
.
NewReader
(
nil
))
c
.
Request
.
Header
.
Set
(
"User-Agent"
,
"curl/8.0"
)
c
.
Request
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
c
.
Request
.
Header
.
Set
(
"OpenAI-Beta"
,
"assistants=v2"
)
body
:=
[]
byte
(
`{"model":"gpt-5.2","stream":false,"prompt_cache_key":"pc-123","access_token":"secret-token","input":[{"type":"text","text":"hello"}]}`
)
account
:=
&
Account
{
ID
:
1001
}
logCodexCLIOnlyDetection
(
context
.
Background
(),
c
,
account
,
2002
,
CodexClientRestrictionDetectionResult
{
Enabled
:
true
,
Matched
:
false
,
Reason
:
CodexClientRestrictionReasonNotMatchedUA
,
},
body
)
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_user_agent"
,
"curl/8.0"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_model"
,
"gpt-5.2"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_query"
,
"trace=1"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_prompt_cache_key_sha256"
,
hashSensitiveValueForLog
(
"pc-123"
)))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_headers"
,
"openai-beta"
))
require
.
True
(
t
,
logSink
.
ContainsField
(
"request_body_size"
))
require
.
False
(
t
,
logSink
.
ContainsField
(
"request_body_preview"
))
}
func
TestLogOpenAIInstructionsRequiredDebug_LogsRequestDetails
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
logSink
,
restore
:=
captureStructuredLog
(
t
)
defer
restore
()
rec
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
rec
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/v1/responses?trace=1"
,
bytes
.
NewReader
(
nil
))
c
.
Request
.
Header
.
Set
(
"User-Agent"
,
"curl/8.0"
)
c
.
Request
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
c
.
Request
.
Header
.
Set
(
"OpenAI-Beta"
,
"assistants=v2"
)
body
:=
[]
byte
(
`{"model":"gpt-5.1-codex","stream":false,"prompt_cache_key":"pc-abc","access_token":"secret-token","input":[{"type":"text","text":"hello"}]}`
)
account
:=
&
Account
{
ID
:
1001
,
Name
:
"codex max套餐"
}
logOpenAIInstructionsRequiredDebug
(
context
.
Background
(),
c
,
account
,
http
.
StatusBadRequest
,
"Instructions are required"
,
body
,
[]
byte
(
`{"error":{"message":"Instructions are required","type":"invalid_request_error","param":"instructions","code":"missing_required_parameter"}}`
),
)
require
.
True
(
t
,
logSink
.
ContainsMessageAtLevel
(
"OpenAI 上游返回 Instructions are required,已记录请求详情用于排查"
,
"warn"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_user_agent"
,
"curl/8.0"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_model"
,
"gpt-5.1-codex"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_query"
,
"trace=1"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"account_name"
,
"codex max套餐"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_headers"
,
"openai-beta"
))
require
.
True
(
t
,
logSink
.
ContainsField
(
"request_body_size"
))
require
.
False
(
t
,
logSink
.
ContainsField
(
"request_body_preview"
))
}
func
TestLogOpenAIInstructionsRequiredDebug_NonTargetErrorSkipped
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
logSink
,
restore
:=
captureStructuredLog
(
t
)
defer
restore
()
rec
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
rec
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/v1/responses"
,
bytes
.
NewReader
(
nil
))
c
.
Request
.
Header
.
Set
(
"User-Agent"
,
"curl/8.0"
)
body
:=
[]
byte
(
`{"model":"gpt-5.1-codex","stream":false}`
)
logOpenAIInstructionsRequiredDebug
(
context
.
Background
(),
c
,
&
Account
{
ID
:
1001
},
http
.
StatusForbidden
,
"forbidden"
,
body
,
[]
byte
(
`{"error":{"message":"forbidden"}}`
),
)
require
.
False
(
t
,
logSink
.
ContainsMessage
(
"OpenAI 上游返回 Instructions are required,已记录请求详情用于排查"
))
}
func
TestOpenAIGatewayService_Forward_LogsInstructionsRequiredDetails
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
logSink
,
restore
:=
captureStructuredLog
(
t
)
defer
restore
()
rec
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
rec
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/v1/responses?trace=1"
,
bytes
.
NewReader
(
nil
))
c
.
Request
.
Header
.
Set
(
"User-Agent"
,
"codex_cli_rs/0.1.0"
)
c
.
Request
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
c
.
Request
.
Header
.
Set
(
"OpenAI-Beta"
,
"assistants=v2"
)
upstream
:=
&
httpUpstreamRecorder
{
resp
:
&
http
.
Response
{
StatusCode
:
http
.
StatusBadRequest
,
Header
:
http
.
Header
{
"Content-Type"
:
[]
string
{
"application/json"
},
"x-request-id"
:
[]
string
{
"rid-upstream"
},
},
Body
:
io
.
NopCloser
(
strings
.
NewReader
(
`{"error":{"message":"Missing required parameter: 'instructions'","type":"invalid_request_error","param":"instructions","code":"missing_required_parameter"}}`
)),
},
}
svc
:=
&
OpenAIGatewayService
{
cfg
:
&
config
.
Config
{
Gateway
:
config
.
GatewayConfig
{
ForceCodexCLI
:
false
},
},
httpUpstream
:
upstream
,
}
account
:=
&
Account
{
ID
:
1001
,
Name
:
"codex max套餐"
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeAPIKey
,
Concurrency
:
1
,
Credentials
:
map
[
string
]
any
{
"api_key"
:
"sk-test"
},
Status
:
StatusActive
,
Schedulable
:
true
,
RateMultiplier
:
f64p
(
1
),
}
body
:=
[]
byte
(
`{"model":"gpt-5.1-codex","stream":false,"input":[{"type":"text","text":"hello"}],"prompt_cache_key":"pc-forward","access_token":"secret-token"}`
)
_
,
err
:=
svc
.
Forward
(
context
.
Background
(),
c
,
account
,
body
)
require
.
Error
(
t
,
err
)
require
.
Equal
(
t
,
http
.
StatusBadGateway
,
rec
.
Code
)
require
.
Contains
(
t
,
err
.
Error
(),
"upstream error: 400"
)
require
.
True
(
t
,
logSink
.
ContainsMessageAtLevel
(
"OpenAI 上游返回 Instructions are required,已记录请求详情用于排查"
,
"warn"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_user_agent"
,
"codex_cli_rs/0.1.0"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_model"
,
"gpt-5.1-codex"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_headers"
,
"openai-beta"
))
require
.
True
(
t
,
logSink
.
ContainsField
(
"request_body_size"
))
require
.
False
(
t
,
logSink
.
ContainsField
(
"request_body_preview"
))
}
backend/internal/service/openai_oauth_passthrough_test.go
View file @
4e894bac
...
@@ -81,6 +81,21 @@ func (s *inMemoryLogSink) ContainsMessage(substr string) bool {
...
@@ -81,6 +81,21 @@ func (s *inMemoryLogSink) ContainsMessage(substr string) bool {
return
false
return
false
}
}
func
(
s
*
inMemoryLogSink
)
ContainsMessageAtLevel
(
substr
,
level
string
)
bool
{
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
wantLevel
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
level
))
for
_
,
ev
:=
range
s
.
events
{
if
ev
==
nil
{
continue
}
if
strings
.
Contains
(
ev
.
Message
,
substr
)
&&
strings
.
ToLower
(
strings
.
TrimSpace
(
ev
.
Level
))
==
wantLevel
{
return
true
}
}
return
false
}
func
(
s
*
inMemoryLogSink
)
ContainsFieldValue
(
field
,
substr
string
)
bool
{
func
(
s
*
inMemoryLogSink
)
ContainsFieldValue
(
field
,
substr
string
)
bool
{
s
.
mu
.
Lock
()
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
defer
s
.
mu
.
Unlock
()
...
@@ -95,6 +110,20 @@ func (s *inMemoryLogSink) ContainsFieldValue(field, substr string) bool {
...
@@ -95,6 +110,20 @@ func (s *inMemoryLogSink) ContainsFieldValue(field, substr string) bool {
return
false
return
false
}
}
func
(
s
*
inMemoryLogSink
)
ContainsField
(
field
string
)
bool
{
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
for
_
,
ev
:=
range
s
.
events
{
if
ev
==
nil
||
ev
.
Fields
==
nil
{
continue
}
if
_
,
ok
:=
ev
.
Fields
[
field
];
ok
{
return
true
}
}
return
false
}
func
captureStructuredLog
(
t
*
testing
.
T
)
(
*
inMemoryLogSink
,
func
())
{
func
captureStructuredLog
(
t
*
testing
.
T
)
(
*
inMemoryLogSink
,
func
())
{
t
.
Helper
()
t
.
Helper
()
structuredLogCaptureMu
.
Lock
()
structuredLogCaptureMu
.
Lock
()
...
@@ -712,7 +741,7 @@ func TestOpenAIGatewayService_OAuthPassthrough_WarnOnTimeoutHeadersForStream(t *
...
@@ -712,7 +741,7 @@ func TestOpenAIGatewayService_OAuthPassthrough_WarnOnTimeoutHeadersForStream(t *
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"timeout_headers"
,
"x-stainless-timeout=10000"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"timeout_headers"
,
"x-stainless-timeout=10000"
))
}
}
func
TestOpenAIGatewayService_OAuthPassthrough_
Warn
WhenStreamEndsWithoutDone
(
t
*
testing
.
T
)
{
func
TestOpenAIGatewayService_OAuthPassthrough_
Info
WhenStreamEndsWithoutDone
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
gin
.
SetMode
(
gin
.
TestMode
)
logSink
,
restore
:=
captureStructuredLog
(
t
)
logSink
,
restore
:=
captureStructuredLog
(
t
)
defer
restore
()
defer
restore
()
...
@@ -750,6 +779,7 @@ func TestOpenAIGatewayService_OAuthPassthrough_WarnWhenStreamEndsWithoutDone(t *
...
@@ -750,6 +779,7 @@ func TestOpenAIGatewayService_OAuthPassthrough_WarnWhenStreamEndsWithoutDone(t *
_
,
err
:=
svc
.
Forward
(
context
.
Background
(),
c
,
account
,
originalBody
)
_
,
err
:=
svc
.
Forward
(
context
.
Background
(),
c
,
account
,
originalBody
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
True
(
t
,
logSink
.
ContainsMessage
(
"上游流在未收到 [DONE] 时结束,疑似断流"
))
require
.
True
(
t
,
logSink
.
ContainsMessage
(
"上游流在未收到 [DONE] 时结束,疑似断流"
))
require
.
True
(
t
,
logSink
.
ContainsMessageAtLevel
(
"上游流在未收到 [DONE] 时结束,疑似断流"
,
"info"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"upstream_request_id"
,
"rid-truncate"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"upstream_request_id"
,
"rid-truncate"
))
}
}
...
...
backend/internal/service/pricing_service.go
View file @
4e894bac
...
@@ -17,6 +17,7 @@ import (
...
@@ -17,6 +17,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
"go.uber.org/zap"
)
)
var
(
var
(
...
@@ -659,7 +660,8 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
...
@@ -659,7 +660,8 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
if
strings
.
HasPrefix
(
model
,
"gpt-5.3-codex-spark"
)
{
if
strings
.
HasPrefix
(
model
,
"gpt-5.3-codex-spark"
)
{
if
pricing
,
ok
:=
s
.
pricingData
[
"gpt-5.1-codex"
];
ok
{
if
pricing
,
ok
:=
s
.
pricingData
[
"gpt-5.1-codex"
];
ok
{
logger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing][SparkBilling] %s -> %s billing"
,
model
,
"gpt-5.1-codex"
)
logger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing][SparkBilling] %s -> %s billing"
,
model
,
"gpt-5.1-codex"
)
logger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
"gpt-5.1-codex"
)
logger
.
With
(
zap
.
String
(
"component"
,
"service.pricing"
))
.
Info
(
fmt
.
Sprintf
(
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
"gpt-5.1-codex"
))
return
pricing
return
pricing
}
}
}
}
...
@@ -669,14 +671,16 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
...
@@ -669,14 +671,16 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
for
_
,
variant
:=
range
variants
{
for
_
,
variant
:=
range
variants
{
if
pricing
,
ok
:=
s
.
pricingData
[
variant
];
ok
{
if
pricing
,
ok
:=
s
.
pricingData
[
variant
];
ok
{
logger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
variant
)
logger
.
With
(
zap
.
String
(
"component"
,
"service.pricing"
))
.
Info
(
fmt
.
Sprintf
(
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
variant
))
return
pricing
return
pricing
}
}
}
}
if
strings
.
HasPrefix
(
model
,
"gpt-5.3-codex"
)
{
if
strings
.
HasPrefix
(
model
,
"gpt-5.3-codex"
)
{
if
pricing
,
ok
:=
s
.
pricingData
[
"gpt-5.2-codex"
];
ok
{
if
pricing
,
ok
:=
s
.
pricingData
[
"gpt-5.2-codex"
];
ok
{
logger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
"gpt-5.2-codex"
)
logger
.
With
(
zap
.
String
(
"component"
,
"service.pricing"
))
.
Info
(
fmt
.
Sprintf
(
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
"gpt-5.2-codex"
))
return
pricing
return
pricing
}
}
}
}
...
...
backend/internal/service/pricing_service_test.go
View file @
4e894bac
...
@@ -33,3 +33,21 @@ func TestGetModelPricing_Gpt53CodexFallbackStillUsesGpt52Codex(t *testing.T) {
...
@@ -33,3 +33,21 @@ func TestGetModelPricing_Gpt53CodexFallbackStillUsesGpt52Codex(t *testing.T) {
got
:=
svc
.
GetModelPricing
(
"gpt-5.3-codex"
)
got
:=
svc
.
GetModelPricing
(
"gpt-5.3-codex"
)
require
.
Same
(
t
,
gpt52CodexPricing
,
got
)
require
.
Same
(
t
,
gpt52CodexPricing
,
got
)
}
}
func
TestGetModelPricing_OpenAIFallbackMatchedLoggedAsInfo
(
t
*
testing
.
T
)
{
logSink
,
restore
:=
captureStructuredLog
(
t
)
defer
restore
()
gpt52CodexPricing
:=
&
LiteLLMModelPricing
{
InputCostPerToken
:
2
}
svc
:=
&
PricingService
{
pricingData
:
map
[
string
]
*
LiteLLMModelPricing
{
"gpt-5.2-codex"
:
gpt52CodexPricing
,
},
}
got
:=
svc
.
GetModelPricing
(
"gpt-5.3-codex"
)
require
.
Same
(
t
,
gpt52CodexPricing
,
got
)
require
.
True
(
t
,
logSink
.
ContainsMessageAtLevel
(
"[Pricing] OpenAI fallback matched gpt-5.3-codex -> gpt-5.2-codex"
,
"info"
))
require
.
False
(
t
,
logSink
.
ContainsMessageAtLevel
(
"[Pricing] OpenAI fallback matched gpt-5.3-codex -> gpt-5.2-codex"
,
"warn"
))
}
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