mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:28:39 +08:00
fix(openai): isolate websocket semantic rate limits
This commit is contained in:
@@ -176,6 +176,44 @@ func TestOpenAIStream429_SparkQuotaUsesQuotaHeaders(t *testing.T) {
|
||||
require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
}
|
||||
|
||||
func TestOpenAIWSErrorEvent_OrdinaryModelIgnoresHandshakeQuotaHeaders(t *testing.T) {
|
||||
repo := &oauth429RateLimitRepo{}
|
||||
rateLimits := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
|
||||
svc := &OpenAIGatewayService{rateLimitService: rateLimits}
|
||||
rateLimits.SetAccountRuntimeBlocker(svc)
|
||||
account := &Account{ID: 430, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
|
||||
headers := http.Header{}
|
||||
headers.Set("x-codex-primary-used-percent", "100")
|
||||
headers.Set("x-codex-primary-reset-after-seconds", "604800")
|
||||
headers.Set("x-codex-primary-window-minutes", "10080")
|
||||
payload := []byte(`{"type":"error","error":{"type":"rate_limit_error","code":"rate_limit_exceeded"}}`)
|
||||
|
||||
svc.persistOpenAIWSRateLimitSignal(context.Background(), account, headers, payload, "rate_limit_exceeded", "rate_limit_error", "quota exhausted", "gpt-5.3-codex")
|
||||
|
||||
require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
require.Zero(t, repo.setRateLimitedCalls)
|
||||
}
|
||||
|
||||
func TestOpenAIWSErrorEvent_SparkQuotaUsesHandshakeQuotaHeaders(t *testing.T) {
|
||||
repo := &oauth429RateLimitRepo{}
|
||||
rateLimits := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
|
||||
svc := &OpenAIGatewayService{rateLimitService: rateLimits}
|
||||
rateLimits.SetAccountRuntimeBlocker(svc)
|
||||
account := &Account{ID: 431, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
|
||||
headers := http.Header{}
|
||||
headers.Set("x-codex-primary-used-percent", "100")
|
||||
headers.Set("x-codex-primary-reset-after-seconds", "604800")
|
||||
headers.Set("x-codex-primary-window-minutes", "10080")
|
||||
payload := []byte(`{"type":"error","error":{"type":"rate_limit_error","code":"rate_limit_exceeded"}}`)
|
||||
|
||||
svc.persistOpenAIWSRateLimitSignal(context.Background(), account, headers, payload, "rate_limit_exceeded", "rate_limit_error", "quota exhausted", "gpt-5.3-codex-spark")
|
||||
|
||||
require.Equal(t, 1, repo.setModelRateLimitCalls)
|
||||
require.Equal(t, "gpt-5.3-codex-spark", repo.lastModelRateLimitKey)
|
||||
require.Greater(t, time.Until(repo.lastModelRateLimitedUntil), 6*24*time.Hour)
|
||||
require.False(t, svc.isOpenAIAccountRuntimeBlocked(account))
|
||||
}
|
||||
|
||||
func TestOpenAI429FastPath_SparkShadowQuotaStaysModelScoped(t *testing.T) {
|
||||
repo := &oauth429RateLimitRepo{}
|
||||
rateLimits := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
|
||||
|
||||
@@ -1577,10 +1577,10 @@ func (s *OpenAIGatewayService) handleOpenAIStreamTerminalAccountSideEffects(
|
||||
model = firstNonEmpty(gjson.GetBytes(payload, "model").String(), gjson.GetBytes(payload, "response.model").String())
|
||||
}
|
||||
accountHeaders := headers
|
||||
if statusCode == http.StatusTooManyRequests && !(isCodexSparkModel(model) && isOpenAIOAuthAccount(account)) {
|
||||
if statusCode == http.StatusTooManyRequests {
|
||||
// 普通模型的流式 429 不能继承外层 HTTP 200 的全局 quota 快照;
|
||||
// 只有 OAuth/SetupToken 的 Spark 配额 429 才需要保留 headers 读取明确的 5h/7d reset。
|
||||
accountHeaders = nil
|
||||
accountHeaders = openAIWSSemantic429Headers(account, model, headers)
|
||||
}
|
||||
return statusCode, s.handleOpenAIAccountUpstreamError(ctx, account, statusCode, accountHeaders, payload, model)
|
||||
default:
|
||||
|
||||
@@ -325,6 +325,9 @@ func (s *OpenAIGatewayService) handleOpenAIWSErrorEventTransientFailure(ctx cont
|
||||
}
|
||||
status := openAIWSPayloadTransientStatus(payload)
|
||||
if status != 0 {
|
||||
if status == http.StatusTooManyRequests {
|
||||
headers = openAIWSSemantic429Headers(account, canonicalModel, headers)
|
||||
}
|
||||
s.handleOpenAIAccountUpstreamError(ctx, account, status, headers, payload, canonicalModel)
|
||||
}
|
||||
}
|
||||
@@ -695,9 +698,22 @@ func (s *OpenAIGatewayService) persistOpenAIWSRateLimitSignal(ctx context.Contex
|
||||
if model == "" {
|
||||
model = firstNonEmpty(gjson.GetBytes(responseBody, "model").String(), gjson.GetBytes(responseBody, "response.model").String())
|
||||
}
|
||||
// 非空 responseBody 表示已建立连接后收到的语义错误事件;握手响应头
|
||||
// 可能只是成功连接时的全局快照,不能用于普通模型的 429 账号级限流。
|
||||
// 实际拨号 HTTP 429 使用 nil responseBody,必须保留响应头。
|
||||
if len(responseBody) > 0 {
|
||||
headers = openAIWSSemantic429Headers(account, model, headers)
|
||||
}
|
||||
s.handleOpenAIAccountUpstreamError(ctx, account, http.StatusTooManyRequests, headers, responseBody, model)
|
||||
}
|
||||
|
||||
func openAIWSSemantic429Headers(account *Account, model string, headers http.Header) http.Header {
|
||||
if isCodexSparkModel(model) && isOpenAIOAuthAccount(account) {
|
||||
return headers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) newOpenAIWSRateLimitFailoverError(account *Account, headers http.Header, responseBody []byte, message string) *UpstreamFailoverError {
|
||||
return s.newOpenAIAccountFailoverError(
|
||||
account,
|
||||
|
||||
Reference in New Issue
Block a user