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