diff --git a/packages/server/src/services/quota-fetcher/providers/grok.ts b/packages/server/src/services/quota-fetcher/providers/grok.ts index 096cda8335..83e7fde837 100644 --- a/packages/server/src/services/quota-fetcher/providers/grok.ts +++ b/packages/server/src/services/quota-fetcher/providers/grok.ts @@ -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, +): 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, diff --git a/packages/server/src/services/quota-fetcher/service.test.ts b/packages/server/src/services/quota-fetcher/service.test.ts index dc324f1621..2c254678fd 100644 --- a/packages/server/src/services/quota-fetcher/service.test.ts +++ b/packages/server/src/services/quota-fetcher/service.test.ts @@ -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(