From 7f0ea8e54699e612510a57f08b8dda4fce990d13 Mon Sep 17 00:00:00 2001 From: Bobby Bones <159389674+saariuslystoned@users.noreply.github.com> Date: Wed, 23 Sep 2026 00:36:33 -0400 Subject: [PATCH] fix: SuperGrok usage shows No usage data when xAI omits creditUsagePercent (#155790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related: #155782 ## What Problem This Solves Fixes: SuperGrok usage cards and `openclaw status --usage` show "No usage data" when xAI returns a valid weekly billing period that omits `creditUsagePercent`. ## User Impact User impact: OAuth SuperGrok accounts now get a specific "Included usage omitted" snapshot, plus plan and prepaid facts, instead of a generic missing-telemetry error. Grok inference was already working; only quota display was wrong. ## Why This Change Was Made xAI can omit a default-zero included-usage scalar on an otherwise valid weekly or monthly billing period. The parser now treats that as omitted included usage rather than inventing a percentage or reading on-demand pay-as-you-go counters as SuperGrok subscription quota. Legacy monthly `used` / `monthlyLimit` parsing and explicit `creditUsagePercent: 0` windows stay unchanged. ## Evidence Exact head: `19aae5939ec429b30333e8db836bf6276d583895`. No code change in this proof pass. Live usage-only billing probe against `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits` (HTTP 200, 413 bytes). Isolated disposable Gateway on `127.0.0.1:63012` (`openclaw gateway run --dev`). Live operator Gateway on `127.0.0.1:18789` (pid 22298) was not restarted, modified, or used for this proof. No inference requests. Sanitized live billing payload: valid weekly period, `creditUsagePercent` omitted, no legacy `used` / `monthlyLimit`: ```json { "config": { "currentPeriod": { "type": "USAGE_PERIOD_TYPE_WEEKLY", "start": "2026-09-21T12:01:29.688516+00:00", "end": "2026-09-28T12:01:29.688516+00:00" }, "onDemandCap": { "val": 0 }, "onDemandUsed": { "val": 0 }, "isUnifiedBillingUser": true, "prepaidBalance": { "val": 0 }, "billingPeriodStart": "2026-09-21T12:01:29.688516+00:00", "billingPeriodEnd": "2026-09-28T12:01:29.688516+00:00" } } ``` `has_creditUsagePercent`: false. Period recognized as weekly. PR-head CLI after the same live account, isolated config/port/session store (exit 0): ```text pnpm openclaw status --usage --timeout 30000 Usage: SuperGrok (SuperGrok) Included usage omitted Prepaid balance: $0.00 ``` ```text pnpm openclaw models status OAuth/token status - xai usage: Included usage omitted ``` Before (same payload on current main / pre-fix adapter): `SuperGrok: No usage data`. Focused tests: `node scripts/run-vitest.mjs extensions/xai/usage.test.ts --maxWorkers=1` — 12 passed. File wall 12.38s on one worker; individual cases 1–9ms. Inspected, sanitized Provider Plans billing-card pair from exact PR-head Control UI (`19aae5939ec429b30333e8db836bf6276d583895`) with a local mocked `usage.status` renderer. Isolated Vite loopback `http://127.0.0.1:51133/`; live operator Gateway `127.0.0.1:18789` (pid 22298) was not used, restarted, or modified. No inference requests. **Before** (pre-fix SuperGrok snapshot: `error: "No usage data"`): ![Before: SuperGrok Provider Plans card showing No usage data](https://github.com/user-attachments/assets/e315c018-edd5-4e58-99af-f6cbdebe6b66) **After** (PR-head SuperGrok snapshot: plan + prepaid `$0.00` + `Included usage omitted`): ![After: SuperGrok Provider Plans card showing Included usage omitted](https://github.com/user-attachments/assets/b6157fb5-1cb8-4876-8e00-2b8321b29a13) No emails, tokens, account identifiers, or private endpoints. Uploaded via the fork `user-attachments` endpoint after upstream `repository_id` returned 404 (no push on `openclaw/openclaw`). Remaining semantic uncertainty: omitted `creditUsagePercent` is reported as omitted, not proven 0%. Cross-client reports (including [stablyai/orca#20826](https://github.com/stablyai/orca/issues/20826)) suggest xAI drops default-zero credit fields, but that is not a documented xAI contract. Made with [Cursor](https://cursor.com) Co-authored-by: Ayaan Zaidi --- docs/providers/xai.md | 6 +- extensions/xai/usage.test.ts | 135 +++++++++++++++++++++++++++++++++++ extensions/xai/usage.ts | 72 ++++++++++++++++--- 3 files changed, 202 insertions(+), 11 deletions(-) diff --git a/docs/providers/xai.md b/docs/providers/xai.md index a40c3908951b..8640586c4530 100644 --- a/docs/providers/xai.md +++ b/docs/providers/xai.md @@ -85,7 +85,11 @@ bundled xAI model provider reuses it as a fallback too. `openclaw status --usage`, `/status`, and the Control UI usage cards show SuperGrok quota when the xAI provider is signed in with OAuth. OpenClaw fetches the Grok billing window for that subscription and reports its reset time through -the normal provider-usage surface. API-key-only xAI setups are intentionally not +the normal provider-usage surface. When xAI omits an included-usage percent on an +otherwise valid weekly or monthly billing period, OpenClaw reports that included +usage was omitted instead of inventing a percentage or showing generic +"No usage data". Pay-as-you-go on-demand counters are not treated as SuperGrok +subscription quota. API-key-only xAI setups are intentionally not shown as SuperGrok usage because xAI Console API credits and SuperGrok subscription quota are separate billing buckets. diff --git a/extensions/xai/usage.test.ts b/extensions/xai/usage.test.ts index ba981a92c7a6..e6995f005570 100644 --- a/extensions/xai/usage.test.ts +++ b/extensions/xai/usage.test.ts @@ -73,6 +73,122 @@ describe("fetchXaiUsage", () => { expect(result.plan).toBe("Premium+"); }); + it("keeps an explicit zero included-usage percent as a weekly window", async () => { + const reset = "2026-09-28T12:01:29Z"; + const mockFetch = createProviderUsageFetch(async () => + makeResponse(200, { + config: { + creditUsagePercent: 0, + currentPeriod: { + type: "USAGE_PERIOD_TYPE_WEEKLY", + start: "2026-09-21T12:01:29Z", + end: reset, + }, + }, + }), + ); + + const result = await fetchXaiUsage("oauth-token", 5000, mockFetch); + + expect(result).toMatchObject({ + displayName: "SuperGrok", + plan: "SuperGrok", + windows: [ + { + label: "Weekly", + usedPercent: 0, + resetAt: new Date(reset).getTime(), + }, + ], + }); + expect(result.error).toBeUndefined(); + expect(result.summary).toBeUndefined(); + }); + + it("reports omitted included usage for a valid weekly billing period without inventing a percent", async () => { + const reset = "2026-09-28T12:01:29Z"; + const mockFetch = createProviderUsageFetch(async () => + makeResponse(200, { + config: { + currentPeriod: { + type: "USAGE_PERIOD_TYPE_WEEKLY", + start: "2026-09-21T12:01:29Z", + end: reset, + }, + onDemandCap: { val: 0 }, + onDemandUsed: { val: 0 }, + isUnifiedBillingUser: true, + prepaidBalance: { val: 0 }, + billingPeriodStart: "2026-09-21T12:01:29Z", + billingPeriodEnd: reset, + }, + }), + ); + + const result = await fetchXaiUsage("oauth-token", 5000, mockFetch); + + expect(result).toEqual({ + provider: "xai", + displayName: "SuperGrok", + windows: [], + billing: [ + { + type: "balance", + label: "Prepaid balance", + amount: 0, + unit: "USD", + }, + ], + plan: "SuperGrok", + summary: "Included usage omitted", + }); + }); + + it("does not treat on-demand counters as included SuperGrok quota", async () => { + const mockFetch = createProviderUsageFetch(async () => + makeResponse(200, { + config: { + currentPeriod: { + type: "USAGE_PERIOD_TYPE_WEEKLY", + start: "2026-09-21T12:01:29Z", + end: "2026-09-28T12:01:29Z", + }, + onDemandCap: { val: 10000 }, + onDemandUsed: { val: 2500 }, + prepaidBalance: { val: 0 }, + }, + }), + ); + + const result = await fetchXaiUsage("oauth-token", 5000, mockFetch); + + expect(result.windows).toEqual([]); + expect(result.summary).toBe("Included usage omitted"); + expect(result.error).toBeUndefined(); + expect(result.plan).toBe("SuperGrok"); + }); + + it("reports omitted included usage for a valid monthly billing period", async () => { + const mockFetch = createProviderUsageFetch(async () => + makeResponse(200, { + config: { + currentPeriod: { + type: "USAGE_PERIOD_TYPE_MONTHLY", + start: "2026-09-01T00:00:00Z", + end: "2026-10-01T00:00:00Z", + }, + }, + }), + ); + + const result = await fetchXaiUsage("oauth-token", 5000, mockFetch); + + expect(result.windows).toEqual([]); + expect(result.summary).toBe("Included usage omitted"); + expect(result.error).toBeUndefined(); + expect(result.plan).toBe("SuperGrok"); + }); + it("returns token-expired errors for billing auth failures", async () => { const mockFetch = createProviderUsageFetch(async () => makeResponse(401, { error: "expired" })); @@ -90,6 +206,25 @@ describe("fetchXaiUsage", () => { ["malformed JSON", "{not-json", "Malformed billing response"], ["missing config", {}, "Malformed billing response"], ["missing usage fields", { config: { prepaidBalance: { val: "100" } } }, "No usage data"], + [ + "weekly period without bounds", + { config: { currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY" } } }, + "No usage data", + ], + [ + "unusable included-usage percent", + { + config: { + creditUsagePercent: -1, + currentPeriod: { + type: "USAGE_PERIOD_TYPE_WEEKLY", + start: "2026-09-21T12:01:29Z", + end: "2026-09-28T12:01:29Z", + }, + }, + }, + "No usage data", + ], ])("returns a stable error for %s", async (_name, payload, error) => { const mockFetch = createProviderUsageFetch(async () => makeResponse(200, payload)); diff --git a/extensions/xai/usage.ts b/extensions/xai/usage.ts index 3fbe14f8f588..bb5f806ccfdb 100644 --- a/extensions/xai/usage.ts +++ b/extensions/xai/usage.ts @@ -69,8 +69,45 @@ function parsePercent(value: unknown): number | undefined { return clampPercent(value); } +function readCurrentPeriod(config: BillingConfig) { + return asOptionalRecord(config["currentPeriod"] ?? config["current_period"]); +} + +function readPeriodType(currentPeriod: Record | undefined): string { + return normalizeOptionalString(currentPeriod?.type) ?? ""; +} + +function readPeriodBoundMs( + config: BillingConfig, + currentPeriod: Record | undefined, + bound: "start" | "end", +): number | undefined { + const periodKey = bound === "start" ? "start" : "end"; + const billingKey = bound === "start" ? "billingPeriodStart" : "billingPeriodEnd"; + const billingSnakeKey = bound === "start" ? "billing_period_start" : "billing_period_end"; + return parseDateStringTimestampMs( + currentPeriod?.[periodKey] ?? config[billingKey] ?? config[billingSnakeKey], + ); +} + +function hasRecognizedUsagePeriod(config: BillingConfig): boolean { + const currentPeriod = readCurrentPeriod(config); + const periodType = readPeriodType(currentPeriod); + if (!periodType.endsWith("WEEKLY") && !periodType.endsWith("MONTHLY")) { + return false; + } + return ( + readPeriodBoundMs(config, currentPeriod, "start") !== undefined || + readPeriodBoundMs(config, currentPeriod, "end") !== undefined + ); +} + +function hasIncludedUsagePercentField(config: BillingConfig): boolean { + return (config["creditUsagePercent"] ?? config["credit_usage_percent"]) !== undefined; +} + function resolveUsageWindow(config: BillingConfig): UsageWindow | undefined { - const currentPeriod = asOptionalRecord(config["currentPeriod"] ?? config["current_period"]); + const currentPeriod = readCurrentPeriod(config); const explicitPercent = parsePercent( config["creditUsagePercent"] ?? config["credit_usage_percent"], ); @@ -85,7 +122,7 @@ function resolveUsageWindow(config: BillingConfig): UsageWindow | undefined { return undefined; } - const periodType = normalizeOptionalString(currentPeriod?.type) ?? ""; + const periodType = readPeriodType(currentPeriod); const label = periodType.endsWith("WEEKLY") ? "Weekly" : periodType.endsWith("MONTHLY") || @@ -94,9 +131,7 @@ function resolveUsageWindow(config: BillingConfig): UsageWindow | undefined { config["billing_period_end"] !== undefined ? "Monthly" : "Usage"; - const resetAt = parseDateStringTimestampMs( - currentPeriod?.end ?? config["billingPeriodEnd"] ?? config["billing_period_end"], - ); + const resetAt = readPeriodBoundMs(config, currentPeriod, "end"); return { label, usedPercent: percent, @@ -132,21 +167,38 @@ function buildSuperGrokUsageSnapshot(data: unknown): ProviderUsageSnapshot { } const window = resolveUsageWindow(config); - if (!window) { + const billing = resolveBilling(config); + const plan = + parsePlan(payload?.["subscription_tier"] ?? payload?.["subscriptionTier"]) ?? "SuperGrok"; + if (window) { + return { + provider: XAI_PROVIDER_ID, + displayName: "SuperGrok", + windows: [window], + billing, + plan, + }; + } + + // xAI omits default-zero included-usage scalars on valid weekly/monthly + // billing responses. Do not invent a percent, and do not read on-demand + // pay-as-you-go counters as SuperGrok subscription quota. + if (!hasIncludedUsagePercentField(config) && hasRecognizedUsagePeriod(config)) { return { provider: XAI_PROVIDER_ID, displayName: "SuperGrok", windows: [], - error: "No usage data", + billing, + plan, + summary: "Included usage omitted", }; } return { provider: XAI_PROVIDER_ID, displayName: "SuperGrok", - windows: [window], - billing: resolveBilling(config), - plan: parsePlan(payload?.["subscription_tier"] ?? payload?.["subscriptionTier"]) ?? "SuperGrok", + windows: [], + error: "No usage data", }; }