diff --git a/internal/agent/antigravity_agent.go b/internal/agent/antigravity_agent.go index e673b358..e9dce68c 100644 --- a/internal/agent/antigravity_agent.go +++ b/internal/agent/antigravity_agent.go @@ -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) } diff --git a/internal/api/antigravity_types.go b/internal/api/antigravity_types.go index 0ef01823..2f3f3fd3 100644 --- a/internal/api/antigravity_types.go +++ b/internal/api/antigravity_types.go @@ -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. @@ -73,10 +80,9 @@ 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: @@ -84,6 +90,17 @@ func AntigravityQuotaGroupForModel(modelID, label string) string { } } +// 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 diff --git a/internal/api/antigravity_types_test.go b/internal/api/antigravity_types_test.go index 2ea6d2a1..4c0dfda5 100644 --- a/internal/api/antigravity_types_test.go +++ b/internal/api/antigravity_types_test.go @@ -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}, } @@ -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] @@ -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 { @@ -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) + } +} diff --git a/internal/api/extra_coverage_test.go b/internal/api/extra_coverage_test.go index 6ccd2077..2a3b6c07 100644 --- a/internal/api/extra_coverage_test.go +++ b/internal/api/extra_coverage_test.go @@ -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 @@ -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 diff --git a/internal/store/antigravity_store.go b/internal/store/antigravity_store.go index 24469276..0c8d801b 100644 --- a/internal/store/antigravity_store.go +++ b/internal/store/antigravity_store.go @@ -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) @@ -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 @@ -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 diff --git a/internal/store/antigravity_store_test.go b/internal/store/antigravity_store_test.go index 06107dab..8df4423a 100644 --- a/internal/store/antigravity_store_test.go +++ b/internal/store/antigravity_store_test.go @@ -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) } } @@ -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) } } diff --git a/internal/web/handlers.go b/internal/web/handlers.go index d9104f00..74b89338 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -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 } } diff --git a/internal/web/handlers_test.go b/internal/web/handlers_test.go index aa573372..ea0d6306 100644 --- a/internal/web/handlers_test.go +++ b/internal/web/handlers_test.go @@ -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)) } } @@ -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", diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 31aa9dee..e726db3f 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -1017,8 +1017,10 @@ const copilotChartColorFallback = [ const antigravityChartColorMap = { antigravity_claude_gpt: { border: '#D97757', bg: 'rgba(217, 119, 87, 0.08)' }, + antigravity_gemini: { border: '#10B981', bg: 'rgba(16, 185, 129, 0.08)' }, + // Legacy Pro/Flash keys (pre shared-pool) map to the same color antigravity_gemini_pro: { border: '#10B981', bg: 'rgba(16, 185, 129, 0.08)' }, - antigravity_gemini_flash: { border: '#3B82F6', bg: 'rgba(59, 130, 246, 0.08)' }, + antigravity_gemini_flash: { border: '#10B981', bg: 'rgba(16, 185, 129, 0.08)' }, // agy CLI bucket rows (weekly + 5h per group) 'gemini-weekly': { border: '#10B981', bg: 'rgba(16, 185, 129, 0.08)' }, 'gemini-5h': { border: '#34D399', bg: 'rgba(52, 211, 153, 0.08)' }, @@ -1130,8 +1132,7 @@ const renewalCategories = { ], antigravity: [ { label: 'Claude+GPT', groupBy: 'antigravity_claude_gpt' }, - { label: 'Gemini Pro', groupBy: 'antigravity_gemini_pro' }, - { label: 'Gemini Flash', groupBy: 'antigravity_gemini_flash' } + { label: 'Gemini', groupBy: 'antigravity_gemini' } ], minimax: [ { label: '5-Hour', groupBy: 'coding_plan' }, @@ -1169,8 +1170,9 @@ const overviewQuotaDisplayNames = { completions: 'Completions', coding_plan: 'Coding', antigravity_claude_gpt: 'Claude + GPT Quota', - antigravity_gemini_pro: 'Gemini Pro Quota', - antigravity_gemini_flash: 'Gemini Flash Quota', + antigravity_gemini: 'Gemini Quota', + antigravity_gemini_pro: 'Gemini Quota', + antigravity_gemini_flash: 'Gemini Quota', credits: 'Credits', total_usage: 'Total Usage', auto_usage: 'Auto + Composer', @@ -7580,11 +7582,10 @@ function renderSessionsTable() { ${c.durationStr} ${fmtWithDelta(session.startSubRequests, session.maxSubRequests)} ${fmtWithDelta(session.startSearchRequests, session.maxSearchRequests)} - ${fmtWithDelta(session.startToolRequests, session.maxToolRequests)} `; const detailRow = ` - +
@@ -7592,13 +7593,9 @@ function renderSessionsTable() { ${fmtPct(session.startSubRequests)} → ${fmtPct(session.maxSubRequests)} (${fmtDelta(session.startSubRequests, session.maxSubRequests)})
- Gemini Pro Quota + Gemini Quota ${fmtPct(session.startSearchRequests)} → ${fmtPct(session.maxSearchRequests)} (${fmtDelta(session.startSearchRequests, session.maxSearchRequests)})
-
- Gemini Flash Quota - ${fmtPct(session.startToolRequests)} → ${fmtPct(session.maxToolRequests)} (${fmtDelta(session.startToolRequests, session.maxToolRequests)}) -
Snapshots ${session.snapshotCount || 0} @@ -10765,8 +10762,7 @@ const _overrideQuotasByProvider = { ], antigravity: [ { key: 'antigravity_claude_gpt', label: 'Claude + GPT Quota' }, - { key: 'antigravity_gemini_pro', label: 'Gemini Pro Quota' }, - { key: 'antigravity_gemini_flash', label: 'Gemini Flash Quota' }, + { key: 'antigravity_gemini', label: 'Gemini Quota' }, ], gemini: [ { key: 'gemini-3-pro-preview', label: 'Gemini 3 Pro' }, diff --git a/internal/web/templates/dashboard.html b/internal/web/templates/dashboard.html index f52cf4ba..8ff2c82e 100644 --- a/internal/web/templates/dashboard.html +++ b/internal/web/templates/dashboard.html @@ -521,8 +521,7 @@

Snapshots {{else if eq .CurrentProvider "antigravity"}} Claude + GPT Quota - Gemini Pro Quota - Gemini Flash Quota + Gemini Quota {{else if eq .CurrentProvider "gemini"}} Peak Usage Session Δ