Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/runtime/executor/antigravity_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ const (
antigravityCreditsHintRefreshTimeout = 5 * time.Second
antigravityShortQuotaCooldownThreshold = 5 * time.Minute
antigravityInstantRetryThreshold = 3 * time.Second
// antigravityQuotaCooldownCeiling bounds hinted reset delays (weekly ~166h)
// so a single model cannot latch NextRecoverAt / shortCool for days.
antigravityQuotaCooldownCeiling = 24 * time.Hour
// systemInstruction = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**"
)

Expand Down
126 changes: 126 additions & 0 deletions internal/runtime/executor/antigravity_executor_cooldown_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package executor

import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"

"github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
)

func TestAntigravityExecutor_WeeklyModelResetDoesNotBlockSibling(t *testing.T) {
resetAntigravityCreditsRetryState()
t.Cleanup(resetAntigravityCreditsRetryState)

const (
exhaustedModel = "claude-opus-4-6"
siblingModel = "gemini-3.8-flash"
)

var opusAttempts, flashAttempts atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, errRead := io.ReadAll(r.Body)
if errRead != nil {
http.Error(w, "failed to read sanitized test request", http.StatusBadRequest)
return
}
switch {
case strings.Contains(string(body), `"model":"`+exhaustedModel+`"`):
opusAttempts.Add(1)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
_, _ = w.Write(antigravityWeeklyReset429Body("RATE_LIMIT_EXCEEDED", "166h9m16s"))
case strings.Contains(string(body), `"model":"`+siblingModel+`"`):
flashAttempts.Add(1)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"response":{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]}}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}}`))
default:
http.Error(w, "unexpected sanitized test model", http.StatusBadRequest)
}
}))
t.Cleanup(server.Close)

cfg := &config.Config{DisableCooling: false}
manager := cliproxyauth.NewManager(nil, nil, nil)
manager.SetRetryConfig(0, 0, 0)
manager.SetConfig(cfg)
manager.RegisterExecutor(NewAntigravityExecutor(cfg))

auth := &cliproxyauth.Auth{
ID: uuid.NewString() + "-antigravity-weekly-scope",
Provider: "antigravity",
Attributes: map[string]string{
"base_url": server.URL,
},
Metadata: map[string]any{
"access_token": "token",
"project_id": "project-1",
"expired": time.Now().Add(time.Hour).Format(time.RFC3339),
},
}
reg := registry.GetGlobalRegistry()
reg.RegisterClient(auth.ID, "antigravity", []*registry.ModelInfo{{ID: exhaustedModel}, {ID: siblingModel}})
t.Cleanup(func() { reg.UnregisterClient(auth.ID) })
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("register auth: %v", err)
}

payloadOpus := []byte(`{"request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`)
_, errOpus := manager.Execute(context.Background(), []string{"antigravity"}, cliproxyexecutor.Request{
Model: exhaustedModel,
Payload: payloadOpus,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatAntigravity})
if errOpus == nil {
t.Fatal("expected weekly model reset to return 429")
}
if got := opusAttempts.Load(); got != 1 {
t.Fatalf("exhausted model upstream attempts = %d, want 1", got)
}

var retry interface{ RetryAfter() *time.Duration }
if !errors.As(errOpus, &retry) || retry == nil || retry.RetryAfter() == nil {
t.Fatalf("expected RetryAfter on weekly reset error, got %v", errOpus)
}
if got := *retry.RetryAfter(); got != antigravityQuotaCooldownCeiling {
t.Fatalf("RetryAfter = %v, want capped %v", got, antigravityQuotaCooldownCeiling)
}

updatedAuth, ok := manager.GetByID(auth.ID)
if !ok || updatedAuth == nil {
t.Fatal("auth not found")
}
opusState := updatedAuth.ModelStates[exhaustedModel]
if opusState == nil {
t.Fatal("exhausted model state not found")
}
if opusState.Quota.NextRecoverAt.After(time.Now().Add(antigravityQuotaCooldownCeiling + time.Minute)) {
t.Fatalf("exhausted model cooldown too long: NextRecoverAt = %v", opusState.Quota.NextRecoverAt)
}
if updatedAuth.Quota.Reason == "credential_quota" {
t.Fatalf("weekly model reset parked the credential: %+v", updatedAuth.Quota)
}

payloadFlash := []byte(`{"request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`)
_, errFlash := manager.Execute(context.Background(), []string{"antigravity"}, cliproxyexecutor.Request{
Model: siblingModel,
Payload: payloadFlash,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatAntigravity})
if errFlash != nil {
t.Fatalf("expected sibling model to reach upstream on the same credential, got: %v", errFlash)
}
if got := flashAttempts.Load(); got != 1 {
t.Fatalf("sibling model upstream attempts = %d, want 1", got)
}
}
20 changes: 17 additions & 3 deletions internal/runtime/executor/antigravity_executor_credits.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func decideAntigravity429(body []byte) antigravity429Decision {
}

if retryAfter, parseErr := helps.ParseRetryDelay(body); parseErr == nil && retryAfter != nil {
decision.retryAfter = retryAfter
decision.retryAfter = capAntigravityRetryAfter(retryAfter)
}

