mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-20 07:51:52 +00:00
233 lines
6.9 KiB
Go
233 lines
6.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
|
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, 0)
|
|
|
|
model := "test-model"
|
|
next := time.Now().Add(5 * time.Second)
|
|
|
|
auth := &Auth{
|
|
ID: "auth-1",
|
|
Provider: "claude",
|
|
Metadata: map[string]any{
|
|
"request_retry": float64(0),
|
|
},
|
|
ModelStates: map[string]*ModelState{
|
|
model: {
|
|
Unavailable: true,
|
|
Status: StatusError,
|
|
NextRetryAfter: next,
|
|
},
|
|
},
|
|
}
|
|
if _, errRegister := m.Register(context.Background(), auth); errRegister != nil {
|
|
t.Fatalf("register auth: %v", errRegister)
|
|
}
|
|
|
|
_, _, 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)
|
|
}
|
|
|
|
auth.Metadata["request_retry"] = float64(1)
|
|
if _, errUpdate := m.Update(context.Background(), auth); errUpdate != nil {
|
|
t.Fatalf("update auth: %v", errUpdate)
|
|
}
|
|
|
|
wait, shouldRetry = m.shouldRetryAfterError(&Error{HTTPStatus: 500, Message: "boom"}, 0, []string{"claude"}, model, maxWait)
|
|
if !shouldRetry {
|
|
t.Fatalf("expected shouldRetry=true for request_retry=1, got false")
|
|
}
|
|
if wait <= 0 {
|
|
t.Fatalf("expected wait > 0, got %v", wait)
|
|
}
|
|
|
|
_, shouldRetry = m.shouldRetryAfterError(&Error{HTTPStatus: 500, Message: "boom"}, 1, []string{"claude"}, model, maxWait)
|
|
if shouldRetry {
|
|
t.Fatalf("expected shouldRetry=false on attempt=1 for request_retry=1, got true")
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
baseID := uuid.NewString()
|
|
auth1 := &Auth{ID: baseID + "-auth-1", Provider: "claude"}
|
|
auth2 := &Auth{ID: baseID + "-auth-2", Provider: "claude"}
|
|
|
|
// Auth selection requires that the global model registry knows each credential supports the model.
|
|
reg := registry.GetGlobalRegistry()
|
|
reg.RegisterClient(auth1.ID, "claude", []*registry.ModelInfo{{ID: "test-model"}})
|
|
reg.RegisterClient(auth2.ID, "claude", []*registry.ModelInfo{{ID: "test-model"}})
|
|
t.Cleanup(func() {
|
|
reg.UnregisterClient(auth1.ID)
|
|
reg.UnregisterClient(auth2.ID)
|
|
})
|
|
|
|
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)
|
|
t.Cleanup(func() { quotaCooldownDisabled.Store(prev) })
|
|
|
|
m := NewManager(nil, nil, nil)
|
|
|
|
auth := &Auth{
|
|
ID: "auth-1",
|
|
Provider: "claude",
|
|
Metadata: map[string]any{
|
|
"disable_cooling": true,
|
|
},
|
|
}
|
|
if _, errRegister := m.Register(context.Background(), auth); errRegister != nil {
|
|
t.Fatalf("register auth: %v", errRegister)
|
|
}
|
|
|
|
model := "test-model"
|
|
m.MarkResult(context.Background(), Result{
|
|
AuthID: "auth-1",
|
|
Provider: "claude",
|
|
Model: model,
|
|
Success: false,
|
|
Error: &Error{HTTPStatus: 500, Message: "boom"},
|
|
})
|
|
|
|
updated, ok := m.GetByID("auth-1")
|
|
if !ok || updated == nil {
|
|
t.Fatalf("expected auth to be present")
|
|
}
|
|
state := updated.ModelStates[model]
|
|
if state == nil {
|
|
t.Fatalf("expected model state to be present")
|
|
}
|
|
if !state.NextRetryAfter.IsZero() {
|
|
t.Fatalf("expected NextRetryAfter to be zero when disable_cooling=true, got %v", state.NextRetryAfter)
|
|
}
|
|
}
|