fix(quota): restore Grok Settings usage for current CLI auth/billing (#2353)

* fix(quota): restore Grok Settings usage for current CLI auth/billing

Grok CLI no longer stores a top-level access_token or usage.creditUsage.
Read nested auth key tokens and config.used.val so Settings → Usage
shows monthly credits again. Keep legacy shapes and env tokens working.

Fixes #2352

* fix(quota): make Grok auth-file tests work on Windows

Inject homeDir into GrokQuotaProvider like Kimi so nested
~/.grok/auth.json tests do not depend on os.homedir() which
ignores $HOME on Windows (USERPROFILE).
This commit is contained in:
Jason@HND
2026-07-28 18:49:21 +08:00
committed by GitHub
parent f91a984348
commit f0d7eeb98c
2 changed files with 155 additions and 11 deletions
@@ -51,6 +51,11 @@ function writeKimiCredentials(dir: string, accessToken: string): void {
);
}
function writeGrokAuth(home: string, auth: Record<string, unknown>): void {
mkdirSync(join(home, ".grok"), { recursive: true });
writeFileSync(join(home, ".grok", "auth.json"), JSON.stringify(auth));
}
function writeMiniMaxConfig(dir: string, payload: Record<string, unknown>): void {
mkdirSync(join(dir, ".mmx"), { recursive: true });
writeFileSync(join(dir, ".mmx", "config.json"), JSON.stringify(payload));
@@ -382,7 +387,13 @@ describe("real provider usage fetchers", () => {
new CopilotQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
new CursorQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
new ZaiQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
new GrokQuotaProvider({ logger, fetch: fetchThroughTestDouble }),
new GrokQuotaProvider({
logger,
fetch: fetchThroughTestDouble,
// Match Kimi: inject temp HOME so nested auth-file tests work on Windows
// (os.homedir() uses USERPROFILE there and ignores process.env.HOME).
homeDir,
}),
new KimiQuotaProvider({
logger,
fetch: fetchThroughTestDouble,
@@ -720,8 +731,7 @@ describe("real provider usage fetchers", () => {
"https://cli-chat-proxy.grok.com/v1/billing",
() =>
jsonResponse({
config: { monthlyLimit: { val: 0 } },
usage: { creditUsage: 0 },
config: { monthlyLimit: { val: 0 }, used: { val: 0 } },
}),
],
]),
@@ -742,6 +752,109 @@ describe("real provider usage fetchers", () => {
});
});
it("fetches Grok usage from live billing shape (config.used.val)", async () => {
process.env["GROK_API_KEY"] = "grok_test_token";
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing",
() =>
jsonResponse({
config: {
monthlyLimit: { val: 150000 },
used: { val: 37886 },
billingPeriodStart: "2026-07-01T00:00:00+00:00",
billingPeriodEnd: "2026-08-01T00:00:00+00:00",
},
}),
],
]),
);
const grok = findProvider(await service().listUsage(), "grok");
expect(grok).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "monthly_credits",
used: 37886,
remaining: 112114,
limit: 150000,
unit: "credits",
}),
],
});
});
it("fetches Grok usage with nested ~/.grok/auth.json key token", async () => {
writeGrokAuth(homeDir, {
"https://auth.x.ai::test-user-id": {
key: "nested_jwt_token",
refresh_token: "rt_nested",
expires_at: "2026-08-01T00:00:00Z",
user_id: "test-user-id",
email: "user@example.com",
},
});
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({
config: {
monthlyLimit: { val: 100 },
used: { val: 25 },
},
});
}) as typeof fetch;
const grok = findProvider(await service().listUsage(), "grok");
expect(authorization).toBe("Bearer nested_jwt_token");
expect(grok).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "monthly_credits",
used: 25,
remaining: 75,
limit: 100,
}),
],
});
});
it("still accepts legacy Grok usage.creditUsage when config.used is absent", async () => {
process.env["GROK_API_KEY"] = "grok_test_token";
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing",
() =>
jsonResponse({
config: { monthlyLimit: { val: 50 } },
usage: { creditUsage: 10 },
}),
],
]),
);
const grok = findProvider(await service().listUsage(), "grok");
expect(grok).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "monthly_credits",
used: 10,
remaining: 40,
limit: 50,
}),
],
});
});
it("fetches Kimi usage from KIMI_TOKEN", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
fetchApi = mockFetch(