Skip to content
Merged
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
5 changes: 0 additions & 5 deletions config/quality/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3307,11 +3307,6 @@
"count": 1
}
},
"src/lib/usage/providerLimits.ts": {
"no-restricted-imports": {
"count": 1
}
},
"src/lib/ws/handshake.ts": {
"no-restricted-imports": {
"count": 1
Expand Down
4 changes: 4 additions & 0 deletions open-sse/services/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { getVertexUsage } from "./usage/vertex.ts";
import { getXiaomiMimoUsage } from "./usage/xiaomi-mimo.ts";
import { getXaiUsage } from "./usage/xai.ts";
import { getXaiOauthUsage } from "./usage/xaiOauth.ts";
import { getGrokCliUsage } from "./usage/grokCli.ts";
import { getFirecrawlUsage } from "./usage/firecrawl.ts";

type JsonRecord = Record<string, unknown>;
Expand Down Expand Up @@ -116,6 +117,7 @@ export const USAGE_FETCHER_PROVIDERS = [
"xai",
"xai-oauth",
"xao",
"grok-cli",
"vertex",
"vertex-partner",
"codebuddy-cn",
Expand Down Expand Up @@ -210,6 +212,8 @@ export async function getUsageForProvider(
case "xai-oauth":
case "xao":
return await getXaiOauthUsage(id || "", accessToken, connection);
case "grok-cli":
return await getGrokCliUsage(accessToken);
case "codebuddy-cn":
return await getCodeBuddyCnUsage(accessToken, apiKey, providerSpecificData);
case "promptql":
Expand Down
278 changes: 278 additions & 0 deletions open-sse/services/usage/grokCli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { z } from "zod";

import { GROK_BUILD_PROXY_BASE_URL, getGrokBuildModelsHeaders } from "../../config/grokBuild.ts";
import {
GROK_BUILD_ADDITIONAL_CREDITS_URL,
type GrokAutoTopUpStatus,
} from "../../../src/shared/utils/grokBilling.ts";

const GROK_BUILD_FETCH_TIMEOUT_MS = 10_000;
const GROK_BUILD_MAX_RESPONSE_BYTES = 256 * 1024;

const optionalNonEmptyString = z
.string()
.trim()
.min(1)
.max(256)
.optional()
.nullable()
.catch(undefined);
const optionalPercent = z.number().finite().min(0).max(100).optional().nullable().catch(undefined);
const centSchema = z
.object({ val: z.number().finite().int().safe().optional() })
.passthrough()
.transform(({ val }) => ({ val: Math.abs(val ?? 0) }));

const userSchema = z
.object({
userId: optionalNonEmptyString,
subscriptionTier: optionalNonEmptyString,
})
.passthrough();

const productUsageSchema = z
.object({
product: z.string().trim().min(1).max(128),
usagePercent: z.number().finite().min(0).max(100),
})
.passthrough();

const productUsageListSchema = z
.array(z.unknown())
.max(100)
.transform((items) =>
items.flatMap((item) => {
const parsed = productUsageSchema.safeParse(item);
return parsed.success ? [parsed.data] : [];
})
);

const currentPeriodSchema = z
.object({
type: optionalNonEmptyString,
start: optionalNonEmptyString,
end: optionalNonEmptyString,
})
.passthrough();

const billingConfigSchema = z
.object({
creditUsagePercent: optionalPercent,
currentPeriod: currentPeriodSchema.optional().nullable().catch(undefined),
productUsage: productUsageListSchema.optional().nullable().catch(undefined),
prepaidBalance: centSchema.optional().nullable().catch(undefined),
})
.passthrough();

const billingSchema = z
.object({
config: billingConfigSchema.optional().nullable().catch(undefined),
})
.passthrough();

const autoTopUpRuleSchema = z
.object({
enabled: z.boolean().optional(),
minBeforeHittingSl: centSchema.optional().nullable().catch(undefined),
topupAmount: centSchema.optional().nullable().catch(undefined),
maxAmountPerMonth: centSchema.optional().nullable().catch(undefined),
})
.passthrough();

const autoTopUpSchema = z
.object({
rule: autoTopUpRuleSchema.optional().nullable().catch(undefined),
})
.passthrough();

type JsonSchema<T> = z.ZodType<T>;
type GrokBuildHeaders = ReturnType<typeof getGrokBuildModelsHeaders>;

function finitePercent(value: number): number {
return Math.max(0, Math.min(100, value));
}

function normalizeProduct(value: string): { key: string; displayName: string } {
const compact = value
.normalize("NFKC")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "");
if (compact === "grokbuild" || compact === "productgrokbuild") {
return { key: "grok_build", displayName: "Grok Build" };
}

const slug = value
.normalize("NFKD")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
return { key: slug || "unknown", displayName: value };
}

function percentageQuota(used: number, resetAt: string | null, displayName?: string) {
const normalizedUsed = finitePercent(used);
const remaining = 100 - normalizedUsed;
return {
...(displayName ? { displayName } : {}),
used: normalizedUsed,
total: 100,
remaining,
remainingPercentage: remaining,
resetAt,
isPercentageOnly: true,
};
}

async function readBoundedJson<T>(response: Response, schema: JsonSchema<T>): Promise<T | null> {
if (!response.ok) return null;

const declaredLength = Number(response.headers.get("content-length"));
if (Number.isFinite(declaredLength) && declaredLength > GROK_BUILD_MAX_RESPONSE_BYTES)
return null;

const reader = response.body?.getReader();
if (!reader) return null;

const chunks: Uint8Array[] = [];
let size = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > GROK_BUILD_MAX_RESPONSE_BYTES) {
await reader.cancel();
return null;
}
chunks.push(value);
}

try {
const bytes = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return schema.parse(JSON.parse(new TextDecoder().decode(bytes)));
} catch {
return null;
}
}

async function fetchGrokBuildJson<T>(
path: string,
headers: GrokBuildHeaders,
schema: JsonSchema<T>
): Promise<T | null> {
try {
const response = await fetch(`${GROK_BUILD_PROXY_BASE_URL}${path}`, {
method: "GET",
headers,
redirect: "error",
signal: AbortSignal.timeout(GROK_BUILD_FETCH_TIMEOUT_MS),
});
return await readBoundedJson(response, schema);
} catch {
return null;
}
}

function buildProductQuotas(
productUsage: z.infer<typeof productUsageSchema>[] | null | undefined,
resetAt: string | null
): Record<string, ReturnType<typeof percentageQuota>> {
const quotas: Record<string, ReturnType<typeof percentageQuota>> = {};
for (const product of productUsage ?? []) {
const normalized = normalizeProduct(product.product);
const baseKey = `product_${normalized.key}`;
let key = baseKey;
let suffix = 2;
while (key in quotas) {
key = `${baseKey}_${suffix++}`;
}
quotas[key] = percentageQuota(product.usagePercent, resetAt, normalized.displayName);
}
return quotas;
}

function buildAutoTopUp(ruleResponse: z.infer<typeof autoTopUpSchema> | null): GrokAutoTopUpStatus {
const rule = ruleResponse?.rule;
if (!rule) return { available: false };

const enabled = rule.enabled === true;
return {
available: true,
enabled,
...(enabled && rule.minBeforeHittingSl
? { thresholdMinorUnits: rule.minBeforeHittingSl.val }
: {}),
...(enabled && rule.topupAmount ? { amountMinorUnits: rule.topupAmount.val } : {}),
...(enabled && rule.maxAmountPerMonth
? { maxMonthlyMinorUnits: rule.maxAmountPerMonth.val }
: {}),
};
}

export async function getGrokCliUsage(accessToken?: string) {
if (!accessToken) {
return { message: "Grok Build usage unavailable" };
}

const baseHeaders = getGrokBuildModelsHeaders({ token: accessToken });
const user = await fetchGrokBuildJson("/user?include=subscription", baseHeaders, userSchema);
const userId = user?.userId || null;
const tier = user?.subscriptionTier || null;
const billing = await fetchGrokBuildJson(
"/billing?format=credits",
userId ? getGrokBuildModelsHeaders({ token: accessToken, userId }) : baseHeaders,
billingSchema
);

if (!billing?.config) {
return {
...(tier ? { plan: tier } : {}),
message: "Grok Build billing status unavailable",
};
}

const config = billing.config;
const resetAt = config.currentPeriod?.end || null;
const quotas: Record<string, ReturnType<typeof percentageQuota>> = {};
if (config.creditUsagePercent != null) {
quotas.weekly = percentageQuota(config.creditUsagePercent, resetAt);
}
Object.assign(quotas, buildProductQuotas(config.productUsage, resetAt));

const autoTopUpResponse = userId
? await fetchGrokBuildJson(
"/auto-topup-rule",
getGrokBuildModelsHeaders({ token: accessToken, userId }),
autoTopUpSchema
)
: null;

return {
quotas,
...(tier ? { plan: tier } : {}),
billing: {
currency: "USD",
...(config.prepaidBalance ? { extraCreditsMinorUnits: config.prepaidBalance.val } : {}),
autoTopUp: buildAutoTopUp(autoTopUpResponse),
additionalCreditsUrl: GROK_BUILD_ADDITIONAL_CREDITS_URL,
},
};
}

export const __testing = {
billingSchema,
userSchema,
autoTopUpSchema,
readBoundedJson,
networkPolicy: {
method: "GET",
redirect: "error",
timeoutMs: GROK_BUILD_FETCH_TIMEOUT_MS,
maxResponseBytes: GROK_BUILD_MAX_RESPONSE_BYTES,
} as const,
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useMemo, useState } from "react";
import Card from "@/shared/components/Card";
import type { GrokBillingStatus } from "@/shared/utils/grokBilling";
import { pickDisplayValue } from "@/shared/utils/maskEmail";
import {
normalizePlanTier,
Expand Down Expand Up @@ -34,6 +35,8 @@ interface QuotaCardProps {
quotas?: any[];
plan?: string | null;
message?: string | null;
billing?: GrokBillingStatus | null;
raw?: { billing?: GrokBillingStatus | null };
stale?: { since?: string; reason?: string } | null;
}
| undefined;
Expand Down Expand Up @@ -89,13 +92,22 @@ export default function QuotaCard({
const tierMeta = useMemo(
() =>
normalizePlanTier(
resolvePlanValue(quota?.plan ?? null, connection.providerSpecificData ?? null)
resolvePlanValue(
quota?.plan ?? null,
connection.providerSpecificData ?? null,
connection.provider
)
),
[quota?.plan, connection.providerSpecificData]
[quota?.plan, connection.providerSpecificData, connection.provider]
);
const resolvedPlan = useMemo(
() => resolvePlanValue(quota?.plan ?? null, connection.providerSpecificData ?? null),
[quota?.plan, connection.providerSpecificData]
() =>
resolvePlanValue(
quota?.plan ?? null,
connection.providerSpecificData ?? null,
connection.provider
),
[quota?.plan, connection.providerSpecificData, connection.provider]
);
const accountLabel = useMemo(
() =>
Expand Down Expand Up @@ -138,6 +150,9 @@ export default function QuotaCard({
loading={loading}
error={error}
message={quota?.message ?? null}
billing={
connection.provider === "grok-cli" ? (quota?.billing ?? quota?.raw?.billing) : null
}
refreshedAt={displayRefreshedAt}
hasStaleData={hasStaleData}
onRefresh={onRefresh}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const PROVIDER_LABEL: Record<string, string> = {
deepseek: "DeepSeek",
"xai-oauth": "xAI OAuth (Grok)",
xao: "xAI OAuth (Grok)",
"grok-cli": "Grok Build",
};

export const PROVIDER_ORDER: Record<string, number> = {
Expand All @@ -36,6 +37,7 @@ export const PROVIDER_ORDER: Record<string, number> = {
nanogpt: 15,
"xai-oauth": 16,
xao: 16,
"grok-cli": 17,
};

export const TIER_FILTERS = [
Expand Down
Loading
Loading