Reuse Gemini CLI project ID for usage (#1428)

This commit is contained in:
Delcado
2026-05-26 06:23:47 +02:00
committed by GitHub
parent 57b3e94e40
commit dcc46f2eee
3 changed files with 115 additions and 7 deletions

View File

@@ -1,3 +1,8 @@
# Unreleased
## Fixes
- Gemini CLI: reuse stored OAuth project IDs for quota checks and show clearer setup guidance when the project is missing (#1271)
# v0.4.59 (2026-05-21)
## Fixes
@@ -508,4 +513,4 @@
# v0.3.89 (2026-04-13)
## Improvements
- Improved dashboard access control by blocking tunnel/Tailscale access when disabled
- Improved dashboard access control by blocking tunnel/Tailscale access when disabled

View File

@@ -58,13 +58,17 @@ const CLAUDE_CONFIG = {
* @returns {Object} Usage data with quotas
*/
export async function getUsageForProvider(connection, proxyOptions = null) {
const { provider, accessToken, apiKey, providerSpecificData } = connection;
const { provider, accessToken, apiKey, providerSpecificData, projectId } = connection;
const providerDataWithProjectId = {
...(providerSpecificData || {}),
...(projectId ? { projectId } : {}),
};
switch (provider) {
case "github":
return await getGitHubUsage(accessToken, providerSpecificData, proxyOptions);
case "gemini-cli":
return await getGeminiUsage(accessToken, providerSpecificData, proxyOptions);
return await getGeminiUsage(accessToken, providerDataWithProjectId, proxyOptions);
case "antigravity":
return await getAntigravityUsage(accessToken, providerSpecificData, proxyOptions);
case "claude":
@@ -222,18 +226,22 @@ async function getGeminiUsage(accessToken, providerSpecificData, proxyOptions =
}
try {
// Resolve project id: prefer connection-stored id, else loadCodeAssist lookup
let projectId = providerSpecificData?.projectId || null;
// Resolve project id: prefer connection-stored id, else loadCodeAssist lookup.
// #1271: OAuth save stores projectId on the connection, not providerSpecificData.
let projectId = normalizeCloudCodeProjectId(providerSpecificData?.projectId);
let plan = "Free";
if (!projectId) {
const subInfo = await getGeminiSubscriptionInfo(accessToken, proxyOptions);
projectId = subInfo?.cloudaicompanionProject || null;
projectId = normalizeCloudCodeProjectId(subInfo?.cloudaicompanionProject);
plan = subInfo?.currentTier?.name || plan;
}
if (!projectId) {
return { plan, message: "Gemini CLI project ID not available." };
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.",
};
}
const controller = new AbortController();
@@ -289,6 +297,14 @@ async function getGeminiUsage(accessToken, providerSpecificData, proxyOptions =
}
}
function normalizeCloudCodeProjectId(project) {
if (typeof project === "string") return project.trim() || null;
if (project && typeof project === "object" && typeof project.id === "string") {
return project.id.trim() || null;
}
return null;
}
/**
* Get Gemini CLI subscription info via loadCodeAssist
*/

View File

@@ -0,0 +1,87 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: vi.fn(),
}));
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
import { getUsageForProvider } from "../../open-sse/services/usage.js";
function jsonResponse(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("Gemini CLI usage project id resolution", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("uses the projectId stored on the provider connection", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({
buckets: [
{
modelId: "gemini-3-flash-preview",
remainingFraction: 0.75,
resetTime: "2026-05-25T12:00:00Z",
},
],
}));
const usage = await getUsageForProvider({
provider: "gemini-cli",
accessToken: "token",
projectId: "cloud-code-project",
});
expect(proxyAwareFetch).toHaveBeenCalledTimes(1);
expect(proxyAwareFetch).toHaveBeenCalledWith(
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
expect.objectContaining({
body: JSON.stringify({ project: "cloud-code-project" }),
}),
null
);
expect(usage.quotas["gemini-3-flash-preview"]).toMatchObject({
used: 250,
total: 1000,
remainingPercentage: 75,
});
});
it("normalizes project objects returned by loadCodeAssist", async () => {
proxyAwareFetch
.mockResolvedValueOnce(jsonResponse({
cloudaicompanionProject: { id: "project-from-load" },
currentTier: { name: "Free" },
}))
.mockResolvedValueOnce(jsonResponse({ buckets: [] }));
await getUsageForProvider({
provider: "gemini-cli",
accessToken: "token",
});
expect(proxyAwareFetch).toHaveBeenLastCalledWith(
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
expect.objectContaining({
body: JSON.stringify({ project: "project-from-load" }),
}),
null
);
});
it("returns actionable guidance when no project id is available", async () => {
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({}));
const usage = await getUsageForProvider({
provider: "gemini-cli",
accessToken: "token",
});
expect(usage.message).toContain("Reconnect Gemini CLI");
expect(usage.message).toContain("Gemini Code Assist");
});
});