feat(xai): track SuperGrok weekly limit + API usage quota

Fetch OAuth quota from cli-chat-proxy billing, GetGrokCreditsConfig weekly
window, and settings plan label so the dashboard matches grok.com usage.
This commit is contained in:
2026-07-13 23:44:36 +07:00
parent b1d368d960
commit ab9a3c1d43
6 changed files with 643 additions and 1 deletions

View File

@@ -25,6 +25,15 @@ export default {
clientId: "b1a00492-073a-47ea-816f-4c329264a828", clientId: "b1a00492-073a-47ea-816f-4c329264a828",
tokenUrl: "https://auth.x.ai/oauth2/token", tokenUrl: "https://auth.x.ai/oauth2/token",
refreshUrl: "https://auth.x.ai/oauth2/token", refreshUrl: "https://auth.x.ai/oauth2/token",
// OAuth-only SuperGrok quota surfaces:
// - url: monthly API usage allotment (JSON)
// - creditsUrl: weekly SuperGrok limit (grpc-web)
// - settingsUrl: plan label (subscription_tier_display)
usage: {
url: "https://cli-chat-proxy.grok.com/v1/billing",
creditsUrl: "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig",
settingsUrl: "https://cli-chat-proxy.grok.com/v1/settings",
},
}, },
models: [ models: [
{ id: "grok-4", name: "Grok 4" }, { id: "grok-4", name: "Grok 4" },
@@ -57,4 +66,7 @@ export default {
endpoint: "https://api.x.ai/v1/responses", endpoint: "https://api.x.ai/v1/responses",
pricingUrl: "https://x.ai/api#pricing", pricingUrl: "https://x.ai/api#pricing",
}, },
features: {
usage: true,
},
}; };

View File

