Skip to content

Commit 4716040

Browse files
Merge pull request #46 from manaflow-ai/fix/status-refreshes-fable-scheduler
Update scheduler from fresh usage status
2 parents eb58a36 + e04fdb6 commit 4716040

2 files changed

Lines changed: 84 additions & 2 deletions

File tree

internal/proxy/claude_ratelimit_routing_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,41 @@ func TestClaudeFableProbeTreatsHeaderless429AsFableExhausted(t *testing.T) {
458458
}
459459
}
460460

461+
func TestUsageStatusFreshFableWindowUpdatesSchedulerModelPool(t *testing.T) {
462+
schedulerRef := selectacct.NewSchedulerRef(selectacct.NewScheduler([]selectacct.Score{
463+
{AccountID: "acct", Provider: accounts.ProviderClaude, Headroom: 1, ShortHeadroom: 1},
464+
}))
465+
server := Server{
466+
Accounts: []accounts.Account{{
467+
ID: "acct",
468+
Provider: accounts.ProviderClaude,
469+
AuthMode: accounts.AuthModeOAuth,
470+
}},
471+
SchedulerRef: schedulerRef,
472+
}
473+
server.updateSchedulerFromUsageStatuses([]AccountUsageStatus{{
474+
AccountStatus: AccountStatus{
475+
ID: "acct",
476+
Provider: accounts.ProviderClaude,
477+
AuthMode: accounts.AuthModeOAuth,
478+
},
479+
UsageFresh: true,
480+
Windows: []accounts.UsageWindow{
481+
{Name: "5h", UsedPercent: 10, LimitWindowSeconds: 5 * 60 * 60},
482+
{Name: "7d", UsedPercent: 40, LimitWindowSeconds: 7 * 24 * 60 * 60},
483+
{Name: agentclaude.FableWindowName, Feature: agentclaude.FableModel, UsedPercent: 100, LimitWindowSeconds: 7 * 24 * 60 * 60},
484+
},
485+
}})
486+
base := schedulerRef.Get()
487+
if base.Exhausted(accounts.ProviderClaude, "acct") {
488+
t.Fatalf("base Claude score should remain usable when only Fable is exhausted")
489+
}
490+
fable := base.ForModel(agentclaude.FableModel)
491+
if !fable.Exhausted(accounts.ProviderClaude, "acct") {
492+
t.Fatalf("Fable model score should be exhausted after fresh status update")
493+
}
494+
}
495+
461496
// TestClaudeShortWindowDrivesRouting proves the routing calculation now prefers
462497
// the account with more 5h headroom even when both accounts have identical
463498
// account-wide (7d) headroom. Before the fix both scored equal short headroom.

internal/proxy/proxy.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ type AccountUsageStatus struct {
277277
Windows []accounts.UsageWindow `json:"windows,omitempty"`
278278
Credits *accounts.CreditsInfo `json:"credits,omitempty"`
279279
ComplimentaryReset *accounts.ComplimentaryResetInfo `json:"complimentary_reset,omitempty"`
280+
UsageFresh bool `json:"-"`
280281
}
281282

282283
func NewAccountRef(store accounts.CodexStore, initial []accounts.Account, client *http.Client) *AccountRef {
@@ -488,6 +489,7 @@ func (r *AccountRef) UsageStatuses(ctx context.Context) []AccountUsageStatus {
488489
restored.Active = status.Active
489490
restored.Refreshed = status.Refreshed
490491
restored.Error = ""
492+
restored.UsageFresh = false
491493
out[i] = restored
492494
}
493495
r.usageStatusCache = append([]AccountUsageStatus(nil), out...)
@@ -592,6 +594,7 @@ func (r *AccountRef) usageStatusesLive(ctx context.Context) []AccountUsageStatus
592594
next.Windows = details.Windows
593595
next.Credits = details.Credits
594596
next.ComplimentaryReset = details.ComplimentaryReset
597+
next.UsageFresh = true
595598
out[i] = next
596599
}()
597600
}
@@ -628,14 +631,15 @@ func (r *AccountRef) usageStatusesLive(ctx context.Context) []AccountUsageStatus
628631
}
629632
next.AuthValid = true
630633
r.replace(account)
631-
windows, _, err := r.FetchUsageWindowsCached(ctx, r.client, account)
634+
windows, fresh, err := r.FetchUsageWindowsCached(ctx, r.client, account)
632635
if err != nil {
633636
next.Error = err.Error()
634637
out[i] = next
635638
return
636639
}
637640
next.PlanType = "claude"
638641
next.Windows = windows
642+
next.UsageFresh = fresh
639643
out[i] = next
640644
}()
641645
}
@@ -953,7 +957,9 @@ func (s Server) handleUsageStatus(w http.ResponseWriter, r *http.Request) {
953957
return
954958
}
955959
if s.AccountRef != nil {
956-
writeJSON(w, s.withRequestTimeExhaustionWindows(s.AccountRef.UsageStatuses(r.Context())))
960+
statuses := s.AccountRef.UsageStatuses(r.Context())
961+
s.updateSchedulerFromUsageStatuses(statuses)
962+
writeJSON(w, s.withRequestTimeExhaustionWindows(statuses))
957963
return
958964
}
959965
accounts := s.accountList()
@@ -972,6 +978,47 @@ func (s Server) handleUsageStatus(w http.ResponseWriter, r *http.Request) {
972978
writeJSON(w, s.withRequestTimeExhaustionWindows(out))
973979
}
974980

981+
func (s Server) updateSchedulerFromUsageStatuses(statuses []AccountUsageStatus) {
982+
if s.SchedulerRef == nil {
983+
return
984+
}
985+
available := oauthAccounts(s.accountList())
986+
if len(available) == 0 {
987+
return
988+
}
989+
current := s.scheduler()
990+
scores := make([]selectacct.Score, 0, len(available))
991+
scoreByID := make(map[string]int, len(available))
992+
for _, account := range available {
993+
seed := current.ScoreFor(account.Provider, account.ID)
994+
seed.AccountID = account.ID
995+
seed.Provider = account.Provider
996+
seed.Fresh = false
997+
scoreByID[selectacct.ScoreKey(account.Provider, account.ID)] = len(scores)
998+
scores = append(scores, seed)
999+
}
1000+
scored := 0
1001+
for _, status := range statuses {
1002+
if !status.UsageFresh || status.AuthMode != accounts.AuthModeOAuth || len(status.Windows) == 0 {
1003+
continue
1004+
}
1005+
if idx, ok := scoreByID[selectacct.ScoreKey(status.Provider, status.ID)]; ok {
1006+
next := scoreFromUsageWindows(status.Provider, status.ID, status.Windows)
1007+
next.Fresh = true
1008+
scores[idx] = next
1009+
scored++
1010+
}
1011+
}
1012+
if scored == 0 {
1013+
return
1014+
}
1015+
scheduler := selectacct.NewScheduler(scores)
1016+
if s.Sessions != nil {
1017+
scheduler = scheduler.WithSessionCounts(s.Sessions.CountByAccount())
1018+
}
1019+
s.SchedulerRef.Set(scheduler)
1020+
}
1021+
9751022
func (s Server) withRequestTimeExhaustionWindows(statuses []AccountUsageStatus) []AccountUsageStatus {
9761023
if s.SchedulerRef == nil {
9771024
return statuses

0 commit comments

Comments
 (0)