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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user