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 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]: {

View File

@@ -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;

View File

@@ -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);
});