Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions apps/server/src/provider/antigravityUsageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ const summary = {
const fixture = Effect.fn("AntigravityUsageTest.fixture")(function* (
options: {
summary?: unknown;
/** Per-host quota summaries; hosts not listed fall back to `summary`. */
summaryByHost?: Record<string, unknown>;
models?: unknown;
summaryStatus?: number;
/** Per-host summary statuses; hosts not listed fall back to `summaryStatus`. */
summaryStatusByHost?: Record<string, number>;
tokenStatus?: number;
refreshToken?: string;
/** Google account id from userinfo; omit to make userinfo fail. */
Expand All @@ -77,7 +81,7 @@ const fixture = Effect.fn("AntigravityUsageTest.fixture")(function* (
);
if (!options.missingToken) yield* writeToken(options.refreshToken ?? "refresh-a");
const requests: Array<{ url: string; body: string }> = [];
const http = HttpClient.make((request, _url, _signal, fiber) =>
const http = HttpClient.make((request, url, _signal, fiber) =>
Effect.sync(() => {
const init = Context.getOrUndefined(fiber.context, FetchHttpClient.RequestInit);
expect(init?.redirect).toBe("manual");
Expand Down Expand Up @@ -107,7 +111,9 @@ const fixture = Effect.fn("AntigravityUsageTest.fixture")(function* (
return HttpClientResponse.fromWeb(
request,
request.url.endsWith(":retrieveUserQuotaSummary")
? Response.json(options.summary ?? summary, { status: options.summaryStatus ?? 200 })
? Response.json(options.summaryByHost?.[url.host] ?? options.summary ?? summary, {
status: options.summaryStatusByHost?.[url.host] ?? options.summaryStatus ?? 200,
})
: Response.json(options.models ?? { models: {} }),
);
}),
Expand Down Expand Up @@ -141,9 +147,56 @@ describe("Antigravity subscription limits", () => {
expect(byId.get("3p-5h")?.usedPercent).toBeCloseTo(56);
yield* probe;
expect(requests.filter((request) => request.url.endsWith("/token"))).toHaveLength(1);
// Both quota endpoints are read on every probe.
expect(
requests.filter((request) => request.url.endsWith(":retrieveUserQuotaSummary")),
).toHaveLength(2);
).toHaveLength(4);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("merges quota groups that only one endpoint reports", () =>
Effect.gen(function* () {
const { probe } = yield* fixture({
summaryByHost: {
// The daily endpoint has answered with only the Claude/GPT group; the
// other endpoint returns the full default summary.
"daily-cloudcode-pa.googleapis.com": {
groups: [
{
displayName: "Claude and GPT models",
buckets: [
{ bucketId: "3p-weekly", window: "weekly", remainingFraction: 0.5 },
{ bucketId: "3p-5h", window: "5h", remainingFraction: 0.3 },
],
},
],
},
},
});
const limits = yield* probe;
expect(limits.windows.map((window) => window.id).toSorted()).toEqual([
"3p-5h",
"3p-weekly",
"gemini-5h",
"gemini-weekly",
]);
const byId = new Map(limits.windows.map((window) => [window.id, window]));
expect(byId.get("gemini-weekly")?.label).toBe("Gemini · Weekly");
// When endpoints disagree about a bucket, the most-used reading wins,
// whichever endpoint reported it.
expect(byId.get("3p-5h")?.usedPercent).toBeCloseTo(70);
expect(byId.get("3p-weekly")?.usedPercent).toBe(100);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("keeps one endpoint's quotas when the other fails", () =>
Effect.gen(function* () {
const { probe } = yield* fixture({
summaryStatusByHost: { "daily-cloudcode-pa.googleapis.com": 503 },
});
const limits = yield* probe;
expect(limits.unavailable).toBeUndefined();
expect(limits.windows).toHaveLength(4);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

Expand Down
40 changes: 26 additions & 14 deletions apps/server/src/provider/antigravityUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,26 +248,38 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")(
accessToken = cached.accessToken;
account = cached.account;
}
// Endpoints can report different quota groups (the daily endpoint has
// returned the Claude/GPT group without Gemini), so merge every endpoint's
// windows. The most-used reading of a bucket wins, as in modelWindows.
const fetchWindows = Effect.fn("AntigravityUsageProbe.fetchWindows")(function* <E>(
operation: string,
parse: (response: unknown) => Effect.Effect<ServerProviderUsageWindow[], E>,
) {
for (const endpoint of ENDPOINTS) {
const windows = yield* request(
HttpClientRequest.post(`${endpoint}/v1internal:${operation}`).pipe(
HttpClientRequest.setHeaders({
Authorization: `Bearer ${accessToken}`,
"User-Agent": `antigravity/hub/1.0.0 (aidev_client; os_type=${platform}; arch=${architecture}; cl=963137146)`,
}),
HttpClientRequest.bodyJsonUnsafe(token.project_id ? { project: token.project_id } : {}),
const responses = yield* Effect.forEach(
ENDPOINTS,
(endpoint) =>
request(
HttpClientRequest.post(`${endpoint}/v1internal:${operation}`).pipe(
HttpClientRequest.setHeaders({
Authorization: `Bearer ${accessToken}`,
"User-Agent": `antigravity/hub/1.0.0 (aidev_client; os_type=${platform}; arch=${architecture}; cl=963137146)`,
}),
HttpClientRequest.bodyJsonUnsafe(
token.project_id ? { project: token.project_id } : {},
),
),
).pipe(
Effect.flatMap(parse),
Effect.orElseSucceed(() => []),
),
).pipe(
Effect.flatMap(parse),
Effect.orElseSucceed(() => []),
);
if (windows.length > 0) return windows;
{ concurrency: "unbounded" },
);
const windows = new Map<string, ServerProviderUsageWindow>();
for (const window of responses.flat()) {
const previous = windows.get(window.id);
if (!previous || window.usedPercent > previous.usedPercent) windows.set(window.id, window);
}
return [];
return [...windows.values()];
});
const summary = yield* fetchWindows("retrieveUserQuotaSummary", (response) =>
decodeSummary(response).pipe(Effect.map(summaryWindows)),
Expand Down
Loading