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: 1 addition & 2 deletions internal/agent/antigravity_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,7 @@ func (a *AntigravityAgent) poll(ctx context.Context) {
}
orderedValues := []float64{
valuesByKey[api.AntigravityQuotaGroupClaudeGPT],
valuesByKey[api.AntigravityQuotaGroupGeminiPro],
valuesByKey[api.AntigravityQuotaGroupGeminiFlash],
valuesByKey[api.AntigravityQuotaGroupGemini],
}
a.sm.ReportPoll(orderedValues)
}
Expand Down
37 changes: 27 additions & 10 deletions internal/api/antigravity_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,34 @@ import (
)

const (
AntigravityQuotaGroupClaudeGPT = "antigravity_claude_gpt"
AntigravityQuotaGroupClaudeGPT = "antigravity_claude_gpt"
// AntigravityQuotaGroupGemini is the single shared Gemini pool (Pro + Flash).
// Antigravity's GetUserStatus and RetrieveUserQuotaSummary both treat Gemini Pro
// and Flash as one shared weekly/5h bucket; splitting them produced duplicate cards.
AntigravityQuotaGroupGemini = "antigravity_gemini"

// Legacy group keys kept for normalize/display of historical UI groupBy values.
AntigravityQuotaGroupGeminiPro = "antigravity_gemini_pro"
AntigravityQuotaGroupGeminiFlash = "antigravity_gemini_flash"
)

var antigravityQuotaGroupOrder = []string{
AntigravityQuotaGroupClaudeGPT,
AntigravityQuotaGroupGeminiPro,
AntigravityQuotaGroupGeminiFlash,
AntigravityQuotaGroupGemini,
}

var antigravityQuotaGroupDisplayNames = map[string]string{
AntigravityQuotaGroupClaudeGPT: "Claude + GPT Quota",
AntigravityQuotaGroupGeminiPro: "Gemini Pro Quota",
AntigravityQuotaGroupGeminiFlash: "Gemini Flash Quota",
AntigravityQuotaGroupGemini: "Gemini Quota",
AntigravityQuotaGroupGeminiPro: "Gemini Quota", // legacy alias
AntigravityQuotaGroupGeminiFlash: "Gemini Quota", // legacy alias
}

var antigravityQuotaGroupColors = map[string]string{
AntigravityQuotaGroupClaudeGPT: "#D97757",
AntigravityQuotaGroupGeminiPro: "#10B981",
AntigravityQuotaGroupGeminiFlash: "#3B82F6",
AntigravityQuotaGroupGemini: "#10B981",
AntigravityQuotaGroupGeminiPro: "#10B981", // legacy alias
AntigravityQuotaGroupGeminiFlash: "#10B981", // legacy alias
}

// AntigravityGroupedQuota represents one canonical logical quota group.
Expand Down Expand Up @@ -73,17 +80,27 @@ func AntigravityQuotaGroupForModel(modelID, label string) string {
text := modelLower + " " + labelLower

switch {
case strings.Contains(text, "gemini") && strings.Contains(text, "flash"):
return AntigravityQuotaGroupGeminiFlash
case strings.Contains(text, "gemini"):
return AntigravityQuotaGroupGeminiPro
// Pro and Flash share one Antigravity quota pool.
return AntigravityQuotaGroupGemini
case strings.Contains(text, "claude"), strings.Contains(text, "gpt"):
return AntigravityQuotaGroupClaudeGPT
default:
return AntigravityQuotaGroupClaudeGPT
}
}

// NormalizeAntigravityQuotaGroup maps legacy Pro/Flash group keys onto the
// canonical shared Gemini pool key.
func NormalizeAntigravityQuotaGroup(groupKey string) string {
switch groupKey {
case AntigravityQuotaGroupGeminiPro, AntigravityQuotaGroupGeminiFlash:
return AntigravityQuotaGroupGemini
default:
return groupKey
}
}

