fix(omp): show Antigravity weekly usage limits
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
|
||||
- Default reasoning context to `all_turns` for all Codex requests
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Antigravity usage reporting to surface named daily and weekly quota windows from Cloud Code Assist model metadata, so `/usage` can show weekly Antigravity limits instead of collapsing them into the default bucket.
|
||||
|
||||
## [16.2.2] - 2026-06-27
|
||||
|
||||
### Added
|
||||
|
||||
@@ -29,7 +29,13 @@ interface AntigravityModelInfo {
|
||||
displayName?: string;
|
||||
quotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
|
||||
quotaInfos?: AntigravityQuotaInfo[];
|
||||
dailyQuotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
|
||||
dailyQuotaInfos?: AntigravityQuotaInfo[];
|
||||
weeklyQuotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
|
||||
weeklyQuotaInfos?: AntigravityQuotaInfo[];
|
||||
quotaInfoByTier?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
|
||||
quotaInfoByWindow?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
|
||||
quotaInfosByWindow?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
|
||||
apiProvider?: string;
|
||||
modelProvider?: string;
|
||||
}
|
||||
@@ -41,6 +47,39 @@ interface AntigravityUsageResponse {
|
||||
const DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
||||
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
|
||||
|
||||
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const ONE_WEEK_MS = 7 * ONE_DAY_MS;
|
||||
|
||||
interface AntigravityWindowDescriptor {
|
||||
id: string;
|
||||
label: string;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
function classifyWindow(id: string | undefined, label: string | undefined): AntigravityWindowDescriptor | undefined {
|
||||
const source = `${id ?? ""} ${label ?? ""}`.toLowerCase();
|
||||
if (source.includes("week") || source.includes("7d") || /7[\s_-]*day/.test(source)) {
|
||||
return { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS };
|
||||
}
|
||||
if (source.includes("day") || source.includes("daily") || source.includes("24h")) {
|
||||
return { id: "daily", label: "Daily", durationMs: ONE_DAY_MS };
|
||||
}
|
||||
if (id || label) return { id: id ?? label ?? "default", label: label ?? id ?? "Default" };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function withWindowDescriptor(
|
||||
info: AntigravityQuotaInfo,
|
||||
descriptor: AntigravityWindowDescriptor | undefined,
|
||||
): AntigravityQuotaInfo {
|
||||
if (!descriptor) return info;
|
||||
return {
|
||||
...info,
|
||||
windowId: info.windowId ?? descriptor.id,
|
||||
windowLabel: info.windowLabel ?? descriptor.label,
|
||||
};
|
||||
}
|
||||
|
||||
function clampFraction(value: number | undefined): number | undefined {
|
||||
if (value === undefined || !Number.isFinite(value)) return undefined;
|
||||
if (value < 0) return 0;
|
||||
@@ -56,13 +95,15 @@ function getUsageStatus(remainingFraction: number | undefined): UsageStatus | un
|
||||
}
|
||||
|
||||
function parseWindow(info: AntigravityQuotaInfo): UsageWindow | undefined {
|
||||
if (!info.resetTime) return undefined;
|
||||
const resetAt = Date.parse(info.resetTime);
|
||||
if (!Number.isFinite(resetAt)) return undefined;
|
||||
const descriptor = classifyWindow(info.windowId, info.windowLabel);
|
||||
const resetAt = info.resetTime ? Date.parse(info.resetTime) : undefined;
|
||||
const hasResetAt = resetAt !== undefined && Number.isFinite(resetAt);
|
||||
if (!descriptor && !hasResetAt) return undefined;
|
||||
return {
|
||||
id: info.windowId ?? "default",
|
||||
label: info.windowLabel ?? "Default",
|
||||
resetsAt: resetAt,
|
||||
id: descriptor?.id ?? info.windowId ?? "default",
|
||||
label: info.windowLabel ?? descriptor?.label ?? "Default",
|
||||
...(descriptor?.durationMs !== undefined ? { durationMs: descriptor.durationMs } : {}),
|
||||
...(hasResetAt ? { resetsAt: resetAt } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,31 +147,44 @@ function normalizeQuotaInfos(info: AntigravityModelInfo): AntigravityQuotaInfo[]
|
||||
...(info.apiProvider ? { apiProvider: info.apiProvider } : {}),
|
||||
...(info.modelProvider ? { modelProvider: info.modelProvider } : {}),
|
||||
};
|
||||
const addInfo = (value: AntigravityQuotaInfo, tier?: string) => {
|
||||
results.push({ ...source, ...value, ...(tier ? { tier } : {}) });
|
||||
const addInfo = (value: AntigravityQuotaInfo, tier?: string, windowDescriptor?: AntigravityWindowDescriptor) => {
|
||||
results.push({ ...source, ...withWindowDescriptor(value, windowDescriptor), ...(tier ? { tier } : {}) });
|
||||
};
|
||||
const addArray = (values?: AntigravityQuotaInfo[]) => {
|
||||
if (!values) return;
|
||||
for (const value of values) addInfo(value);
|
||||
const addValue = (
|
||||
value: AntigravityQuotaInfo | AntigravityQuotaInfo[] | undefined,
|
||||
tier?: string,
|
||||
windowDescriptor?: AntigravityWindowDescriptor,
|
||||
) => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value)) {
|
||||
for (const entry of value) addInfo(entry, tier, windowDescriptor);
|
||||
return;
|
||||
}
|
||||
addInfo(value, tier, windowDescriptor);
|
||||
};
|
||||
|
||||
if (Array.isArray(info.quotaInfo)) {
|
||||
addArray(info.quotaInfo);
|
||||
} else if (info.quotaInfo) {
|
||||
addInfo(info.quotaInfo);
|
||||
}
|
||||
addArray(info.quotaInfos);
|
||||
addValue(info.quotaInfo);
|
||||
addValue(info.quotaInfos);
|
||||
addValue(info.dailyQuotaInfo, undefined, classifyWindow("daily", "Daily"));
|
||||
addValue(info.dailyQuotaInfos, undefined, classifyWindow("daily", "Daily"));
|
||||
addValue(info.weeklyQuotaInfo, undefined, classifyWindow("weekly", "Weekly"));
|
||||
addValue(info.weeklyQuotaInfos, undefined, classifyWindow("weekly", "Weekly"));
|
||||
|
||||
if (info.quotaInfoByTier) {
|
||||
for (const [tier, value] of Object.entries(info.quotaInfoByTier)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const entry of value) addInfo(entry, tier);
|
||||
} else if (value) {
|
||||
addInfo(value, tier);
|
||||
}
|
||||
addValue(value, tier);
|
||||
}
|
||||
}
|
||||
|
||||
const addWindowMap = (values?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>) => {
|
||||
if (!values) return;
|
||||
for (const [windowId, value] of Object.entries(values)) {
|
||||
addValue(value, undefined, classifyWindow(windowId, undefined));
|
||||
}
|
||||
};
|
||||
addWindowMap(info.quotaInfoByWindow);
|
||||
addWindowMap(info.quotaInfosByWindow);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -231,9 +285,10 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
||||
const tierKey = (quotaInfo.tier ?? "default").toLowerCase();
|
||||
const counterName = formatCounterName(quotaInfo);
|
||||
const counterKey = counterName?.toLowerCase() ?? "default";
|
||||
// Use quotaInfo.windowId even when parseWindow returns undefined
|
||||
// (no resetTime) — separate windows must not collapse to "default".
|
||||
const windowId = quotaInfo.windowId ?? window?.id ?? "default";
|
||||
// Use the parsed window id when available so provider enum names like
|
||||
// WINDOW_WEEKLY normalize into the same visible `/usage` group as
|
||||
// weeklyQuotaInfo entries.
|
||||
const windowId = window?.id ?? quotaInfo.windowId ?? "default";
|
||||
const key = `${counterKey}|${tierKey}|${windowId}`;
|
||||
const existing = deduped.get(key);
|
||||
if (!existing) {
|
||||
@@ -324,8 +379,6 @@ export const antigravityUsageProvider: UsageProvider = {
|
||||
supports: params => params.provider === "google-antigravity",
|
||||
};
|
||||
|
||||
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function getAntigravityCounterKeyForModel(context: CredentialRankingContext | undefined): string | undefined {
|
||||
const modelId = context?.modelId?.toLowerCase();
|
||||
if (!modelId) return undefined;
|
||||
@@ -361,17 +414,16 @@ function rankAntigravityLimits(report: UsageReport, context: CredentialRankingCo
|
||||
}
|
||||
|
||||
/**
|
||||
* Antigravity quotas reset daily and are returned per backend counter
|
||||
* (Anthropic / Google / OpenAI) without a fixed "primary vs secondary"
|
||||
* split. `fetchAntigravityUsage` already sorts `limits` ascending by
|
||||
* `remainingFraction`; after model-family scoping, the most-pressured
|
||||
* relevant counter is index 0.
|
||||
* Antigravity quotas are returned per backend counter (Anthropic / Google /
|
||||
* OpenAI) and can include both daily and weekly windows. `fetchAntigravityUsage`
|
||||
* sorts `limits` ascending by `remainingFraction`; after model-family scoping,
|
||||
* the most-pressured relevant counter/window is index 0.
|
||||
*
|
||||
* Leave `secondary` unset: AuthStorage compares secondary metrics before
|
||||
* primary metrics, which is correct for providers with explicit long-window
|
||||
* limits but wrong here. Ranking Antigravity by the bottleneck counter first
|
||||
* avoids preferring an account at 95% Gemini / 0% Claude over one at
|
||||
* 80% Gemini / 70% Claude.
|
||||
* primary metrics, which is correct for providers with a fixed short/long
|
||||
* split but wrong here. Ranking Antigravity by the bottleneck counter first
|
||||
* avoids preferring an account at 95% Gemini daily / 0% Claude weekly over one
|
||||
* with healthier Gemini headroom.
|
||||
*/
|
||||
export const antigravityRankingStrategy: CredentialRankingStrategy = {
|
||||
findWindowLimits(report, context) {
|
||||
@@ -384,8 +436,8 @@ export const antigravityRankingStrategy: CredentialRankingStrategy = {
|
||||
const counterKey = getAntigravityCounterKeyForModel(context);
|
||||
return `counter:${counterKey ?? "unknown"}`;
|
||||
},
|
||||
// Antigravity windows omit `durationMs`; the endpoint is
|
||||
// `daily-cloudcode-pa.googleapis.com`, so fall back to 24h when computing
|
||||
// drain rate.
|
||||
// Antigravity windows carry `durationMs` when the response identifies them
|
||||
// as daily/weekly. Fall back to daily for legacy unlabelled quotaInfo
|
||||
// entries from `daily-cloudcode-pa.googleapis.com`.
|
||||
windowDefaults: { primaryMs: ONE_DAY_MS, secondaryMs: ONE_DAY_MS },
|
||||
};
|
||||
|
||||
@@ -188,6 +188,60 @@ describe("antigravity usage provider", () => {
|
||||
expect(report!.limits.length).toBe(2);
|
||||
});
|
||||
|
||||
it("surfaces named daily and weekly quota windows as separate limits", async () => {
|
||||
const now = Date.now();
|
||||
const dailyReset = new Date(now + 12 * 3600_000).toISOString();
|
||||
const weeklyReset = new Date(now + 6 * 24 * 3600_000).toISOString();
|
||||
const payload = {
|
||||
models: {
|
||||
gemini: {
|
||||
displayName: "Gemini",
|
||||
modelProvider: "MODEL_PROVIDER_GOOGLE",
|
||||
quotaInfo: { remainingFraction: 0.8, resetTime: dailyReset, windowId: "WINDOW_DAILY" },
|
||||
weeklyQuotaInfo: { remainingFraction: 0.4, resetTime: weeklyReset },
|
||||
},
|
||||
},
|
||||
};
|
||||
const report = await antigravityUsageProvider.fetchUsage!(
|
||||
{ provider: "google-antigravity", credential: makeCredential(), signal: undefined },
|
||||
makeCtx(fakeFetch(payload)),
|
||||
);
|
||||
|
||||
const daily = report!.limits.find(limit => limit.scope.windowId === "daily");
|
||||
const weekly = report!.limits.find(limit => limit.scope.windowId === "weekly");
|
||||
expect(report!.limits.length).toBe(2);
|
||||
expect(daily?.label).toBe("Usage (Google)");
|
||||
expect(daily?.window?.label).toBe("Daily");
|
||||
expect(daily?.window?.durationMs).toBe(24 * 60 * 60 * 1000);
|
||||
expect(daily?.amount.remainingFraction).toBe(0.8);
|
||||
expect(weekly?.label).toBe("Usage (Google)");
|
||||
expect(weekly?.window?.label).toBe("Weekly");
|
||||
expect(weekly?.window?.durationMs).toBe(7 * 24 * 60 * 60 * 1000);
|
||||
expect(weekly?.amount.remainingFraction).toBe(0.4);
|
||||
});
|
||||
|
||||
it("normalizes 7 Day window ids to the weekly usage window", async () => {
|
||||
const payload = {
|
||||
models: {
|
||||
gemini: {
|
||||
displayName: "Gemini",
|
||||
modelProvider: "MODEL_PROVIDER_GOOGLE",
|
||||
quotaInfoByWindow: {
|
||||
WINDOW_7_DAY: { remainingFraction: 0.6 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const report = await antigravityUsageProvider.fetchUsage!(
|
||||
{ provider: "google-antigravity", credential: makeCredential(), signal: undefined },
|
||||
makeCtx(fakeFetch(payload)),
|
||||
);
|
||||
|
||||
expect(report!.limits).toHaveLength(1);
|
||||
expect(report!.limits[0]!.scope.windowId).toBe("weekly");
|
||||
expect(report!.limits[0]!.window?.label).toBe("Weekly");
|
||||
});
|
||||
|
||||
it("includes email and projectId in report metadata", async () => {
|
||||
const payload = { models: { m: makeApiModel("M", { remainingFraction: 1 }) } };
|
||||
const report = await antigravityUsageProvider.fetchUsage!(
|
||||
@@ -274,6 +328,24 @@ describe("antigravity ranking strategy", () => {
|
||||
expect(secondary).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lets a weekly Antigravity quota become the ranking bottleneck", () => {
|
||||
const daily = makeLimit(0.8, "Google Daily");
|
||||
const weekly = makeLimit(0.2, "Google Weekly");
|
||||
daily.scope.windowId = "daily";
|
||||
daily.window = { id: "daily", label: "Daily", durationMs: 24 * 60 * 60 * 1000 };
|
||||
weekly.scope.windowId = "weekly";
|
||||
weekly.window = { id: "weekly", label: "Weekly", durationMs: 7 * 24 * 60 * 60 * 1000 };
|
||||
const report = {
|
||||
provider: "google-antigravity" as const,
|
||||
fetchedAt: Date.now(),
|
||||
limits: [weekly, daily],
|
||||
};
|
||||
|
||||
const { primary, secondary } = antigravityRankingStrategy.findWindowLimits(report);
|
||||
expect(primary).toBe(weekly);
|
||||
expect(secondary).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined windows when the credential has no usage limits", () => {
|
||||
const report = {
|
||||
provider: "google-antigravity" as const,
|
||||
|
||||
@@ -213,6 +213,38 @@ describe("formatUsageBreakdown", () => {
|
||||
expect(disclaimerIdx).toBeLessThan(firstLimitIdx);
|
||||
});
|
||||
|
||||
it("renders Antigravity weekly windows in the usage breakdown", () => {
|
||||
const now = Date.parse("2026-01-01T00:00:00.000Z");
|
||||
const reports: UsageReport[] = [
|
||||
{
|
||||
provider: "google-antigravity",
|
||||
fetchedAt: now,
|
||||
metadata: { email: "ag@example.test", projectId: "proj-1" },
|
||||
limits: [
|
||||
{
|
||||
id: "google-antigravity:google:default:weekly",
|
||||
label: "Usage (Google)",
|
||||
scope: { provider: "google-antigravity", projectId: "proj-1", windowId: "weekly" },
|
||||
window: {
|
||||
id: "weekly",
|
||||
label: "Weekly",
|
||||
durationMs: SEVEN_DAYS,
|
||||
resetsAt: now + SEVEN_DAYS,
|
||||
},
|
||||
amount: { unit: "percent", usedFraction: 0.6, remainingFraction: 0.4 },
|
||||
status: "ok",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const text = stripVTControlCharacters(formatUsageBreakdown(reports, [], now));
|
||||
expect(text).toContain("Google Antigravity");
|
||||
expect(text).toContain("Usage (Google) (Weekly)");
|
||||
expect(text).toContain("60.0% used");
|
||||
expect(text).toContain("0.40× quota left");
|
||||
});
|
||||
|
||||
it("renders saved reset expiry state for future and expired credits", () => {
|
||||
const now = Date.parse("2026-01-01T00:00:00.000Z");
|
||||
const reports: UsageReport[] = [
|
||||
|
||||
Reference in New Issue
Block a user