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:
@@ -25,10 +25,18 @@ export function _clearWeeklyCache() {
|
||||
weeklyCache.clear();
|
||||
}
|
||||
|
||||
// — Group-name to stable key mapping ——————————————————————
|
||||
const GROUP_MATCHERS = [
|
||||
{ pattern: /gemini/i, key: "gemini_weekly", displayName: "Gemini (Weekly)" },
|
||||
{ pattern: /claude|gpt/i, key: "claude_gpt_weekly", displayName: "Claude & GPT (Weekly)" },
|
||||
// — Group-name and window to stable key mapping ——————————————————————
|
||||
const GROUP_CONFIGS = [
|
||||
{
|
||||
pattern: /gemini/i,
|
||||
weekly: { key: "gemini_weekly", displayName: "Gemini (Weekly)" },
|
||||
session: { key: "gemini_session", displayName: "Gemini (5h)" },
|
||||
},
|
||||
{
|
||||
pattern: /claude|gpt/i,
|
||||
weekly: { key: "claude_gpt_weekly", displayName: "Claude & GPT (Weekly)" },
|
||||
session: { key: "claude_gpt_session", displayName: "Claude & GPT (5h)" },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -60,32 +68,40 @@ export function parseWeeklyQuotaSummary(data) {
|
||||
for (const bucket of buckets) {
|
||||
if (!bucket || typeof bucket !== "object") continue;
|
||||
|
||||
// Identify weekly buckets by checking bucketId + displayName for "weekly"
|
||||
const windowType = String(bucket.window || "").toLowerCase();
|
||||
const bucketText = `${bucket.bucketId || ""} ${bucket.displayName || ""}`.toLowerCase();
|
||||
if (!bucketText.includes("weekly")) continue;
|
||||
const isWeekly = windowType === "weekly" || bucketText.includes("weekly");
|
||||
const isSession = windowType === "5h" || bucketText.includes("five hour") || bucketText.includes("5h") || bucketText.includes("daily") || windowType === "daily";
|
||||
|
||||
// Skip disabled buckets
|
||||
if (bucket.disabled === true) continue;
|
||||
if (!isWeekly && !isSession) continue;
|
||||
|
||||
const remainingFraction = Number(bucket.remainingFraction);
|
||||
// If a session (5h) bucket is marked disabled by upstream (because weekly was hit),
|
||||
// keep it so the UI shows the 5h row, but with remainingFraction: 0.
|
||||
// Disabled weekly buckets are truly disabled and skipped.
|
||||
if (bucket.disabled === true && isWeekly) continue;
|
||||
|
||||
const remainingFraction = bucket.disabled === true ? 0 : Number(bucket.remainingFraction);
|
||||
if (!Number.isFinite(remainingFraction)) continue;
|
||||
|
||||
// Match group to a known family
|
||||
for (const matcher of GROUP_MATCHERS) {
|
||||
if (matcher.pattern.test(displayName)) {
|
||||
for (const config of GROUP_CONFIGS) {
|
||||
if (config.pattern.test(displayName)) {
|
||||
const target = isWeekly ? config.weekly : config.session;
|
||||
if (result[target.key]) break; // first matching bucket per type wins
|
||||
|
||||
const total = 1000;
|
||||
const remaining = Math.round(total * remainingFraction);
|
||||
const used = Math.max(0, total - remaining);
|
||||
|
||||
result[matcher.key] = {
|
||||
result[target.key] = {
|
||||
used,
|
||||
total,
|
||||
resetAt: parseResetTime(bucket.resetTime),
|
||||
remainingPercentage: remainingFraction * 100,
|
||||
unlimited: false,
|
||||
displayName: matcher.displayName,
|
||||
displayName: target.displayName,
|
||||
};
|
||||
break; // first matching bucket per family wins
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,39 +228,37 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro
|
||||
proxyOptions
|
||||
);
|
||||
|
||||
// Reconcile weekly quota against model family status:
|
||||
// Reconcile short-window session quota if models are exhausted:
|
||||
// If every model in a family is locked/exhausted (remainingPercentage === 0)
|
||||
// until a future reset time, the weekly limit cannot be 100% available.
|
||||
// On Google's Free Starter tier, retrieveUserQuotaSummary buggily reports
|
||||
// remainingFraction: 1 even after the starter quota is depleted and all models 429.
|
||||
// until a future reset time, update the 5h session row (not the weekly row).
|
||||
const entries = Object.entries(quotas);
|
||||
const geminiModels = entries.filter(([k]) => k.startsWith("gemini-") && !k.includes("image"));
|
||||
const claudeModels = entries.filter(([k]) => k.startsWith("claude-"));
|
||||
|
||||
if (weeklyQuotas.gemini_weekly && geminiModels.length > 0) {
|
||||
if (weeklyQuotas.gemini_session && geminiModels.length > 0) {
|
||||
const allGeminiExhausted = geminiModels.every(([, q]) => (q.remainingPercentage ?? 0) === 0);
|
||||
if (allGeminiExhausted && weeklyQuotas.gemini_weekly.remainingPercentage > 0) {
|
||||
if (allGeminiExhausted && weeklyQuotas.gemini_session.remainingPercentage > 0) {
|
||||
const maxResetAt = geminiModels.reduce((max, [, q]) =>
|
||||
!max || (q.resetAt && new Date(q.resetAt) > new Date(max)) ? q.resetAt : max, null
|
||||
);
|
||||
weeklyQuotas.gemini_weekly.used = weeklyQuotas.gemini_weekly.total;
|
||||
weeklyQuotas.gemini_weekly.remainingPercentage = 0;
|
||||
weeklyQuotas.gemini_session.used = weeklyQuotas.gemini_session.total;
|
||||
weeklyQuotas.gemini_session.remainingPercentage = 0;
|
||||
if (maxResetAt) {
|
||||
weeklyQuotas.gemini_weekly.resetAt = maxResetAt;
|
||||
weeklyQuotas.gemini_session.resetAt = maxResetAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (weeklyQuotas.claude_gpt_weekly && claudeModels.length > 0) {
|
||||
if (weeklyQuotas.claude_gpt_session && claudeModels.length > 0) {
|
||||
const allClaudeExhausted = claudeModels.every(([, q]) => (q.remainingPercentage ?? 0) === 0);
|
||||
if (allClaudeExhausted && weeklyQuotas.claude_gpt_weekly.remainingPercentage > 0) {
|
||||
if (allClaudeExhausted && weeklyQuotas.claude_gpt_session.remainingPercentage > 0) {
|
||||
const maxResetAt = claudeModels.reduce((max, [, q]) =>
|
||||
!max || (q.resetAt && new Date(q.resetAt) > new Date(max)) ? q.resetAt : max, null
|
||||
);
|
||||
weeklyQuotas.claude_gpt_weekly.used = weeklyQuotas.claude_gpt_weekly.total;
|
||||
weeklyQuotas.claude_gpt_weekly.remainingPercentage = 0;
|
||||
weeklyQuotas.claude_gpt_session.used = weeklyQuotas.claude_gpt_session.total;
|
||||
weeklyQuotas.claude_gpt_session.remainingPercentage = 0;
|
||||
if (maxResetAt) {
|
||||
weeklyQuotas.claude_gpt_weekly.resetAt = maxResetAt;
|
||||
weeklyQuotas.claude_gpt_session.resetAt = maxResetAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user