fix(usage): reuse Gemini CLI project ID for quota checks (#4454)

Integrated into release/v3.8.32
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-20 20:25:00 -03:00
committed by GitHub
parent 0225a0a02d
commit 84fd18726d
3 changed files with 183 additions and 9 deletions

View File

@@ -16,6 +16,7 @@ _In development — bullets added per PR; finalized at release._
- **fix(embeddings):** forward output dimensions to Gemini for consistent embedding dims. (thanks @nguyenha935)
- **fix(translator):** sanitize Read tool args from non-Anthropic models to prevent retry loops. (thanks @GodrezJr2)
- **fix(usage):** reuse Gemini CLI project ID for quota checks (avoid re-discovery). (thanks @Delcado19)
- **fix(combo): round-robin members fail over faster under concurrency saturation via a configurable queue depth** — when a round-robin combo member was saturated, requests sat in the per-model semaphore's **unbounded** queue and only failed over to the next member after the full `queueTimeoutMs` (default 30s) elapsed — so a burst of agentic requests deep-queued one hot member instead of spilling to healthy ones. The per-model semaphore now accepts a bounded queue depth and emits `SEMAPHORE_QUEUE_FULL` once it is full (the round-robin loop already cascades on that code), so a configured low depth fails over immediately. A new `queueDepth` combo-config knob (global default / provider override / per-combo, default **20** for backward compatibility; **0** = never queue → fail over now) is exposed in Settings → Combo Defaults. ([#3872](https://github.com/diegosouzapw/OmniRoute/issues/3872) — thanks @KooshaPari)
---

View File

@@ -1812,6 +1812,23 @@ function inferGitHubPlanName(data: JsonRecord, premiumQuota: UsageQuota | null):
const _geminiCliSubCache = new Map<string, SubscriptionCacheEntry>();
const GEMINI_CLI_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
/**
* Normalize a Cloud Code project value into a trimmed string (or null).
* The upstream `loadCodeAssist` endpoint returns the project either as a bare
* string or as an object of the form `{ id: "..." }`, and stored connection
* project ids can carry stray whitespace. Centralized here so the Gemini CLI
* usage path matches the executor/oauth normalization already shipped in
* `open-sse/executors/gemini-cli.ts` and `src/lib/oauth/services/gemini.ts`.
*/
function normalizeCloudCodeProjectId(project: unknown): string | null {
if (typeof project === "string") return project.trim() || null;
if (project && typeof project === "object") {
const candidate = (project as { id?: unknown }).id;
if (typeof candidate === "string") return candidate.trim() || null;
}
return null;
}
/**
* Gemini CLI Usage — fetch per-model quota from Cloud Code Assist API.
* Gemini CLI and Antigravity share the same upstream (cloudcode-pa.googleapis.com),
@@ -1827,17 +1844,28 @@ async function getGeminiUsage(
}
try {
const subscriptionInfo = await getGeminiCliSubscriptionInfoCached(accessToken);
const projectId =
connectionProjectId ||
providerSpecificData?.projectId ||
toRecord(subscriptionInfo).cloudaicompanionProject ||
null;
const plan = getGeminiCliPlanLabel(subscriptionInfo);
// #1271: the OAuth save path stores `projectId` on the connection (not always in
// `providerSpecificData`), and `loadCodeAssist` may return the project either as a
// bare string or wrapped in `{ id: "..." }`. Normalize both so the quota lookup
// reuses the stored project id and skips a redundant `loadCodeAssist` round-trip
// when it is already known.
let projectId =
normalizeCloudCodeProjectId(connectionProjectId) ||
normalizeCloudCodeProjectId(providerSpecificData?.projectId);
let plan = "Free";
if (!projectId) {
return { plan, message: "Gemini CLI project ID not available." };
const subscriptionInfo = await getGeminiCliSubscriptionInfoCached(accessToken);
projectId = normalizeCloudCodeProjectId(toRecord(subscriptionInfo).cloudaicompanionProject);
plan = getGeminiCliPlanLabel(subscriptionInfo);
}
if (!projectId) {
return {
plan,
message:
"Gemini CLI project ID not available. Reconnect Gemini CLI, or configure a Google Cloud project with Gemini Code Assist access before checking quota.",
};
}
// Use retrieveUserQuota (same endpoint as Gemini CLI /stats command).

View File

@@ -0,0 +1,145 @@
/**
* Ported from decolua/9router#1428 by @Delcado19 — reuses stored Gemini CLI
* project IDs for quota checks and normalizes {id: ...} object shapes that
* loadCodeAssist returns.
*/
import test from "node:test";
import assert from "node:assert/strict";
const usage = await import("../../open-sse/services/usage.ts");
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
test("getUsageForProvider(gemini-cli) reuses the projectId stored on the connection", async () => {
const originalFetch = globalThis.fetch;
const calls: { url: string; init?: RequestInit }[] = [];
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
calls.push({ url, init });
return jsonResponse({
buckets: [
{
modelId: "gemini-3-flash-preview",
remainingFraction: 0.75,
resetTime: "2026-05-25T12:00:00Z",
},
],
});
}) as typeof fetch;
try {
const result = (await usage.getUsageForProvider({
id: "gemini-cli-stored",
provider: "gemini-cli",
accessToken: "token",
projectId: "cloud-code-project",
})) as { quotas?: Record<string, { remainingPercentage?: number }> };
// Only the retrieveUserQuota call — no loadCodeAssist round-trip,
// because the stored projectId short-circuits it.
assert.equal(calls.length, 1, "must skip loadCodeAssist when projectId is stored");
assert.equal(
calls[0].url,
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota"
);
assert.equal(
JSON.parse(String(calls[0].init?.body)).project,
"cloud-code-project",
"must pass the stored projectId into retrieveUserQuota"
);
assert.equal(result.quotas?.["gemini-3-flash-preview"]?.remainingPercentage, 75);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider(gemini-cli) normalizes project objects returned by loadCodeAssist", async () => {
const originalFetch = globalThis.fetch;
const calls: { url: string; init?: RequestInit }[] = [];
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
calls.push({ url, init });
if (url.endsWith("loadCodeAssist")) {
return jsonResponse({
cloudaicompanionProject: { id: "project-from-load" },
currentTier: { name: "Free" },
});
}
return jsonResponse({ buckets: [] });
}) as typeof fetch;
try {
await usage.getUsageForProvider({
id: "gemini-cli-obj-shape",
provider: "gemini-cli",
accessToken: "token-obj-shape",
});
const quotaCall = calls.find((c) => c.url.endsWith("retrieveUserQuota"));
assert.ok(quotaCall, "quota lookup must occur after loadCodeAssist resolves");
assert.equal(
JSON.parse(String(quotaCall!.init?.body)).project,
"project-from-load",
"must unwrap {id: ...} into the bare project id"
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider(gemini-cli) trims whitespace-padded stored project ids", async () => {
const originalFetch = globalThis.fetch;
const calls: { url: string; init?: RequestInit }[] = [];
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
calls.push({ url, init });
return jsonResponse({ buckets: [] });
}) as typeof fetch;
try {
await usage.getUsageForProvider({
id: "gemini-cli-padded",
provider: "gemini-cli",
accessToken: "token-padded",
projectId: " padded-project ",
});
const quotaCall = calls.find((c) => c.url.endsWith("retrieveUserQuota"));
assert.ok(quotaCall, "quota lookup must run with the trimmed project id");
assert.equal(JSON.parse(String(quotaCall!.init?.body)).project, "padded-project");
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider(gemini-cli) returns actionable guidance when no project id is available", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => jsonResponse({})) as typeof fetch;
try {
const result = (await usage.getUsageForProvider({
id: "gemini-cli-no-project",
provider: "gemini-cli",
accessToken: "token-no-project",
})) as { message?: string };
assert.ok(result.message, "must surface a message when no projectId is resolvable");
assert.match(
result.message!,
/Reconnect Gemini CLI/i,
"error must guide the operator to reconnect"
);
assert.match(
result.message!,
/Gemini Code Assist/i,
"error must mention the Code Assist project requirement"
);
} finally {
globalThis.fetch = originalFetch;
}
});