fix: SuperGrok usage shows No usage data when xAI omits creditUsagePercent (#155790)

<!--
Related: #155782

Required PR title:
fix: SuperGrok usage shows No usage data when xAI omits creditUsagePercent
-->

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 <hi@obviy.us>
This commit is contained in:
Bobby Bones 2026-09-23 00:36:33 -04:00 • committed by GitHub
parent a298fa9b67
commit 7f0ea8e546
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 202 additions and 11 deletions

View file

@ -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 `openclaw status --usage`, `/status`, and the Control UI usage cards show
SuperGrok quota when the xAI provider is signed in with OAuth. OpenClaw fetches 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 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 shown as SuperGrok usage because xAI Console API credits and SuperGrok
subscription quota are separate billing buckets. subscription quota are separate billing buckets.

View file

@ -73,6 +73,122 @@ describe("fetchXaiUsage", () => {
expect(result.plan).toBe("Premium+"); 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 () => { it("returns token-expired errors for billing auth failures", async () => {
const mockFetch = createProviderUsageFetch(async () => makeResponse(401, { error: "expired" })); const mockFetch = createProviderUsageFetch(async () => makeResponse(401, { error: "expired" }));
@ -90,6 +206,25 @@ describe("fetchXaiUsage", () => {
["malformed JSON", "{not-json", "Malformed billing response"], ["malformed JSON", "{not-json", "Malformed billing response"],
["missing config", {}, "Malformed billing response"], ["missing config", {}, "Malformed billing response"],
["missing usage fields", { config: { prepaidBalance: { val: "100" } } }, "No usage data"], ["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) => { ])("returns a stable error for %s", async (_name, payload, error) => {
const mockFetch = createProviderUsageFetch(async () => makeResponse(200, payload)); const mockFetch = createProviderUsageFetch(async () => makeResponse(200, payload));

View file

@ -69,8 +69,45 @@ function parsePercent(value: unknown): number | undefined {
return clampPercent(value); return clampPercent(value);
} }
function readCurrentPeriod(config: BillingConfig) {
return asOptionalRecord(config["currentPeriod"] ?? config["current_period"]);
}
function readPeriodType(currentPeriod: Record<string, unknown> | undefined): string {
return normalizeOptionalString(currentPeriod?.type) ?? "";
}
function readPeriodBoundMs(
config: BillingConfig,
currentPeriod: Record<string, unknown> | 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 { function resolveUsageWindow(config: BillingConfig): UsageWindow | undefined {
const currentPeriod = asOptionalRecord(config["currentPeriod"] ?? config["current_period"]); const currentPeriod = readCurrentPeriod(config);
const explicitPercent = parsePercent( const explicitPercent = parsePercent(
config["creditUsagePercent"] ?? config["credit_usage_percent"], config["creditUsagePercent"] ?? config["credit_usage_percent"],
); );
@ -85,7 +122,7 @@ function resolveUsageWindow(config: BillingConfig): UsageWindow | undefined {
return undefined; return undefined;
} }
const periodType = normalizeOptionalString(currentPeriod?.type) ?? ""; const periodType = readPeriodType(currentPeriod);
const label = periodType.endsWith("WEEKLY") const label = periodType.endsWith("WEEKLY")
? "Weekly" ? "Weekly"
: periodType.endsWith("MONTHLY") || : periodType.endsWith("MONTHLY") ||
@ -94,9 +131,7 @@ function resolveUsageWindow(config: BillingConfig): UsageWindow | undefined {
config["billing_period_end"] !== undefined config["billing_period_end"] !== undefined
? "Monthly" ? "Monthly"
: "Usage"; : "Usage";
const resetAt = parseDateStringTimestampMs( const resetAt = readPeriodBoundMs(config, currentPeriod, "end");
currentPeriod?.end ?? config["billingPeriodEnd"] ?? config["billing_period_end"],
);
return { return {
label, label,
usedPercent: percent, usedPercent: percent,
@ -132,21 +167,38 @@ function buildSuperGrokUsageSnapshot(data: unknown): ProviderUsageSnapshot {
} }
const window = resolveUsageWindow(config); 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 { return {
provider: XAI_PROVIDER_ID, provider: XAI_PROVIDER_ID,
displayName: "SuperGrok", displayName: "SuperGrok",
windows: [], windows: [],
error: "No usage data", billing,
plan,
summary: "Included usage omitted",
}; };
} }
return { return {
provider: XAI_PROVIDER_ID, provider: XAI_PROVIDER_ID,
displayName: "SuperGrok", displayName: "SuperGrok",
windows: [window], windows: [],
billing: resolveBilling(config), error: "No usage data",
plan: parsePlan(payload?.["subscription_tier"] ?? payload?.["subscriptionTier"]) ?? "SuperGrok",
}; };
} }