fix: harden pooled model-support fallback state
This commit is contained in:
@@ -111,9 +111,11 @@ func (e *credentialRetryLimitExecutor) Calls() int {
|
||||
type authFallbackExecutor struct {
|
||||
id string
|
||||
|
||||
mu sync.Mutex
|
||||
executeCalls []string
|
||||
executeErrors map[string]error
|
||||
mu sync.Mutex
|
||||
executeCalls []string
|
||||
streamCalls []string
|
||||
executeErrors map[string]error
|
||||
streamFirstErrors map[string]error
|
||||
}
|
||||
|
||||
func (e *authFallbackExecutor) Identifier() string {
|
||||
@@ -131,8 +133,21 @@ func (e *authFallbackExecutor) Execute(_ context.Context, auth *Auth, _ cliproxy
|
||||
return cliproxyexecutor.Response{Payload: []byte(auth.ID)}, nil
|
||||
}
|
||||
|
||||
func (e *authFallbackExecutor) ExecuteStream(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) {
|
||||
return nil, &Error{HTTPStatus: 500, Message: "not implemented"}
|
||||
func (e *authFallbackExecutor) ExecuteStream(_ context.Context, auth *Auth, _ cliproxyexecutor.Request, _ cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) {
|
||||
e.mu.Lock()
|
||||
e.streamCalls = append(e.streamCalls, auth.ID)
|
||||
err := e.streamFirstErrors[auth.ID]
|
||||
e.mu.Unlock()
|
||||
|
||||
ch := make(chan cliproxyexecutor.StreamChunk, 1)
|
||||
if err != nil {
|
||||
ch <- cliproxyexecutor.StreamChunk{Err: err}
|
||||
close(ch)
|
||||
return &cliproxyexecutor.StreamResult{Headers: http.Header{"X-Auth": {auth.ID}}, Chunks: ch}, nil
|
||||
}
|
||||
ch <- cliproxyexecutor.StreamChunk{Payload: []byte(auth.ID)}
|
||||
close(ch)
|
||||
return &cliproxyexecutor.StreamResult{Headers: http.Header{"X-Auth": {auth.ID}}, Chunks: ch}, nil
|
||||
}
|
||||
|
||||
func (e *authFallbackExecutor) Refresh(_ context.Context, auth *Auth) (*Auth, error) {
|
||||
@@ -155,6 +170,14 @@ func (e *authFallbackExecutor) ExecuteCalls() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *authFallbackExecutor) StreamCalls() []string {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
out := make([]string, len(e.streamCalls))
|
||||
copy(out, e.streamCalls)
|
||||
return out
|
||||
}
|
||||
|
||||
func newCredentialRetryLimitTestManager(t *testing.T, maxRetryCredentials int) (*Manager, *credentialRetryLimitExecutor) {
|
||||
t.Helper()
|
||||
|
||||
@@ -308,6 +331,83 @@ func TestManager_ModelSupportBadRequest_FallsBackAndSuspendsAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerExecuteStream_ModelSupportBadRequestFallsBackAndSuspendsAuth(t *testing.T) {
|
||||
m := NewManager(nil, nil, nil)
|
||||
executor := &authFallbackExecutor{
|
||||
id: "claude",
|
||||
streamFirstErrors: map[string]error{
|
||||
"aa-bad-auth": &Error{
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Message: "invalid_request_error: The requested model is not supported.",
|
||||
},
|
||||
},
|
||||
}
|
||||
m.RegisterExecutor(executor)
|
||||
|
||||
model := "claude-opus-4-6"
|
||||
badAuth := &Auth{ID: "aa-bad-auth", Provider: "claude"}
|
||||
goodAuth := &Auth{ID: "bb-good-auth", Provider: "claude"}
|
||||
|
||||
reg := registry.GetGlobalRegistry()
|
||||
reg.RegisterClient(badAuth.ID, "claude", []*registry.ModelInfo{{ID: model}})
|
||||
reg.RegisterClient(goodAuth.ID, "claude", []*registry.ModelInfo{{ID: model}})
|
||||
t.Cleanup(func() {
|
||||
reg.UnregisterClient(badAuth.ID)
|
||||
reg.UnregisterClient(goodAuth.ID)
|
||||
})
|
||||
|
||||
if _, errRegister := m.Register(context.Background(), badAuth); errRegister != nil {
|
||||
t.Fatalf("register bad auth: %v", errRegister)
|
||||
}
|
||||
if _, errRegister := m.Register(context.Background(), goodAuth); errRegister != nil {
|
||||
t.Fatalf("register good auth: %v", errRegister)
|
||||
}
|
||||
|
||||
request := cliproxyexecutor.Request{Model: model}
|
||||
for i := 0; i < 2; i++ {
|
||||
streamResult, errExecute := m.ExecuteStream(context.Background(), []string{"claude"}, request, cliproxyexecutor.Options{})
|
||||
if errExecute != nil {
|
||||
t.Fatalf("execute stream %d error = %v, want success", i, errExecute)
|
||||
}
|
||||
var payload []byte
|
||||
for chunk := range streamResult.Chunks {
|
||||
if chunk.Err != nil {
|
||||
t.Fatalf("execute stream %d chunk error = %v, want success", i, chunk.Err)
|
||||
}
|
||||
payload = append(payload, chunk.Payload...)
|
||||
}
|
||||
if string(payload) != goodAuth.ID {
|
||||
t.Fatalf("execute stream %d payload = %q, want %q", i, string(payload), goodAuth.ID)
|
||||
}
|
||||
}
|
||||
|
||||
got := executor.StreamCalls()
|
||||
want := []string{badAuth.ID, goodAuth.ID, goodAuth.ID}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("stream calls = %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("stream call %d auth = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
|
||||
updatedBad, ok := m.GetByID(badAuth.ID)
|
||||
if !ok || updatedBad == nil {
|
||||
t.Fatalf("expected bad auth to remain registered")
|
||||
}
|
||||
state := updatedBad.ModelStates[model]
|
||||
if state == nil {
|
||||
t.Fatalf("expected model state for %q", model)
|
||||
}
|
||||
if !state.Unavailable {
|
||||
t.Fatalf("expected bad auth model state to be unavailable")
|
||||
}
|
||||
if state.NextRetryAfter.IsZero() {
|
||||
t.Fatalf("expected bad auth model state cooldown to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_MarkResult_RespectsAuthDisableCoolingOverride(t *testing.T) {
|
||||
prev := quotaCooldownDisabled.Load()
|
||||
quotaCooldownDisabled.Store(false)
|
||||
|
||||
Reference in New Issue
Block a user