fix(quota): show Grok unified-billing usage as a weekly window (#4029)

xAI moved Grok accounts to unified billing. The plain /v1/billing
response now returns a zeroed legacy monthly shape (monthlyLimit.val 0),
which rendered as "0 credits left" with no bar in Settings -> Usage.

Fetch /v1/billing?format=credits — the same query the Grok CLI /usage
command uses — and map creditUsagePercent + currentPeriod onto a usage
window, matching the Claude/Codex weekly window convention. The legacy
monthly-credits balance path is unchanged; unified-billing responses
omit monthlyLimit/used, so it naturally yields no balance.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Lite
2026-08-29 22:20:20 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent eef9be8863
commit a88aa49f4c
2 changed files with 80 additions and 6 deletions
@@ -3,14 +3,20 @@ import { homedir } from "node:os";
import { join } from "node:path";
import type { Logger } from "pino";
import { z } from "zod";
import type { ProviderUsage, ProviderUsageBalance } from "../../../server/messages.js";
import type {
ProviderUsage,
ProviderUsageBalance,
ProviderUsageWindow,
} from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import {
ApiNumberSchema,
ApiOptionalStringSchema,
toneFromUsedPct,
usedPctOf,
fetchProviderApi,
unavailableUsage,
windowFromUsedPct,
} from "../usage.js";
const GrokUsageResponseSchema = z.object({
@@ -26,6 +32,13 @@ const GrokUsageResponseSchema = z.object({
val: ApiNumberSchema.optional(),
})
.nullish(),
creditUsagePercent: ApiNumberSchema.optional(),
currentPeriod: z
.object({
type: ApiOptionalStringSchema,
end: ApiOptionalStringSchema,
})
.nullish(),
})
.nullish(),
usage: z
@@ -84,6 +97,22 @@ function grokMonthlyCreditBalance(
};
}
function grokUsageWindow(
response: z.infer<typeof GrokUsageResponseSchema>,
): ProviderUsageWindow | null {
const percent = response.config?.creditUsagePercent;
if (typeof percent !== "number") return null;
const period = response.config?.currentPeriod;
const weekly = (period?.type ?? "").toUpperCase().includes("WEEKLY");
return windowFromUsedPct({
id: weekly ? "weekly" : "monthly",
label: weekly ? "Weekly" : "Monthly",
utilizationPct: percent,
resetsAt: period?.end ?? null,
tone: toneFromUsedPct(percent),
});
}
export class GrokQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "grok";
readonly displayName = "Grok";
@@ -104,9 +133,11 @@ export class GrokQuotaProvider implements ProviderUsageFetcher {
if (!token) return unavailableUsage(this);
// The Grok CLI's /usage uses ?format=credits; without it, unified-billing accounts
// get a zeroed legacy monthly shape (monthlyLimit.val 0) instead of real usage.
const res = await fetchProviderApi(
this.fetchApi,
"https://cli-chat-proxy.grok.com/v1/billing",
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
{
headers: {
Authorization: `Bearer ${token}`,
@@ -123,13 +154,14 @@ export class GrokQuotaProvider implements ProviderUsageFetcher {
const resp = GrokUsageResponseSchema.parse(await res.json());
const balance = grokMonthlyCreditBalance(resp);
const window = grokUsageWindow(resp);
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: null,
windows: [],
windows: window ? [window] : [],
balances: balance ? [balance] : [],
details: [],
error: null,
@@ -871,7 +871,7 @@ describe("real provider usage fetchers", () => {
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing",
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
() =>
jsonResponse({
config: { monthlyLimit: { val: 0 }, used: { val: 0 } },
@@ -900,7 +900,7 @@ describe("real provider usage fetchers", () => {
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing",
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
() =>
jsonResponse({
config: {
@@ -973,7 +973,7 @@ describe("real provider usage fetchers", () => {
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing",
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
() =>
jsonResponse({
config: { monthlyLimit: { val: 50 } },
@@ -998,6 +998,48 @@ describe("real provider usage fetchers", () => {
});
});
it("fetches Grok unified-billing usage as a weekly window", async () => {
process.env["GROK_API_KEY"] = "grok_test_token";
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
() =>
jsonResponse({
config: {
currentPeriod: {
type: "USAGE_PERIOD_TYPE_WEEKLY",
start: "2026-08-24T09:41:40.001370+00:00",
end: "2026-08-31T09:41:40.001370+00:00",
},
creditUsagePercent: 76.0,
isUnifiedBillingUser: true,
billingPeriodStart: "2026-08-24T09:41:40.001370+00:00",
billingPeriodEnd: "2026-08-31T09:41:40.001370+00:00",
},
}),
],
]),
);
const grok = findProvider(await service().listUsage(), "grok");
expect(grok).toMatchObject({
status: "available",
windows: [
{
id: "weekly",
label: "Weekly",
usedPct: 76,
remainingPct: 24,
resetsAt: "2026-08-31T09:41:40.001370+00:00",
tone: "warning",
},
],
balances: [],
});
});
it("fetches Kimi usage from KIMI_TOKEN", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
fetchApi = mockFetch(