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
57e8abcb
Commit
57e8abcb
authored
Feb 14, 2026
by
yangjianbo
Browse files
fix(openai): 自动透传预检 instructions 并本地 403 拦截
parent
ed31c549
Changes
2
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/openai_gateway_service.go
View file @
57e8abcb
...
...
@@ -1276,6 +1276,29 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
startTime
time
.
Time
,
)
(
*
OpenAIForwardResult
,
error
)
{
if
account
!=
nil
&&
account
.
Type
==
AccountTypeOAuth
{
if
rejectReason
:=
detectOpenAIPassthroughInstructionsRejectReason
(
reqModel
,
body
);
rejectReason
!=
""
{
rejectMsg
:=
"OpenAI codex passthrough requires a non-empty instructions field"
setOpsUpstreamError
(
c
,
http
.
StatusForbidden
,
rejectMsg
,
""
)
appendOpsUpstreamError
(
c
,
OpsUpstreamErrorEvent
{
Platform
:
account
.
Platform
,
AccountID
:
account
.
ID
,
AccountName
:
account
.
Name
,
UpstreamStatusCode
:
http
.
StatusForbidden
,
Passthrough
:
true
,
Kind
:
"request_error"
,
Message
:
rejectMsg
,
Detail
:
rejectReason
,
})
logOpenAIPassthroughInstructionsRejected
(
ctx
,
c
,
account
,
reqModel
,
rejectReason
,
body
)
c
.
JSON
(
http
.
StatusForbidden
,
gin
.
H
{
"error"
:
gin
.
H
{
"type"
:
"forbidden_error"
,
"message"
:
rejectMsg
,
},
})
return
nil
,
fmt
.
Errorf
(
"openai passthrough rejected before upstream: %s"
,
rejectReason
)
}
normalizedBody
,
normalized
,
err
:=
normalizeOpenAIPassthroughOAuthBody
(
body
)
if
err
!=
nil
{
return
nil
,
err
...
...
@@ -1395,6 +1418,37 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
},
nil
}
func
logOpenAIPassthroughInstructionsRejected
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
account
*
Account
,
reqModel
string
,
rejectReason
string
,
body
[]
byte
,
)
{
if
ctx
==
nil
{
ctx
=
context
.
Background
()
}
accountID
:=
int64
(
0
)
accountName
:=
""
accountType
:=
""
if
account
!=
nil
{
accountID
=
account
.
ID
accountName
=
strings
.
TrimSpace
(
account
.
Name
)
accountType
=
strings
.
TrimSpace
(
string
(
account
.
Type
))
}
fields
:=
[]
zap
.
Field
{
zap
.
String
(
"component"
,
"service.openai_gateway"
),
zap
.
Int64
(
"account_id"
,
accountID
),
zap
.
String
(
"account_name"
,
accountName
),
zap
.
String
(
"account_type"
,
accountType
),
zap
.
String
(
"request_model"
,
strings
.
TrimSpace
(
reqModel
)),
zap
.
String
(
"reject_reason"
,
strings
.
TrimSpace
(
rejectReason
)),
}
fields
=
appendCodexCLIOnlyRejectedRequestFields
(
fields
,
c
,
body
)
logger
.
FromContext
(
ctx
)
.
With
(
fields
...
)
.
Warn
(
"OpenAI passthrough 本地拦截:Codex 请求缺少有效 instructions"
)
}
func
(
s
*
OpenAIGatewayService
)
buildUpstreamRequestOpenAIPassthrough
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
...
...
@@ -2948,3 +3002,22 @@ func normalizeOpenAIReasoningEffort(raw string) string {
return
""
}
}
func
detectOpenAIPassthroughInstructionsRejectReason
(
reqModel
string
,
body
[]
byte
)
string
{
model
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
reqModel
))
if
!
strings
.
Contains
(
model
,
"codex"
)
{
return
""
}
instructions
:=
gjson
.
GetBytes
(
body
,
"instructions"
)
if
!
instructions
.
Exists
()
{
return
"instructions_missing"
}
if
instructions
.
Type
!=
gjson
.
String
{
return
"instructions_not_string"
}
if
strings
.
TrimSpace
(
instructions
.
String
())
==
""
{
return
"instructions_empty"
}
return
""
}
backend/internal/service/openai_oauth_passthrough_test.go
View file @
57e8abcb
...
...
@@ -164,7 +164,7 @@ func TestOpenAIGatewayService_OAuthPassthrough_StreamKeepsToolNameAndBodyNormali
c
.
Request
.
Header
.
Set
(
"Proxy-Authorization"
,
"Basic abc"
)
c
.
Request
.
Header
.
Set
(
"X-Test"
,
"keep"
)
originalBody
:=
[]
byte
(
`{"model":"gpt-5.2","stream":true,"store":true,"input":[{"type":"text","text":"hi"}]}`
)
originalBody
:=
[]
byte
(
`{"model":"gpt-5.2","stream":true,"store":true,"
instructions":"local-test-instructions","
input":[{"type":"text","text":"hi"}]}`
)
upstreamSSE
:=
strings
.
Join
([]
string
{
`data: {"type":"response.output_item.added","item":{"type":"tool_call","tool_calls":[{"function":{"name":"apply_patch"}}]}}`
,
...
...
@@ -211,6 +211,7 @@ func TestOpenAIGatewayService_OAuthPassthrough_StreamKeepsToolNameAndBodyNormali
// 1) 透传 OAuth 请求体与旧链路关键行为保持一致:store=false + stream=true。
require
.
Equal
(
t
,
false
,
gjson
.
GetBytes
(
upstream
.
lastBody
,
"store"
)
.
Bool
())
require
.
Equal
(
t
,
true
,
gjson
.
GetBytes
(
upstream
.
lastBody
,
"stream"
)
.
Bool
())
require
.
Equal
(
t
,
"local-test-instructions"
,
strings
.
TrimSpace
(
gjson
.
GetBytes
(
upstream
.
lastBody
,
"instructions"
)
.
String
()))
// 其余关键字段保持原值。
require
.
Equal
(
t
,
"gpt-5.2"
,
gjson
.
GetBytes
(
upstream
.
lastBody
,
"model"
)
.
String
())
require
.
Equal
(
t
,
"hi"
,
gjson
.
GetBytes
(
upstream
.
lastBody
,
"input.0.text"
)
.
String
())
...
...
@@ -235,6 +236,59 @@ func TestOpenAIGatewayService_OAuthPassthrough_StreamKeepsToolNameAndBodyNormali
require
.
NotContains
(
t
,
body
,
"
\"
name
\"
:
\"
edit
\"
"
)
}
func
TestOpenAIGatewayService_OAuthPassthrough_CodexMissingInstructionsRejectedBeforeUpstream
(
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.98.0 (Windows 10.0.19045; x86_64) unknown"
)
c
.
Request
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
c
.
Request
.
Header
.
Set
(
"OpenAI-Beta"
,
"responses=experimental"
)
// Codex 模型且缺少 instructions,应在本地直接 403 拒绝,不触达上游。
originalBody
:=
[]
byte
(
`{"model":"gpt-5.1-codex-max","stream":false,"store":true,"input":[{"type":"text","text":"hi"}]}`
)
upstream
:=
&
httpUpstreamRecorder
{
resp
:
&
http
.
Response
{
StatusCode
:
http
.
StatusOK
,
Header
:
http
.
Header
{
"Content-Type"
:
[]
string
{
"application/json"
},
"x-request-id"
:
[]
string
{
"rid"
}},
Body
:
io
.
NopCloser
(
strings
.
NewReader
(
`{"output":[],"usage":{"input_tokens":1,"output_tokens":1}}`
)),
},
}
svc
:=
&
OpenAIGatewayService
{
cfg
:
&
config
.
Config
{
Gateway
:
config
.
GatewayConfig
{
ForceCodexCLI
:
false
}},
httpUpstream
:
upstream
,
}
account
:=
&
Account
{
ID
:
123
,
Name
:
"acc"
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeOAuth
,
Concurrency
:
1
,
Credentials
:
map
[
string
]
any
{
"access_token"
:
"oauth-token"
,
"chatgpt_account_id"
:
"chatgpt-acc"
},
Extra
:
map
[
string
]
any
{
"openai_passthrough"
:
true
},
Status
:
StatusActive
,
Schedulable
:
true
,
RateMultiplier
:
f64p
(
1
),
}
result
,
err
:=
svc
.
Forward
(
context
.
Background
(),
c
,
account
,
originalBody
)
require
.
Error
(
t
,
err
)
require
.
Nil
(
t
,
result
)
require
.
Equal
(
t
,
http
.
StatusForbidden
,
rec
.
Code
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"requires a non-empty instructions field"
)
require
.
Nil
(
t
,
upstream
.
lastReq
)
require
.
True
(
t
,
logSink
.
ContainsMessage
(
"OpenAI passthrough 本地拦截:Codex 请求缺少有效 instructions"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"request_user_agent"
,
"codex_cli_rs/0.98.0 (Windows 10.0.19045; x86_64) unknown"
))
require
.
True
(
t
,
logSink
.
ContainsFieldValue
(
"reject_reason"
,
"instructions_missing"
))
}
func
TestOpenAIGatewayService_OAuthPassthrough_DisabledUsesLegacyTransform
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
...
...
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