diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js index 683ea53d..568c4808 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js @@ -596,6 +596,17 @@ export default function ProviderLimits() { const providerVisibility = previous[provider] || {}; const hidden = new Set(providerVisibility.hidden || []); hidden.add(key); + if (provider === "antigravity") { + if (key === "gemini") { + for (const k of hidden) { + if (k.startsWith("gemini-") && !k.includes("image")) hidden.delete(k); + } + } else if (key === "claude") { + for (const k of hidden) { + if (k.startsWith("claude-")) hidden.delete(k); + } + } + } const next = { ...previous, [provider]: { @@ -614,6 +625,17 @@ export default function ProviderLimits() { const providerVisibility = previous[provider] || {}; const hidden = new Set(providerVisibility.hidden || []); hidden.delete(key); + if (provider === "antigravity") { + if (key === "gemini") { + for (const k of hidden) { + if (k.startsWith("gemini-") && !k.includes("image")) hidden.delete(k); + } + } else if (key === "claude") { + for (const k of hidden) { + if (k.startsWith("claude-")) hidden.delete(k); + } + } + } const next = { ...previous, [provider]: { diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js index 2a1a9236..96a77678 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js @@ -316,21 +316,33 @@ export function getQuotaVisibilityKey(quota) { return String(quota.modelKey || quota.name || "").trim(); } -function getProviderHiddenQuotaSet(provider, quotaVisibility) { +/** + * Trim hidden quota keys to only those matching currently valid quotas. + * Stale or obsolete model keys are dropped. + */ +export function trimHiddenQuotaKeys(hidden = [], quotas = []) { + if (!Array.isArray(hidden) || hidden.length === 0) return []; + const validKeys = new Set(quotas.map(getQuotaVisibilityKey).filter(Boolean)); + return [...new Set(hidden.map((k) => String(k).trim()).filter((k) => validKeys.has(k)))]; +} + +function getProviderHiddenQuotaSet(provider, quotaVisibility, quotas = []) { const hidden = quotaVisibility?.[provider]?.hidden; - return new Set(Array.isArray(hidden) ? hidden.map(String) : []); + if (!Array.isArray(hidden) || hidden.length === 0) return new Set(); + const trimmed = quotas.length > 0 ? trimHiddenQuotaKeys(hidden, quotas) : hidden; + return new Set(trimmed.map(String)); } export function filterQuotasByVisibility(provider, quotas = [], quotaVisibility = {}) { if (!Array.isArray(quotas) || quotas.length === 0) return []; - const hidden = getProviderHiddenQuotaSet(provider, quotaVisibility); + const hidden = getProviderHiddenQuotaSet(provider, quotaVisibility, quotas); if (hidden.size === 0) return quotas; return quotas.filter((quota) => !hidden.has(getQuotaVisibilityKey(quota))); } export function getHiddenQuotaRows(provider, quotas = [], quotaVisibility = {}) { if (!Array.isArray(quotas) || quotas.length === 0) return []; - const hidden = getProviderHiddenQuotaSet(provider, quotaVisibility); + const hidden = getProviderHiddenQuotaSet(provider, quotaVisibility, quotas); if (hidden.size === 0) return []; return quotas.filter((quota) => hidden.has(getQuotaVisibilityKey(quota))); } @@ -363,10 +375,55 @@ export function parseQuotaData(provider, data) { case "antigravity": if (data.quotas) { - Object.entries(data.quotas).forEach(([modelKey, quota]) => { + const entries = Object.entries(data.quotas); + 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 otherModels = entries.filter(([k]) => !k.startsWith("gemini-") && !k.startsWith("claude-") && !k.includes("image")); + + 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, + }); + } + + 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, + }); + } + + imageModels.forEach(([modelKey, quota]) => { normalizedQuotas.push({ name: quota.displayName || modelKey, - modelKey: modelKey, // Keep modelKey for sorting + modelKey, + used: quota.used || 0, + total: quota.total || 0, + resetAt: quota.resetAt || null, + remainingPercentage: quota.remainingPercentage, + }); + }); + + otherModels.forEach(([modelKey, quota]) => { + normalizedQuotas.push({ + name: quota.displayName || modelKey, + modelKey, used: quota.used || 0, total: quota.total || 0, resetAt: quota.resetAt || null, @@ -613,9 +670,13 @@ export function parseQuotaData(provider, data) { const orderMap = new Map(modelOrder.map((m, i) => [m.id, i])); normalizedQuotas.sort((a, b) => { - // Use modelKey for antigravity, otherwise use name - const keyA = a.modelKey || a.name; - const keyB = b.modelKey || b.name; + // 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"; const orderA = orderMap.get(keyA) ?? 999; const orderB = orderMap.get(keyB) ?? 999; return orderA - orderB; diff --git a/tests/unit/provider-quota-visibility.test.js b/tests/unit/provider-quota-visibility.test.js index d6616b81..8c69ee62 100644 --- a/tests/unit/provider-quota-visibility.test.js +++ b/tests/unit/provider-quota-visibility.test.js @@ -3,6 +3,7 @@ import { filterQuotasByVisibility, getHiddenQuotaRows, parseQuotaData, + trimHiddenQuotaKeys, } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js"; describe("provider quota visibility", () => { @@ -13,22 +14,26 @@ describe("provider quota visibility", () => { used: 200, total: 1000, resetAt: "2026-07-04T00:00:00Z", + remainingPercentage: 80, }, "claude-opus-4-6-thinking": { displayName: "Claude Opus 4.6 (Thinking)", used: 100, total: 1000, resetAt: "2026-07-04T00:00:00Z", + remainingPercentage: 90, }, }, }; - it("keeps Antigravity modelKey so hidden settings use stable quota ids", () => { + it("groups Antigravity model quotas into Gemini and Claude families", () => { const quotas = parseQuotaData("antigravity", data); expect(quotas.map((q) => q.modelKey)).toEqual([ - "gemini-pro-agent", - "claude-opus-4-6-thinking", + "gemini", + "claude", ]); + expect(quotas[0].name).toBe("Gemini (Flash / Pro)"); + expect(quotas[1].name).toBe("Claude (Sonnet / Opus)"); }); it("shows all quotas by default and hides configured provider rows", () => { @@ -36,19 +41,34 @@ describe("provider quota visibility", () => { expect(filterQuotasByVisibility("antigravity", quotas, {})).toHaveLength(2); const visibility = { - antigravity: { hidden: ["claude-opus-4-6-thinking"] }, + antigravity: { hidden: ["claude"] }, }; const visible = filterQuotasByVisibility("antigravity", quotas, visibility); const hidden = getHiddenQuotaRows("antigravity", quotas, visibility); - expect(visible.map((q) => q.modelKey)).toEqual(["gemini-pro-agent"]); - expect(hidden.map((q) => q.modelKey)).toEqual(["claude-opus-4-6-thinking"]); + expect(visible.map((q) => q.modelKey)).toEqual(["gemini"]); + expect(hidden.map((q) => q.modelKey)).toEqual(["claude"]); + }); + + it("trims stale or obsolete model keys", () => { + const quotas = parseQuotaData("antigravity", data); + const trimmed = trimHiddenQuotaKeys(["claude", "stale-model-xyz", "gemini-3.8-flash-low"], quotas); + expect(trimmed).toEqual(["claude"]); + + const visibility = { + antigravity: { hidden: ["claude", "stale-model-xyz"] }, + }; + const visible = filterQuotasByVisibility("antigravity", quotas, visibility); + const hidden = getHiddenQuotaRows("antigravity", quotas, visibility); + + expect(visible.map((q) => q.modelKey)).toEqual(["gemini"]); + expect(hidden.map((q) => q.modelKey)).toEqual(["claude"]); }); it("does not apply one provider hidden list to another provider", () => { const quotas = parseQuotaData("antigravity", data); const visibility = { - codex: { hidden: ["gemini-pro-agent"] }, + codex: { hidden: ["gemini"] }, }; expect(filterQuotasByVisibility("antigravity", quotas, visibility)).toHaveLength(2); });