Repository navigation
Spread soonest-reset picks across accounts whose weekly resets fall within an hour (HOUSE-84) #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,8 +3,10 @@ package auth | |
| import ( | ||
| "context" | ||
| "net/http" | ||
| "slices" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor" | ||
|
|
@@ -14,6 +16,7 @@ const ( | |
| // weeklyWindowMinutes is the length of the weekly quota window advertised by Codex. | ||
| weeklyWindowMinutes = 7 * 24 * 60 | ||
| week = 7 * 24 * time.Hour | ||
| resetGroupTolerance = time.Hour | ||
| ) | ||
|
|
||
| // unknownResetLast orders a credential after every credential with a known reset. | ||
|
|
@@ -31,9 +34,16 @@ var unknownResetLast = time.Unix(1<<62, 0) | |
| // An unknown Claude weekly reset sorts first: the window runs on a fixed per-account | ||
| // schedule, so one request learns it. A passed Claude reset rolls forward by whole weeks. | ||
| // An unknown or passed Codex weekly reset sorts last: that window is usually unstarted and | ||
| // starting it early can waste it. Ties keep input order (ID order), so providers without | ||
| // quota signals behave like fill-first. | ||
| type SoonestResetSelector struct{} | ||
| // starting it early can waste it. Unknown resets keep input order (ID order), so providers | ||
| // without quota signals behave like fill-first. | ||
| // | ||
| // When the soonest credential is not skipped and its weekly reset is known, new picks rotate | ||
| // in ID order through the credentials of its provider that are not skipped and whose known | ||
| // weekly reset is at most one hour after it. Use it by pointer: it holds the rotation cursor. | ||
| type SoonestResetSelector struct { | ||
| mu sync.Mutex | ||
| cursors map[string]int | ||
| } | ||
|
|
||
| // Pick selects the available auth whose weekly quota window resets soonest. | ||
| func (s *SoonestResetSelector) Pick(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, auths []*Auth) (*Auth, error) { | ||
|
|
@@ -53,7 +63,29 @@ func (s *SoonestResetSelector) Pick(ctx context.Context, provider, model string, | |
| picked, pickedGated, pickedReset = auth, gated, resetAt | ||
| } | ||
| } | ||
| return picked, nil | ||
| if picked == nil || pickedGated || pickedReset.IsZero() || pickedReset.Equal(unknownResetLast) { | ||
| return picked, nil | ||
| } | ||
| groupProvider := strings.ToLower(strings.TrimSpace(picked.Provider)) | ||
| var group []*Auth | ||
| for _, auth := range available { | ||
| if !strings.EqualFold(strings.TrimSpace(auth.Provider), groupProvider) { | ||
| continue | ||
| } | ||
| gated, resetAt := soonestResetRank(auth, now) | ||
| if !gated && resetAt.Sub(pickedReset) <= resetGroupTolerance { | ||
| group = append(group, auth) | ||
| } | ||
| } | ||
| slices.SortFunc(group, func(a, b *Auth) int { return strings.Compare(a.ID, b.ID) }) | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| if s.cursors == nil { | ||
| s.cursors = make(map[string]int) | ||
| } | ||
| cursor := s.cursors[groupProvider] | ||
| s.cursors[groupProvider] = cursor + 1 | ||
| return group[cursor%len(group)], nil | ||
|
Comment on lines
+87
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Critical · Signed rotation cursor can overflow and crash selection The per-provider cursor is an unbounded There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Retry exclusions bias rotation toward later credential IDs Retries remove already-tried credentials before calling the selector, so applying the accumulated cursor to the shortened group changes the rotation position. For [a,b,c], selecting a then retrying [b,c] selects c and skips b. With five equal-reset credentials and two non-cooling retries per session, a reproduced 50-session sequence yields attempt counts a=20, b=21, c=25, d=34, e=50. This concentrates retry load on later IDs rather than continuing the ID-order rotation, independently of the cross-model cursor issue. Also reported for this defect: - Numeric indexing biases rotation when retries shrink the group (sdk/cliproxy/auth/selector_soonest_reset.go:89): Retry selection excludes already-tried credentials, but the cursor is applied to the newly shortened slice. With one model and five eligible accounts [a,b,c,d,e], selecting a followed by a retry over [b,c,d,e] selects c, skipping b. With transient cooldowns disabled, repeating 50 three-attempt requests yields selection counts a:20, b:21, c:25, d:34, e:50. The tail account receives traffic from every request rather than the eligible accounts being picked in turn. Rotation must preserve its position across retry exclusions. Also affects: |
||
| } | ||
|
|
||
| // soonestResetRank reports whether an observed window of auth is exhausted until a future | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 High · Provider-wide cursor can starve an independent model pool
The manager filters candidates by model, but all models consume the same provider cursor. With Codex model X served by [a,b] and model Y served by [c], all with equal known resets, alternating fresh sessions gives X cursor values 0,2,4,… and always selects a; b never receives a session. This reproduces through the manager with session affinity enabled, so unrelated model traffic can completely defeat the new load spreading. Also reported for this defect: - Shared provider cursor can starve independent model pools (sdk/cliproxy/auth/selector_soonest_reset.go:87-88): The manager filters candidates by model before calling this shared selector. If Codex accounts [a,b] serve one model and [c,d] serve another, all with equal known future resets, alternating fresh sessions between the models produces a,d,a,d indefinitely. One pool receives only even cursor values and the other only odd values, so b and c are never selected. This reproduces through Manager with session affinity enabled and defeats spreading on normal multi-model traffic.
Also affects: