Reuse Gemini CLI project ID for usage (#1428)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
87
tests/unit/gemini-usage-projectid.test.js
Normal file
87
tests/unit/gemini-usage-projectid.test.js
Normal 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user