@@ -20,6 +20,11 @@ type RoundRobinSelector struct {
2020 cursors map [string ]int
2121}
2222
23+ // FillFirstSelector selects the first available credential (deterministic ordering).
24+ // This "burns" one account before moving to the next, which can help stagger
25+ // rolling-window subscription caps (e.g. chat message limits).
26+ type FillFirstSelector struct {}
27+
2328type blockReason int
2429
2530const (
@@ -98,20 +103,8 @@ func (e *modelCooldownError) Headers() http.Header {
98103 return headers
99104}
100105
101- // Pick selects the next available auth for the provider in a round-robin manner.
102- func (s * RoundRobinSelector ) Pick (ctx context.Context , provider , model string , opts cliproxyexecutor.Options , auths []* Auth ) (* Auth , error ) {
103- _ = ctx
104- _ = opts
105- if len (auths ) == 0 {
106- return nil , & Error {Code : "auth_not_found" , Message : "no auth candidates" }
107- }
108- if s .cursors == nil {
109- s .cursors = make (map [string ]int )
110- }
111- available := make ([]* Auth , 0 , len (auths ))
112- now := time .Now ()
113- cooldownCount := 0
114- var earliest time.Time
106+ func collectAvailable (auths []* Auth , model string , now time.Time ) (available []* Auth , cooldownCount int , earliest time.Time ) {
107+ available = make ([]* Auth , 0 , len (auths ))
115108 for i := 0 ; i < len (auths ); i ++ {
116109 candidate := auths [i ]
117110 blocked , reason , next := isAuthBlockedForModel (candidate , model , now )
@@ -126,6 +119,18 @@ func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, o
126119 }
127120 }
128121 }
122+ if len (available ) > 1 {
123+ sort .Slice (available , func (i , j int ) bool { return available [i ].ID < available [j ].ID })
124+ }
125+ return available , cooldownCount , earliest
126+ }
127+
128+ func getAvailableAuths (auths []* Auth , provider , model string , now time.Time ) ([]* Auth , error ) {
129+ if len (auths ) == 0 {
130+ return nil , & Error {Code : "auth_not_found" , Message : "no auth candidates" }
131+ }
132+
133+ available , cooldownCount , earliest := collectAvailable (auths , model , now )
129134 if len (available ) == 0 {
130135 if cooldownCount == len (auths ) && ! earliest .IsZero () {
131136 resetIn := earliest .Sub (now )
@@ -136,12 +141,24 @@ func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, o
136141 }
137142 return nil , & Error {Code : "auth_unavailable" , Message : "no auth available" }
138143 }
139- // Make round-robin deterministic even if caller's candidate order is unstable.
140- if len (available ) > 1 {
141- sort .Slice (available , func (i , j int ) bool { return available [i ].ID < available [j ].ID })
144+
145+ return available , nil
146+ }
147+
148+ // Pick selects the next available auth for the provider in a round-robin manner.
149+ func (s * RoundRobinSelector ) Pick (ctx context.Context , provider , model string , opts cliproxyexecutor.Options , auths []* Auth ) (* Auth , error ) {
150+ _ = ctx
151+ _ = opts
152+ now := time .Now ()
153+ available , err := getAvailableAuths (auths , provider , model , now )
154+ if err != nil {
155+ return nil , err
142156 }
143157 key := provider + ":" + model
144158 s .mu .Lock ()
159+ if s .cursors == nil {
160+ s .cursors = make (map [string ]int )
161+ }
145162 index := s .cursors [key ]
146163
147164 if index >= 2_147_483_640 {
@@ -154,6 +171,18 @@ func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, o
154171 return available [index % len (available )], nil
155172}
156173
174+ // Pick selects the first available auth for the provider in a deterministic manner.
175+ func (s * FillFirstSelector ) Pick (ctx context.Context , provider , model string , opts cliproxyexecutor.Options , auths []* Auth ) (* Auth , error ) {
176+ _ = ctx
177+ _ = opts
178+ now := time .Now ()
179+ available , err := getAvailableAuths (auths , provider , model , now )
180+ if err != nil {
181+ return nil , err
182+ }
183+ return available [0 ], nil
184+ }
185+
157186func isAuthBlockedForModel (auth * Auth , model string , now time.Time ) (bool , blockReason , time.Time ) {
158187 if auth == nil {
159188 return true , blockReasonOther , time.Time {}
0 commit comments