Fixed: #1747
feat(auth): add configurable max-retry-credentials for finer control over cross-credential retries
This commit is contained in:
@@ -2,13 +2,17 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||
)
|
||||
|
||||
func TestManager_ShouldRetryAfterError_RespectsAuthRequestRetryOverride(t *testing.T) {
|
||||
m := NewManager(nil, nil, nil)
|
||||
m.SetRetryConfig(3, 30*time.Second)
|
||||
m.SetRetryConfig(3, 30*time.Second, 0)
|
||||
|
||||
model := "test-model"
|
||||
next := time.Now().Add(5 * time.Second)
|
||||
@@ -31,7 +35,7 @@ func TestManager_ShouldRetryAfterError_RespectsAuthRequestRetryOverride(t *testi
|
||||
t.Fatalf("register auth: %v", errRegister)
|
||||
}
|
||||
|
||||
_, maxWait := m.retrySettings()
|
||||
_, _, maxWait := m.retrySettings()
|
||||
wait, shouldRetry := m.shouldRetryAfterError(&Error{HTTPStatus: 500, Message: "boom"}, 0, []string{"claude"}, model, maxWait)
|
||||
if shouldRetry {
|
||||
t.Fatalf("expected shouldRetry=false for request_retry=0, got true (wait=%v)", wait)
|
||||
@@ -56,6 +60,124 @@ func TestManager_ShouldRetryAfterError_RespectsAuthRequestRetryOverride(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
type credentialRetryLimitExecutor struct {
|
||||
id string
|
||||
|
||||
mu sync.Mutex
|
||||
calls int
|
||||
}
|
||||
|
||||
func (e *credentialRetryLimitExecutor) Identifier() string {
|
||||
return e.id
|
||||
}
|
||||
|
||||
func (e *credentialRetryLimitExecutor) Execute(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
||||
e.recordCall()
|
||||
return cliproxyexecutor.Response{}, &Error{HTTPStatus: 500, Message: "boom"}
|
||||
}
|
||||
|
||||
func (e *credentialRetryLimitExecutor) ExecuteStream(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) {
|
||||
e.recordCall()
|
||||
return nil, &Error{HTTPStatus: 500, Message: "boom"}
|
||||
}
|
||||
|
||||
func (e *credentialRetryLimitExecutor) Refresh(_ context.Context, auth *Auth) (*Auth, error) {
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (e *credentialRetryLimitExecutor) CountTokens(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
||||
e.recordCall()
|
||||
return cliproxyexecutor.Response{}, &Error{HTTPStatus: 500, Message: "boom"}
|
||||
}
|
||||
|
||||
func (e *credentialRetryLimitExecutor) HttpRequest(context.Context, *Auth, *http.Request) (*http.Response, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *credentialRetryLimitExecutor) recordCall() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.calls++
|
||||
}
|
||||
|
||||
func (e *credentialRetryLimitExecutor) Calls() int {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.calls
|
||||
}
|
||||
|
||||
func newCredentialRetryLimitTestManager(t *testing.T, maxRetryCredentials int) (*Manager, *credentialRetryLimitExecutor) {
|
||||
t.Helper()
|
||||
|
||||
m := NewManager(nil, nil, nil)
|
||||
m.SetRetryConfig(0, 0, maxRetryCredentials)
|
||||
|
||||
executor := &credentialRetryLimitExecutor{id: "claude"}
|
||||
m.RegisterExecutor(executor)
|
||||
|
||||
auth1 := &Auth{ID: "auth-1", Provider: "claude"}
|
||||
auth2 := &Auth{ID: "auth-2", Provider: "claude"}
|
||||
if _, errRegister := m.Register(context.Background(), auth1); errRegister != nil {
|
||||
t.Fatalf("register auth1: %v", errRegister)
|
||||
}
|
||||
if _, errRegister := m.Register(context.Background(), auth2); errRegister != nil {
|
||||
t.Fatalf("register auth2: %v", errRegister)
|
||||
}
|
||||
|
||||
return m, executor
|
||||
}
|
||||
|
||||
func TestManager_MaxRetryCredentials_LimitsCrossCredentialRetries(t *testing.T) {
|
||||
request := cliproxyexecutor.Request{Model: "test-model"}
|
||||
testCases := []struct {
|
||||
name string
|
||||
invoke func(*Manager) error
|
||||
}{
|
||||
{
|
||||
name: "execute",
|
||||
invoke: func(m *Manager) error {
|
||||
_, errExecute := m.Execute(context.Background(), []string{"claude"}, request, cliproxyexecutor.Options{})
|
||||
return errExecute
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "execute_count",
|
||||
invoke: func(m *Manager) error {
|
||||
_, errExecute := m.ExecuteCount(context.Background(), []string{"claude"}, request, cliproxyexecutor.Options{})
|
||||
return errExecute
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "execute_stream",
|
||||
invoke: func(m *Manager) error {
|
||||
_, errExecute := m.ExecuteStream(context.Background(), []string{"claude"}, request, cliproxyexecutor.Options{})
|
||||
return errExecute
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
limitedManager, limitedExecutor := newCredentialRetryLimitTestManager(t, 1)
|
||||
if errInvoke := tc.invoke(limitedManager); errInvoke == nil {
|
||||
t.Fatalf("expected error for limited retry execution")
|
||||
}
|
||||
if calls := limitedExecutor.Calls(); calls != 1 {
|
||||
t.Fatalf("expected 1 call with max-retry-credentials=1, got %d", calls)
|
||||
}
|
||||
|
||||
unlimitedManager, unlimitedExecutor := newCredentialRetryLimitTestManager(t, 0)
|
||||
if errInvoke := tc.invoke(unlimitedManager); errInvoke == nil {
|
||||
t.Fatalf("expected error for unlimited retry execution")
|
||||
}
|
||||
if calls := unlimitedExecutor.Calls(); calls != 2 {
|
||||
t.Fatalf("expected 2 calls with max-retry-credentials=0, got %d", calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_MarkResult_RespectsAuthDisableCoolingOverride(t *testing.T) {
|
||||
prev := quotaCooldownDisabled.Load()
|
||||
quotaCooldownDisabled.Store(false)
|
||||
|
||||
Reference in New Issue
Block a user