func GroupAntigravityModelsByLogicalQuota(models []AntigravityModelQuota) []AntigravityGroupedQuota {
type accumulator struct {
modelIDs []string
Expand Down
50 changes: 33 additions & 17 deletions internal/api/antigravity_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,9 @@ func TestAntigravityQuotaGroupForModel(t *testing.T) {
}{
{name: "Claude model", modelID: "claude-4-5-sonnet", want: AntigravityQuotaGroupClaudeGPT},
{name: "GPT model", modelID: "gpt-5", want: AntigravityQuotaGroupClaudeGPT},
{name: "Gemini Pro model", modelID: "gemini-3-pro", want: AntigravityQuotaGroupGeminiPro},
{name: "Gemini Flash model", modelID: "gemini-3-flash", want: AntigravityQuotaGroupGeminiFlash},
{name: "Label fallback Gemini Flash", modelID: "unknown", label: "Gemini Flash Lite", want: AntigravityQuotaGroupGeminiFlash},
{name: "Gemini Pro model", modelID: "gemini-3-pro", want: AntigravityQuotaGroupGemini},
{name: "Gemini Flash model", modelID: "gemini-3-flash", want: AntigravityQuotaGroupGemini},
{name: "Label fallback Gemini Flash", modelID: "unknown", label: "Gemini Flash Lite", want: AntigravityQuotaGroupGemini},
{name: "Unknown defaults to Claude+GPT", modelID: "other", label: "Other", want: AntigravityQuotaGroupClaudeGPT},
}

Expand Down Expand Up @@ -401,18 +401,15 @@ func TestGroupAntigravityModelsByLogicalQuota(t *testing.T) {
}

groups := GroupAntigravityModelsByLogicalQuota(models)
if len(groups) != 3 {
t.Fatalf("expected 3 groups, got %d", len(groups))
if len(groups) != 2 {
t.Fatalf("expected 2 groups, got %d", len(groups))
}

if groups[0].GroupKey != AntigravityQuotaGroupClaudeGPT {
t.Fatalf("expected first group %q, got %q", AntigravityQuotaGroupClaudeGPT, groups[0].GroupKey)
}
if groups[1].GroupKey != AntigravityQuotaGroupGeminiPro {
t.Fatalf("expected second group %q, got %q", AntigravityQuotaGroupGeminiPro, groups[1].GroupKey)
}
if groups[2].GroupKey != AntigravityQuotaGroupGeminiFlash {
t.Fatalf("expected third group %q, got %q", AntigravityQuotaGroupGeminiFlash, groups[2].GroupKey)
if groups[1].GroupKey != AntigravityQuotaGroupGemini {
t.Fatalf("expected second group %q, got %q", AntigravityQuotaGroupGemini, groups[1].GroupKey)
}

claudeGPT := groups[0]
Expand All @@ -432,19 +429,26 @@ func TestGroupAntigravityModelsByLogicalQuota(t *testing.T) {
t.Fatalf("expected Claude+GPT color #D97757, got %q", claudeGPT.Color)
}

geminiFlash := groups[2]
if geminiFlash.RemainingFraction < 0.199 || geminiFlash.RemainingFraction > 0.201 {
t.Fatalf("expected Gemini Flash remaining fraction ~0.20, got %.4f", geminiFlash.RemainingFraction)
// Pro (0.60) + Flash (0.20) share one Gemini pool → average remaining 0.40 / usage 60.
gemini := groups[1]
if gemini.DisplayName != "Gemini Quota" {
t.Fatalf("expected Gemini display name, got %q", gemini.DisplayName)
}
if gemini.RemainingFraction < 0.399 || gemini.RemainingFraction > 0.401 {
t.Fatalf("expected shared Gemini remaining fraction ~0.40, got %.4f", gemini.RemainingFraction)
}
if geminiFlash.UsagePercent < 79.9 || geminiFlash.UsagePercent > 80.1 {
t.Fatalf("expected Gemini Flash usage percent ~80, got %.4f", geminiFlash.UsagePercent)
if gemini.UsagePercent < 59.9 || gemini.UsagePercent > 60.1 {
t.Fatalf("expected shared Gemini usage percent ~60, got %.4f", gemini.UsagePercent)
}
if len(gemini.ModelIDs) != 2 {
t.Fatalf("expected both Gemini Pro and Flash in shared pool, got %v", gemini.ModelIDs)
}
}

func TestGroupAntigravityModelsByLogicalQuota_EmptyStillReturnsFixedGroups(t *testing.T) {
groups := GroupAntigravityModelsByLogicalQuota(nil)
if len(groups) != 3 {
t.Fatalf("expected 3 fixed groups, got %d", len(groups))
if len(groups) != 2 {
t.Fatalf("expected 2 fixed groups, got %d", len(groups))
}
for _, g := range groups {
if g.RemainingFraction != 1.0 {
Expand All @@ -455,3 +459,15 @@ func TestGroupAntigravityModelsByLogicalQuota_EmptyStillReturnsFixedGroups(t *te
}
}
}

func TestNormalizeAntigravityQuotaGroup(t *testing.T) {
if got := NormalizeAntigravityQuotaGroup(AntigravityQuotaGroupGeminiPro); got != AntigravityQuotaGroupGemini {
t.Fatalf("pro alias = %q, want %q", got, AntigravityQuotaGroupGemini)
}
if got := NormalizeAntigravityQuotaGroup(AntigravityQuotaGroupGeminiFlash); got != AntigravityQuotaGroupGemini {
t.Fatalf("flash alias = %q, want %q", got, AntigravityQuotaGroupGemini)
}
if got := NormalizeAntigravityQuotaGroup(AntigravityQuotaGroupClaudeGPT); got != AntigravityQuotaGroupClaudeGPT {
t.Fatalf("claude group should pass through, got %q", got)
}
}
15 changes: 6 additions & 9 deletions internal/api/extra_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,17 +424,14 @@ func TestCodexFloat64_UnmarshalJSON_InvalidValue(t *testing.T) {

func TestAntigravityQuotaGroupOrder_ReturnsCopy(t *testing.T) {
order := AntigravityQuotaGroupOrder()
if len(order) != 3 {
t.Fatalf("expected 3 groups, got %d", len(order))
if len(order) != 2 {
t.Fatalf("expected 2 groups, got %d", len(order))
}
if order[0] != AntigravityQuotaGroupClaudeGPT {
t.Errorf("order[0] = %q, want %q", order[0], AntigravityQuotaGroupClaudeGPT)
}
if order[1] != AntigravityQuotaGroupGeminiPro {
t.Errorf("order[1] = %q, want %q", order[1], AntigravityQuotaGroupGeminiPro)
}
if order[2] != AntigravityQuotaGroupGeminiFlash {
t.Errorf("order[2] = %q, want %q", order[2], AntigravityQuotaGroupGeminiFlash)
if order[1] != AntigravityQuotaGroupGemini {
t.Errorf("order[1] = %q, want %q", order[1], AntigravityQuotaGroupGemini)
}

// Verify it returns a copy - mutation should not affect the original
Expand Down Expand Up @@ -1836,8 +1833,8 @@ func TestGroupAntigravityModelsByLogicalQuota_ExhaustedGroup(t *testing.T) {
}

groups := GroupAntigravityModelsByLogicalQuota(models)
if len(groups) != 3 {
t.Fatalf("expected 3 groups, got %d", len(groups))
if len(groups) != 2 {
t.Fatalf("expected 2 groups, got %d", len(groups))
}

// Find the Claude+GPT group
Expand Down
6 changes: 4 additions & 2 deletions internal/store/antigravity_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ func (s *Store) QueryAntigravityCycleOverview(groupBy string, limit int) ([]Cycl
if groupBy == "" {
groupBy = api.AntigravityQuotaGroupClaudeGPT
}
groupBy = api.NormalizeAntigravityQuotaGroup(groupBy)

if !isAntigravityQuotaGroup(groupBy) {
return nil, fmt.Errorf("invalid antigravity group: %s", groupBy)
Expand Down Expand Up @@ -554,6 +555,7 @@ func (s *Store) QueryAntigravityCycleOverview(groupBy string, limit int) ([]Cycl
}

func (s *Store) QueryAntigravityModelIDsForGroup(groupKey string) ([]string, error) {
groupKey = api.NormalizeAntigravityQuotaGroup(groupKey)
rows, err := s.db.Query(
`SELECT DISTINCT mv.model_id, mv.label
FROM antigravity_model_values mv
Expand Down Expand Up @@ -701,8 +703,8 @@ func (s *Store) getAntigravityGroupedCrossQuotasAt(referenceTime time.Time) ([]C
}

func isAntigravityQuotaGroup(groupKey string) bool {
switch groupKey {
case api.AntigravityQuotaGroupClaudeGPT, api.AntigravityQuotaGroupGeminiPro, api.AntigravityQuotaGroupGeminiFlash:
switch api.NormalizeAntigravityQuotaGroup(groupKey) {
case api.AntigravityQuotaGroupClaudeGPT, api.AntigravityQuotaGroupGemini:
return true
default:
return false
Expand Down
44 changes: 19 additions & 25 deletions internal/store/antigravity_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,20 @@ func TestQueryAntigravityModelIDsForGroup(t *testing.T) {
t.Fatalf("expected 2 claude+gpt IDs, got %d (%v)", len(claudeGPT), claudeGPT)
}

geminiPro, err := s.QueryAntigravityModelIDsForGroup(api.AntigravityQuotaGroupGeminiPro)
gemini, err := s.QueryAntigravityModelIDsForGroup(api.AntigravityQuotaGroupGemini)
if err != nil {
t.Fatalf("failed querying gemini pro IDs: %v", err)
t.Fatalf("failed querying gemini IDs: %v", err)
}
if len(geminiPro) != 1 || geminiPro[0] != "gemini-3-pro" {
t.Fatalf("expected [gemini-3-pro], got %v", geminiPro)
if len(gemini) != 2 {
t.Fatalf("expected 2 shared gemini IDs (pro+flash), got %d (%v)", len(gemini), gemini)
}

geminiFlash, err := s.QueryAntigravityModelIDsForGroup(api.AntigravityQuotaGroupGeminiFlash)
// Legacy pro/flash keys normalize onto the shared Gemini pool.
geminiViaLegacy, err := s.QueryAntigravityModelIDsForGroup(api.AntigravityQuotaGroupGeminiPro)
if err != nil {
t.Fatalf("failed querying gemini flash IDs: %v", err)
t.Fatalf("failed querying via legacy pro key: %v", err)
}
if len(geminiFlash) != 1 || geminiFlash[0] != "gemini-3-flash" {
t.Fatalf("expected [gemini-3-flash], got %v", geminiFlash)
if len(geminiViaLegacy) != 2 {
t.Fatalf("expected legacy pro key to resolve to shared gemini pool, got %v", geminiViaLegacy)
}
}

Expand Down Expand Up @@ -193,26 +193,20 @@ func TestQueryAntigravityCycleOverview_GroupedCrossQuotas(t *testing.T) {
t.Fatalf("expected claude group delta ~20%%, got %.2f", claudeGroup.Delta)
}

geminiProGroup, ok := crossByName[api.AntigravityQuotaGroupGeminiPro]
// Shared Gemini pool: start rem avg (0.90+0.95)/2=0.925 → usage 7.5%;
// end rem avg (0.85+0.90)/2=0.875 → usage 12.5%; delta 5%.
geminiGroup, ok := crossByName[api.AntigravityQuotaGroupGemini]
if !ok {
t.Fatalf("missing cross quota for %s", api.AntigravityQuotaGroupGeminiPro)
}
if geminiProGroup.Percent < 14.9 || geminiProGroup.Percent > 15.1 {
t.Fatalf("expected gemini pro usage ~15%%, got %.2f", geminiProGroup.Percent)
}
if geminiProGroup.Delta < 4.9 || geminiProGroup.Delta > 5.1 {
t.Fatalf("expected gemini pro delta ~5%%, got %.2f", geminiProGroup.Delta)
t.Fatalf("missing cross quota for %s", api.AntigravityQuotaGroupGemini)
}

geminiFlashGroup, ok := crossByName[api.AntigravityQuotaGroupGeminiFlash]
if !ok {
t.Fatalf("missing cross quota for %s", api.AntigravityQuotaGroupGeminiFlash)
if geminiGroup.Percent < 12.4 || geminiGroup.Percent > 12.6 {
t.Fatalf("expected shared gemini usage ~12.5%%, got %.2f", geminiGroup.Percent)
}
if geminiFlashGroup.Percent < 9.9 || geminiFlashGroup.Percent > 10.1 {
t.Fatalf("expected gemini flash usage ~10%%, got %.2f", geminiFlashGroup.Percent)
if geminiGroup.StartPercent < 7.4 || geminiGroup.StartPercent > 7.6 {
t.Fatalf("expected shared gemini start ~7.5%%, got %.2f", geminiGroup.StartPercent)
}
if geminiFlashGroup.Delta < 4.9 || geminiFlashGroup.Delta > 5.1 {
t.Fatalf("expected gemini flash delta ~5%%, got %.2f", geminiFlashGroup.Delta)
if geminiGroup.Delta < 4.9 || geminiGroup.Delta > 5.1 {
t.Fatalf("expected shared gemini delta ~5%%, got %.2f", geminiGroup.Delta)
}
}

Expand Down
5 changes: 3 additions & 2 deletions internal/web/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10595,15 +10595,16 @@ func (h *Handler) cycleOverviewCodex(w http.ResponseWriter, r *http.Request) {
}

func normalizeAntigravityGroupBy(groupBy string) string {
groupBy = api.NormalizeAntigravityQuotaGroup(groupBy)
switch groupBy {
case api.AntigravityQuotaGroupClaudeGPT, api.AntigravityQuotaGroupGeminiPro, api.AntigravityQuotaGroupGeminiFlash:
case api.AntigravityQuotaGroupClaudeGPT, api.AntigravityQuotaGroupGemini:
return groupBy
}

if groupBy != "" {
mapped := api.AntigravityQuotaGroupForModel(groupBy, groupBy)
switch mapped {
case api.AntigravityQuotaGroupClaudeGPT, api.AntigravityQuotaGroupGeminiPro, api.AntigravityQuotaGroupGeminiFlash:
case api.AntigravityQuotaGroupClaudeGPT, api.AntigravityQuotaGroupGemini:
return mapped
}
}
Expand Down
17 changes: 11 additions & 6 deletions internal/web/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3115,8 +3115,8 @@ func TestHandler_Current_BothIncludesAnthropicAndAntigravity(t *testing.T) {
if !ok {
t.Fatalf("expected antigravity quotas array, got %T", ag["quotas"])
}
if len(quotas) != 3 {
t.Fatalf("expected 3 antigravity quota groups, got %d", len(quotas))
if len(quotas) != 2 {
t.Fatalf("expected 2 antigravity quota groups (Claude+GPT, Gemini), got %d", len(quotas))
}
}

Expand Down Expand Up @@ -10755,14 +10755,19 @@ func TestNormalizeAntigravityGroupBy(t *testing.T) {
expect string
}{
{
name: "passes through canonical group",
name: "passes through canonical gemini group",
input: api.AntigravityQuotaGroupGemini,
expect: api.AntigravityQuotaGroupGemini,
},
{
name: "maps legacy pro group to shared gemini",
input: api.AntigravityQuotaGroupGeminiPro,
expect: api.AntigravityQuotaGroupGeminiPro,
expect: api.AntigravityQuotaGroupGemini,
},
{
name: "maps model id to group",
name: "maps model id to shared gemini group",
input: "gemini-2.5-flash",
expect: api.AntigravityQuotaGroupGeminiFlash,
expect: api.AntigravityQuotaGroupGemini,
},
{
name: "falls back for unknown",
Expand Down
Loading
Loading