@@ -11,6 +11,7 @@ export { consumeCodexRateLimitResetCredit, getCodexRateLimitResetCredits };
import { getKiroUsage } from "./usage/kiro.js"; import { getKiroUsage } from "./usage/kiro.js";
import { getMiniMaxUsage } from "./usage/minimax.js"; import { getMiniMaxUsage } from "./usage/minimax.js";
import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.js"; import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.js";
import { getXaiUsage } from "./usage/xai.js";
import { import {
getQwenUsage, getQwenUsage,
getIflowUsage, getIflowUsage,
@@ -43,6 +44,7 @@ const USAGE_HANDLERS = {
"minimax-cn": (c) => getMiniMaxUsage(c.apiKey, c.provider, c.proxyOptions), "minimax-cn": (c) => getMiniMaxUsage(c.apiKey, c.provider, c.proxyOptions),
"vercel-ai-gateway": (c) => getVercelAiGatewayUsage(c.apiKey, c.proxyOptions), "vercel-ai-gateway": (c) => getVercelAiGatewayUsage(c.apiKey, c.proxyOptions),
"codebuddy-cn": (c) => getCodeBuddyCnUsage(c.accessToken, c.apiKey, c.providerSpecificData, c.proxyOptions), "codebuddy-cn": (c) => getCodeBuddyCnUsage(c.accessToken, c.apiKey, c.providerSpecificData, c.proxyOptions),
xai: (c) => getXaiUsage(c.accessToken, c.proxyOptions),
}; };
export async function getUsageForProvider(connection, proxyOptions = null) { export async function getUsageForProvider(connection, proxyOptions = null) {

View File

@@ -0,0 +1,326 @@
/**
* xAI (Grok) OAuth usage handler
*
* SuperGrok quota is split across two upstream surfaces (OAuth only):
*
* 1) Monthly / API usage allotment (JSON)
* GET https://cli-chat-proxy.grok.com/v1/billing
* {
* "config": {
* "monthlyLimit": { "val": 15000 },
* "used": { "val": 733 },
* "onDemandCap": { "val": 0 },
* "billingPeriodStart": "...",
* "billingPeriodEnd": "..."
* }
* }
*
* 2) Weekly SuperGrok limit (grpc-web protobuf)
* POST https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig
* Empty request frame; response message1 contains:
* usedPercent (float32), window start/end timestamps, nested windows.
* This is what the grok.com usage page labels "Weekly limit" / "Resets …".
*
* Plan label comes from cli-chat-proxy settings:
* GET https://cli-chat-proxy.grok.com/v1/settings → subscription_tier_display
*
* Note: grok.com/rest/rate-limits is a short chat-window (e.g. 2h query count)
* behind Cloudflare browser cookies — not usable with pure OAuth bearer.
*/
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { U, parseResetTime, toFiniteNumber } from "./shared.js";
// Empty grpc-web request frame: flag(0) + length(0) + no payload.
const GRPC_WEB_EMPTY_FRAME = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00]);
function moneyVal(wrapper) {
if (wrapper == null) return null;
if (typeof wrapper === "number") return toFiniteNumber(wrapper, null);
if (typeof wrapper === "object" && wrapper.val != null) {
return toFiniteNumber(wrapper.val, null);
}
return null;
}
function authHeaders(accessToken, extra = {}) {
return {
Authorization: `Bearer ${accessToken}`,
...extra,
};
}
function readVarint(buf, offset) {
let val = 0;
let shift = 0;
let i = offset;
while (i < buf.length) {
const b = buf[i++];
val |= (b & 0x7f) << shift;
if ((b & 0x80) === 0) return { value: val >>> 0, offset: i };
shift += 7;
if (shift > 35) break;
}
return null;
}
/**
* Minimal protobuf decoder for GetGrokCreditsConfig.
* Only understands varint / fixed32 / fixed64 / length-delimited.
*/
function parseProtobufFields(buf) {
const fields = [];
let i = 0;
while (i < buf.length) {
const key = readVarint(buf, i);
if (!key) break;
i = key.offset;
const field = key.value >>> 3;
const wt = key.value & 7;
if (wt === 0) {
const v = readVarint(buf, i);
if (!v) break;
i = v.offset;
fields.push({ field, type: "varint", value: v.value });
} else if (wt === 1) {
if (i + 8 > buf.length) break;
fields.push({ field, type: "fixed64", value: buf.subarray(i, i + 8) });
i += 8;
} else if (wt === 5) {
if (i + 4 > buf.length) break;
fields.push({
field,
type: "fixed32",
value: buf.readFloatLE(i),
});
i += 4;
} else if (wt === 2) {
const ln = readVarint(buf, i);
if (!ln) break;
i = ln.offset;
if (i + ln.value > buf.length) break;
fields.push({
field,
type: "bytes",
value: buf.subarray(i, i + ln.value),
});
i += ln.value;
} else {
break;
}
}
return fields;
}
function parseTimestamp(bytes) {
if (!bytes || !bytes.length) return null;
const fields = parseProtobufFields(bytes);
const seconds = fields.find((f) => f.field === 1 && f.type === "varint")?.value;
if (!Number.isFinite(seconds) || seconds <= 0) return null;
return new Date(seconds * 1000).toISOString();
}
/**
* Parse grpc-web response bytes from GetGrokCreditsConfig.
* Returns { usedPercent, resetAt, periodStart } or null.
*/
export function parseGrokCreditsConfig(raw) {
if (!raw || !raw.length) return null;
const buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
// grpc-web data frame: 1-byte flag + 4-byte big-endian length + message
if (buf.length < 5) return null;
const flag = buf[0];
// Data frames have flag 0; ignore trailer frames (flag 0x80).
if (flag !== 0) return null;
const msgLen = buf.readUInt32BE(1);
if (msgLen <= 0 || 5 + msgLen > buf.length) return null;
const msg = buf.subarray(5, 5 + msgLen);
// Response is typically { 1: CreditsConfig }
const top = parseProtobufFields(msg);
const configBytes = top.find((f) => f.field === 1 && f.type === "bytes")?.value || msg;
const fields = parseProtobufFields(configBytes);
const usedPercentRaw = fields.find((f) => f.field === 1 && f.type === "fixed32")?.value;
const periodStart = parseTimestamp(fields.find((f) => f.field === 4 && f.type === "bytes")?.value);
const periodEnd = parseTimestamp(fields.find((f) => f.field === 5 && f.type === "bytes")?.value);
if (usedPercentRaw == null || !Number.isFinite(usedPercentRaw)) return null;
const usedPercent = Math.max(0, Math.min(100, usedPercentRaw));
return {
usedPercent,
remainingPercent: Math.max(0, 100 - usedPercent),
periodStart,
resetAt: periodEnd,
};
}
async function fetchBilling(accessToken, billingUrl, proxyOptions) {
const response = await proxyAwareFetch(billingUrl, {
method: "GET",
headers: authHeaders(accessToken, { Accept: "application/json" }),
}, proxyOptions);
if (response.status === 401 || response.status === 403) {
return { error: "auth", status: response.status };
}
if (!response.ok) {
return { error: "http", status: response.status };
}
const data = await response.json().catch(() => null);
if (!data || typeof data !== "object") {
return { error: "json" };
}
return { data };
}
async function fetchWeeklyCredits(accessToken, creditsUrl, proxyOptions) {
if (!creditsUrl) return null;
try {
const response = await proxyAwareFetch(creditsUrl, {
method: "POST",
headers: authHeaders(accessToken, {
"Content-Type": "application/grpc-web+proto",
"x-grpc-web": "1",
"x-user-agent": "connect-es/2.1.1",
Accept: "*/*",
Origin: "https://grok.com",
Referer: "https://grok.com/?_s=usage",
}),
body: GRPC_WEB_EMPTY_FRAME,
}, proxyOptions);
if (!response.ok) return null;
const ab = await response.arrayBuffer();
return parseGrokCreditsConfig(Buffer.from(ab));
} catch {
return null;
}
}
async function fetchPlanLabel(accessToken, settingsUrl, proxyOptions) {
if (!settingsUrl) return null;
try {
const response = await proxyAwareFetch(settingsUrl, {
method: "GET",
headers: authHeaders(accessToken, { Accept: "application/json" }),
}, proxyOptions);
if (!response.ok) return null;
const data = await response.json().catch(() => null);
const label = data?.subscription_tier_display;
return typeof label === "string" && label.trim() ? label.trim() : null;
} catch {
return null;
}
}
/**
* @param {string} accessToken - xAI OAuth access token
* @param {object|null} proxyOptions
*/
export async function getXaiUsage(accessToken, proxyOptions = null) {
if (!accessToken) {
return { message: "xAI usage unavailable: no access token. Re-authorize the connection." };
}
const cfg = U("xai") || {};
const billingUrl = cfg.url;
if (!billingUrl) {
return { message: "xAI usage endpoint is not configured." };
}
try {
const [billingResult, weekly, planLabel] = await Promise.all([
fetchBilling(accessToken, billingUrl, proxyOptions),
fetchWeeklyCredits(accessToken, cfg.creditsUrl, proxyOptions),
fetchPlanLabel(accessToken, cfg.settingsUrl, proxyOptions),
]);
if (billingResult.error === "auth") {
return { message: "xAI OAuth token expired or unauthorized. Please re-authorize." };
}
const quotas = {};
let periodStart = null;
let periodEnd = null;
let onDemandCap = 0;
if (billingResult.data) {
const config =
billingResult.data.config && typeof billingResult.data.config === "object"
? billingResult.data.config
: billingResult.data;
const monthlyLimit = moneyVal(config.monthlyLimit);
const used = moneyVal(config.used);
onDemandCap = moneyVal(config.onDemandCap) ?? 0;
periodEnd = parseResetTime(config.billingPeriodEnd);
periodStart = parseResetTime(config.billingPeriodStart);
// Absolute credit counts — do NOT put remaining credits on `remaining`
// (QuotaTable treats remaining as a 0-100 percentage; same pitfall as Qoder).
if (monthlyLimit != null && monthlyLimit > 0) {
const usedSafe = Math.max(0, used ?? 0);
quotas.api_usage = {
used: usedSafe,
total: monthlyLimit,
remainingCredits: Math.max(0, monthlyLimit - usedSafe),
unit: "credits",
resetAt: periodEnd,
unlimited: false,
};
}
if (onDemandCap > 0) {
quotas.on_demand = {
used: 0,
total: onDemandCap,
remainingCredits: onDemandCap,
unit: "credits",
resetAt: periodEnd,
unlimited: false,
};
}
}
// Weekly SuperGrok window — percentage-based like Claude/Codex windows.
if (weekly) {
quotas.weekly = {
used: weekly.usedPercent,
total: 100,
remaining: weekly.remainingPercent,
remainingPercentage: weekly.remainingPercent,
resetAt: weekly.resetAt || null,
unlimited: false,
};
if (!periodStart && weekly.periodStart) periodStart = weekly.periodStart;
}
if (Object.keys(quotas).length === 0) {
const statusHint =
billingResult.error === "http"
? ` Billing API temporarily unavailable (${billingResult.status}).`
: "";
return {
plan: planLabel || "xAI",
message: `xAI connected. No quota allotment reported for this account.${statusHint}`,
periodStart,
periodEnd,
quotas: {},
};
}
return {
plan: planLabel || "xAI",
periodStart,
periodEnd,
onDemandCap,
quotas,
};
} catch (error) {
return { message: `xAI connected. Unable to fetch billing: ${error.message}` };
}
}

View File

@@ -451,6 +451,48 @@ export function parseQuotaData(provider, data) {
} }
break; break;
case "xai":
// xAI mixes:
// - weekly: percentage window (used/total 0-100) from GetGrokCreditsConfig
// - api_usage: absolute monthly credits from /v1/billing
// For absolute rows, do not forward remainingCredits as `remaining`
// (QuotaTable treats remaining as a 0-100 percentage; same pitfall as Qoder).
if (data.quotas) {
Object.entries(data.quotas).forEach(([quotaType, quota]) => {
const name =
quotaType === "weekly"
? "Weekly limit"
: quotaType === "api_usage"
? "Api usage"
: quotaType === "monthly"
? "Api usage"
: quotaType === "on_demand"
? "On-demand"
: quotaType;
if (quotaType === "weekly") {
normalizedQuotas.push({
name,
used: quota.used || 0,
total: quota.total || 100,
remaining: quota.remaining,
remainingPercentage: quota.remainingPercentage ?? quota.remaining,
resetAt: quota.resetAt || null,
});
return;
}
normalizedQuotas.push({
name,
used: quota.used || 0,
total: quota.total || 0,
unit: quota.unit,
resetAt: quota.resetAt || null,
});
});
}
break;
default: default:
// Generic fallback for unknown providers // Generic fallback for unknown providers
if (data.quotas) { if (data.quotas) {

View File

@@ -15,7 +15,7 @@ const load = () => import("../../open-sse/services/usage.js");
const SUPPORTED = [ const SUPPORTED = [
"github", "gemini-cli", "antigravity", "claude", "codex", "kiro", "github", "gemini-cli", "antigravity", "claude", "codex", "kiro",
"qoder", "qwen", "iflow", "ollama", "glm", "glm-cn", "qoder", "qwen", "iflow", "ollama", "glm", "glm-cn",
"minimax", "minimax-cn", "vercel-ai-gateway", "minimax", "minimax-cn", "vercel-ai-gateway", "xai",
]; ];
describe("usage dispatch", () => { describe("usage dispatch", () => {

View File

@@ -0,0 +1,260 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
proxyAwareFetch: vi.fn(),
}));
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
import { getUsageForProvider } from "../../open-sse/services/usage.js";
import { parseGrokCreditsConfig } from "../../open-sse/services/usage/xai.js";
import { parseQuotaData } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
function billingResponse(config) {
return new Response(JSON.stringify({ config }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
function settingsResponse(tier = "SuperGrok") {
return new Response(JSON.stringify({ subscription_tier_display: tier }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
/**
* Build a minimal grpc-web GetGrokCreditsConfig payload matching the live
* SuperGrok response shape: usedPercent float + start/end timestamps.
*/
function buildCreditsGrpcFrame({ usedPercent = 18, startSec = 1783673158, endSec = 1784277958 }) {
// Encode protobuf Timestamp {1: seconds}
const encodeVarint = (n) => {
const out = [];
let v = n >>> 0;
while (v >= 0x80) {
out.push((v & 0x7f) | 0x80);
v >>>= 7;
}
out.push(v);
return Buffer.from(out);
};
const encodeKey = (field, wt) => encodeVarint((field << 3) | wt);
const encodeTimestamp = (seconds) => {
const body = Buffer.concat([encodeKey(1, 0), encodeVarint(seconds)]);
return body;
};
const encodeLen = (field, bytes) =>
Buffer.concat([encodeKey(field, 2), encodeVarint(bytes.length), bytes]);
const encodeFloat = (field, f) => {
const buf = Buffer.alloc(4);
buf.writeFloatLE(f, 0);
return Buffer.concat([encodeKey(field, 5), buf]);
};
const startTs = encodeTimestamp(startSec);
const endTs = encodeTimestamp(endSec);
const config = Buffer.concat([
encodeFloat(1, usedPercent),
encodeLen(4, startTs),
encodeLen(5, endTs),
]);
const msg = encodeLen(1, config);
const frame = Buffer.alloc(5 + msg.length);
frame[0] = 0;
frame.writeUInt32BE(msg.length, 1);
msg.copy(frame, 5);
return frame;
}
function creditsResponse(opts = {}) {
const frame = buildCreditsGrpcFrame(opts);
return new Response(frame, {
status: 200,
headers: { "Content-Type": "application/grpc-web+proto" },
});
}
function mockXaiHappyPath({ used = 733, limit = 15000, weeklyPercent = 18 } = {}) {
proxyAwareFetch.mockImplementation(async (url) => {
if (String(url).includes("/v1/billing")) {
return billingResponse({
monthlyLimit: { val: limit },
used: { val: used },
onDemandCap: { val: 0 },
billingPeriodStart: "2026-07-01T00:00:00+00:00",
billingPeriodEnd: "2026-08-01T00:00:00+00:00",
});
}
if (String(url).includes("GetGrokCreditsConfig")) {
return creditsResponse({ usedPercent: weeklyPercent });
}
if (String(url).includes("/v1/settings")) {
return settingsResponse("SuperGrok");
}
return new Response("{}", { status: 404 });
});
}
describe("xAI usage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("parseGrokCreditsConfig extracts weekly used% and reset timestamp", () => {
const frame = buildCreditsGrpcFrame({
usedPercent: 18,
startSec: 1783673158,
endSec: 1784277958,
});
const parsed = parseGrokCreditsConfig(frame);
expect(parsed).toMatchObject({
usedPercent: 18,
remainingPercent: 82,
});
expect(parsed.resetAt).toBe(new Date(1784277958 * 1000).toISOString());
expect(parsed.periodStart).toBe(new Date(1783673158 * 1000).toISOString());
});
it("fetches billing + weekly credits + settings in parallel", async () => {
mockXaiHappyPath();
const usage = await getUsageForProvider({
provider: "xai",
accessToken: "tok-abc",
});
const urls = proxyAwareFetch.mock.calls.map((c) => String(c[0]));
expect(urls).toEqual(
expect.arrayContaining([
"https://cli-chat-proxy.grok.com/v1/billing",
"https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig",
"https://cli-chat-proxy.grok.com/v1/settings",
]),
);
expect(usage.plan).toBe("SuperGrok");
expect(usage.quotas.weekly).toMatchObject({
used: 18,
total: 100,
remaining: 82,
});
expect(usage.quotas.api_usage).toMatchObject({
used: 733,
total: 15000,
unit: "credits",
});
expect(usage.quotas.api_usage.resetAt).toBe("2026-08-01T00:00:00.000Z");
});
it("still returns weekly when billing fails but credits succeed", async () => {
proxyAwareFetch.mockImplementation(async (url) => {
if (String(url).includes("/v1/billing")) {
return new Response("nope", { status: 503 });
}
if (String(url).includes("GetGrokCreditsConfig")) {
return creditsResponse({ usedPercent: 42 });
}
if (String(url).includes("/v1/settings")) {
return settingsResponse("SuperGrok");
}
return new Response("{}", { status: 404 });
});
const usage = await getUsageForProvider({
provider: "xai",
accessToken: "tok",
});
expect(usage.plan).toBe("SuperGrok");
expect(usage.quotas.weekly).toMatchObject({ used: 42, total: 100, remaining: 58 });
expect(usage.quotas.api_usage).toBeUndefined();
});
it("includes on-demand row when onDemandCap > 0", async () => {
proxyAwareFetch.mockImplementation(async (url) => {
if (String(url).includes("/v1/billing")) {
return billingResponse({
monthlyLimit: { val: 1000 },
used: { val: 100 },
onDemandCap: { val: 500 },
billingPeriodEnd: "2026-08-01T00:00:00+00:00",
});
}
if (String(url).includes("GetGrokCreditsConfig")) {
return creditsResponse({ usedPercent: 10 });
}
if (String(url).includes("/v1/settings")) {
return settingsResponse("SuperGrok");
}
return new Response("{}", { status: 404 });
});
const usage = await getUsageForProvider({
provider: "xai",
accessToken: "tok",
});
expect(usage.quotas.on_demand).toMatchObject({
used: 0,
total: 500,
remainingCredits: 500,
});
});
it("returns auth message on 401 billing", async () => {
proxyAwareFetch.mockImplementation(async (url) => {
if (String(url).includes("/v1/billing")) {
return new Response("unauthorized", { status: 401 });
}
return new Response("{}", { status: 404 });
});
const usage = await getUsageForProvider({
provider: "xai",
accessToken: "tok",
});
expect(usage.message).toMatch(/re-authorize/i);
});
it("parseQuotaData maps weekly + api_usage labels", () => {
const rows = parseQuotaData("xai", {
quotas: {
weekly: {
used: 18,
total: 100,
remaining: 82,
remainingPercentage: 82,
resetAt: "2026-07-17T08:45:58.000Z",
},
api_usage: {
used: 733,
total: 15000,
remainingCredits: 14267,
unit: "credits",
resetAt: "2026-08-01T00:00:00.000Z",
},
},
});
expect(rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "Weekly limit",
used: 18,
total: 100,
remaining: 82,
}),
expect.objectContaining({
name: "Api usage",
used: 733,
total: 15000,
unit: "credits",
}),
]),
);
const api = rows.find((r) => r.name === "Api usage");
expect(api.remaining).toBeUndefined();
});
});