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
b95ffce2
Unverified
Commit
b95ffce2
authored
Apr 25, 2026
by
Wesley Liddick
Committed by
GitHub
Apr 25, 2026
Browse files
Merge pull request #1772 from KnowSky404/fix/openai-test-state-reconciliation
[codex] reconcile OpenAI admin test rate-limit state
parents
8f28a834
f3ea878b
Changes
3
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/account_test_service.go
View file @
b95ffce2
...
@@ -538,6 +538,9 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
...
@@ -538,6 +538,9 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
if
resp
.
StatusCode
!=
http
.
StatusOK
{
if
resp
.
StatusCode
!=
http
.
StatusOK
{
body
,
_
:=
io
.
ReadAll
(
resp
.
Body
)
body
,
_
:=
io
.
ReadAll
(
resp
.
Body
)
if
resp
.
StatusCode
==
http
.
StatusTooManyRequests
{
s
.
reconcileOpenAI429State
(
ctx
,
account
,
resp
.
Header
,
body
)
}
// 401 Unauthorized: 标记账号为永久错误
// 401 Unauthorized: 标记账号为永久错误
if
resp
.
StatusCode
==
http
.
StatusUnauthorized
&&
s
.
accountRepo
!=
nil
{
if
resp
.
StatusCode
==
http
.
StatusUnauthorized
&&
s
.
accountRepo
!=
nil
{
errMsg
:=
fmt
.
Sprintf
(
"Authentication failed (401): %s"
,
string
(
body
))
errMsg
:=
fmt
.
Sprintf
(
"Authentication failed (401): %s"
,
string
(
body
))
...
@@ -550,6 +553,39 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
...
@@ -550,6 +553,39 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
return
s
.
processOpenAIStream
(
c
,
resp
.
Body
)
return
s
.
processOpenAIStream
(
c
,
resp
.
Body
)
}
}
func
(
s
*
AccountTestService
)
reconcileOpenAI429State
(
ctx
context
.
Context
,
account
*
Account
,
headers
http
.
Header
,
body
[]
byte
)
{
if
s
==
nil
||
s
.
accountRepo
==
nil
||
account
==
nil
{
return
}
var
resetAt
*
time
.
Time
if
calculated
:=
calculateOpenAI429ResetTime
(
headers
);
calculated
!=
nil
{
resetAt
=
calculated
}
else
if
unixTs
:=
parseOpenAIRateLimitResetTime
(
body
);
unixTs
!=
nil
{
t
:=
time
.
Unix
(
*
unixTs
,
0
)
resetAt
=
&
t
}
if
resetAt
==
nil
{
return
}
if
err
:=
s
.
accountRepo
.
SetRateLimited
(
ctx
,
account
.
ID
,
*
resetAt
);
err
!=
nil
{
return
}
now
:=
time
.
Now
()
account
.
RateLimitedAt
=
&
now
account
.
RateLimitResetAt
=
resetAt
if
account
.
Status
==
StatusError
{
if
err
:=
s
.
accountRepo
.
ClearError
(
ctx
,
account
.
ID
);
err
!=
nil
{
return
}
account
.
Status
=
StatusActive
account
.
ErrorMessage
=
""
}
}
// testGeminiAccountConnection tests a Gemini account's connection
// testGeminiAccountConnection tests a Gemini account's connection
func
(
s
*
AccountTestService
)
testGeminiAccountConnection
(
c
*
gin
.
Context
,
account
*
Account
,
modelID
string
,
prompt
string
)
error
{
func
(
s
*
AccountTestService
)
testGeminiAccountConnection
(
c
*
gin
.
Context
,
account
*
Account
,
modelID
string
,
prompt
string
)
error
{
ctx
:=
c
.
Request
.
Context
()
ctx
:=
c
.
Request
.
Context
()
...
...
backend/internal/service/account_test_service_openai_test.go
View file @
b95ffce2
...
@@ -61,9 +61,12 @@ func newTestContext() (*gin.Context, *httptest.ResponseRecorder) {
...
@@ -61,9 +61,12 @@ func newTestContext() (*gin.Context, *httptest.ResponseRecorder) {
type
openAIAccountTestRepo
struct
{
type
openAIAccountTestRepo
struct
{
mockAccountRepoForGemini
mockAccountRepoForGemini
updatedExtra
map
[
string
]
any
updatedExtra
map
[
string
]
any
rateLimitedID
int64
rateLimitedID
int64
rateLimitedAt
*
time
.
Time
rateLimitedAt
*
time
.
Time
clearedErrorID
int64
setErrorID
int64
setErrorMsg
string
}
}
func
(
r
*
openAIAccountTestRepo
)
UpdateExtra
(
_
context
.
Context
,
_
int64
,
updates
map
[
string
]
any
)
error
{
func
(
r
*
openAIAccountTestRepo
)
UpdateExtra
(
_
context
.
Context
,
_
int64
,
updates
map
[
string
]
any
)
error
{
...
@@ -77,6 +80,17 @@ func (r *openAIAccountTestRepo) SetRateLimited(_ context.Context, id int64, rese
...
@@ -77,6 +80,17 @@ func (r *openAIAccountTestRepo) SetRateLimited(_ context.Context, id int64, rese
return
nil
return
nil
}
}
func
(
r
*
openAIAccountTestRepo
)
ClearError
(
_
context
.
Context
,
id
int64
)
error
{
r
.
clearedErrorID
=
id
return
nil
}
func
(
r
*
openAIAccountTestRepo
)
SetError
(
_
context
.
Context
,
id
int64
,
errorMsg
string
)
error
{
r
.
setErrorID
=
id
r
.
setErrorMsg
=
errorMsg
return
nil
}
func
TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders
(
t
*
testing
.
T
)
{
func
TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
recorder
:=
newTestContext
()
ctx
,
recorder
:=
newTestContext
()
...
@@ -111,11 +125,11 @@ func TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders(t *testing.
...
@@ -111,11 +125,11 @@ func TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders(t *testing.
require
.
Contains
(
t
,
recorder
.
Body
.
String
(),
"test_complete"
)
require
.
Contains
(
t
,
recorder
.
Body
.
String
(),
"test_complete"
)
}
}
func
TestAccountTestService_OpenAI429PersistsSnapshot
Without
RateLimit
(
t
*
testing
.
T
)
{
func
TestAccountTestService_OpenAI429PersistsSnapshot
And
RateLimit
State
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
_
:=
newTestContext
()
ctx
,
_
:=
newTestContext
()
resp
:=
newJSONResponse
(
http
.
StatusTooManyRequests
,
`{"error":{"type":"usage_limit_reached","message":"limit reached"}}`
)
resp
:=
newJSONResponse
(
http
.
StatusTooManyRequests
,
`{"error":{"type":"usage_limit_reached","message":"limit reached"
,"resets_at":1777283883
}}`
)
resp
.
Header
.
Set
(
"x-codex-primary-used-percent"
,
"100"
)
resp
.
Header
.
Set
(
"x-codex-primary-used-percent"
,
"100"
)
resp
.
Header
.
Set
(
"x-codex-primary-reset-after-seconds"
,
"604800"
)
resp
.
Header
.
Set
(
"x-codex-primary-reset-after-seconds"
,
"604800"
)
resp
.
Header
.
Set
(
"x-codex-primary-window-minutes"
,
"10080"
)
resp
.
Header
.
Set
(
"x-codex-primary-window-minutes"
,
"10080"
)
...
@@ -130,6 +144,7 @@ func TestAccountTestService_OpenAI429PersistsSnapshotWithoutRateLimit(t *testing
...
@@ -130,6 +144,7 @@ func TestAccountTestService_OpenAI429PersistsSnapshotWithoutRateLimit(t *testing
ID
:
88
,
ID
:
88
,
Platform
:
PlatformOpenAI
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeOAuth
,
Type
:
AccountTypeOAuth
,
Status
:
StatusError
,
Concurrency
:
1
,
Concurrency
:
1
,
Credentials
:
map
[
string
]
any
{
"access_token"
:
"test-token"
},
Credentials
:
map
[
string
]
any
{
"access_token"
:
"test-token"
},
}
}
...
@@ -138,7 +153,123 @@ func TestAccountTestService_OpenAI429PersistsSnapshotWithoutRateLimit(t *testing
...
@@ -138,7 +153,123 @@ func TestAccountTestService_OpenAI429PersistsSnapshotWithoutRateLimit(t *testing
require
.
Error
(
t
,
err
)
require
.
Error
(
t
,
err
)
require
.
NotEmpty
(
t
,
repo
.
updatedExtra
)
require
.
NotEmpty
(
t
,
repo
.
updatedExtra
)
require
.
Equal
(
t
,
100.0
,
repo
.
updatedExtra
[
"codex_5h_used_percent"
])
require
.
Equal
(
t
,
100.0
,
repo
.
updatedExtra
[
"codex_5h_used_percent"
])
require
.
Equal
(
t
,
account
.
ID
,
repo
.
rateLimitedID
)
require
.
NotNil
(
t
,
repo
.
rateLimitedAt
)
require
.
Equal
(
t
,
account
.
ID
,
repo
.
clearedErrorID
)
require
.
Equal
(
t
,
StatusActive
,
account
.
Status
)
require
.
Empty
(
t
,
account
.
ErrorMessage
)
require
.
NotNil
(
t
,
account
.
RateLimitResetAt
)
}
func
TestAccountTestService_OpenAI429BodyOnlyPersistsRateLimitAndClearsStaleError
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
_
:=
newTestContext
()
resp
:=
newJSONResponse
(
http
.
StatusTooManyRequests
,
`{"error":{"type":"usage_limit_reached","message":"limit reached","resets_at":"1777283883"}}`
)
repo
:=
&
openAIAccountTestRepo
{}
upstream
:=
&
queuedHTTPUpstream
{
responses
:
[]
*
http
.
Response
{
resp
}}
svc
:=
&
AccountTestService
{
accountRepo
:
repo
,
httpUpstream
:
upstream
}
account
:=
&
Account
{
ID
:
77
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeOAuth
,
Status
:
StatusError
,
ErrorMessage
:
"Access forbidden (403): account may be suspended or lack permissions"
,
Concurrency
:
1
,
Credentials
:
map
[
string
]
any
{
"access_token"
:
"test-token"
},
}
err
:=
svc
.
testOpenAIAccountConnection
(
ctx
,
account
,
"gpt-5.4"
,
""
)
require
.
Error
(
t
,
err
)
require
.
Equal
(
t
,
account
.
ID
,
repo
.
rateLimitedID
)
require
.
NotNil
(
t
,
repo
.
rateLimitedAt
)
require
.
Equal
(
t
,
account
.
ID
,
repo
.
clearedErrorID
)
require
.
Equal
(
t
,
StatusActive
,
account
.
Status
)
require
.
Empty
(
t
,
account
.
ErrorMessage
)
require
.
NotNil
(
t
,
account
.
RateLimitResetAt
)
require
.
Empty
(
t
,
repo
.
updatedExtra
)
}
func
TestAccountTestService_OpenAI429ActiveAccountDoesNotClearError
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
_
:=
newTestContext
()
resp
:=
newJSONResponse
(
http
.
StatusTooManyRequests
,
`{"error":{"type":"usage_limit_reached","message":"limit reached","resets_in_seconds":3600}}`
)
repo
:=
&
openAIAccountTestRepo
{}
upstream
:=
&
queuedHTTPUpstream
{
responses
:
[]
*
http
.
Response
{
resp
}}
svc
:=
&
AccountTestService
{
accountRepo
:
repo
,
httpUpstream
:
upstream
}
account
:=
&
Account
{
ID
:
78
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeOAuth
,
Status
:
StatusActive
,
Concurrency
:
1
,
Credentials
:
map
[
string
]
any
{
"access_token"
:
"test-token"
},
}
err
:=
svc
.
testOpenAIAccountConnection
(
ctx
,
account
,
"gpt-5.4"
,
""
)
require
.
Error
(
t
,
err
)
require
.
Equal
(
t
,
account
.
ID
,
repo
.
rateLimitedID
)
require
.
NotNil
(
t
,
repo
.
rateLimitedAt
)
require
.
Zero
(
t
,
repo
.
clearedErrorID
)
require
.
Equal
(
t
,
StatusActive
,
account
.
Status
)
require
.
NotNil
(
t
,
account
.
RateLimitResetAt
)
}
func
TestAccountTestService_OpenAI429WithoutResetSignalDoesNotMutateRuntimeState
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
_
:=
newTestContext
()
resp
:=
newJSONResponse
(
http
.
StatusTooManyRequests
,
`{"error":{"type":"usage_limit_reached","message":"limit reached"}}`
)
repo
:=
&
openAIAccountTestRepo
{}
upstream
:=
&
queuedHTTPUpstream
{
responses
:
[]
*
http
.
Response
{
resp
}}
svc
:=
&
AccountTestService
{
accountRepo
:
repo
,
httpUpstream
:
upstream
}
account
:=
&
Account
{
ID
:
79
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeOAuth
,
Status
:
StatusError
,
ErrorMessage
:
"stale 403"
,
Concurrency
:
1
,
Credentials
:
map
[
string
]
any
{
"access_token"
:
"test-token"
},
}
err
:=
svc
.
testOpenAIAccountConnection
(
ctx
,
account
,
"gpt-5.4"
,
""
)
require
.
Error
(
t
,
err
)
require
.
Zero
(
t
,
repo
.
rateLimitedID
)
require
.
Zero
(
t
,
repo
.
rateLimitedID
)
require
.
Nil
(
t
,
repo
.
rateLimitedAt
)
require
.
Nil
(
t
,
repo
.
rateLimitedAt
)
require
.
Zero
(
t
,
repo
.
clearedErrorID
)
require
.
Equal
(
t
,
StatusError
,
account
.
Status
)
require
.
Equal
(
t
,
"stale 403"
,
account
.
ErrorMessage
)
require
.
Nil
(
t
,
account
.
RateLimitResetAt
)
}
func
TestAccountTestService_OpenAI401SetsPermanentErrorOnly
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
_
:=
newTestContext
()
resp
:=
newJSONResponse
(
http
.
StatusUnauthorized
,
`{"error":"bad token"}`
)
repo
:=
&
openAIAccountTestRepo
{}
upstream
:=
&
queuedHTTPUpstream
{
responses
:
[]
*
http
.
Response
{
resp
}}
svc
:=
&
AccountTestService
{
accountRepo
:
repo
,
httpUpstream
:
upstream
}
account
:=
&
Account
{
ID
:
80
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeOAuth
,
Status
:
StatusActive
,
Concurrency
:
1
,
Credentials
:
map
[
string
]
any
{
"access_token"
:
"test-token"
},
}
err
:=
svc
.
testOpenAIAccountConnection
(
ctx
,
account
,
"gpt-5.4"
,
""
)
require
.
Error
(
t
,
err
)
require
.
Equal
(
t
,
account
.
ID
,
repo
.
setErrorID
)
require
.
Contains
(
t
,
repo
.
setErrorMsg
,
"Authentication failed (401)"
)
require
.
Zero
(
t
,
repo
.
rateLimitedID
)
require
.
Zero
(
t
,
repo
.
clearedErrorID
)
require
.
Nil
(
t
,
account
.
RateLimitResetAt
)
require
.
Nil
(
t
,
account
.
RateLimitResetAt
)
}
}
backend/internal/service/ratelimit_service.go
View file @
b95ffce2
...
@@ -931,7 +931,7 @@ func (s *RateLimitService) handle429(ctx context.Context, account *Account, head
...
@@ -931,7 +931,7 @@ func (s *RateLimitService) handle429(ctx context.Context, account *Account, head
// calculateOpenAI429ResetTime 从 OpenAI 429 响应头计算正确的重置时间
// calculateOpenAI429ResetTime 从 OpenAI 429 响应头计算正确的重置时间
// 返回 nil 表示无法从响应头中确定重置时间
// 返回 nil 表示无法从响应头中确定重置时间
func
(
s
*
RateLimitService
)
calculateOpenAI429ResetTime
(
headers
http
.
Header
)
*
time
.
Time
{
func
calculateOpenAI429ResetTime
(
headers
http
.
Header
)
*
time
.
Time
{
snapshot
:=
ParseCodexRateLimitHeaders
(
headers
)
snapshot
:=
ParseCodexRateLimitHeaders
(
headers
)
if
snapshot
==
nil
{
if
snapshot
==
nil
{
return
nil
return
nil
...
@@ -977,6 +977,10 @@ func (s *RateLimitService) calculateOpenAI429ResetTime(headers http.Header) *tim
...
@@ -977,6 +977,10 @@ func (s *RateLimitService) calculateOpenAI429ResetTime(headers http.Header) *tim
return
nil
return
nil
}
}
func
(
s
*
RateLimitService
)
calculateOpenAI429ResetTime
(
headers
http
.
Header
)
*
time
.
Time
{
return
calculateOpenAI429ResetTime
(
headers
)
}
// anthropic429Result holds the parsed Anthropic 429 rate-limit information.
// anthropic429Result holds the parsed Anthropic 429 rate-limit information.
type
anthropic429Result
struct
{
type
anthropic429Result
struct
{
resetAt
time
.
Time
// The correct reset time to use for SetRateLimited
resetAt
time
.
Time
// The correct reset time to use for SetRateLimited
...
...
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