fix(registry): treat credential_quota as quota cooldown in model availability

Closes: #6545
This commit is contained in:
Luis Pater
2026-10-12 18:08:08 +08:00
parent 9126772dfd
commit bbcd5734f8
2 changed files with 107 additions and 13 deletions

View File

@@ -1305,6 +1305,16 @@ func (r *ModelRegistry) GetAvailableModels(handlerType string) []map[string]any
return models
}
// isQuotaSuspension reports whether a suspension is a quota cooldown. A model in a
// quota cooldown stays listed: the limit is temporary, and a request meanwhile gets a
// 429 with its retry time instead of an unknown model. A credential-wide quota
// ("credential_quota") suspends every model of that credential, so it is a quota
// cooldown too; counting it as another failure hides all of them when no other
// credential serves them.
func isQuotaSuspension(reason string) bool {
return strings.EqualFold(reason, "quota") || strings.EqualFold(reason, "credential_quota")
}
func modelRegistrationAvailability(registration *ModelRegistration, now time.Time) (bool, time.Time) {
if registration == nil {
return false, time.Time{}
@@ -1328,23 +1338,23 @@ func modelRegistrationAvailability(registration *ModelRegistration, now time.Tim
cooldownSuspended := 0
otherSuspended := 0
quotaAndOtherSuspended := 0
quotaAndSuspended := 0
if registration.SuspendedClients != nil {
for clientID, reason := range registration.SuspendedClients {
if strings.EqualFold(reason, "quota") {
if quotaTime := registration.QuotaExceededClients[clientID]; quotaTime != nil && now.Before(quotaTime.Add(modelQuotaExceededWindow)) {
quotaAndSuspended++
}
if isQuotaSuspension(reason) {
cooldownSuspended++
continue
}
otherSuspended++
if quotaTime := registration.QuotaExceededClients[clientID]; quotaTime != nil && now.Before(quotaTime.Add(modelQuotaExceededWindow)) {
quotaAndOtherSuspended++
}
}
}
// A credential-wide quota can mark the same client both quota-exceeded and
// A quota cooldown can mark the same client both quota-exceeded and
// suspended. Count that unavailable client only once.
effectiveClients := availableClients - expiredClients - otherSuspended + quotaAndOtherSuspended
effectiveClients := availableClients - expiredClients - cooldownSuspended - otherSuspended + quotaAndSuspended
if effectiveClients < 0 {
effectiveClients = 0
}
@@ -1505,7 +1515,7 @@ func (r *ModelRegistry) GetAvailableModelsByProvider(provider string) []*ModelIn
expiredClients := 0
cooldownSuspended := 0
otherSuspended := 0
quotaAndOtherSuspended := 0
quotaAndSuspended := 0
if ok && registration != nil {
if registration.QuotaExceededClients != nil {
for clientID, quotaTime := range registration.QuotaExceededClients {
@@ -1528,20 +1538,20 @@ func (r *ModelRegistry) GetAvailableModelsByProvider(provider string) []*ModelIn
if p, okProvider := r.clientProviders[clientID]; !okProvider || p != provider {
continue
}
if strings.EqualFold(reason, "quota") {
if quotaTime := registration.QuotaExceededClients[clientID]; quotaTime != nil && now.Before(quotaTime.Add(modelQuotaExceededWindow)) {
quotaAndSuspended++
}
if isQuotaSuspension(reason) {
cooldownSuspended++
continue
}
otherSuspended++
if quotaTime := registration.QuotaExceededClients[clientID]; quotaTime != nil && now.Before(quotaTime.Add(modelQuotaExceededWindow)) {
quotaAndOtherSuspended++
}
}
}
}
availableClients := entry.count
effectiveClients := availableClients - expiredClients - otherSuspended + quotaAndOtherSuspended
effectiveClients := availableClients - expiredClients - cooldownSuspended - otherSuspended + quotaAndSuspended
if effectiveClients < 0 {
effectiveClients = 0
}

View File

@@ -0,0 +1,84 @@
package registry
import (
"testing"
"time"
)
// A credential-wide 429 marks the model that received it "quota" and the credential's
// other models "credential_quota". With no other credential to serve them, all of them
// must stay listed like any quota cooldown instead of leaving only the model that
// received the 429 until a later request re-projects the credential.
func TestCredentialQuotaKeepsSoleCredentialModelsListed(t *testing.T) {
r := newTestModelRegistry()
models := []*ModelInfo{
{ID: "claude-sonnet-5", OwnedBy: "anthropic", Type: "claude"},
{ID: "claude-opus-5-5", OwnedBy: "anthropic", Type: "claude"},
{ID: "claude-haiku-4-5", OwnedBy: "anthropic", Type: "claude"},
}
r.RegisterClient("claude-oauth", "claude", models)
_, epoch := r.GetModelsAndEpochForClient("claude-oauth")
if !r.ApplyClientModelProjections("claude-oauth", epoch, 1, []ClientModelProjection{
{ModelID: "claude-sonnet-5", Suspended: true, SuspendReason: "quota", QuotaExceeded: true},
{ModelID: "claude-opus-5-5", Suspended: true, SuspendReason: "credential_quota", QuotaExceeded: true},
// A sibling without a recorded state of its own inherits the credential's suspension.
{ModelID: "claude-haiku-4-5", Suspended: true, SuspendReason: "credential_quota"},
}) {
t.Fatal("failed to apply the credential-wide quota state")
}
if got := len(r.GetAvailableModels("claude")); got != len(models) {
t.Errorf("/v1/models lists %d models during a credential-wide quota, want %d", got, len(models))
}
if got := len(r.GetAvailableModelsByProvider("claude")); got != len(models) {
t.Errorf("provider catalog lists %d models during a credential-wide quota, want %d", got, len(models))
}
if got := len(r.GetAvailableModelInfos()); got != len(models) {
t.Errorf("metadata catalog lists %d models during a credential-wide quota, want %d", got, len(models))
}
// After the quota window, the suspension alone still keeps each model listed.
registration := r.models["claude-opus-5-5"]
past := time.Now().Add(-2 * modelQuotaExceededWindow)
registration.QuotaExceededClients["claude-oauth"] = &past
if available, _ := modelRegistrationAvailability(registration, time.Now()); !available {
t.Error("a credential-wide quota hid the model after the quota window")
}
if got := len(r.GetAvailableModelsByProvider("claude")); got != len(models) {
t.Errorf("provider catalog lists %d models after the quota window, want %d", got, len(models))
}
// A suspension that is not a quota cooldown still hides the model.
_, epoch = r.GetModelsAndEpochForClient("claude-oauth")
if !r.ApplyClientModelProjections("claude-oauth", epoch, 2, []ClientModelProjection{
{ModelID: "claude-opus-5-5", Suspended: true, SuspendReason: "unauthorized"},
}) {
t.Fatal("failed to apply the unauthorized state")
}
for _, model := range r.GetAvailableModelsByProvider("claude") {
if model.ID == "claude-opus-5-5" {
t.Error("a model whose only credential is unauthorized is still listed")
}
}
// When a second credential is suspended for a non-quota reason, a sibling model
// with only credential_quota (no active 5-minute quota timestamp) must also be hidden.
r.RegisterClient("claude-api-backup", "claude", []*ModelInfo{
{ID: "claude-haiku-4-5", OwnedBy: "anthropic", Type: "claude"},
})
_, backupEpoch := r.GetModelsAndEpochForClient("claude-api-backup")
if !r.ApplyClientModelProjections("claude-api-backup", backupEpoch, 1, []ClientModelProjection{
{ModelID: "claude-haiku-4-5", Suspended: true, SuspendReason: "manual"},
}) {
t.Fatal("failed to apply the manual suspension to backup client")
}
for _, model := range r.GetAvailableModelsByProvider("claude") {
if model.ID == "claude-haiku-4-5" {
t.Error("a model with one credential_quota client and one manual-suspended client is still listed in provider catalog")
}
}
for _, model := range r.GetAvailableModels("claude") {
if id, _ := model["id"].(string); id == "claude-haiku-4-5" {
t.Error("a model with one credential_quota client and one manual-suspended client is still listed in /v1/models")
}
}
}