/** * SuperGrok (`xai-oauth`) subscription usage provider. * * Reads utilization from the Grok CLI billing endpoint. Prefer the legacy * weekly `format=credits` payload (creditUsagePercent / productUsage). When * xAI marks the account as unified billing and omits those fields, fall back * to the default monthly included-quota shape (`monthlyLimit` / `used`). * Only OAuth access credentials are accepted; paid API keys are a separate * product and must never be sent here. */ import { toNumber } from "@oh-my-pi/pi-catalog/utils"; import { isUsageLimitExhausted } from "../auth/usage-report"; import { buildXAICliBillingUrl, extractXAIAccessTokenSubject, fetchXAIOAuthIdentity, getXAICliBillingHeaders, } from "../registry/oauth/xai-oauth"; import type { CredentialRankingStrategy, UsageAmount, UsageFetchContext, UsageFetchParams, UsageLimit, UsageProvider, UsageReport, UsageWindow, } from "../usage"; import { isRecord } from "../utils"; import { DAY_MS, HOUR_MS, parseIsoTimestamp, usageStatus, WEEK_MS } from "./shared"; const PROVIDER_ID = "xai-oauth"; const BILLING_SOURCE = "cli-chat-proxy.grok.com/v1/billing"; interface XaiBillingPeriod { start: string; end: string; type: string; } interface XaiProductUsage { product: string; usagePercent: number; } /** Legacy SuperGrok weekly credits (`?format=credits`). */ interface XaiWeeklyBillingConfig { kind: "weekly"; currentPeriod: XaiBillingPeriod; creditUsagePercent: number; productUsage: XaiProductUsage[]; onDemandCap?: number; onDemandUsed?: number; inferredPercent?: boolean; } /** * Unified-billing monthly included quota. * Live `isUnifiedBillingUser` accounts omit creditUsagePercent on * `?format=credits` and expose monthlyLimit/used on the default billing URL. */ interface XaiMonthlyBillingConfig { kind: "monthly"; periodStart: string; periodEnd: string; used: number; limit: number; onDemandCap?: number; onDemandUsed?: number; } type XaiBillingConfig = XaiWeeklyBillingConfig | XaiMonthlyBillingConfig; function parsePercent(value: unknown): number | undefined { const percent = toNumber(value); return percent !== undefined && percent >= 0 && percent <= 100 ? percent : undefined; } function parseOnDemandAmount(value: unknown): number | undefined { if (!isRecord(value)) return undefined; const amount = toNumber(value.val); return amount !== undefined && amount >= 0 ? amount : undefined; } function buildPercentAmount(usagePercent: number): UsageAmount { const usedFraction = usagePercent / 100; return { used: usagePercent, limit: 100, remaining: 100 - usagePercent, usedFraction, remainingFraction: 1 - usedFraction, unit: "percent", }; } function slugifyProduct(product: string): string { return product .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function buildPeriodWindow(period: XaiBillingPeriod): UsageWindow { return { id: "1w", label: "Weekly", durationMs: WEEK_MS, resetsAt: parseIsoTimestamp(period.end), }; } function buildMonthlyWindow(periodStart: string, periodEnd: string): UsageWindow | undefined { const startMs = parseIsoTimestamp(periodStart); const endMs = parseIsoTimestamp(periodEnd); if (startMs === undefined || endMs === undefined || endMs <= startMs) return undefined; // Real calendar months vary; use the observed period length from the API. const durationMs = endMs - startMs; const approxDays = Math.max(1, Math.round(durationMs / DAY_MS)); return { id: "1mo", label: approxDays === 30 || approxDays === 31 ? "Monthly" : `${approxDays}d`, durationMs, resetsAt: endMs, }; } function parseWeeklyBillingConfig(raw: Record): XaiWeeklyBillingConfig | null { if (!isRecord(raw.currentPeriod)) return null; const start = typeof raw.currentPeriod.start === "string" ? parseIsoTimestamp(raw.currentPeriod.start) : undefined; const end = typeof raw.currentPeriod.end === "string" ? parseIsoTimestamp(raw.currentPeriod.end) : undefined; const type = typeof raw.currentPeriod.type === "string" ? raw.currentPeriod.type : ""; // Keep recently-ended weekly windows so /usage still renders across period // rollover while the billing API is mid-refresh. Reject only inverted ranges // and non-weekly period types. if (start === undefined || end === undefined || end <= start || !type.toUpperCase().includes("WEEK")) { return null; } // Fresh weekly periods (or accounts with 0 usage) omit creditUsagePercent; // default to 0 only when the weekly period is active (end > now). // Expired periods without explicit usage data are rejected to retain last good cache. const inferredPercent = raw.creditUsagePercent === undefined || raw.creditUsagePercent === null; let creditUsagePercent: number | undefined; if (inferredPercent) { creditUsagePercent = end > Date.now() ? 0 : undefined; } else { creditUsagePercent = parsePercent(raw.creditUsagePercent); } if (creditUsagePercent === undefined) return null; const productUsage: XaiProductUsage[] = []; if (raw.productUsage !== undefined) { if (!Array.isArray(raw.productUsage)) return null; for (const item of raw.productUsage) { if (!isRecord(item)) continue; const product = typeof item.product === "string" ? item.product.trim() : ""; const usagePercent = item.usagePercent === undefined || item.usagePercent === null ? 0 : parsePercent(item.usagePercent); if (!product || usagePercent === undefined) continue; productUsage.push({ product, usagePercent }); } } return { kind: "weekly", currentPeriod: { start: raw.currentPeriod.start as string, end: raw.currentPeriod.end as string, type, }, creditUsagePercent, productUsage, onDemandCap: parseOnDemandAmount(raw.onDemandCap), onDemandUsed: parseOnDemandAmount(raw.onDemandUsed), inferredPercent, }; } function parseMonthlyBillingConfig(raw: Record): XaiMonthlyBillingConfig | null { const periodStart = typeof raw.billingPeriodStart === "string" ? raw.billingPeriodStart : ""; const periodEnd = typeof raw.billingPeriodEnd === "string" ? raw.billingPeriodEnd : ""; const startMs = parseIsoTimestamp(periodStart); const endMs = parseIsoTimestamp(periodEnd); if (!periodStart || !periodEnd || startMs === undefined || endMs === undefined || endMs <= startMs) { return null; } const limit = parseOnDemandAmount(raw.monthlyLimit); const used = parseOnDemandAmount(raw.used); // Require a positive included quota; zero/missing is not a usable report. if (limit === undefined || limit <= 0 || used === undefined) return null; return { kind: "monthly", periodStart, periodEnd, used, limit, onDemandCap: parseOnDemandAmount(raw.onDemandCap), onDemandUsed: parseOnDemandAmount(raw.onDemandUsed), }; } function confirmsNoMonthlyQuota(raw: Record): boolean { const limit = parseOnDemandAmount(raw.monthlyLimit); if (limit !== undefined) return limit === 0; // Some weekly accounts return the credits shape from the default endpoint too. return parseWeeklyBillingConfig(raw)?.inferredPercent === true; } function buildOnDemandLimit( onDemandCap: number | undefined, onDemandUsed: number | undefined, accountId: string | undefined, ): UsageLimit | undefined { if (onDemandCap === undefined || onDemandCap <= 0 || onDemandUsed === undefined) return undefined; const usedFraction = Math.min(onDemandUsed / onDemandCap, 1); return { id: `${PROVIDER_ID}:on-demand`, label: "On-demand", scope: { provider: PROVIDER_ID, ...(accountId ? { accountId } : {}), shared: true, }, amount: { used: onDemandUsed, limit: onDemandCap, remaining: Math.max(0, onDemandCap - onDemandUsed), usedFraction, remainingFraction: 1 - usedFraction, unit: "unknown", }, status: usageStatus(usedFraction), }; } function buildLimits(config: XaiBillingConfig, accountId: string | undefined): UsageLimit[] { if (config.kind === "weekly") { const window = buildPeriodWindow(config.currentPeriod); const scope = { provider: PROVIDER_ID, ...(accountId ? { accountId } : {}), windowId: window.id, shared: true as const, }; const overall = buildPercentAmount(config.creditUsagePercent); const limits: UsageLimit[] = [ { id: `${PROVIDER_ID}:credits:1w`, label: "SuperGrok Weekly Credits", scope, window, amount: overall, status: usageStatus(overall.usedFraction ?? 0), }, ]; for (const item of config.productUsage) { const amount = buildPercentAmount(item.usagePercent); const slug = slugifyProduct(item.product); if (!slug) continue; limits.push({ id: `${PROVIDER_ID}:product:${slug}:1w`, label: `${item.product === "GrokBuild" ? "Grok Build" : item.product === "Api" ? "API" : item.product} (Weekly)`, scope, window, amount, status: usageStatus(amount.usedFraction ?? 0), }); } const onDemand = buildOnDemandLimit(config.onDemandCap, config.onDemandUsed, accountId); if (onDemand) limits.push(onDemand); return limits; } const window = buildMonthlyWindow(config.periodStart, config.periodEnd); if (!window) return []; const usedFraction = Math.min(config.used / config.limit, 1); const limits: UsageLimit[] = [ { id: `${PROVIDER_ID}:included:1mo`, label: "SuperGrok Monthly Included", scope: { provider: PROVIDER_ID, ...(accountId ? { accountId } : {}), windowId: window.id, shared: true, }, window, amount: { used: config.used, limit: config.limit, remaining: Math.max(0, config.limit - config.used), usedFraction, remainingFraction: 1 - usedFraction, // xAI does not label the unit; amounts match the dashboard quota points. unit: "unknown", }, status: usageStatus(usedFraction), }, ]; const onDemand = buildOnDemandLimit(config.onDemandCap, config.onDemandUsed, accountId); if (onDemand) limits.push(onDemand); return limits; } async function fetchBillingPayload( url: string, accessToken: string, ctx: UsageFetchContext, signal: AbortSignal | undefined, ): Promise { try { const response = await ctx.fetch(url, { headers: getXAICliBillingHeaders({ accessToken }), redirect: "error", signal, }); if (!response.ok) return null; return await response.json(); } catch { return null; } } export const xaiOauthUsageProvider: UsageProvider = { id: PROVIDER_ID, supports(params: UsageFetchParams): boolean { return params.provider === PROVIDER_ID && params.credential.type === "oauth" && !!params.credential.accessToken; }, async fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise { if (params.provider !== PROVIDER_ID || params.credential.type !== "oauth") return null; const accessToken = params.credential.accessToken?.trim(); if (!accessToken) return null; if (params.credential.expiresAt !== undefined && params.credential.expiresAt <= Date.now()) return null; let accountId = params.credential.accountId?.trim() || extractXAIAccessTokenSubject(accessToken); let email = params.credential.email?.trim().toLowerCase(); if (!email) { try { const identity = await fetchXAIOAuthIdentity(accessToken, ctx.fetch, params.signal); email = identity?.email?.trim().toLowerCase() || undefined; accountId ??= identity?.accountId?.trim() || undefined; } catch { // Identity enrichment is best effort; billing remains authoritative. } } // Always probe weekly credits first (legacy SuperGrok shape). const creditsUrl = buildXAICliBillingUrl(); const monthlyUrl = buildXAICliBillingUrl(""); const creditsPayload = await fetchBillingPayload(creditsUrl, accessToken, ctx, params.signal); const weekly = creditsPayload && isRecord(creditsPayload) && isRecord(creditsPayload.config) ? parseWeeklyBillingConfig(creditsPayload.config) : null; const creditsLooksUnified = !!creditsPayload && isRecord(creditsPayload) && isRecord(creditsPayload.config) && creditsPayload.config.isUnifiedBillingUser === true; // Unified accounts expose a separate monthly included-quota payload on the // default billing URL. Fetch it when credits is missing/unusable, or when // credits itself marks the account unified (even if weekly percents exist — // live responses sometimes include both shapes). let monthlyPayload: unknown | null = null; let monthly: XaiMonthlyBillingConfig | null = null; const shouldProbeMonthly = (!weekly || creditsLooksUnified) && monthlyUrl !== creditsUrl; if (shouldProbeMonthly) { monthlyPayload = await fetchBillingPayload(monthlyUrl, accessToken, ctx, params.signal); monthly = monthlyPayload && isRecord(monthlyPayload) && isRecord(monthlyPayload.config) ? parseMonthlyBillingConfig(monthlyPayload.config) : null; } // A positive monthly quota normally wins over an inferred weekly percentage. // If that monthly counter is already over its limit while weekly credits // have an active period, the two shapes cannot establish which one gates // requests. Keep the counter visible but do not use it for dispatch. // A failed monthly fetch still rejects inferred weekly usage so // AuthStorage can retain its last good snapshot. let effectiveWeekly = weekly; if (weekly?.inferredPercent && creditsLooksUnified) { if (monthly) { effectiveWeekly = null; } else { const monthlyConfig = monthlyPayload && isRecord(monthlyPayload) && isRecord(monthlyPayload.config) ? monthlyPayload.config : null; if (!monthlyConfig || !confirmsNoMonthlyQuota(monthlyConfig)) { effectiveWeekly = null; } } } const monthlyQuotaAdvisory = weekly?.inferredPercent === true && creditsLooksUnified && monthly !== null && monthly.used >= monthly.limit; if (!effectiveWeekly && !monthly) return null; const limits: UsageLimit[] = []; if (effectiveWeekly) limits.push(...buildLimits(effectiveWeekly, accountId)); if (monthly) { const monthlyLimits = buildLimits(monthly, accountId); if (monthlyQuotaAdvisory && monthlyLimits[0]) { monthlyLimits[0].status = "unknown"; monthlyLimits[0].notes = [ "Monthly counter exceeds its limit, but active weekly credits leave enforcement uncertain.", ]; } limits.push(...monthlyLimits); } // Deduplicate on-demand if both shapes carried the same cap (keep first). const seen = new Set(); const deduped = limits.filter(limit => { if (seen.has(limit.id)) return false; seen.add(limit.id); return true; }); if (deduped.length === 0) return null; const billingKind = effectiveWeekly && monthly ? "unified" : effectiveWeekly ? "weekly" : "monthly"; const endpoint = effectiveWeekly && monthly ? `${creditsUrl} + ${monthlyUrl}` : effectiveWeekly ? creditsUrl : monthlyUrl; const raw = effectiveWeekly && monthly ? { credits: creditsPayload, monthly: monthlyPayload } : effectiveWeekly ? creditsPayload : monthlyPayload; return { provider: PROVIDER_ID, fetchedAt: Date.now(), limits: deduped, metadata: { endpoint, source: BILLING_SOURCE, billingKind, ...(monthlyQuotaAdvisory ? { monthlyQuotaAdvisory: true } : {}), ...(accountId ? { accountId } : {}), ...(email ? { email } : {}), }, raw, }; }, }; /** * Ranks SuperGrok accounts by weekly credits (or unified monthly included quota). * xAI reports no short window, so the meter maps to `secondary`, which drives drain ranking. */ export const xaiOauthRankingStrategy: CredentialRankingStrategy = { scopeLimits(report) { if (report.metadata?.monthlyQuotaAdvisory === true) return []; // Spent credits/included quota keeps serving on the on-demand cap; only hard-block // the credential once no on-demand headroom remains. const onDemand = report.limits.find(limit => limit.id === `${PROVIDER_ID}:on-demand`); if (onDemand && !isUsageLimitExhausted(onDemand)) return []; return report.limits.filter( limit => limit.id === `${PROVIDER_ID}:credits:1w` || limit.id === `${PROVIDER_ID}:included:1mo`, ); }, findWindowLimits(report) { if (report.metadata?.monthlyQuotaAdvisory === true) return {}; const credits = report.limits.find(limit => limit.id === `${PROVIDER_ID}:credits:1w`); const included = report.limits.find(limit => limit.id === `${PROVIDER_ID}:included:1mo`); return { secondary: credits ?? included, }; }, windowDefaults: { // Inert: findWindowLimits never reports a primary window. primaryMs: 5 * HOUR_MS, secondaryMs: WEEK_MS, }, };