From bfdc0b3989a1f089555994491430ddb2ae3b964b Mon Sep 17 00:00:00 2001 From: sususu98 Date: Wed, 13 May 2026 18:17:22 +0800 Subject: [PATCH] fix: scope antigravity credits fallback gate --- sdk/cliproxy/auth/antigravity_credits_test.go | 84 +++++++++++++++++++ sdk/cliproxy/auth/conductor.go | 25 +++--- 2 files changed, 95 insertions(+), 14 deletions(-) diff --git a/sdk/cliproxy/auth/antigravity_credits_test.go b/sdk/cliproxy/auth/antigravity_credits_test.go index 34a475dc6..59d5aaa62 100644 --- a/sdk/cliproxy/auth/antigravity_credits_test.go +++ b/sdk/cliproxy/auth/antigravity_credits_test.go @@ -4,12 +4,14 @@ import ( "context" "fmt" "net/http" + "strings" "testing" "time" internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" + log "github.com/sirupsen/logrus" ) type antigravityCreditsFallbackExecutor struct { @@ -48,6 +50,43 @@ func (e *antigravityCreditsFallbackExecutor) HttpRequest(context.Context, *Auth, return nil, &Error{HTTPStatus: http.StatusNotImplemented, Message: "HttpRequest not implemented"} } +type codexOnlyFailureExecutor struct{} + +func (codexOnlyFailureExecutor) Identifier() string { return "codex" } + +func (codexOnlyFailureExecutor) Execute(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { + return cliproxyexecutor.Response{}, &Error{HTTPStatus: http.StatusTooManyRequests, Message: "codex quota exhausted"} +} + +func (codexOnlyFailureExecutor) ExecuteStream(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) { + return nil, &Error{HTTPStatus: http.StatusTooManyRequests, Message: "codex quota exhausted"} +} + +func (codexOnlyFailureExecutor) Refresh(_ context.Context, auth *Auth) (*Auth, error) { + return auth, nil +} + +func (codexOnlyFailureExecutor) CountTokens(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { + return cliproxyexecutor.Response{}, &Error{HTTPStatus: http.StatusTooManyRequests, Message: "codex quota exhausted"} +} + +func (codexOnlyFailureExecutor) HttpRequest(context.Context, *Auth, *http.Request) (*http.Response, error) { + return nil, &Error{HTTPStatus: http.StatusTooManyRequests, Message: "codex quota exhausted"} +} + +type captureLogHook struct { + messages []string +} + +func (h *captureLogHook) Levels() []log.Level { + return log.AllLevels +} + +func (h *captureLogHook) Fire(entry *log.Entry) error { + h.messages = append(h.messages, entry.Message) + return nil +} + func TestManagerExecuteStream_AntigravityCreditsFallbackAfterBootstrap429(t *testing.T) { const model = "claude-opus-4-6-thinking" executor := &antigravityCreditsFallbackExecutor{} @@ -88,6 +127,51 @@ func TestManagerExecuteStream_AntigravityCreditsFallbackAfterBootstrap429(t *tes } } +func TestManagerExecuteStream_CodexOnlyDoesNotEnterAntigravityCreditsFallback(t *testing.T) { + const model = "gpt-5.5" + logger := log.StandardLogger() + oldLevel := logger.GetLevel() + oldHooks := logger.ReplaceHooks(make(log.LevelHooks)) + hook := &captureLogHook{} + logger.SetLevel(log.DebugLevel) + logger.AddHook(hook) + t.Cleanup(func() { + logger.SetLevel(oldLevel) + logger.ReplaceHooks(oldHooks) + }) + + manager := NewManager(nil, nil, nil) + manager.SetConfig(&internalconfig.Config{ + QuotaExceeded: internalconfig.QuotaExceeded{AntigravityCredits: true}, + }) + manager.RegisterExecutor(codexOnlyFailureExecutor{}) + manager.RegisterExecutor(&antigravityCreditsFallbackExecutor{}) + reg := registry.GetGlobalRegistry() + reg.RegisterClient("codex-only", "codex", []*registry.ModelInfo{{ID: model}}) + reg.RegisterClient("ag-unrelated", "antigravity", []*registry.ModelInfo{{ID: "gemini-3-flash"}}) + t.Cleanup(func() { + reg.UnregisterClient("codex-only") + reg.UnregisterClient("ag-unrelated") + }) + if _, errRegister := manager.Register(context.Background(), &Auth{ID: "codex-only", Provider: "codex"}); errRegister != nil { + t.Fatalf("register codex auth: %v", errRegister) + } + if _, errRegister := manager.Register(context.Background(), &Auth{ID: "ag-unrelated", Provider: "antigravity"}); errRegister != nil { + t.Fatalf("register antigravity auth: %v", errRegister) + } + + _, errExecute := manager.ExecuteStream(context.Background(), []string{"codex"}, cliproxyexecutor.Request{Model: model}, cliproxyexecutor.Options{}) + if errExecute == nil { + t.Fatal("expected codex execution failure") + } + + for _, message := range hook.messages { + if strings.Contains(message, "shouldAttemptAntigravityCreditsFallback") { + t.Fatalf("codex-only request entered antigravity credits fallback gate; messages=%v", hook.messages) + } + } +} + func TestStatusCodeFromError_UnwrapsStreamBootstrap429(t *testing.T) { bootstrapErr := newStreamBootstrapError(&Error{HTTPStatus: http.StatusTooManyRequests, Message: "quota exhausted"}, nil) wrappedErr := fmt.Errorf("conductor stream failed: %w", bootstrapErr) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index d44809b0c..2d56390ae 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -1238,7 +1238,7 @@ func (m *Manager) Execute(ctx context.Context, providers []string, req cliproxye } } if lastErr != nil { - if shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) { + if hasAntigravityProvider(normalized) && shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) { if resp, ok := m.tryAntigravityCreditsExecute(ctx, req, opts); ok { return resp, nil } @@ -1304,7 +1304,7 @@ func (m *Manager) ExecuteStream(ctx context.Context, providers []string, req cli } } if lastErr != nil { - if shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) { + if hasAntigravityProvider(normalized) && shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) { if result, ok := m.tryAntigravityCreditsExecuteStream(ctx, req, opts); ok { return result, nil } @@ -3513,6 +3513,15 @@ type creditsCandidateEntry struct { provider string } +func hasAntigravityProvider(providers []string) bool { + for _, p := range providers { + if strings.EqualFold(strings.TrimSpace(p), "antigravity") { + return true + } + } + return false +} + func shouldAttemptAntigravityCreditsFallback(m *Manager, lastErr error, providers []string) bool { status := statusCodeFromError(lastErr) log.WithFields(log.Fields{ @@ -3523,18 +3532,6 @@ func shouldAttemptAntigravityCreditsFallback(m *Manager, lastErr error, provider if m == nil || lastErr == nil { return false } - if len(providers) > 0 { - hasAntigravity := false - for _, p := range providers { - if strings.EqualFold(strings.TrimSpace(p), "antigravity") { - hasAntigravity = true - break - } - } - if !hasAntigravity { - return false - } - } cfg, _ := m.runtimeConfig.Load().(*internalconfig.Config) if cfg == nil || !cfg.QuotaExceeded.AntigravityCredits { return false