Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ gateway:
- 修改 Compose `.env` 后需要重建容器以更新环境变量。调大等待时间时,需要确认客户端及反向代理的首字节超时。
- 升级自动应用 `237_add_api_key_concurrency_limit.sql`,旧 Key 默认为 `0`。回退二进制不会撤销数据库新增字段。

### 中转 API Key 的上游 429

- OpenAI 平台普通 API Key 收到上游 429 时,使用管理端已有的“429 兜底冷却”设置(默认启用、5 秒),不再把透传的 ChatGPT/Codex 周窗口、`resets_at` 或使用率快照写成整个 Key 的配额状态。关闭该设置时不新增本地兜底冷却,上游错误仍按原有响应与故障转移逻辑处理。
- OAuth / Setup Token 保留真实配额窗口;Ollama Cloud、国产供应商和 OpenCode Go 的已识别专用配额逻辑,以及池模式、自定义错误策略和显式临时停调规则,仍按原有优先级处理。
- 管理端测试 API Key 时,不再用透传的 ChatGPT 429 重置时间更新 Key 的调度状态。升级不会自动清理此前已写入的长冷却;核实状态后可使用管理端恢复功能处理。

## 技术栈

| 组件 | 技术 |
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/service/account_test_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2641,6 +2641,12 @@ func (s *AccountTestService) reconcileOpenAI429State(ctx context.Context, accoun
return
}

// A relayed ChatGPT quota window is not the API key's own quota. Manual
// tests must not turn it into a persistent key-wide scheduling block.
if !account.IsOpenAIOAuthLike() {
return
}

persistOpenAI429PlanType(ctx, s.accountRepo, account, body)

var resetAt *time.Time
Expand Down
6 changes: 3 additions & 3 deletions backend/internal/service/account_test_service_openai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,9 +444,9 @@ func TestAccountTestService_OpenAIAPIKey429DoesNotSyncUpstreamPlanType(t *testin
}
require.Equal(t, AccountTypeAPIKey, account.Type)
require.Equal(t, "test-key", account.GetCredential("api_key"))
require.Equal(t, account.ID, repo.rateLimitedID)
require.NotNil(t, account.RateLimitResetAt)
require.Equal(t, int64(4102444800), account.RateLimitResetAt.Unix())
require.Zero(t, repo.rateLimitedID)
require.Nil(t, account.RateLimitedAt)
require.Nil(t, account.RateLimitResetAt)
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,10 @@ func TestOpenAI429FastPath_OpenCodeGoUsageLimitUsesMessageResetDuration(t *testi
repo := &rateLimit429AccountRepoStub{}
rateLimitService := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
svc := &OpenAIGatewayService{rateLimitService: rateLimitService}
rateLimitService.SetAccountRuntimeBlocker(svc)
account := &Account{ID: 44, Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
blocker := &runtimeBlockRecorder{}
rateLimitService.SetAccountRuntimeBlocker(blocker)
// Only an identified provider owns this quota; generic relayed OpenAI keys use fallback.
account := &Account{ID: 44, Platform: PlatformOpenCodeGo, Type: AccountTypeAPIKey}
body := []byte(`{"type":"error","error":{"type":"GoUsageLimitError","message":"5-hour usage limit reached. Resets in 4hr 59min. To continue using this model now, enable usage from your available balance: https://opencode.ai/workspace/wrk_test/go"},"metadata":{"workspace":"wrk_test","limitName":"5 hour"}}`)

before := time.Now()
Expand All @@ -391,7 +393,8 @@ func TestOpenAI429FastPath_OpenCodeGoUsageLimitUsesMessageResetDuration(t *testi
expectedResetAfter := 4*time.Hour + 59*time.Minute
require.False(t, repo.lastRateLimitReset.Before(before.Add(expectedResetAfter-time.Second)))
require.False(t, repo.lastRateLimitReset.After(after.Add(expectedResetAfter)))
require.True(t, svc.isOpenAIAccountRuntimeBlocked(account))
require.Equal(t, []*Account{account}, blocker.accounts)
require.Equal(t, []time.Time{repo.lastRateLimitReset}, blocker.until)
}

// TestOpenAI429FastPath_SkipsSparkShadow 外审第8轮 P1:spark 影子被选中后若 /responses 返回 429,
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/service/openai_oauth_passthrough_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1635,10 +1635,10 @@ func TestOpenAIGatewayService_OpenAIPassthrough_RetryableStatusesTriggerFailover
return fmt.Sprintf(`{"error":{"message":"The usage limit has been reached","type":"usage_limit_reached","resets_at":%d}}`, resetAt)
}(),
expectFailover: true,
assertRepo: func(t *testing.T, repo *openAIPassthroughFailoverRepo, _ time.Time) {
assertRepo: func(t *testing.T, repo *openAIPassthroughFailoverRepo, start time.Time) {
require.Len(t, repo.rateLimitCalls, 1)
require.Empty(t, repo.overloadCalls)
require.True(t, time.Until(repo.rateLimitCalls[0]) > 24*time.Hour)
requireOpenAIWSQuotaCooldown(t, AccountTypeAPIKey, repo.rateLimitCalls[0], start, time.Now(), time.Time{})
},
},
{
Expand Down
Loading
Loading