feat(dashboard): group Antigravity model quotas and trim hidden keys

- Group Antigravity Gemini text models into single 'Gemini (Flash / Pro)' quota
- Group Claude models into single 'Claude (Sonnet / Opus)' quota
- Prune stale or legacy model keys from hidden quota visibility list

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
decolua
2026-09-05 21:56:07 +07:00
parent e74db4d0a6
commit f615a83cb2
3 changed files with 119 additions and 16 deletions

View File

@@ -596,6 +596,17 @@ export default function ProviderLimits() {
const providerVisibility = previous[provider] || {}; const providerVisibility = previous[provider] || {};
const hidden = new Set(providerVisibility.hidden || []); const hidden = new Set(providerVisibility.hidden || []);
hidden.add(key); 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 = { const next = {
...previous, ...previous,
[provider]: { [provider]: {
@@ -614,6 +625,17 @@ export default function ProviderLimits() {
const providerVisibility = previous[provider] || {}; const providerVisibility = previous[provider] || {};
const hidden = new Set(providerVisibility.hidden || []); const hidden = new Set(providerVisibility.hidden || []);
hidden.delete(key); 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 = { const next = {
...previous, ...previous,
[provider]: { [provider]: {

View File

@@ -316,21 +316,33 @@ export function getQuotaVisibilityKey(quota) {
return String(quota.modelKey || quota.name || "").trim(); 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; 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 = {}) { export function filterQuotasByVisibility(provider, quotas = [], quotaVisibility = {}) {
if (!Array.isArray(quotas) || quotas.length === 0) return []; 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; if (hidden.size === 0) return quotas;
return quotas.filter((quota) => !hidden.has(getQuotaVisibilityKey(quota))); return quotas.filter((quota) => !hidden.has(getQuotaVisibilityKey(quota)));
} }
export function getHiddenQuotaRows(provider, quotas = [], quotaVisibility = {}) { export function getHiddenQuotaRows(provider, quotas = [], quotaVisibility = {}) {
if (!Array.isArray(quotas) || quotas.length === 0) return []; if (!Array.isArray(quotas) || quotas.length === 0) return [];
const hidden = getProviderHiddenQuotaSet(provider, quotaVisibility); const hidden = getProviderHiddenQuotaSet(provider, quotaVisibility, quotas);
if (hidden.size === 0) return []; if (hidden.size === 0) return [];
return quotas.filter((quota) => hidden.has(getQuotaVisibilityKey(quota))); return quotas.filter((quota) => hidden.has(getQuotaVisibilityKey(quota)));
} }
@@ -363,10 +375,55 @@ export function parseQuotaData(provider, data) {
case "antigravity": case "antigravity":
if (data.quotas) { 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({ normalizedQuotas.push({
name: quota.displayName || modelKey, 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, used: quota.used || 0,
total: quota.total || 0, total: quota.total || 0,
resetAt: quota.resetAt || null, resetAt: quota.resetAt || null,
@@ -613,9 +670,13 @@ export function parseQuotaData(provider, data) {
const orderMap = new Map(modelOrder.map((m, i) => [m.id, i])); const orderMap = new Map(modelOrder.map((m, i) => [m.id, i]));
normalizedQuotas.sort((a, b) => { normalizedQuotas.sort((a, b) => {
// Use modelKey for antigravity, otherwise use name // Use modelKey for antigravity (mapped to family anchor), otherwise use name
const keyA = a.modelKey || a.name; let keyA = a.modelKey || a.name;
const keyB = b.modelKey || b.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 orderA = orderMap.get(keyA) ?? 999;
const orderB = orderMap.get(keyB) ?? 999; const orderB = orderMap.get(keyB) ?? 999;
return orderA - orderB; return orderA - orderB;

View File

@@ -3,6 +3,7 @@ import {
filterQuotasByVisibility, filterQuotasByVisibility,
getHiddenQuotaRows, getHiddenQuotaRows,
parseQuotaData, parseQuotaData,
trimHiddenQuotaKeys,
} from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js"; } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
describe("provider quota visibility", () => { describe("provider quota visibility", () => {
@@ -13,22 +14,26 @@ describe("provider quota visibility", () => {
used: 200, used: 200,
total: 1000, total: 1000,
resetAt: "2026-07-04T00:00:00Z", resetAt: "2026-07-04T00:00:00Z",
remainingPercentage: 80,
}, },
"claude-opus-4-6-thinking": { "claude-opus-4-6-thinking": {
displayName: "Claude Opus 4.6 (Thinking)", displayName: "Claude Opus 4.6 (Thinking)",
used: 100, used: 100,
total: 1000, total: 1000,
resetAt: "2026-07-04T00:00:00Z", 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); const quotas = parseQuotaData("antigravity", data);
expect(quotas.map((q) => q.modelKey)).toEqual([ expect(quotas.map((q) => q.modelKey)).toEqual([
"gemini-pro-agent", "gemini",
"claude-opus-4-6-thinking", "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", () => { 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); expect(filterQuotasByVisibility("antigravity", quotas, {})).toHaveLength(2);
const visibility = { const visibility = {
antigravity: { hidden: ["claude-opus-4-6-thinking"] }, antigravity: { hidden: ["claude"] },
}; };
const visible = filterQuotasByVisibility("antigravity", quotas, visibility); const visible = filterQuotasByVisibility("antigravity", quotas, visibility);
const hidden = getHiddenQuotaRows("antigravity", quotas, visibility); const hidden = getHiddenQuotaRows("antigravity", quotas, visibility);
expect(visible.map((q) => q.modelKey)).toEqual(["gemini-pro-agent"]); expect(visible.map((q) => q.modelKey)).toEqual(["gemini"]);
expect(hidden.map((q) => q.modelKey)).toEqual(["claude-opus-4-6-thinking"]); 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", () => { it("does not apply one provider hidden list to another provider", () => {
const quotas = parseQuotaData("antigravity", data); const quotas = parseQuotaData("antigravity", data);
const visibility = { const visibility = {
codex: { hidden: ["gemini-pro-agent"] }, codex: { hidden: ["gemini"] },
}; };
expect(filterQuotasByVisibility("antigravity", quotas, visibility)).toHaveLength(2); expect(filterQuotasByVisibility("antigravity", quotas, visibility)).toHaveLength(2);
}); });