mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
feat(usage): show Grok Build billing limits (#9205)
* feat(usage): show Grok Build billing limits * test(usage): keep Grok quota reset fixture in the future * fix(i18n): add Grok billing labels to pt-BR * fix(i18n): add Grok billing labels to Vietnamese
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
@@ -116,6 +117,7 @@ export const USAGE_FETCHER_PROVIDERS = [
|
||||
"xai",
|
||||
"xai-oauth",
|
||||
"xao",
|
||||
"grok-cli",
|
||||
"vertex",
|
||||
"vertex-partner",
|
||||
"codebuddy-cn",
|
||||
@@ -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":
|
||||
|
||||
278
open-sse/services/usage/grokCli.ts
Normal file
278
open-sse/services/usage/grokCli.ts
Normal file
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
@@ -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(
|
||||
() =>
|
||||
@@ -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}
|
||||
|
||||
@@ -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> = {
|
||||
@@ -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 = [
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
formatQuotaLabel,
|
||||
formatCountdown,
|
||||
normalizePlanTier,
|
||||
resolvePlanValue,
|
||||
buildProviderLimitsResolvedPlans,
|
||||
calculatePercentage,
|
||||
matchesProviderFilter,
|
||||
buildProviderOptions,
|
||||
@@ -535,13 +535,10 @@ export default function ProviderLimits({
|
||||
}, [filteredConnections]);
|
||||
const visibleQuotaData = useVisibleQuotaData(sortedConnections, quotaData);
|
||||
|
||||
const resolvedPlanByConnection = useMemo(() => {
|
||||
const out: Record<string, string | null> = {};
|
||||
for (const conn of sortedConnections) {
|
||||
out[conn.id] = resolvePlanValue(quotaData[conn.id]?.plan, conn.providerSpecificData);
|
||||
}
|
||||
return out;
|
||||
}, [sortedConnections, quotaData]);
|
||||
const resolvedPlanByConnection = useMemo(
|
||||
() => buildProviderLimitsResolvedPlans(sortedConnections, quotaData),
|
||||
[sortedConnections, quotaData]
|
||||
);
|
||||
|
||||
const tierByConnection = useMemo(() => {
|
||||
const out: Record<string, ReturnType<typeof normalizePlanTier>> = {};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { buildGrokBillingCardRows, type GrokBillingStatus } from "@/shared/utils/grokBilling";
|
||||
import {
|
||||
formatCountdown,
|
||||
formatQuotaLabel,
|
||||
@@ -26,6 +27,47 @@ const CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
|
||||
const DEFAULT_VISIBLE_ROWS = 3;
|
||||
|
||||
function GrokBillingDetails({ billing }: { billing: GrokBillingStatus }) {
|
||||
const t = useTranslations("usage");
|
||||
const locale = useLocale();
|
||||
const rows = buildGrokBillingCardRows(billing, locale, (key, fallback) =>
|
||||
translateUsageOrFallback(t, key, fallback)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 border-t border-border/40 pt-2 text-[11px] text-text-main">
|
||||
{rows.map((row) =>
|
||||
row.kind === "link" ? (
|
||||
<a
|
||||
key={row.kind}
|
||||
href={row.href}
|
||||
target={row.target}
|
||||
rel={row.rel}
|
||||
className="inline-flex w-fit items-center gap-1 font-medium text-primary hover:underline"
|
||||
>
|
||||
{row.label}
|
||||
<span className="material-symbols-outlined text-[12px]">open_in_new</span>
|
||||
</a>
|
||||
) : (
|
||||
<div
|
||||
key={row.kind}
|
||||
className={`flex justify-between gap-2 ${
|
||||
row.kind === "status" ? "items-start" : "items-center"
|
||||
}`}
|
||||
>
|
||||
<span>{row.label}</span>
|
||||
<span
|
||||
className={`font-semibold ${row.kind === "status" ? "text-right" : "tabular-nums"}`}
|
||||
>
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Pure helper — sorts quotas by remaining percentage, highest first. */
|
||||
export function sortQuotasByRemaining(quotas: any[]): any[] {
|
||||
return [...quotas].sort(
|
||||
@@ -73,6 +115,7 @@ interface Props {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
message?: string | null;
|
||||
billing?: GrokBillingStatus | null;
|
||||
refreshedAt?: string;
|
||||
hasStaleData: boolean;
|
||||
onRefresh: () => void;
|
||||
@@ -240,6 +283,7 @@ export default function QuotaCardExpanded({
|
||||
loading,
|
||||
error,
|
||||
message,
|
||||
billing,
|
||||
refreshedAt,
|
||||
hasStaleData,
|
||||
onRefresh,
|
||||
@@ -313,6 +357,8 @@ export default function QuotaCardExpanded({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{providerId === "grok-cli" && billing && <GrokBillingDetails billing={billing} />}
|
||||
|
||||
{hiddenQuotaRows.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-1 border-t border-border/40 pt-1.5 text-[10px] text-text-muted">
|
||||
<span className="material-symbols-outlined text-[12px]">visibility_off</span>
|
||||
|
||||
@@ -69,6 +69,10 @@ function normalizeQuotaEntry(name: string, quota: any = {}, extras: any = {}) {
|
||||
? { extraCreditsInferred: Number(quota.extraCreditsInferred) || 0 }
|
||||
: {}),
|
||||
...(quota?.overPlan !== undefined ? { overPlan: quota.overPlan === true } : {}),
|
||||
...(quota?.displayName !== undefined ? { displayName: String(quota.displayName) } : {}),
|
||||
...(quota?.isPercentageOnly !== undefined
|
||||
? { isPercentageOnly: quota.isPercentageOnly === true }
|
||||
: {}),
|
||||
...extras,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,9 +180,11 @@ export function calculatePercentage(used, total) {
|
||||
* Resolve the best available plan label using live usage first, then persisted
|
||||
* provider-specific connection metadata.
|
||||
*/
|
||||
export function resolvePlanValue(plan, providerSpecificData) {
|
||||
const psd = toRecord(providerSpecificData);
|
||||
export function resolvePlanValue(plan, providerSpecificData, providerId) {
|
||||
const livePlan = normalizePlanCandidate(plan);
|
||||
if (String(providerId || "").toLowerCase() === "grok-cli") return livePlan || null;
|
||||
|
||||
const psd = toRecord(providerSpecificData);
|
||||
const persistedCandidates = [
|
||||
psd.workspacePlanType,
|
||||
psd.plan,
|
||||
@@ -214,6 +216,29 @@ export function resolvePlanValue(plan, providerSpecificData) {
|
||||
return livePlan || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page-level Provider Limits plan map used by tier stats/filters.
|
||||
* Always passes provider so grok-cli never classifies from persisted PSD tiers.
|
||||
*/
|
||||
export function buildProviderLimitsResolvedPlans(
|
||||
connections: Array<{
|
||||
id: string;
|
||||
provider?: string | null;
|
||||
providerSpecificData?: unknown;
|
||||
}>,
|
||||
quotaData: Record<string, { plan?: unknown } | null | undefined>
|
||||
): Record<string, string | null> {
|
||||
const out: Record<string, string | null> = {};
|
||||
for (const conn of connections) {
|
||||
out[conn.id] = resolvePlanValue(
|
||||
quotaData[conn.id]?.plan,
|
||||
conn.providerSpecificData,
|
||||
conn.provider
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function unknownPlanTier(raw: string | null = null) {
|
||||
return { key: "unknown", label: "Unknown", variant: "default", rank: 0, raw };
|
||||
}
|
||||
|
||||
@@ -8465,6 +8465,16 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Usage",
|
||||
"grokExtraUsageCredits": "Extra Usage Credits",
|
||||
"grokAutoTopUp": "Auto Top-Up",
|
||||
"grokAutoTopUpUnavailable": "Unavailable",
|
||||
"grokAutoTopUpEnabled": "Enabled",
|
||||
"grokAutoTopUpDisabled": "Disabled",
|
||||
"grokAutoTopUpAt": "at",
|
||||
"grokAutoTopUpAdd": "add",
|
||||
"grokAutoTopUpMax": "max",
|
||||
"grokAutoTopUpMonth": "month",
|
||||
"grokAdditionalCredits": "Additional Credits",
|
||||
"loggerTab": "Logger",
|
||||
"proxyTab": "Proxy",
|
||||
"budgetManagement": "Budget Management",
|
||||
|
||||
@@ -8465,6 +8465,16 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Uso",
|
||||
"grokExtraUsageCredits": "Créditos de uso extra",
|
||||
"grokAutoTopUp": "Recarga automática",
|
||||
"grokAutoTopUpUnavailable": "Indisponível",
|
||||
"grokAutoTopUpEnabled": "Ativada",
|
||||
"grokAutoTopUpDisabled": "Desativada",
|
||||
"grokAutoTopUpAt": "em",
|
||||
"grokAutoTopUpAdd": "adicionar",
|
||||
"grokAutoTopUpMax": "máximo",
|
||||
"grokAutoTopUpMonth": "mês",
|
||||
"grokAdditionalCredits": "Créditos adicionais",
|
||||
"loggerTab": "Logger",
|
||||
"proxyTab": "Proxy",
|
||||
"budgetManagement": "Gerenciamento de Orçamento",
|
||||
|
||||
@@ -8465,6 +8465,16 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Mức sử dụng",
|
||||
"grokExtraUsageCredits": "Tín dụng sử dụng bổ sung",
|
||||
"grokAutoTopUp": "Tự động nạp thêm",
|
||||
"grokAutoTopUpUnavailable": "Không khả dụng",
|
||||
"grokAutoTopUpEnabled": "Đã bật",
|
||||
"grokAutoTopUpDisabled": "Đã tắt",
|
||||
"grokAutoTopUpAt": "tại",
|
||||
"grokAutoTopUpAdd": "thêm",
|
||||
"grokAutoTopUpMax": "tối đa",
|
||||
"grokAutoTopUpMonth": "tháng",
|
||||
"grokAdditionalCredits": "Tín dụng bổ sung",
|
||||
"loggerTab": "Logger",
|
||||
"proxyTab": "Proxy",
|
||||
"budgetManagement": "Quản lý ngân sách",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { sanitizeGrokBillingStatus, type GrokBillingStatus } from "@/shared/utils/grokBilling";
|
||||
import { getDbInstance, isBuildPhase, isCloud } from "./core";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
@@ -25,6 +26,7 @@ export interface ProviderLimitsCacheEntry {
|
||||
fetchedAt: string;
|
||||
source?: string | null;
|
||||
bankedResetCredits?: number;
|
||||
billing?: GrokBillingStatus;
|
||||
}
|
||||
|
||||
const PROVIDER_LIMITS_CACHE_NAMESPACE = "providerLimitsCache";
|
||||
@@ -41,6 +43,12 @@ function toRecord(value: unknown): JsonRecord | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
|
||||
}
|
||||
|
||||
function sanitizeCacheEntryForStorage(entry: ProviderLimitsCacheEntry): ProviderLimitsCacheEntry {
|
||||
const { billing: rawBilling, ...rest } = entry;
|
||||
const billing = sanitizeGrokBillingStatus(rawBilling);
|
||||
return billing ? { ...rest, billing } : rest;
|
||||
}
|
||||
|
||||
function normalizeCacheEntry(value: unknown): ProviderLimitsCacheEntry | null {
|
||||
const record = toRecord(value);
|
||||
if (!record) return null;
|
||||
@@ -50,6 +58,7 @@ function normalizeCacheEntry(value: unknown): ProviderLimitsCacheEntry | null {
|
||||
if (!fetchedAt) return null;
|
||||
|
||||
const bankedResetCredits = Number(record.bankedResetCredits);
|
||||
const billing = sanitizeGrokBillingStatus(record.billing);
|
||||
|
||||
return {
|
||||
quotas: toRecord(record.quotas),
|
||||
@@ -58,6 +67,7 @@ function normalizeCacheEntry(value: unknown): ProviderLimitsCacheEntry | null {
|
||||
fetchedAt,
|
||||
source: typeof record.source === "string" ? record.source : null,
|
||||
...(Number.isFinite(bankedResetCredits) ? { bankedResetCredits } : {}),
|
||||
...(billing ? { billing } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,14 +102,15 @@ export function setProviderLimitsCache(
|
||||
connectionId: string,
|
||||
entry: ProviderLimitsCacheEntry
|
||||
): ProviderLimitsCacheEntry {
|
||||
if (isBuildPhase || isCloud) return entry;
|
||||
const sanitized = sanitizeCacheEntryForStorage(entry);
|
||||
if (isBuildPhase || isCloud) return sanitized;
|
||||
const db = getDbInstance() as unknown as DbLike;
|
||||
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
||||
PROVIDER_LIMITS_CACHE_NAMESPACE,
|
||||
connectionId,
|
||||
JSON.stringify(entry)
|
||||
JSON.stringify(sanitized)
|
||||
);
|
||||
return entry;
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export function setProviderLimitsCacheBatch(
|
||||
@@ -113,7 +124,11 @@ export function setProviderLimitsCacheBatch(
|
||||
const tx = db.transaction(
|
||||
(items: Array<{ connectionId: string; entry: ProviderLimitsCacheEntry }>) => {
|
||||
for (const item of items) {
|
||||
insert.run(PROVIDER_LIMITS_CACHE_NAMESPACE, item.connectionId, JSON.stringify(item.entry));
|
||||
insert.run(
|
||||
PROVIDER_LIMITS_CACHE_NAMESPACE,
|
||||
item.connectionId,
|
||||
JSON.stringify(sanitizeCacheEntryForStorage(item.entry))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import {
|
||||
getAllProviderLimitsCache,
|
||||
getProviderConnectionById,
|
||||
getProviderConnections,
|
||||
updateProviderConnection,
|
||||
} from "@/lib/db/providers";
|
||||
import { getSettings, resolveProxyForConnection, updateSettings } from "@/lib/db/settings";
|
||||
import {
|
||||
getAllProviderLimitsCache,
|
||||
getProviderLimitsCache,
|
||||
getSettings,
|
||||
resolveProxyForConnection,
|
||||
setProviderLimitsCache,
|
||||
setProviderLimitsCacheBatch,
|
||||
updateProviderConnection,
|
||||
updateSettings,
|
||||
type ProviderLimitsCacheEntry,
|
||||
} from "@/lib/localDb";
|
||||
} from "@/lib/db/providerLimits";
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { setQuotaCache } from "@/domain/quotaCache";
|
||||
import { buildClaudeExtraUsageConnectionUpdate } from "@/lib/providers/claudeExtraUsage";
|
||||
import { clearRecoveredProviderState } from "@/sse/services/auth";
|
||||
import { getMachineId } from "@/shared/utils/machine";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { mergeProviderLimitsCacheEntry, toProviderLimitsCacheEntry } from "./providerLimitsCache";
|
||||
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
|
||||
import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
|
||||
import {
|
||||
@@ -94,22 +95,6 @@ const PROVIDER_LIMITS_AUTO_SYNC_SETTING_KEY = "provider_limits_auto_sync_last_ru
|
||||
const DEFAULT_PROVIDER_LIMITS_POST_USAGE_REFRESH_DELAY_MS = 5_000;
|
||||
const pendingPostUsageRefreshes = new Set<string>();
|
||||
|
||||
function toProviderLimitsCacheEntry(
|
||||
usage: JsonRecord,
|
||||
source: SyncSource,
|
||||
fetchedAt = new Date().toISOString()
|
||||
): ProviderLimitsCacheEntry {
|
||||
const value = Number(usage.bankedResetCredits);
|
||||
return {
|
||||
quotas: isRecord(usage.quotas) ? usage.quotas : null,
|
||||
plan: usage.plan ?? null,
|
||||
message: typeof usage.message === "string" ? usage.message : null,
|
||||
fetchedAt,
|
||||
source,
|
||||
bankedResetCredits: Number.isFinite(value) ? value : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function getProviderLimitsPostUsageRefreshDelayMs(): number {
|
||||
const raw = Number(process.env.PROVIDER_LIMITS_POST_USAGE_REFRESH_DELAY_MS ?? "");
|
||||
return Number.isFinite(raw) && raw >= 0
|
||||
@@ -890,30 +875,32 @@ export async function fetchAndPersistProviderLimits(
|
||||
allowRotatingRefresh: opts.allowRotatingRefresh,
|
||||
});
|
||||
const newCache = toProviderLimitsCacheEntry(usage, source);
|
||||
const previous = getProviderLimitsCache(connectionId);
|
||||
const cache = mergeProviderLimitsCacheEntry(connection.provider, newCache, previous);
|
||||
|
||||
// Don't persist error-only entries (429 etc.) — would wipe prior good cache.
|
||||
// Serve the prior entry instead; only successful fetches update the cache.
|
||||
const fetchFailed = !newCache.quotas && newCache.message;
|
||||
if (fetchFailed) {
|
||||
const previous = getProviderLimitsCache(connectionId);
|
||||
if (previous?.quotas && Object.keys(previous.quotas).length > 0) {
|
||||
const staleUsage: JsonRecord = {
|
||||
...usage,
|
||||
quotas: previous.quotas,
|
||||
plan: previous.plan ?? usage.plan ?? null,
|
||||
bankedResetCredits: previous.bankedResetCredits,
|
||||
message: null,
|
||||
_stale: true,
|
||||
_staleSince: previous.fetchedAt,
|
||||
_staleReason: newCache.message,
|
||||
};
|
||||
return { connection, usage: staleUsage, cache: previous };
|
||||
}
|
||||
return { connection, usage, cache: newCache };
|
||||
if (cache === previous && newCache.message) {
|
||||
const staleUsage: JsonRecord = {
|
||||
...usage,
|
||||
quotas: previous.quotas,
|
||||
plan: previous.plan ?? usage.plan ?? null,
|
||||
bankedResetCredits: previous.bankedResetCredits,
|
||||
billing: previous.billing,
|
||||
message: null,
|
||||
_stale: true,
|
||||
_staleSince: previous.fetchedAt,
|
||||
_staleReason: newCache.message,
|
||||
};
|
||||
return { connection, usage: staleUsage, cache: previous };
|
||||
}
|
||||
|
||||
setProviderLimitsCache(connectionId, newCache);
|
||||
return { connection, usage, cache: newCache };
|
||||
const mergedUsage: JsonRecord = {
|
||||
...usage,
|
||||
...(cache.billing ? { billing: cache.billing } : {}),
|
||||
};
|
||||
setProviderLimitsCache(connectionId, cache);
|
||||
return { connection, usage: mergedUsage, cache };
|
||||
}
|
||||
|
||||
export async function syncAllProviderLimits(
|
||||
@@ -942,14 +929,9 @@ export async function syncAllProviderLimits(
|
||||
) => {
|
||||
if (result.status === "fulfilled") {
|
||||
const { cache } = result.value;
|
||||
// Don't persist error-only entries; show prior cache or pass through.
|
||||
if (!cache.quotas && cache.message) {
|
||||
const previous = getProviderLimitsCache(connectionId);
|
||||
if (previous?.quotas && Object.keys(previous.quotas).length > 0) {
|
||||
caches[connectionId] = previous;
|
||||
} else {
|
||||
caches[connectionId] = cache;
|
||||
}
|
||||
const previous = getProviderLimitsCache(connectionId);
|
||||
if (cache === previous) {
|
||||
caches[connectionId] = cache;
|
||||
return;
|
||||
}
|
||||
cacheEntries.push({ connectionId, entry: cache });
|
||||
@@ -968,7 +950,8 @@ export async function syncAllProviderLimits(
|
||||
const { usage } = await fetchLiveProviderLimitsWithOptions(connection.id, {
|
||||
forceRefresh,
|
||||
});
|
||||
const cache = toProviderLimitsCacheEntry(usage, source);
|
||||
const nextCache = toProviderLimitsCacheEntry(usage, source);
|
||||
const cache = mergeProviderLimitsCacheEntry(connection.provider, nextCache, existingCache);
|
||||
return { connectionId: connection.id, cache };
|
||||
};
|
||||
|
||||
|
||||
57
src/lib/usage/providerLimitsCache.ts
Normal file
57
src/lib/usage/providerLimitsCache.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { ProviderLimitsCacheEntry } from "@/lib/db/providerLimits";
|
||||
import { sanitizeGrokBillingStatus } from "@/shared/utils/grokBilling";
|
||||
|
||||
const GROK_CLI_PROVIDER = "grok-cli";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function isRecord(value: unknown): value is JsonRecord {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function hasUsableCachedData(cache: ProviderLimitsCacheEntry | null | undefined): boolean {
|
||||
return Boolean(cache?.billing || (cache?.quotas && Object.keys(cache.quotas).length > 0));
|
||||
}
|
||||
|
||||
export function toProviderLimitsCacheEntry(
|
||||
usage: JsonRecord,
|
||||
source: string,
|
||||
fetchedAt = new Date().toISOString()
|
||||
): ProviderLimitsCacheEntry {
|
||||
const bankedResetCredits = Number(usage.bankedResetCredits);
|
||||
return {
|
||||
quotas: isRecord(usage.quotas) ? usage.quotas : null,
|
||||
plan: usage.plan ?? null,
|
||||
message: typeof usage.message === "string" ? usage.message : null,
|
||||
fetchedAt,
|
||||
source,
|
||||
bankedResetCredits: Number.isFinite(bankedResetCredits) ? bankedResetCredits : undefined,
|
||||
billing: sanitizeGrokBillingStatus(usage.billing),
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeProviderLimitsCacheEntry(
|
||||
provider: string,
|
||||
next: ProviderLimitsCacheEntry,
|
||||
previous: ProviderLimitsCacheEntry | null | undefined
|
||||
): ProviderLimitsCacheEntry {
|
||||
if (!previous) return next;
|
||||
|
||||
if (!next.quotas && next.message && hasUsableCachedData(previous)) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
if (provider !== GROK_CLI_PROVIDER) return next;
|
||||
|
||||
const nextBilling = next.billing;
|
||||
const previousAutoTopUp = previous.billing?.autoTopUp;
|
||||
if (!nextBilling || nextBilling.autoTopUp.available || !previousAutoTopUp) return next;
|
||||
|
||||
return {
|
||||
...next,
|
||||
billing: {
|
||||
...nextBilling,
|
||||
autoTopUp: previousAutoTopUp,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -453,6 +453,8 @@ export const USAGE_SUPPORTED_PROVIDERS = [
|
||||
// xAI OAuth (Grok) weekly quota (id + public alias, same pattern as ha/agy)
|
||||
"xai-oauth",
|
||||
"xao",
|
||||
// Grok Build subscription, billing credits, and auto top-up status
|
||||
"grok-cli",
|
||||
// Firecrawl team credits (GET /v2/team/credit-usage)
|
||||
"firecrawl",
|
||||
];
|
||||
|
||||
161
src/shared/utils/grokBilling.ts
Normal file
161
src/shared/utils/grokBilling.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
export const GROK_BUILD_ADDITIONAL_CREDITS_URL = "https://grok.com/build?_s=usage";
|
||||
|
||||
export interface GrokAutoTopUpStatus {
|
||||
available: boolean;
|
||||
enabled?: boolean;
|
||||
thresholdMinorUnits?: number;
|
||||
amountMinorUnits?: number;
|
||||
maxMonthlyMinorUnits?: number;
|
||||
}
|
||||
|
||||
export interface GrokBillingStatus {
|
||||
currency: "USD";
|
||||
extraCreditsMinorUnits?: number;
|
||||
autoTopUp: GrokAutoTopUpStatus;
|
||||
additionalCreditsUrl: typeof GROK_BUILD_ADDITIONAL_CREDITS_URL;
|
||||
}
|
||||
|
||||
export type GrokBillingTranslationKey =
|
||||
| "grokExtraUsageCredits"
|
||||
| "grokAutoTopUp"
|
||||
| "grokAutoTopUpUnavailable"
|
||||
| "grokAutoTopUpEnabled"
|
||||
| "grokAutoTopUpDisabled"
|
||||
| "grokAutoTopUpAt"
|
||||
| "grokAutoTopUpAdd"
|
||||
| "grokAutoTopUpMax"
|
||||
| "grokAutoTopUpMonth"
|
||||
| "grokAdditionalCredits";
|
||||
|
||||
export type GrokBillingTranslator = (key: GrokBillingTranslationKey, fallback: string) => string;
|
||||
|
||||
export type GrokBillingCardRow =
|
||||
| { kind: "balance" | "status"; label: string; value: string }
|
||||
| {
|
||||
kind: "link";
|
||||
label: string;
|
||||
href: typeof GROK_BUILD_ADDITIONAL_CREDITS_URL;
|
||||
target: "_blank";
|
||||
rel: "noreferrer noopener";
|
||||
};
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function toRecord(value: unknown): JsonRecord | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
|
||||
}
|
||||
|
||||
function minorUnits(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
||||
}
|
||||
|
||||
export function sanitizeGrokBillingStatus(value: unknown): GrokBillingStatus | undefined {
|
||||
const billing = toRecord(value);
|
||||
if (!billing || billing.currency !== "USD") return undefined;
|
||||
if (billing.additionalCreditsUrl !== GROK_BUILD_ADDITIONAL_CREDITS_URL) return undefined;
|
||||
|
||||
const rawAutoTopUp = toRecord(billing.autoTopUp);
|
||||
if (!rawAutoTopUp || typeof rawAutoTopUp.available !== "boolean") return undefined;
|
||||
|
||||
const available = rawAutoTopUp.available;
|
||||
const enabled =
|
||||
available && typeof rawAutoTopUp.enabled === "boolean" ? rawAutoTopUp.enabled : undefined;
|
||||
const extraCreditsMinorUnits = minorUnits(billing.extraCreditsMinorUnits);
|
||||
const thresholdMinorUnits =
|
||||
enabled === true ? minorUnits(rawAutoTopUp.thresholdMinorUnits) : undefined;
|
||||
const amountMinorUnits = enabled === true ? minorUnits(rawAutoTopUp.amountMinorUnits) : undefined;
|
||||
const maxMonthlyMinorUnits =
|
||||
enabled === true ? minorUnits(rawAutoTopUp.maxMonthlyMinorUnits) : undefined;
|
||||
|
||||
return {
|
||||
currency: "USD",
|
||||
...(extraCreditsMinorUnits !== undefined ? { extraCreditsMinorUnits } : {}),
|
||||
autoTopUp: {
|
||||
available,
|
||||
...(enabled !== undefined ? { enabled } : {}),
|
||||
...(thresholdMinorUnits !== undefined ? { thresholdMinorUnits } : {}),
|
||||
...(amountMinorUnits !== undefined ? { amountMinorUnits } : {}),
|
||||
...(maxMonthlyMinorUnits !== undefined ? { maxMonthlyMinorUnits } : {}),
|
||||
},
|
||||
additionalCreditsUrl: GROK_BUILD_ADDITIONAL_CREDITS_URL,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatGrokMinorUnits(
|
||||
value: number | undefined,
|
||||
currency: GrokBillingStatus["currency"],
|
||||
locales?: Intl.LocalesArgument
|
||||
): string | null {
|
||||
if (value === undefined) return null;
|
||||
return new Intl.NumberFormat(locales, {
|
||||
style: "currency",
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value / 100);
|
||||
}
|
||||
|
||||
const fallbackTranslation: GrokBillingTranslator = (_key, fallback) => fallback;
|
||||
|
||||
export function buildGrokBillingCardRows(
|
||||
billing: GrokBillingStatus,
|
||||
locales?: Intl.LocalesArgument,
|
||||
translate: GrokBillingTranslator = fallbackTranslation
|
||||
): GrokBillingCardRow[] {
|
||||
const rows: GrokBillingCardRow[] = [];
|
||||
const extraCredits = formatGrokMinorUnits(
|
||||
billing.extraCreditsMinorUnits,
|
||||
billing.currency,
|
||||
locales
|
||||
);
|
||||
if (extraCredits !== null) {
|
||||
rows.push({
|
||||
kind: "balance",
|
||||
label: translate("grokExtraUsageCredits", "Extra Usage Credits"),
|
||||
value: extraCredits,
|
||||
});
|
||||
}
|
||||
|
||||
const autoTopUp = billing.autoTopUp;
|
||||
let autoTopUpValue: string;
|
||||
if (!autoTopUp.available) {
|
||||
autoTopUpValue = translate("grokAutoTopUpUnavailable", "Unavailable");
|
||||
} else if (!autoTopUp.enabled) {
|
||||
autoTopUpValue = translate("grokAutoTopUpDisabled", "Disabled");
|
||||
} else {
|
||||
const threshold = formatGrokMinorUnits(
|
||||
autoTopUp.thresholdMinorUnits,
|
||||
billing.currency,
|
||||
locales
|
||||
);
|
||||
const amount = formatGrokMinorUnits(autoTopUp.amountMinorUnits, billing.currency, locales);
|
||||
const maximum = formatGrokMinorUnits(autoTopUp.maxMonthlyMinorUnits, billing.currency, locales);
|
||||
autoTopUpValue = [
|
||||
translate("grokAutoTopUpEnabled", "Enabled"),
|
||||
threshold ? `${translate("grokAutoTopUpAt", "at")} ${threshold}` : null,
|
||||
amount ? `${translate("grokAutoTopUpAdd", "add")} ${amount}` : null,
|
||||
maximum
|
||||
? `${translate("grokAutoTopUpMax", "max")} ${maximum}/${translate(
|
||||
"grokAutoTopUpMonth",
|
||||
"month"
|
||||
)}`
|
||||
: null,
|
||||
]
|
||||
.filter((part): part is string => part !== null)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
rows.push({
|
||||
kind: "status",
|
||||
label: translate("grokAutoTopUp", "Auto Top-Up"),
|
||||
value: autoTopUpValue,
|
||||
});
|
||||
rows.push({
|
||||
kind: "link",
|
||||
label: translate("grokAdditionalCredits", "Additional Credits"),
|
||||
href: billing.additionalCreditsUrl,
|
||||
target: "_blank",
|
||||
rel: "noreferrer noopener",
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
303
tests/unit/grok-cli-provider-limits-ui.test.ts
Normal file
303
tests/unit/grok-cli-provider-limits-ui.test.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-grok-limits-ui-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.STORAGE_ENCRYPTION_KEY = "grok-provider-limits-ui-test-key-32-bytes-minimum";
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const { parseQuotaData, resolvePlanValue, buildProviderLimitsResolvedPlans, normalizePlanTier } =
|
||||
await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx");
|
||||
const { PROVIDER_LABEL } =
|
||||
await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/constants.ts");
|
||||
const { USAGE_SUPPORTED_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const {
|
||||
buildGrokBillingCardRows,
|
||||
formatGrokMinorUnits,
|
||||
GROK_BUILD_ADDITIONAL_CREDITS_URL,
|
||||
sanitizeGrokBillingStatus,
|
||||
} = await import("../../src/shared/utils/grokBilling.ts");
|
||||
type GrokBillingTranslator =
|
||||
typeof import("../../src/shared/utils/grokBilling.ts").GrokBillingTranslator;
|
||||
|
||||
const baseBilling = {
|
||||
currency: "USD" as const,
|
||||
autoTopUp: { available: false },
|
||||
additionalCreditsUrl: GROK_BUILD_ADDITIONAL_CREDITS_URL,
|
||||
};
|
||||
|
||||
test.after(() => {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("Grok Build product aliases normalize to one stable row and preserve collisions", () => {
|
||||
const parsed = parseQuotaData("grok-cli", {
|
||||
quotas: {
|
||||
weekly: {
|
||||
used: 37.25,
|
||||
total: 100,
|
||||
remaining: 62.75,
|
||||
remainingPercentage: 62.75,
|
||||
resetAt: "2099-08-03T00:00:00.000Z",
|
||||
isPercentageOnly: true,
|
||||
},
|
||||
product_grok_build: {
|
||||
displayName: "Grok Build",
|
||||
used: 12.5,
|
||||
total: 100,
|
||||
remaining: 87.5,
|
||||
remainingPercentage: 87.5,
|
||||
resetAt: "2099-08-03T00:00:00.000Z",
|
||||
isPercentageOnly: true,
|
||||
},
|
||||
product_grok_build_2: {
|
||||
displayName: "Grok Build",
|
||||
used: 25,
|
||||
total: 100,
|
||||
remaining: 75,
|
||||
remainingPercentage: 75,
|
||||
resetAt: "2099-08-03T00:00:00.000Z",
|
||||
isPercentageOnly: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
parsed.map(({ name, displayName, remainingPercentage }) => ({
|
||||
name,
|
||||
displayName,
|
||||
remainingPercentage,
|
||||
})),
|
||||
[
|
||||
{ name: "weekly", displayName: undefined, remainingPercentage: 62.75 },
|
||||
{ name: "product_grok_build", displayName: "Grok Build", remainingPercentage: 87.5 },
|
||||
{ name: "product_grok_build_2", displayName: "Grok Build", remainingPercentage: 75 },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test("grok-cli plan display never infers persisted provider-specific tiers", () => {
|
||||
assert.equal(
|
||||
resolvePlanValue(
|
||||
null,
|
||||
{ subscriptionTier: "Persisted Secret Tier", plan: "Persisted Plan" },
|
||||
"grok-cli"
|
||||
),
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
resolvePlanValue(
|
||||
"Future Experimental Tier",
|
||||
{ subscriptionTier: "Persisted Tier" },
|
||||
"grok-cli"
|
||||
),
|
||||
"Future Experimental Tier"
|
||||
);
|
||||
});
|
||||
|
||||
test("page-level tier stats/filters ignore persisted Grok Free/Enterprise without live plan", () => {
|
||||
const connections = [
|
||||
{
|
||||
id: "grok-free",
|
||||
provider: "grok-cli",
|
||||
providerSpecificData: {
|
||||
tier: "Free",
|
||||
plan: "Free",
|
||||
subscriptionTier: "Free",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "grok-enterprise",
|
||||
provider: "grok-cli",
|
||||
providerSpecificData: {
|
||||
tier: "Enterprise",
|
||||
plan: "Enterprise",
|
||||
subscriptionTier: "Enterprise",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "grok-live",
|
||||
provider: "grok-cli",
|
||||
providerSpecificData: {
|
||||
tier: "Free",
|
||||
plan: "Free",
|
||||
subscriptionTier: "Free",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "codex-fallback",
|
||||
provider: "codex",
|
||||
providerSpecificData: { chatgptPlanType: "Pro" },
|
||||
},
|
||||
{
|
||||
id: "claude-fallback",
|
||||
provider: "claude",
|
||||
providerSpecificData: { plan: "Pro" },
|
||||
},
|
||||
];
|
||||
|
||||
const quotaData = {
|
||||
"grok-free": { plan: null },
|
||||
"grok-enterprise": {},
|
||||
"grok-live": { plan: "Enterprise" },
|
||||
"codex-fallback": { plan: "unknown" },
|
||||
"claude-fallback": { plan: null },
|
||||
};
|
||||
|
||||
const resolvedPlans = buildProviderLimitsResolvedPlans(connections, quotaData);
|
||||
assert.equal(resolvedPlans["grok-free"], null);
|
||||
assert.equal(resolvedPlans["grok-enterprise"], null);
|
||||
assert.equal(resolvedPlans["grok-live"], "Enterprise");
|
||||
assert.equal(resolvedPlans["codex-fallback"], "Pro");
|
||||
assert.equal(resolvedPlans["claude-fallback"], "Pro");
|
||||
|
||||
const tierByConnection = Object.fromEntries(
|
||||
connections.map((conn) => [conn.id, normalizePlanTier(resolvedPlans[conn.id])])
|
||||
);
|
||||
|
||||
assert.equal(tierByConnection["grok-free"].key, "unknown");
|
||||
assert.equal(tierByConnection["grok-enterprise"].key, "unknown");
|
||||
assert.equal(tierByConnection["grok-live"].key, "enterprise");
|
||||
assert.equal(tierByConnection["codex-fallback"].key, "pro");
|
||||
assert.equal(tierByConnection["claude-fallback"].key, "pro");
|
||||
|
||||
// Filter/stat bucket classification must not invent Free/Enterprise from PSD.
|
||||
assert.notEqual(tierByConnection["grok-free"].key, "free");
|
||||
assert.notEqual(tierByConnection["grok-enterprise"].key, "enterprise");
|
||||
|
||||
const tierCounts = {
|
||||
free: 0,
|
||||
enterprise: 0,
|
||||
pro: 0,
|
||||
unknown: 0,
|
||||
};
|
||||
for (const conn of connections) {
|
||||
const key = tierByConnection[conn.id]?.key || "unknown";
|
||||
if (key in tierCounts) tierCounts[key] += 1;
|
||||
}
|
||||
|
||||
assert.equal(tierCounts.free, 0);
|
||||
assert.equal(tierCounts.enterprise, 1); // only live Grok Enterprise
|
||||
assert.equal(tierCounts.pro, 2); // Codex + Claude fallbacks unchanged
|
||||
assert.equal(tierCounts.unknown, 2); // persisted Free + Enterprise without live plan
|
||||
});
|
||||
|
||||
test("Grok billing rows omit a missing balance and show an explicit localized zero", () => {
|
||||
const missing = buildGrokBillingCardRows(baseBilling, "en-US");
|
||||
assert.equal(
|
||||
missing.some((row) => row.kind === "balance"),
|
||||
false
|
||||
);
|
||||
assert.deepEqual(missing[0], {
|
||||
kind: "status",
|
||||
label: "Auto Top-Up",
|
||||
value: "Unavailable",
|
||||
});
|
||||
|
||||
const zero = buildGrokBillingCardRows({ ...baseBilling, extraCreditsMinorUnits: 0 }, "de-DE");
|
||||
assert.deepEqual(zero[0], {
|
||||
kind: "balance",
|
||||
label: "Extra Usage Credits",
|
||||
value: "0,00 $",
|
||||
});
|
||||
});
|
||||
|
||||
test("Grok billing rows distinguish disabled and unavailable and translate enabled details", () => {
|
||||
const translate: GrokBillingTranslator = (key, fallback) =>
|
||||
({
|
||||
grokExtraUsageCredits: "Credits translated",
|
||||
grokAutoTopUp: "Top-up translated",
|
||||
grokAutoTopUpEnabled: "On translated",
|
||||
grokAutoTopUpAt: "threshold translated",
|
||||
grokAutoTopUpAdd: "add translated",
|
||||
grokAutoTopUpMax: "maximum translated",
|
||||
grokAutoTopUpMonth: "month translated",
|
||||
grokAdditionalCredits: "Buy translated",
|
||||
})[key] ?? fallback;
|
||||
|
||||
const disabled = buildGrokBillingCardRows(
|
||||
{ ...baseBilling, autoTopUp: { available: true, enabled: false } },
|
||||
"en-US",
|
||||
translate
|
||||
);
|
||||
assert.equal(disabled.find((row) => row.kind === "status")?.value, "Disabled");
|
||||
|
||||
const enabled = buildGrokBillingCardRows(
|
||||
{
|
||||
...baseBilling,
|
||||
extraCreditsMinorUnits: 0,
|
||||
autoTopUp: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
thresholdMinorUnits: 500,
|
||||
amountMinorUnits: 2000,
|
||||
maxMonthlyMinorUnits: 10000,
|
||||
},
|
||||
},
|
||||
"en-US",
|
||||
translate
|
||||
);
|
||||
assert.deepEqual(enabled, [
|
||||
{ kind: "balance", label: "Credits translated", value: "$0.00" },
|
||||
{
|
||||
kind: "status",
|
||||
label: "Top-up translated",
|
||||
value:
|
||||
"On translated · threshold translated $5.00 · add translated $20.00 · maximum translated $100.00/month translated",
|
||||
},
|
||||
{
|
||||
kind: "link",
|
||||
label: "Buy translated",
|
||||
href: GROK_BUILD_ADDITIONAL_CREDITS_URL,
|
||||
target: "_blank",
|
||||
rel: "noreferrer noopener",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("Provider Limits exposes only the sanitized Grok billing contract", () => {
|
||||
assert.ok(USAGE_SUPPORTED_PROVIDERS.includes("grok-cli"));
|
||||
assert.equal(PROVIDER_LABEL["grok-cli"], "Grok Build");
|
||||
|
||||
const billing = sanitizeGrokBillingStatus({
|
||||
currency: "USD",
|
||||
extraCreditsMinorUnits: 0,
|
||||
autoTopUp: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
thresholdMinorUnits: 500,
|
||||
amountMinorUnits: 2000,
|
||||
maxMonthlyMinorUnits: 10000,
|
||||
paymentMethodId: "secret",
|
||||
},
|
||||
additionalCreditsUrl: GROK_BUILD_ADDITIONAL_CREDITS_URL,
|
||||
rawBody: "secret",
|
||||
});
|
||||
|
||||
assert.deepEqual(billing, {
|
||||
currency: "USD",
|
||||
extraCreditsMinorUnits: 0,
|
||||
autoTopUp: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
thresholdMinorUnits: 500,
|
||||
amountMinorUnits: 2000,
|
||||
maxMonthlyMinorUnits: 10000,
|
||||
},
|
||||
additionalCreditsUrl: GROK_BUILD_ADDITIONAL_CREDITS_URL,
|
||||
});
|
||||
assert.equal(formatGrokMinorUnits(billing?.extraCreditsMinorUnits, "USD", "en-US"), "$0.00");
|
||||
assert.equal(formatGrokMinorUnits(billing?.autoTopUp.amountMinorUnits, "USD", "en-US"), "$20.00");
|
||||
|
||||
assert.equal(
|
||||
sanitizeGrokBillingStatus({
|
||||
currency: "USD",
|
||||
autoTopUp: { available: false },
|
||||
additionalCreditsUrl: "https://attacker.invalid/credits",
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
494
tests/unit/grok-cli-provider-limits.test.ts
Normal file
494
tests/unit/grok-cli-provider-limits.test.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-grok-limits-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.STORAGE_ENCRYPTION_KEY = "grok-provider-limits-test-key-32-bytes-minimum";
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { getUsageForProvider, USAGE_FETCHER_PROVIDERS } =
|
||||
await import("../../open-sse/services/usage.ts");
|
||||
const { __testing: grokTesting } = await import("../../open-sse/services/usage/grokCli.ts");
|
||||
const providerLimitsDb = await import("../../src/lib/db/providerLimits.ts");
|
||||
const { mergeProviderLimitsCacheEntry } =
|
||||
await import("../../src/lib/usage/providerLimitsCache.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
interface FetchCall {
|
||||
url: string;
|
||||
init: RequestInit;
|
||||
}
|
||||
|
||||
function response(value: unknown, init: ResponseInit = {}) {
|
||||
return new Response(JSON.stringify(value), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
...init,
|
||||
});
|
||||
}
|
||||
|
||||
function successFixtures(
|
||||
options: {
|
||||
tier?: unknown;
|
||||
userId?: unknown;
|
||||
prepaidBalance?: Record<string, unknown> | null | undefined;
|
||||
productUsage?: unknown;
|
||||
} = {}
|
||||
) {
|
||||
const tier = "tier" in options ? options.tier : "SuperGrok Heavy";
|
||||
const userId = "userId" in options ? options.userId : "canonical-user-id";
|
||||
const prepaidBalance =
|
||||
"prepaidBalance" in options ? options.prepaidBalance : ({ val: 1234 } as const);
|
||||
const productUsage =
|
||||
"productUsage" in options
|
||||
? options.productUsage
|
||||
: [
|
||||
{ product: "API", usagePercent: 12.5 },
|
||||
{ product: "Grok Code", usagePercent: 44 },
|
||||
];
|
||||
|
||||
return async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/user?include=subscription")) {
|
||||
return response({
|
||||
...(userId === undefined ? {} : { userId }),
|
||||
...(tier === undefined ? {} : { subscriptionTier: tier }),
|
||||
email: "must-not-be-exposed@example.invalid",
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/billing?format=credits")) {
|
||||
return response({
|
||||
config: {
|
||||
creditUsagePercent: 37.25,
|
||||
currentPeriod: {
|
||||
type: "WEEKLY",
|
||||
start: "2026-07-27T00:00:00.000Z",
|
||||
end: "2026-08-03T00:00:00.000Z",
|
||||
},
|
||||
productUsage,
|
||||
...(prepaidBalance === undefined ? {} : { prepaidBalance }),
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/auto-topup-rule")) {
|
||||
return response({
|
||||
rule: {
|
||||
enabled: true,
|
||||
minBeforeHittingSl: { val: 500 },
|
||||
topupAmount: { val: 2000 },
|
||||
maxAmountPerMonth: { val: 10000 },
|
||||
paymentMethodId: "must-not-be-exposed",
|
||||
},
|
||||
});
|
||||
}
|
||||
return new Response(null, { status: 404 });
|
||||
};
|
||||
}
|
||||
|
||||
interface UsageResult {
|
||||
plan?: string;
|
||||
message?: string;
|
||||
quotas?: Record<
|
||||
string,
|
||||
{
|
||||
displayName?: string;
|
||||
used: number;
|
||||
total: number;
|
||||
remaining: number;
|
||||
remainingPercentage: number;
|
||||
resetAt: string | null;
|
||||
isPercentageOnly: boolean;
|
||||
}
|
||||
>;
|
||||
billing?: {
|
||||
currency: "USD";
|
||||
extraCreditsMinorUnits?: number;
|
||||
autoTopUp: {
|
||||
available: boolean;
|
||||
enabled?: boolean;
|
||||
thresholdMinorUnits?: number;
|
||||
amountMinorUnits?: number;
|
||||
maxMonthlyMinorUnits?: number;
|
||||
};
|
||||
additionalCreditsUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function getUsage(fetchImpl: typeof fetch): Promise<UsageResult> {
|
||||
globalThis.fetch = fetchImpl;
|
||||
return (await getUsageForProvider({
|
||||
id: "connection-id",
|
||||
provider: "grok-cli",
|
||||
accessToken: "fixture-access-token",
|
||||
})) as UsageResult;
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("grok-cli fetches the fixed read-only surfaces with the full Grok client profile", async () => {
|
||||
const calls: FetchCall[] = [];
|
||||
const fixtureFetch = successFixtures();
|
||||
const usage = await getUsage((async (input: string | URL | Request, init: RequestInit = {}) => {
|
||||
calls.push({ url: String(input), init });
|
||||
return fixtureFetch(input);
|
||||
}) as typeof fetch);
|
||||
|
||||
assert.equal(usage.plan, "SuperGrok Heavy");
|
||||
assert.deepEqual(usage.quotas?.weekly, {
|
||||
used: 37.25,
|
||||
total: 100,
|
||||
remaining: 62.75,
|
||||
remainingPercentage: 62.75,
|
||||
resetAt: "2026-08-03T00:00:00.000Z",
|
||||
isPercentageOnly: true,
|
||||
});
|
||||
assert.deepEqual(usage.quotas?.product_api, {
|
||||
displayName: "API",
|
||||
used: 12.5,
|
||||
total: 100,
|
||||
remaining: 87.5,
|
||||
remainingPercentage: 87.5,
|
||||
resetAt: "2026-08-03T00:00:00.000Z",
|
||||
isPercentageOnly: true,
|
||||
});
|
||||
assert.deepEqual(usage.billing, {
|
||||
currency: "USD",
|
||||
extraCreditsMinorUnits: 1234,
|
||||
autoTopUp: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
thresholdMinorUnits: 500,
|
||||
amountMinorUnits: 2000,
|
||||
maxMonthlyMinorUnits: 10000,
|
||||
},
|
||||
additionalCreditsUrl: "https://grok.com/build?_s=usage",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
calls.map((call) => call.url),
|
||||
[
|
||||
"https://cli-chat-proxy.grok.com/v1/user?include=subscription",
|
||||
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
|
||||
"https://cli-chat-proxy.grok.com/v1/auto-topup-rule",
|
||||
]
|
||||
);
|
||||
for (const { init } of calls) {
|
||||
assert.equal(init.method, "GET");
|
||||
assert.equal(init.redirect, "error");
|
||||
assert.equal(init.body, undefined);
|
||||
assert.ok(init.signal instanceof AbortSignal);
|
||||
const headers = new Headers(init.headers);
|
||||
assert.equal(headers.get("accept"), "application/json");
|
||||
assert.equal(headers.get("authorization"), "Bearer fixture-access-token");
|
||||
assert.equal(headers.get("x-xai-token-auth"), "xai-grok-cli");
|
||||
assert.ok(headers.get("user-agent"));
|
||||
assert.ok(headers.get("x-grok-client-version"));
|
||||
assert.ok(headers.get("x-grok-client-identifier"));
|
||||
assert.equal(headers.get("x-grok-client-mode"), "headless");
|
||||
}
|
||||
assert.equal(new Headers(calls[0].init.headers).has("x-userid"), false);
|
||||
assert.equal(new Headers(calls[2].init.headers).get("x-userid"), "canonical-user-id");
|
||||
assert.deepEqual(grokTesting.networkPolicy, {
|
||||
method: "GET",
|
||||
redirect: "error",
|
||||
timeoutMs: 10_000,
|
||||
maxResponseBytes: 256 * 1024,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(usage);
|
||||
for (const sensitive of [
|
||||
"fixture-access-token",
|
||||
"canonical-user-id",
|
||||
"must-not-be-exposed@example.invalid",
|
||||
"paymentMethodId",
|
||||
]) {
|
||||
assert.equal(serialized.includes(sensitive), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("grok-cli preserves unknown and missing values without fabricating billing state", async () => {
|
||||
for (const tier of [undefined, null, "", " "]) {
|
||||
const usage = await getUsage(successFixtures({ tier }) as typeof fetch);
|
||||
assert.equal(usage.plan, undefined);
|
||||
}
|
||||
const future = await getUsage(
|
||||
successFixtures({ tier: "Future Experimental Tier" }) as typeof fetch
|
||||
);
|
||||
assert.equal(future.plan, "Future Experimental Tier");
|
||||
|
||||
const missing = await getUsage(successFixtures({ prepaidBalance: undefined }) as typeof fetch);
|
||||
assert.ok(missing.billing);
|
||||
assert.equal("extraCreditsMinorUnits" in missing.billing, false);
|
||||
|
||||
const explicitZero = await getUsage(
|
||||
successFixtures({ prepaidBalance: { val: 0 } }) as typeof fetch
|
||||
);
|
||||
assert.equal(explicitZero.billing?.extraCreditsMinorUnits, 0);
|
||||
|
||||
const calls: string[] = [];
|
||||
const withoutUserId = successFixtures({ userId: undefined });
|
||||
const noIdentity = await getUsage((async (input: string | URL | Request) => {
|
||||
calls.push(String(input));
|
||||
return withoutUserId(input);
|
||||
}) as typeof fetch);
|
||||
assert.ok(calls.some((url) => url.endsWith("/billing?format=credits")));
|
||||
assert.equal(
|
||||
calls.some((url) => url.endsWith("/auto-topup-rule")),
|
||||
false
|
||||
);
|
||||
assert.deepEqual(noIdentity.billing?.autoTopUp, { available: false });
|
||||
});
|
||||
|
||||
test("official Cent wrappers distinguish omission and normalize signed minor units", async () => {
|
||||
for (const [prepaidBalance, expected] of [
|
||||
[undefined, undefined],
|
||||
[{}, 0],
|
||||
[{ val: 0 }, 0],
|
||||
[{ val: 1234 }, 1234],
|
||||
[{ val: -1234 }, 1234],
|
||||
] as const) {
|
||||
const usage = await getUsage(successFixtures({ prepaidBalance }) as typeof fetch);
|
||||
assert.equal(usage.billing?.extraCreditsMinorUnits, expected);
|
||||
}
|
||||
|
||||
for (const [amount, expected] of [
|
||||
[undefined, undefined],
|
||||
[{}, 0],
|
||||
[{ val: 0 }, 0],
|
||||
[{ val: 1234 }, 1234],
|
||||
[{ val: -1234 }, 1234],
|
||||
] as const) {
|
||||
const fixture = successFixtures();
|
||||
const usage = await getUsage((async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (!url.endsWith("/auto-topup-rule")) return fixture(input);
|
||||
return response({
|
||||
rule: {
|
||||
enabled: true,
|
||||
...(amount === undefined
|
||||
? {}
|
||||
: {
|
||||
minBeforeHittingSl: amount,
|
||||
topupAmount: amount,
|
||||
maxAmountPerMonth: amount,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}) as typeof fetch);
|
||||
assert.equal(usage.billing?.autoTopUp.thresholdMinorUnits, expected);
|
||||
assert.equal(usage.billing?.autoTopUp.amountMinorUnits, expected);
|
||||
assert.equal(usage.billing?.autoTopUp.maxMonthlyMinorUnits, expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("auto top-up distinguishes disabled rules from unavailable responses", async () => {
|
||||
for (const rule of [{}, { enabled: false }]) {
|
||||
const fixture = successFixtures();
|
||||
const usage = await getUsage((async (input: string | URL | Request) =>
|
||||
String(input).endsWith("/auto-topup-rule")
|
||||
? response({ rule })
|
||||
: fixture(input)) as typeof fetch);
|
||||
assert.deepEqual(usage.billing?.autoTopUp, { available: true, enabled: false });
|
||||
}
|
||||
|
||||
for (const payload of [
|
||||
{},
|
||||
{ rule: null },
|
||||
{ rule: "malformed" },
|
||||
{ rule: { enabled: "malformed" } },
|
||||
]) {
|
||||
const fixture = successFixtures();
|
||||
const usage = await getUsage((async (input: string | URL | Request) =>
|
||||
String(input).endsWith("/auto-topup-rule")
|
||||
? response(payload)
|
||||
: fixture(input)) as typeof fetch);
|
||||
assert.deepEqual(usage.billing?.autoTopUp, { available: false });
|
||||
}
|
||||
|
||||
const fixture = successFixtures();
|
||||
const failed = await getUsage((async (input: string | URL | Request) =>
|
||||
String(input).endsWith("/auto-topup-rule")
|
||||
? new Response(null, { status: 500 })
|
||||
: fixture(input)) as typeof fetch);
|
||||
assert.deepEqual(failed.billing?.autoTopUp, { available: false });
|
||||
});
|
||||
|
||||
test("empty tiers retain the canonical user id for the auto-topup request", async () => {
|
||||
for (const tier of [undefined, null, "", " "]) {
|
||||
const calls: FetchCall[] = [];
|
||||
const fixture = successFixtures({ tier, userId: " canonical-user-id " });
|
||||
const usage = await getUsage((async (input: string | URL | Request, init: RequestInit = {}) => {
|
||||
calls.push({ url: String(input), init });
|
||||
return fixture(input);
|
||||
}) as typeof fetch);
|
||||
|
||||
assert.equal(usage.plan, undefined);
|
||||
const autoTopUpCall = calls.find((call) => call.url.endsWith("/auto-topup-rule"));
|
||||
assert.ok(autoTopUpCall);
|
||||
assert.equal(new Headers(autoTopUpCall.init.headers).get("x-userid"), "canonical-user-id");
|
||||
}
|
||||
});
|
||||
|
||||
test("Provider Limits cache merges last-known-good Grok auto top-up independently", () => {
|
||||
const fetchedAt = "2026-08-02T00:00:00.000Z";
|
||||
for (const previousAutoTopUp of [
|
||||
{ available: true, enabled: true, amountMinorUnits: 2000 },
|
||||
{ available: true, enabled: false },
|
||||
] as const) {
|
||||
const previous = {
|
||||
quotas: null,
|
||||
plan: "Previous Tier",
|
||||
message: null,
|
||||
fetchedAt: "2026-08-01T00:00:00.000Z",
|
||||
billing: {
|
||||
currency: "USD" as const,
|
||||
extraCreditsMinorUnits: 100,
|
||||
autoTopUp: previousAutoTopUp,
|
||||
additionalCreditsUrl: "https://grok.com/build?_s=usage" as const,
|
||||
},
|
||||
};
|
||||
const next = {
|
||||
quotas: { weekly: { remainingPercentage: 80 } },
|
||||
plan: "New Tier",
|
||||
message: null,
|
||||
fetchedAt,
|
||||
billing: {
|
||||
currency: "USD" as const,
|
||||
extraCreditsMinorUnits: 250,
|
||||
autoTopUp: { available: false },
|
||||
additionalCreditsUrl: "https://grok.com/build?_s=usage" as const,
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(mergeProviderLimitsCacheEntry("grok-cli", next, previous), {
|
||||
...next,
|
||||
billing: { ...next.billing, autoTopUp: previousAutoTopUp },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("Provider Limits overall failure preservation accepts billing-only previous data", () => {
|
||||
const previous = {
|
||||
quotas: null,
|
||||
plan: "Previous Tier",
|
||||
message: null,
|
||||
fetchedAt: "2026-08-01T00:00:00.000Z",
|
||||
billing: {
|
||||
currency: "USD" as const,
|
||||
autoTopUp: { available: true, enabled: false },
|
||||
additionalCreditsUrl: "https://grok.com/build?_s=usage" as const,
|
||||
},
|
||||
};
|
||||
const failure = {
|
||||
quotas: null,
|
||||
plan: null,
|
||||
message: "Grok Build billing status unavailable",
|
||||
fetchedAt: "2026-08-02T00:00:00.000Z",
|
||||
};
|
||||
assert.equal(mergeProviderLimitsCacheEntry("grok-cli", failure, previous), previous);
|
||||
assert.equal(
|
||||
mergeProviderLimitsCacheEntry("grok-cli", failure, {
|
||||
...previous,
|
||||
quotas: {},
|
||||
billing: undefined,
|
||||
}),
|
||||
failure
|
||||
);
|
||||
});
|
||||
|
||||
test("grok-cli keeps valid fields across sparse partial failures and bounded malformed responses", async () => {
|
||||
const partial = await getUsage(
|
||||
successFixtures({
|
||||
productUsage: [
|
||||
{ product: "GrokBuild", usagePercent: 25 },
|
||||
{ product: "PRODUCT_GROK_BUILD", usagePercent: 50 },
|
||||
{ product: "Future Product", usagePercent: 10 },
|
||||
{ product: "Future Product", usagePercent: 20 },
|
||||
{ product: "invalid", usagePercent: "secret-invalid-value" },
|
||||
],
|
||||
prepaidBalance: { val: -1 },
|
||||
}) as typeof fetch
|
||||
);
|
||||
assert.equal(partial.quotas?.weekly.remainingPercentage, 62.75);
|
||||
assert.equal(partial.quotas?.product_grok_build.displayName, "Grok Build");
|
||||
assert.equal(partial.quotas?.product_grok_build.remainingPercentage, 75);
|
||||
assert.equal(partial.quotas?.product_grok_build_2.displayName, "Grok Build");
|
||||
assert.equal(partial.quotas?.product_grok_build_2.remainingPercentage, 50);
|
||||
assert.equal(partial.quotas?.product_future_product.displayName, "Future Product");
|
||||
assert.equal(partial.quotas?.product_future_product_2.displayName, "Future Product");
|
||||
assert.equal(partial.quotas?.product_invalid, undefined);
|
||||
assert.equal(partial.billing?.extraCreditsMinorUnits, 1);
|
||||
|
||||
const sensitive = "token-secret canonical-user-id secret@example.invalid raw-body";
|
||||
for (const status of [401, 403, 429, 500]) {
|
||||
const usage = await getUsage((async () => new Response(sensitive, { status })) as typeof fetch);
|
||||
const serialized = JSON.stringify(usage);
|
||||
assert.equal(usage.quotas, undefined);
|
||||
assert.equal(serialized.includes(sensitive), false);
|
||||
assert.equal(serialized.includes("fixture-access-token"), false);
|
||||
}
|
||||
|
||||
const invalid = await getUsage(
|
||||
(async () => new Response("{invalid", { status: 200 })) as typeof fetch
|
||||
);
|
||||
assert.equal(invalid.quotas, undefined);
|
||||
|
||||
const oversized = await getUsage(
|
||||
(async () =>
|
||||
new Response(JSON.stringify({ padding: "x".repeat(300_000) }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as typeof fetch
|
||||
);
|
||||
assert.equal(oversized.quotas, undefined);
|
||||
});
|
||||
|
||||
test("Provider Limits cache persists only the public Grok billing contract", () => {
|
||||
const cached = providerLimitsDb.setProviderLimitsCache("grok-connection", {
|
||||
quotas: { weekly: { remainingPercentage: 62.75 } },
|
||||
plan: "Future Experimental Tier",
|
||||
message: null,
|
||||
fetchedAt: "2026-08-02T00:00:00.000Z",
|
||||
source: "manual",
|
||||
billing: {
|
||||
currency: "USD",
|
||||
extraCreditsMinorUnits: 0,
|
||||
autoTopUp: {
|
||||
available: true,
|
||||
enabled: true,
|
||||
amountMinorUnits: 2000,
|
||||
},
|
||||
additionalCreditsUrl: "https://grok.com/build?_s=usage",
|
||||
rawBody: "secret",
|
||||
userId: "secret",
|
||||
} as unknown as NonNullable<
|
||||
Parameters<typeof providerLimitsDb.setProviderLimitsCache>[1]["billing"]
|
||||
>,
|
||||
});
|
||||
|
||||
assert.deepEqual(cached.billing, {
|
||||
currency: "USD",
|
||||
extraCreditsMinorUnits: 0,
|
||||
autoTopUp: { available: true, enabled: true, amountMinorUnits: 2000 },
|
||||
additionalCreditsUrl: "https://grok.com/build?_s=usage",
|
||||
});
|
||||
assert.deepEqual(providerLimitsDb.getProviderLimitsCache("grok-connection"), cached);
|
||||
assert.equal(JSON.stringify(cached).includes("secret"), false);
|
||||
});
|
||||
|
||||
test("grok-cli is registered on the public Provider Limits usage seam", () => {
|
||||
assert.ok((USAGE_FETCHER_PROVIDERS as readonly string[]).includes("grok-cli"));
|
||||
});
|
||||
Reference in New Issue
Block a user