fix: scope antigravity credits fallback gate
This commit is contained in:
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type antigravityCreditsFallbackExecutor struct {
|
type antigravityCreditsFallbackExecutor struct {
|
||||||
@@ -48,6 +50,43 @@ func (e *antigravityCreditsFallbackExecutor) HttpRequest(context.Context, *Auth,
|
|||||||
return nil, &Error{HTTPStatus: http.StatusNotImplemented, Message: "HttpRequest not implemented"}
|
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) {
|
func TestManagerExecuteStream_AntigravityCreditsFallbackAfterBootstrap429(t *testing.T) {
|
||||||
const model = "claude-opus-4-6-thinking"
|
const model = "claude-opus-4-6-thinking"
|
||||||
executor := &antigravityCreditsFallbackExecutor{}
|
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) {
|
func TestStatusCodeFromError_UnwrapsStreamBootstrap429(t *testing.T) {
|
||||||
bootstrapErr := newStreamBootstrapError(&Error{HTTPStatus: http.StatusTooManyRequests, Message: "quota exhausted"}, nil)
|
bootstrapErr := newStreamBootstrapError(&Error{HTTPStatus: http.StatusTooManyRequests, Message: "quota exhausted"}, nil)
|
||||||
wrappedErr := fmt.Errorf("conductor stream failed: %w", bootstrapErr)
|
wrappedErr := fmt.Errorf("conductor stream failed: %w", bootstrapErr)
|
||||||
|
|||||||
@@ -1238,7 +1238,7 @@ func (m *Manager) Execute(ctx context.Context, providers []string, req cliproxye
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
if shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) {
|
if hasAntigravityProvider(normalized) && shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) {
|
||||||
if resp, ok := m.tryAntigravityCreditsExecute(ctx, req, opts); ok {
|
if resp, ok := m.tryAntigravityCreditsExecute(ctx, req, opts); ok {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -1304,7 +1304,7 @@ func (m *Manager) ExecuteStream(ctx context.Context, providers []string, req cli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
if shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) {
|
if hasAntigravityProvider(normalized) && shouldAttemptAntigravityCreditsFallback(m, lastErr, normalized) {
|
||||||
if result, ok := m.tryAntigravityCreditsExecuteStream(ctx, req, opts); ok {
|
if result, ok := m.tryAntigravityCreditsExecuteStream(ctx, req, opts); ok {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -3513,6 +3513,15 @@ type creditsCandidateEntry struct {
|
|||||||
provider string
|
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 {
|
func shouldAttemptAntigravityCreditsFallback(m *Manager, lastErr error, providers []string) bool {
|
||||||
status := statusCodeFromError(lastErr)
|
status := statusCodeFromError(lastErr)
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
@@ -3523,18 +3532,6 @@ func shouldAttemptAntigravityCreditsFallback(m *Manager, lastErr error, provider
|
|||||||
if m == nil || lastErr == nil {
|
if m == nil || lastErr == nil {
|
||||||
return false
|
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)
|
cfg, _ := m.runtimeConfig.Load().(*internalconfig.Config)
|
||||||
if cfg == nil || !cfg.QuotaExceeded.AntigravityCredits {
|
if cfg == nil || !cfg.QuotaExceeded.AntigravityCredits {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user