Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features
- **xAI**: Grok Imagine video generation (`/v1/videos`) + CLI
- **xAI**: show OAuth account plan and available credits in Quota Tracker
- **CLI tools**: Grok Build setup — writes `[model.9router]` to `~/.grok/config.toml`
- **GitHub Copilot**: route Claude models through Copilot's native `/v1/messages`
- **Kiro**: add GPT-5.6 model family (#2596)
Expand Down Expand Up @@ -407,4 +408,4 @@
# v0.4.46 (2026-05-15)

## Breaking Changes
- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL
- Tunnel public URL changed — old tunnel links no longer work, please reconnect to get the new URL
11 changes: 11 additions & 0 deletions open-sse/providers/registry/xai.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { GROK_CLI_BASE_URL } from "../../config/grokCli.js";

export default {
id: "xai",
priority: 280,
Expand Down Expand Up @@ -25,6 +27,12 @@ export default {
clientId: "b1a00492-073a-47ea-816f-4c329264a828",
tokenUrl: "https://auth.x.ai/oauth2/token",
refreshUrl: "https://auth.x.ai/oauth2/token",
// xAI OAuth grants include grok-cli:access, so the same token can read
// the billing and subscription endpoints used by the official Grok CLI.
usage: {
url: `${GROK_CLI_BASE_URL}/billing?format=credits`,
userUrl: `${GROK_CLI_BASE_URL}/user?include=subscription`,
},
},
models: [
{ id: "grok-4", name: "Grok 4" },
Expand All @@ -44,4 +52,7 @@ export default {
endpoint: "https://api.x.ai/v1/responses",
pricingUrl: "https://x.ai/api#pricing",
},
features: {
usage: true,
},
};
3 changes: 2 additions & 1 deletion open-sse/services/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export { consumeCodexRateLimitResetCredit, getCodexRateLimitResetCredits };
import { getKiroUsage } from "./usage/kiro.js";
import { getMiniMaxUsage } from "./usage/minimax.js";
import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.js";
import { getGrokCliUsage } from "./usage/grok-cli.js";
import { getGrokCliUsage, getXaiUsage } from "./usage/grok-cli.js";
import {
getQwenUsage,
getIflowUsage,
Expand Down Expand Up @@ -45,6 +45,7 @@ const USAGE_HANDLERS = {
"vercel-ai-gateway": (c) => getVercelAiGatewayUsage(c.apiKey, c.proxyOptions),
"codebuddy-cn": (c) => getCodeBuddyCnUsage(c.accessToken, c.apiKey, c.providerSpecificData, c.proxyOptions),
"grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
xai: (c) => getXaiUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
};

export async function getUsageForProvider(connection, proxyOptions = null) {
Expand Down
37 changes: 25 additions & 12 deletions open-sse/services/usage/grok-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ import {
GROK_CLI_VERSION,
} from "../../config/grokCli.js";

const USAGE = U("grok-cli");
const BILLING_URL = USAGE.url || "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
const USER_URL = USAGE.userUrl || "https://cli-chat-proxy.grok.com/v1/user?include=subscription";

/** Unwrap protobuf-json `{ val: n }` or plain numbers/strings. */
function unwrapVal(value, fallback = 0) {
if (value == null) return fallback;
Expand Down Expand Up @@ -261,41 +257,50 @@ export function parseGrokCliBilling(billing, user = null) {
* @param {object|null} providerSpecificData
* @param {object|null} proxyOptions
*/
export async function getGrokCliUsage(accessToken, providerSpecificData = null, proxyOptions = null) {
async function getGrokUsage(
provider,
providerLabel,
accessToken,
providerSpecificData = null,
proxyOptions = null,
) {
if (!accessToken) {
return { message: "Grok CLI access token not available." };
return { message: `${providerLabel} access token not available.` };
}

const usage = U(provider);
const billingUrl = usage.url || "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
const userUrl = usage.userUrl || "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
const headers = buildGrokCliHeaders(accessToken, providerSpecificData);

try {
// Fetch billing + user profile in parallel (same pattern as official CLI startup)
const [billingRes, userRes] = await Promise.all([
proxyAwareFetch(
BILLING_URL,
billingUrl,
{ method: "GET", headers },
proxyOptions,
),
proxyAwareFetch(
USER_URL,
userUrl,
{ method: "GET", headers },
proxyOptions,
).catch(() => null),
]);

if (billingRes.status === 401 || billingRes.status === 403) {
return { message: "Grok CLI authentication expired. Please re-authorize." };
return { message: `${providerLabel} authentication expired. Please re-authorize.` };
}

if (!billingRes.ok) {
const errText = await billingRes.text().catch(() => "");
const trimmed = errText ? `: ${errText.slice(0, 200)}` : "";
return { message: `Grok CLI billing API error (${billingRes.status})${trimmed}` };
return { message: `${providerLabel} billing API error (${billingRes.status})${trimmed}` };
}

const billing = await billingRes.json().catch(() => null);
if (!billing || typeof billing !== "object") {
return { message: "Grok CLI billing response was not JSON." };
return { message: `${providerLabel} billing response was not JSON.` };
}

let user = null;
Expand Down Expand Up @@ -323,6 +328,14 @@ export async function getGrokCliUsage(accessToken, providerSpecificData = null,
quotas: parsed.quotas,
};
} catch (error) {
return { message: `Grok CLI usage error: ${error.message}` };
return { message: `${providerLabel} usage error: ${error.message}` };
}
}

export async function getGrokCliUsage(accessToken, providerSpecificData = null, proxyOptions = null) {
return getGrokUsage("grok-cli", "Grok CLI", accessToken, providerSpecificData, proxyOptions);
}

export async function getXaiUsage(accessToken, providerSpecificData = null, proxyOptions = null) {
return getGrokUsage("xai", "xAI", accessToken, providerSpecificData, proxyOptions);
}
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ export function parseQuotaData(provider, data) {
break;

case "grok-cli":
case "xai":
// Grok Build credits (on-demand window + prepaid balance).
// Do NOT forward absolute `remaining` — getRemainingPercentage treats
// it as a 0–100 percentage (same as Qoder). Use remainingPercentage.
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/grok-cli-usage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ describe("grok-cli registry usage flag", () => {
});
});

describe("xai registry usage flag", () => {
it("exposes Grok billing transport urls", () => {
const cfg = PROVIDERS.xai;
expect(cfg.usage?.url).toContain("/v1/billing");
expect(cfg.usage?.userUrl).toContain("/v1/user");
});

it("is listed in USAGE_SUPPORTED_PROVIDERS", () => {
expect(USAGE_SUPPORTED_PROVIDERS).toContain("xai");
});
});

describe("parseGrokCliBilling", () => {
it("maps on-demand cap/used + prepaid balance", () => {
const parsed = parseGrokCliBilling(ACTIVE_BILLING, USER_PROFILE);
Expand Down Expand Up @@ -225,6 +237,47 @@ describe("getUsageForProvider(grok-cli)", () => {
});
});

describe("getUsageForProvider(xai)", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("reuses Grok billing endpoints for xAI OAuth connections", async () => {
proxyAwareFetch
.mockResolvedValueOnce(jsonResponse(ACTIVE_BILLING))
.mockResolvedValueOnce(jsonResponse(USER_PROFILE));

const usage = await getUsageForProvider({
provider: "xai",
accessToken: "xai-oauth-token",
});

expect(usage.plan).toBe("Grok Code");
expect(usage.quotas["On-demand"]).toMatchObject({
used: 35,
total: 100,
remainingPercentage: 65,
});
expect(proxyAwareFetch.mock.calls[0][0]).toContain("/v1/billing");
expect(proxyAwareFetch.mock.calls[0][1].headers.Authorization).toBe(
"Bearer xai-oauth-token",
);
});

it("uses an xAI-specific reauthorization message", async () => {
proxyAwareFetch
.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401))
.mockResolvedValueOnce(jsonResponse(USER_PROFILE));

const usage = await getUsageForProvider({
provider: "xai",
accessToken: "expired",
});

expect(usage.message).toMatch(/^xAI authentication expired/);
});
});

describe("parseQuotaData(grok-cli)", () => {
it("forwards remainingPercentage for dashboard bars", () => {
const rows = parseQuotaData("grok-cli", {
Expand All @@ -248,4 +301,23 @@ describe("parseQuotaData(grok-cli)", () => {
remainingPercentage: 65,
});
});

it("preserves remainingPercentage for xAI OAuth quotas", () => {
const rows = parseQuotaData("xai", {
quotas: {
"On-demand": {
used: 35,
total: 100,
remainingPercentage: 65,
resetAt: "2026-07-15T00:00:00.000Z",
},
},
});

expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
name: "On-demand",
remainingPercentage: 65,
});
});
});
2 changes: 1 addition & 1 deletion tests/unit/usage-dispatch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const load = () => import("../../open-sse/services/usage.js");
const SUPPORTED = [
"github", "gemini-cli", "antigravity", "claude", "codex", "kiro",
"qoder", "qwen", "iflow", "ollama", "glm", "glm-cn",
"minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli",
"minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli", "xai",
];

describe("usage dispatch", () => {
Expand Down