mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
fix(quota): show Grok unified-billing usage as a weekly window (#4029)
xAI moved Grok accounts to unified billing. The plain /v1/billing response now returns a zeroed legacy monthly shape (monthlyLimit.val 0), which rendered as "0 credits left" with no bar in Settings -> Usage. Fetch /v1/billing?format=credits — the same query the Grok CLI /usage command uses — and map creditUsagePercent + currentPeriod onto a usage window, matching the Claude/Codex weekly window convention. The legacy monthly-credits balance path is unchanged; unified-billing responses omit monthlyLimit/used, so it naturally yields no balance. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,14 +3,20 @@ import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { Logger } from "pino";
|
||||
import { z } from "zod";
|
||||
import type { ProviderUsage, ProviderUsageBalance } from "../../../server/messages.js";
|
||||
import type {
|
||||
ProviderUsage,
|
||||
ProviderUsageBalance,
|
||||
ProviderUsageWindow,
|
||||
} from "../../../server/messages.js";
|
||||
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
|
||||
import {
|
||||
ApiNumberSchema,
|
||||
ApiOptionalStringSchema,
|
||||
toneFromUsedPct,
|
||||
usedPctOf,
|
||||
fetchProviderApi,
|
||||
unavailableUsage,
|
||||
windowFromUsedPct,
|
||||
} from "../usage.js";
|
||||
|
||||
const GrokUsageResponseSchema = z.object({
|
||||
@@ -26,6 +32,13 @@ const GrokUsageResponseSchema = z.object({
|
||||
val: ApiNumberSchema.optional(),
|
||||
})
|
||||
.nullish(),
|
||||
creditUsagePercent: ApiNumberSchema.optional(),
|
||||
currentPeriod: z
|
||||
.object({
|
||||
type: ApiOptionalStringSchema,
|
||||
end: ApiOptionalStringSchema,
|
||||
})
|
||||
.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
usage: z
|
||||
@@ -84,6 +97,22 @@ function grokMonthlyCreditBalance(
|
||||
};
|
||||
}
|
||||
|
||||
function grokUsageWindow(
|
||||
response: z.infer<typeof GrokUsageResponseSchema>,
|
||||
): ProviderUsageWindow | null {
|
||||
const percent = response.config?.creditUsagePercent;
|
||||
if (typeof percent !== "number") return null;
|
||||
const period = response.config?.currentPeriod;
|
||||
const weekly = (period?.type ?? "").toUpperCase().includes("WEEKLY");
|
||||
return windowFromUsedPct({
|
||||
id: weekly ? "weekly" : "monthly",
|
||||
label: weekly ? "Weekly" : "Monthly",
|
||||
utilizationPct: percent,
|
||||
resetsAt: period?.end ?? null,
|
||||
tone: toneFromUsedPct(percent),
|
||||
});
|
||||
}
|
||||
|
||||
export class GrokQuotaProvider implements ProviderUsageFetcher {
|
||||
readonly providerId = "grok";
|
||||
readonly displayName = "Grok";
|
||||
@@ -104,9 +133,11 @@ export class GrokQuotaProvider implements ProviderUsageFetcher {
|
||||
|
||||
if (!token) return unavailableUsage(this);
|
||||
|
||||
// The Grok CLI's /usage uses ?format=credits; without it, unified-billing accounts
|
||||
// get a zeroed legacy monthly shape (monthlyLimit.val 0) instead of real usage.
|
||||
const res = await fetchProviderApi(
|
||||
this.fetchApi,
|
||||
"https://cli-chat-proxy.grok.com/v1/billing",
|
||||
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -123,13 +154,14 @@ export class GrokQuotaProvider implements ProviderUsageFetcher {
|
||||
|
||||
const resp = GrokUsageResponseSchema.parse(await res.json());
|
||||
const balance = grokMonthlyCreditBalance(resp);
|
||||
const window = grokUsageWindow(resp);
|
||||
|
||||
return {
|
||||
providerId: this.providerId,
|
||||
displayName: this.displayName,
|
||||
status: "available",
|
||||
planLabel: null,
|
||||
windows: [],
|
||||
windows: window ? [window] : [],
|
||||
balances: balance ? [balance] : [],
|
||||
details: [],
|
||||
error: null,
|
||||
|
||||
@@ -871,7 +871,7 @@ describe("real provider usage fetchers", () => {
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://cli-chat-proxy.grok.com/v1/billing",
|
||||
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
|
||||
() =>
|
||||
jsonResponse({
|
||||
config: { monthlyLimit: { val: 0 }, used: { val: 0 } },
|
||||
@@ -900,7 +900,7 @@ describe("real provider usage fetchers", () => {
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://cli-chat-proxy.grok.com/v1/billing",
|
||||
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
|
||||
() =>
|
||||
jsonResponse({
|
||||
config: {
|
||||
@@ -973,7 +973,7 @@ describe("real provider usage fetchers", () => {
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://cli-chat-proxy.grok.com/v1/billing",
|
||||
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
|
||||
() =>
|
||||
jsonResponse({
|
||||
config: { monthlyLimit: { val: 50 } },
|
||||
@@ -998,6 +998,48 @@ describe("real provider usage fetchers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches Grok unified-billing usage as a weekly window", async () => {
|
||||
process.env["GROK_API_KEY"] = "grok_test_token";
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
|
||||
() =>
|
||||
jsonResponse({
|
||||
config: {
|
||||
currentPeriod: {
|
||||
type: "USAGE_PERIOD_TYPE_WEEKLY",
|
||||
start: "2026-08-24T09:41:40.001370+00:00",
|
||||
end: "2026-08-31T09:41:40.001370+00:00",
|
||||
},
|
||||
creditUsagePercent: 76.0,
|
||||
isUnifiedBillingUser: true,
|
||||
billingPeriodStart: "2026-08-24T09:41:40.001370+00:00",
|
||||
billingPeriodEnd: "2026-08-31T09:41:40.001370+00:00",
|
||||
},
|
||||
}),
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const grok = findProvider(await service().listUsage(), "grok");
|
||||
|
||||
expect(grok).toMatchObject({
|
||||
status: "available",
|
||||
windows: [
|
||||
{
|
||||
id: "weekly",
|
||||
label: "Weekly",
|
||||
usedPct: 76,
|
||||
remainingPct: 24,
|
||||
resetsAt: "2026-08-31T09:41:40.001370+00:00",
|
||||
tone: "warning",
|
||||
},
|
||||
],
|
||||
balances: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches Kimi usage from KIMI_TOKEN", async () => {
|
||||
process.env["KIMI_TOKEN"] = "kimi_test_token";
|
||||
fetchApi = mockFetch(
|
||||
|
||||
Reference in New Issue
Block a user