mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
Reuse Gemini CLI project ID for usage (#1428)
This commit is contained in:
+6
-1
@@ -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)
|
# v0.4.59 (2026-05-21)
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
@@ -508,4 +513,4 @@
|
|||||||
# v0.3.89 (2026-04-13)
|
# v0.3.89 (2026-04-13)
|
||||||
|
|
||||||
## Improvements
|
## 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
|
* @returns {Object} Usage data with quotas
|
||||||
*/
|
*/
|
||||||
export async function getUsageForProvider(connection, proxyOptions = null) {
|
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) {
|
switch (provider) {
|
||||||
case "github":
|
case "github":
|
||||||
return await getGitHubUsage(accessToken, providerSpecificData, proxyOptions);
|
return await getGitHubUsage(accessToken, providerSpecificData, proxyOptions);
|
||||||
case "gemini-cli":
|
case "gemini-cli":
|
||||||
return await getGeminiUsage(accessToken, providerSpecificData, proxyOptions);
|
return await getGeminiUsage(accessToken, providerDataWithProjectId, proxyOptions);
|
||||||
case "antigravity":
|
case "antigravity":
|
||||||
return await getAntigravityUsage(accessToken, providerSpecificData, proxyOptions);
|
return await getAntigravityUsage(accessToken, providerSpecificData, proxyOptions);
|
||||||
case "claude":
|
case "claude":
|
||||||
@@ -222,18 +226,22 @@ async function getGeminiUsage(accessToken, providerSpecificData, proxyOptions =
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Resolve project id: prefer connection-stored id, else loadCodeAssist lookup
|
// Resolve project id: prefer connection-stored id, else loadCodeAssist lookup.
|
||||||
let projectId = providerSpecificData?.projectId || null;
|
// #1271: OAuth save stores projectId on the connection, not providerSpecificData.
|
||||||
|
let projectId = normalizeCloudCodeProjectId(providerSpecificData?.projectId);
|
||||||
let plan = "Free";
|
let plan = "Free";
|
||||||
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
const subInfo = await getGeminiSubscriptionInfo(accessToken, proxyOptions);
|
const subInfo = await getGeminiSubscriptionInfo(accessToken, proxyOptions);
|
||||||
projectId = subInfo?.cloudaicompanionProject || null;
|
projectId = normalizeCloudCodeProjectId(subInfo?.cloudaicompanionProject);
|
||||||
plan = subInfo?.currentTier?.name || plan;
|
plan = subInfo?.currentTier?.name || plan;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!projectId) {
|
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();
|
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
|
* Get Gemini CLI subscription info via loadCodeAssist
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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