mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
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"`):  **After** (PR-head SuperGrok snapshot: plan + prepaid `$0.00` + `Included usage omitted`):  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:
parent
a298fa9b67
commit
7f0ea8e546
3 changed files with 202 additions and 11 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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 {
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue