| @@ -263,7 +263,6 @@ func (m *Manager) Execute(ctx context.Context, providers []string, req cliproxye · 262 unmodified lines |
| 263 | 263 | return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "no provider supplied"} |
| 264 | 264 | } |
| 265 | 265 | rotated := m.rotateProviders(req.Model, normalized) |
| 266 | | − defer m.advanceProviderCursor(req.Model, normalized) |
| 267 | 266 | |
| 268 | 267 | retryTimes, maxWait := m.retrySettings() |
| 269 | 268 | attempts := retryTimes + 1 |
| @@ -302,7 +301,6 @@ func (m *Manager) ExecuteCount(ctx context.Context, providers []string, req clip · 32 unmodified lines |
| 302 | 301 | return cliproxyexecutor.Response{}, &Error{Code: "provider_not_found", Message: "no provider supplied"} |
| 303 | 302 | } |
| 304 | 303 | rotated := m.rotateProviders(req.Model, normalized) |
| 305 | | − defer m.advanceProviderCursor(req.Model, normalized) |
| 306 | 304 | |
| 307 | 305 | retryTimes, maxWait := m.retrySettings() |
| 308 | 306 | attempts := retryTimes + 1 |
| @@ -341,7 +339,6 @@ func (m *Manager) ExecuteStream(ctx context.Context, providers []string, req cli · 32 unmodified lines |
| 341 | 339 | return nil, &Error{Code: "provider_not_found", Message: "no provider supplied"} |
| 342 | 340 | } |
| 343 | 341 | rotated := m.rotateProviders(req.Model, normalized) |
| 344 | | − defer m.advanceProviderCursor(req.Model, normalized) |
| 345 | 342 | |
| 346 | 343 | retryTimes, maxWait := m.retrySettings() |
| 347 | 344 | attempts := retryTimes + 1 |
| @@ -640,13 +637,20 @@ func (m *Manager) normalizeProviders(providers []string) []string { · 292 unmodified lines |
| 640 | 637 | return result |
| 641 | 638 | } |
| 642 | 639 | |
| 640 | +// rotateProviders returns a rotated view of the providers list starting from the |
| 641 | +// current offset for the model, and atomically increments the offset for the next call. |
| 642 | +// This ensures concurrent requests get different starting providers. |
| 643 | 643 | func (m *Manager) rotateProviders(model string, providers []string) []string { |
| 644 | 644 | if len(providers) == 0 { |
| 645 | 645 | return nil |
| 646 | 646 | } |
| 647 | | − m.mu.RLock() |
| 647 | + |
| 648 | + // Atomic read-and-increment: get current offset and advance cursor in one lock |
| 649 | + m.mu.Lock() |
| 648 | 650 | offset := m.providerOffsets[model] |
| 649 | | − m.mu.RUnlock() |
| 651 | + m.providerOffsets[model] = (offset + 1) % len(providers) |
| 652 | + m.mu.Unlock() |
| 653 | + |
| 650 | 654 | if len(providers) > 0 { |
| 651 | 655 | offset %= len(providers) |
| 652 | 656 | } |
| @@ -662,19 +666,6 @@ func (m *Manager) rotateProviders(model string, providers []string) []string { · 9 unmodified lines |
| 662 | 666 | return rotated |
| 663 | 667 | } |
| 664 | 668 | |
| 665 | | −func (m *Manager) advanceProviderCursor(model string, providers []string) { |
| 666 | | − if len(providers) == 0 { |
| 667 | | − m.mu.Lock() |
| 668 | | − delete(m.providerOffsets, model) |
| 669 | | − m.mu.Unlock() |
| 670 | | − return |
| 671 | | − } |
| 672 | | − m.mu.Lock() |
| 673 | | − current := m.providerOffsets[model] |
| 674 | | − m.providerOffsets[model] = (current + 1) % len(providers) |
| 675 | | − m.mu.Unlock() |
| 676 | | −} |
| 677 | | − |
| 678 | 669 | func (m *Manager) retrySettings() (int, time.Duration) { |
| 679 | 670 | if m == nil { |
| 680 | 671 | return 0, 0 |