status := strings.TrimSpace(gjson.GetBytes(body, "error.status").String())
Expand Down Expand Up @@ -251,7 +251,10 @@ func decideAntigravity429(body []byte) antigravity429Decision {
case *decision.retryAfter < antigravityShortQuotaCooldownThreshold:
decision.kind = antigravity429DecisionShortCooldownSwitchAuth
default:
decision.kind = antigravity429DecisionFullQuotaExhausted
// Long per-model capacity resets (weekly ~166h) are still
// RATE_LIMIT_EXCEEDED. Rotate this model; do not treat the
// credential as fully exhausted.
decision.kind = antigravity429DecisionShortCooldownSwitchAuth
}
return decision
}
Expand Down Expand Up @@ -337,11 +340,22 @@ func newAntigravityStatusErr(statusCode int, body []byte) statusErr {
err := statusErr{code: statusCode, msg: string(body)}
if statusCode == http.StatusTooManyRequests {
if retryAfter, parseErr := helps.ParseRetryDelay(body); parseErr == nil && retryAfter != nil {
err.retryAfter = retryAfter
err.retryAfter = capAntigravityRetryAfter(retryAfter)
}
}
return err
}

func capAntigravityRetryAfter(d *time.Duration) *time.Duration {
if d == nil {
return nil
}
if *d <= antigravityQuotaCooldownCeiling {
return d
}
capped := antigravityQuotaCooldownCeiling
return &capped
}
func (e *AntigravityExecutor) maybeRefreshAntigravityCreditsHint(ctx context.Context, auth *cliproxyauth.Auth, accessToken string) {
if e == nil || auth == nil || !antigravityCreditsRetryEnabled(e.cfg) || antigravityCoolingDisabled(auth, e.cfg) {
return
Expand Down
84 changes: 84 additions & 0 deletions internal/runtime/executor/antigravity_executor_credits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,90 @@ func TestClassifyAntigravity429(t *testing.T) {
t.Fatalf("classifyAntigravity429() = %q, want %q", got, antigravity429SoftRateLimit)
}
})

t.Run("weekly rate-limit hint is capped and stays model-scoped", func(t *testing.T) {
body := antigravityWeeklyReset429Body("RATE_LIMIT_EXCEEDED", "166h9m16s")
if got := classifyAntigravity429(body); got != antigravity429RateLimited {
t.Fatalf("classifyAntigravity429() = %q, want %q", got, antigravity429RateLimited)
}
decision := decideAntigravity429(body)
if decision.kind != antigravity429DecisionShortCooldownSwitchAuth {
t.Fatalf("decideAntigravity429().kind = %q, want %q", decision.kind, antigravity429DecisionShortCooldownSwitchAuth)
}
if decision.retryAfter == nil {
t.Fatal("decideAntigravity429().retryAfter = nil")
}
if *decision.retryAfter != antigravityQuotaCooldownCeiling {
t.Fatalf("decideAntigravity429().retryAfter = %v, want capped %v", *decision.retryAfter, antigravityQuotaCooldownCeiling)
}
})

t.Run("short rate-limit rotate-account delay is unchanged", func(t *testing.T) {
body := antigravityWeeklyReset429Body("RATE_LIMIT_EXCEEDED", "90s")
decision := decideAntigravity429(body)
if decision.kind != antigravity429DecisionShortCooldownSwitchAuth {
t.Fatalf("decideAntigravity429().kind = %q, want %q", decision.kind, antigravity429DecisionShortCooldownSwitchAuth)
}
if decision.retryAfter == nil || *decision.retryAfter != 90*time.Second {
t.Fatalf("decideAntigravity429().retryAfter = %v, want 90s", decision.retryAfter)
}
})

t.Run("true full quota stays exhausted and is still capped", func(t *testing.T) {
body := antigravityWeeklyReset429Body("QUOTA_EXHAUSTED", "166h27m18s")
if got := classifyAntigravity429(body); got != antigravity429QuotaExhausted {
t.Fatalf("classifyAntigravity429() = %q, want %q", got, antigravity429QuotaExhausted)
}
decision := decideAntigravity429(body)
if decision.kind != antigravity429DecisionFullQuotaExhausted {
t.Fatalf("decideAntigravity429().kind = %q, want %q", decision.kind, antigravity429DecisionFullQuotaExhausted)
}
if decision.retryAfter == nil {
t.Fatal("decideAntigravity429().retryAfter = nil")
}
if *decision.retryAfter != antigravityQuotaCooldownCeiling {
t.Fatalf("decideAntigravity429().retryAfter = %v, want capped %v", *decision.retryAfter, antigravityQuotaCooldownCeiling)
}
})
}

func TestNewAntigravityStatusErr_WeeklyResetHintIsCapped(t *testing.T) {
body := antigravityWeeklyReset429Body("RATE_LIMIT_EXCEEDED", "166h9m16s")
err := newAntigravityStatusErr(http.StatusTooManyRequests, body)
if err.retryAfter == nil {
t.Fatal("retryAfter = nil")
}
if *err.retryAfter != antigravityQuotaCooldownCeiling {
t.Fatalf("retryAfter = %v, want capped %v", *err.retryAfter, antigravityQuotaCooldownCeiling)
}
if err.IsCredentialScoped() {
t.Fatal("weekly model reset was credential-scoped")
}
}

func antigravityWeeklyReset429Body(reason, delay string) []byte {
return []byte(`{
"error": {
"code": 429,
"message": "You have exhausted your capacity on this model. Your quota will reset after ` + delay + `.",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "` + reason + `",
"domain": "cloudcode-pa.googleapis.com",
"metadata": {
"model": "claude-opus-4-6",
"quotaResetDelay": "` + delay + `"
}
},
{
"@type": "type.googleapis.com/google.rpc.RetryInfo",
"retryDelay": "` + delay + `"
}
]
}
}`)
}

func TestInjectEnabledCreditTypes(t *testing.T) {
Expand Down
Loading