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