Skip to content

Commit ce8520c

Browse files
committed
fix(openai): persist passthrough 429 rate limits
1 parent bda7c39 commit ce8520c

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

backend/internal/service/openai_gateway_service.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2598,6 +2598,12 @@ func (s *OpenAIGatewayService) handleErrorResponsePassthrough(
25982598
}
25992599
setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
26002600
logOpenAIInstructionsRequiredDebug(ctx, c, account, resp.StatusCode, upstreamMsg, requestBody, body)
2601+
if s.rateLimitService != nil {
2602+
// Passthrough mode preserves the raw upstream error response, but runtime
2603+
// account state still needs to be updated so sticky routing can stop
2604+
// reusing a freshly rate-limited account.
2605+
_ = s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, body)
2606+
}
26012607
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
26022608
Platform: account.Platform,
26032609
AccountID: account.ID,

backend/internal/service/openai_oauth_passthrough_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,55 @@ func TestOpenAIGatewayService_OAuthPassthrough_UpstreamErrorIncludesPassthroughF
536536
require.True(t, arr[len(arr)-1].Passthrough)
537537
}
538538

539+
func TestOpenAIGatewayService_OAuthPassthrough_429PersistsRateLimit(t *testing.T) {
540+
gin.SetMode(gin.TestMode)
541+
542+
rec := httptest.NewRecorder()
543+
c, _ := gin.CreateTestContext(rec)
544+
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
545+
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.1.0")
546+
547+
originalBody := []byte(`{"model":"gpt-5.2","stream":false,"instructions":"local-test-instructions","input":[{"type":"text","text":"hi"}]}`)
548+
resetAt := time.Now().Add(7 * 24 * time.Hour).Unix()
549+
resp := &http.Response{
550+
StatusCode: http.StatusTooManyRequests,
551+
Header: http.Header{
552+
"Content-Type": []string{"application/json"},
553+
"x-request-id": []string{"rid-rate-limit"},
554+
},
555+
Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`{"error":{"message":"The usage limit has been reached","type":"usage_limit_reached","resets_at":%d}}`, resetAt))),
556+
}
557+
upstream := &httpUpstreamRecorder{resp: resp}
558+
repo := &openAIWSRateLimitSignalRepo{}
559+
rateSvc := &RateLimitService{accountRepo: repo}
560+
561+
svc := &OpenAIGatewayService{
562+
cfg: &config.Config{Gateway: config.GatewayConfig{ForceCodexCLI: false}},
563+
httpUpstream: upstream,
564+
rateLimitService: rateSvc,
565+
}
566+
567+
account := &Account{
568+
ID: 123,
569+
Name: "acc",
570+
Platform: PlatformOpenAI,
571+
Type: AccountTypeOAuth,
572+
Concurrency: 1,
573+
Credentials: map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc"},
574+
Extra: map[string]any{"openai_passthrough": true},
575+
Status: StatusActive,
576+
Schedulable: true,
577+
RateMultiplier: f64p(1),
578+
}
579+
580+
_, err := svc.Forward(context.Background(), c, account, originalBody)
581+
require.Error(t, err)
582+
require.Equal(t, http.StatusTooManyRequests, rec.Code)
583+
require.Contains(t, rec.Body.String(), "usage_limit_reached")
584+
require.Len(t, repo.rateLimitCalls, 1)
585+
require.WithinDuration(t, time.Unix(resetAt, 0), repo.rateLimitCalls[0], 2*time.Second)
586+
}
587+
539588
func TestOpenAIGatewayService_OAuthPassthrough_NonCodexUAFallbackToCodexUA(t *testing.T) {
540589
gin.SetMode(gin.TestMode)
541590

0 commit comments

Comments
 (0)