fix(antigravity): separate weekly and short-window quotas and clean up redundant rows

- Track both weekly and 5-hour session buckets in parseWeeklyQuotaSummary,
  distinguishing sliding-window limits from multi-day weekly limits
- Preserve disabled session buckets at 0% rather than dropping them when weekly limits are reached
- Target 5-hour session rows (not weekly rows) during family exhaustion reconciliation in getAntigravityUsage
- Suppress synthesized per-model duplicate rows in dashboard normalization when family summaries are present
- Add unit test coverage for multi-bucket extraction, reconciliation isolation, and dashboard deduplication
This commit is contained in:
Christian Gennari
2026-09-21 20:15:56 +07:00
parent 2daf25ffbe
commit be3bc764b1
5 changed files with 281 additions and 92 deletions
@@ -377,51 +377,112 @@ export function parseQuotaData(provider, data) {
if (data.quotas) {
const entries = Object.entries(data.quotas);
const weeklyKeys = new Set(["gemini_weekly", "claude_gpt_weekly"]);
const sessionKeys = new Set(["gemini_session", "claude_gpt_session"]);
const summaryKeys = new Set([...weeklyKeys, ...sessionKeys]);
const geminiModels = entries.filter(([k]) => k.startsWith("gemini-") && !k.includes("image"));
const claudeModels = entries.filter(([k]) => k.startsWith("claude-"));
const imageModels = entries.filter(([k]) => k.includes("image"));
const weeklyModels = entries.filter(([k]) => weeklyKeys.has(k));
const otherModels = entries.filter(([k]) => !k.startsWith("gemini-") && !k.startsWith("claude-") && !k.includes("image") && !weeklyKeys.has(k));
const summaryModels = entries.filter(([k]) => summaryKeys.has(k));
const otherModels = entries.filter(([k]) => !k.startsWith("gemini-") && !k.startsWith("claude-") && !k.includes("image") && !summaryKeys.has(k));
if (geminiModels.length > 0) {
// Summary keys from retrieveUserQuotaSummary
const hasGeminiWeekly = Boolean(data.quotas.gemini_weekly);
const hasGeminiSession = Boolean(data.quotas.gemini_session);
const hasClaudeWeekly = Boolean(data.quotas.claude_gpt_weekly);
const hasClaudeSession = Boolean(data.quotas.claude_gpt_session);
// 1. Gemini Family:
if (hasGeminiSession) {
summaryModels.filter(([k]) => k === "gemini_session").forEach(([modelKey, quota]) => {
normalizedQuotas.push({
name: quota.displayName || modelKey,
modelKey,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
} else if (geminiModels.length > 0) {
const rep = geminiModels.reduce((min, cur) =>
(cur[1].remainingPercentage ?? 100) < (min[1].remainingPercentage ?? 100) ? cur : min
)[1];
normalizedQuotas.push({
name: "Gemini (Flash / Pro)",
modelKey: "gemini",
used: rep.used || 0,
total: rep.total || 0,
resetAt: rep.resetAt || null,
remainingPercentage: rep.remainingPercentage,
// Only show synthesized Gemini row if its resetAt differs from weekly (i.e. it represents a separate 5h window)
const weeklyResetAt = data.quotas.gemini_weekly?.resetAt;
const isDuplicateOfWeekly = hasGeminiWeekly && rep.resetAt === weeklyResetAt && (rep.remainingPercentage ?? 0) === 0;
if (!isDuplicateOfWeekly) {
normalizedQuotas.push({
name: "Gemini (Flash / Pro)",
modelKey: "gemini",
used: rep.used || 0,
total: rep.total || 0,
resetAt: rep.resetAt || null,
remainingPercentage: rep.remainingPercentage,
});
}
}
// Show Gemini weekly row if present
if (hasGeminiWeekly) {
summaryModels.filter(([k]) => k === "gemini_weekly").forEach(([modelKey, quota]) => {
normalizedQuotas.push({
name: quota.displayName || modelKey,
modelKey,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
}
if (claudeModels.length > 0) {
// 2. Claude & GPT Family:
if (hasClaudeSession) {
summaryModels.filter(([k]) => k === "claude_gpt_session").forEach(([modelKey, quota]) => {
normalizedQuotas.push({
name: quota.displayName || modelKey,
modelKey,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
} else if (claudeModels.length > 0) {
const rep = claudeModels.reduce((min, cur) =>
(cur[1].remainingPercentage ?? 100) < (min[1].remainingPercentage ?? 100) ? cur : min
)[1];
normalizedQuotas.push({
name: "Claude (Sonnet / Opus)",
modelKey: "claude",
used: rep.used || 0,
total: rep.total || 0,
resetAt: rep.resetAt || null,
remainingPercentage: rep.remainingPercentage,
const weeklyResetAt = data.quotas.claude_gpt_weekly?.resetAt;
const isDuplicateOfWeekly = hasClaudeWeekly && rep.resetAt === weeklyResetAt && (rep.remainingPercentage ?? 0) === 0;
if (!isDuplicateOfWeekly) {
normalizedQuotas.push({
name: "Claude (Sonnet / Opus)",
modelKey: "claude",
used: rep.used || 0,
total: rep.total || 0,
resetAt: rep.resetAt || null,
remainingPercentage: rep.remainingPercentage,
});
}
}
// Show Claude & GPT weekly row if present
if (hasClaudeWeekly) {
summaryModels.filter(([k]) => k === "claude_gpt_weekly").forEach(([modelKey, quota]) => {
normalizedQuotas.push({
name: quota.displayName || modelKey,
modelKey,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
}
weeklyModels.forEach(([modelKey, quota]) => {
normalizedQuotas.push({
name: quota.displayName || modelKey,
modelKey,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
// 3. Standalone Image Generation Models (unique usage)
imageModels.forEach(([modelKey, quota]) => {
normalizedQuotas.push({
name: quota.displayName || modelKey,
@@ -433,16 +494,23 @@ export function parseQuotaData(provider, data) {
});
});
otherModels.forEach(([modelKey, quota]) => {
normalizedQuotas.push({
name: quota.displayName || modelKey,
modelKey,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
// 4. Other models:
// In Antigravity, GPT-OSS is explicitly documented by Google as part of the "Claude and GPT models" group:
// ("Models within this group: Claude Opus, Claude Sonnet, GPT-OSS").
// When summary quotas (claude_gpt_session / claude_gpt_weekly) are present, GPT-OSS is already represented
// by the "Claude & GPT" family rows. We only include otherModels if no summary exists for that pool.
if (!hasClaudeWeekly && !hasClaudeSession) {
otherModels.forEach(([modelKey, quota]) => {
normalizedQuotas.push({
name: quota.displayName || modelKey,
modelKey,
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
});
});
});
}
}
break;
@@ -702,10 +770,10 @@ export function parseQuotaData(provider, data) {
// Use modelKey for antigravity (mapped to family anchor), otherwise use name
let keyA = a.modelKey || a.name;
let keyB = b.modelKey || b.name;
if (keyA === "gemini") keyA = "gemini-3.8-flash-high";
if (keyA === "claude") keyA = "claude-sonnet-4-6";
if (keyB === "gemini") keyB = "gemini-3.8-flash-high";
if (keyB === "claude") keyB = "claude-sonnet-4-6";
if (keyA === "gemini" || keyA === "gemini_session") keyA = "gemini-3.8-flash-high";
if (keyA === "claude" || keyA === "claude_gpt_session") keyA = "claude-sonnet-4-6";
if (keyB === "gemini" || keyB === "gemini_session") keyB = "gemini-3.8-flash-high";
if (keyB === "claude" || keyB === "claude_gpt_session") keyB = "claude-sonnet-4-6";
const orderA = orderMap.get(keyA) ?? 999;
const orderB = orderMap.get(keyB) ?? 999;
return orderA - orderB;