diff --git a/.changeset/fn-8629-grok-usage-percent.md b/.changeset/fn-8629-grok-usage-percent.md new file mode 100644 index 0000000000..3f3e0a3d70 --- /dev/null +++ b/.changeset/fn-8629-grok-usage-percent.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent false 100% Grok usage when the CLI omits a credit percentage. +category: fix +dev: Grok billing windows now require a finite API-supplied creditUsagePercent value. diff --git a/packages/dashboard/src/__tests__/usage.test.ts b/packages/dashboard/src/__tests__/usage.test.ts index 166180099a..846449c368 100644 --- a/packages/dashboard/src/__tests__/usage.test.ts +++ b/packages/dashboard/src/__tests__/usage.test.ts @@ -3819,7 +3819,8 @@ describe("usage", () => { expect(mockRequest).toHaveBeenCalledTimes(1); }); - it("treats an omitted exhausted percentage as 100% used for a valid weekly CLI billing period", async () => { + it("does not fabricate a weekly credit window when the CLI billing percentage is omitted", async () => { + vi.stubEnv("GROK_API_KEY", "env-grok-key"); mockReadFile.mockImplementation(async (filePath: string) => { if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; return Promise.reject(new Error("File not found")); @@ -3843,14 +3844,74 @@ describe("usage", () => { const grok = providers.find((provider) => provider.name === "Grok")!; expect(grok.status).toBe("ok"); + expect(grok.windows).toEqual([]); + expect(grok.windows.some((window) => window.percentUsed === 100)).toBe(false); + expect(mockRequest).toHaveBeenCalledTimes(2); + }); + + it("renders a zero-percent weekly CLI credit window", async () => { + mockReadFile.mockImplementation(async (filePath: string) => { + if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; + return Promise.reject(new Error("File not found")); + }); + const periodEnd = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(); + mockGrokBillingResponse(200, { + config: { + currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end: periodEnd }, + billingPeriodEnd: periodEnd, + creditUsagePercent: 0, + }, + }); + + const providers = await fetchAllProviderUsage(); + const grok = providers.find((provider) => provider.name === "Grok")!; + expect(grok.windows).toHaveLength(1); expect(grok.windows[0]).toMatchObject({ label: "Weekly (credits)", - percentUsed: 100, - percentLeft: 0, + percentUsed: 0, + percentLeft: 100, + }); + }); + + it.each(["not-a-number", null])("falls back without a CLI credit window for a non-numeric percentage: %j", async (creditUsagePercent) => { + mockReadFile.mockImplementation(async (filePath: string) => { + if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; + return Promise.reject(new Error("File not found")); + }); + mockGrokBillingResponse(200, { + config: { creditUsagePercent }, + }); + + const providers = await fetchAllProviderUsage(); + const grok = providers.find((provider) => provider.name === "Grok")!; + + expect(grok.status).toBe("error"); + expect(grok.windows).toEqual([]); + expect(grok.error).toContain("grok login"); + }); + + it("retains the Credits label for a non-weekly numeric CLI percentage", async () => { + mockReadFile.mockImplementation(async (filePath: string) => { + if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; + return Promise.reject(new Error("File not found")); + }); + mockGrokBillingResponse(200, { + config: { + currentPeriod: { type: "USAGE_PERIOD_TYPE_MONTHLY" }, + creditUsagePercent: 25, + }, + }); + + const providers = await fetchAllProviderUsage(); + const grok = providers.find((provider) => provider.name === "Grok")!; + + expect(grok.windows).toHaveLength(1); + expect(grok.windows[0]).toMatchObject({ + label: "Credits", + percentUsed: 25, + percentLeft: 75, }); - expect(grok.windows[0].resetText).toContain("resets in"); - expect(mockRequest).toHaveBeenCalledTimes(1); }); it("falls back to the xAI API-key validity card when CLI billing fails", async () => { diff --git a/packages/dashboard/src/usage.ts b/packages/dashboard/src/usage.ts index 3cf1253277..456f774945 100644 --- a/packages/dashboard/src/usage.ts +++ b/packages/dashboard/src/usage.ts @@ -1718,16 +1718,11 @@ async function fetchGrokCliBillingUsage(token: string, usage: ProviderUsage): Pr const parsedReset = _parseResetTimestamp(config.billingPeriodEnd ?? config.currentPeriod?.end); const isWeekly = config.currentPeriod?.type === "USAGE_PERIOD_TYPE_WEEKLY"; /* - FNXC:UsageProviders 2026-07-14-14:47: - Grok's billing endpoint omits `creditUsagePercent` when the weekly allowance is exhausted. Grok Build renders that valid reduced config as “Weekly limit: 0%” (zero allowance remaining), while Fusion's usage model stores percent consumed. Therefore the omitted exhausted value maps to 100% used—not 0% used. Only infer exhaustion when the response still proves a weekly billing period and reset boundary, so malformed payloads continue to fail closed. + FNXC:UsageProviders 2026-07-31-20:31: + A real account reported zero Grok credit usage while its billing response omitted `creditUsagePercent`, disproving the former omitted-field-to-100% inference. Emit a credits window only for a finite API-supplied percentage; field absence must fall through to the API-key validity or CLI-auth error card rather than fabricate consumption. */ - const rawPercentUsed = config.creditUsagePercent; - const pctUsed = typeof rawPercentUsed === "number" && Number.isFinite(rawPercentUsed) - ? rawPercentUsed - : isWeekly && parsedReset - ? 100 - : undefined; - if (pctUsed === undefined) return false; + const pctUsed = config.creditUsagePercent; + if (typeof pctUsed !== "number" || !Number.isFinite(pctUsed)) return false; usage.windows.push({ label: isWeekly ? "Weekly (credits)" : "Credits",