diff --git a/packages/ui/src/components/sections/usage/UsagePage.tsx b/packages/ui/src/components/sections/usage/UsagePage.tsx index 4a83e8b7..d1e5a412 100644 --- a/packages/ui/src/components/sections/usage/UsagePage.tsx +++ b/packages/ui/src/components/sections/usage/UsagePage.tsx @@ -76,6 +76,9 @@ export const UsagePage: React.FC = () => { const providerMeta = QUOTA_PROVIDERS.find((provider) => provider.id === selectedProviderId); const providerName = providerMeta?.name ?? selectedProviderId ?? t('settings.usage.sidebar.title'); const usage = selectedResult?.usage; + const selectedProviderError = selectedResult?.configured && !selectedResult.ok + ? selectedResult.error + : null; const showInDropdown = selectedProviderId ? dropdownProviderIds.includes(selectedProviderId) : false; const hasCredentialsForm = selectedProviderId === 'opencode-go' || selectedProviderId === 'ollama-cloud' || selectedProviderId === 'cursor'; const handleDropdownToggle = React.useCallback((enabled: boolean) => { @@ -179,10 +182,10 @@ export const UsagePage: React.FC = () => {

{t('settings.usage.page.state.noData')}

)} - {error && ( + {(error || selectedProviderError) && (

{t('settings.usage.page.state.refreshFailedTitle')}

-

{error}

+

{error ?? selectedProviderError}

)} diff --git a/packages/ui/src/lib/quota/providers/index.ts b/packages/ui/src/lib/quota/providers/index.ts index 5d1798c3..96a4906c 100644 --- a/packages/ui/src/lib/quota/providers/index.ts +++ b/packages/ui/src/lib/quota/providers/index.ts @@ -24,4 +24,5 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [ { id: 'crof', name: 'CrofAI' }, { id: 'deepseek', name: 'DeepSeek' }, { id: 'neuralwatt', name: 'NeuralWatt' }, + { id: 'xai', name: 'xAI' }, ]; diff --git a/packages/ui/src/types/quota.ts b/packages/ui/src/types/quota.ts index 365f588d..fc634a78 100644 --- a/packages/ui/src/types/quota.ts +++ b/packages/ui/src/types/quota.ts @@ -18,7 +18,8 @@ export type QuotaProviderId = | 'opencode-go' | 'crof' | 'deepseek' - | 'neuralwatt'; + | 'neuralwatt' + | 'xai'; export interface UsageWindow { usedPercent: number | null; diff --git a/packages/vscode/src/opencodeAuth.ts b/packages/vscode/src/opencodeAuth.ts index cbac7e14..06c2c40e 100644 --- a/packages/vscode/src/opencodeAuth.ts +++ b/packages/vscode/src/opencodeAuth.ts @@ -5,7 +5,7 @@ import os from 'node:os'; const OPENCODE_DATA_DIR = path.join(os.homedir(), '.local', 'share', 'opencode'); const AUTH_FILE = path.join(OPENCODE_DATA_DIR, 'auth.json'); -type AuthEntry = Record; +export type AuthEntry = Record; type AuthFile = Record; const readAuthFile = (): AuthFile => { @@ -46,6 +46,16 @@ const writeAuthFile = (auth: AuthFile): void => { } }; +export const updateProviderAuth = (providerId: string, entry: AuthEntry): void => { + if (!providerId || typeof providerId !== 'string') { + throw new Error('Provider ID is required'); + } + + const auth = readAuthFile(); + auth[providerId] = entry; + writeAuthFile(auth); +}; + export const removeProviderAuth = (providerId: string): boolean => { if (!providerId || typeof providerId !== 'string') { throw new Error('Provider ID is required'); diff --git a/packages/vscode/src/quotaProviders.ts b/packages/vscode/src/quotaProviders.ts index f759e351..7aa3bd73 100644 --- a/packages/vscode/src/quotaProviders.ts +++ b/packages/vscode/src/quotaProviders.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import os from 'node:os'; import { fetchOpenCodeGoUsage } from './opencodeGoQuota'; import { readCredential } from './quotaCredentials'; +import { getProviderAuth, updateProviderAuth } from './opencodeAuth'; type AuthEntry = Record | string; type AuthFile = Record; @@ -174,6 +175,21 @@ const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode'); const OPENCODE_DATA_DIR = path.join(os.homedir(), '.local', 'share', 'opencode'); const AUTH_FILE = path.join(OPENCODE_DATA_DIR, 'auth.json'); +const XAI_USAGE_ENDPOINT = 'https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig'; +const XAI_TOKEN_ENDPOINT = 'https://auth.x.ai/oauth2/token'; +const XAI_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828'; +const XAI_REFRESH_SKEW_MS = 120_000; +const XAI_DEFAULT_EXPIRES_IN_SECONDS = 3600; + +type XaiAuthEntry = Record & { + type: 'oauth'; + access?: string; + refresh?: string; + expires?: unknown; +}; + +let xaiRefreshPromise: Promise | null = null; + const ANTIGRAVITY_ACCOUNTS_PATHS = [ path.join(OPENCODE_CONFIG_DIR, 'antigravity-accounts.json'), @@ -403,6 +419,304 @@ const buildResult = (data: { fetchedAt: Date.now(), }); +const resolveXaiAuth = (): XaiAuthEntry | null => { + const entry = getProviderAuth('xai'); + if (!entry || typeof entry !== 'object' || entry.type !== 'oauth') return null; + + const access = asNonEmptyString(entry.access); + const refresh = asNonEmptyString(entry.refresh); + if (!access && !refresh) return null; + + return { + ...entry, + type: 'oauth', + ...(access ? { access } : {}), + ...(refresh ? { refresh } : {}), + ...(entry.expires !== undefined ? { expires: entry.expires } : {}), + }; +}; + +const jwtExpiryMilliseconds = (accessToken: string): number | null => { + const payload = accessToken.split('.')[1]; + if (!payload) return null; + + try { + const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record; + return typeof decoded.exp === 'number' && Number.isFinite(decoded.exp) + ? decoded.exp * 1000 + : null; + } catch { + return null; + } +}; + +const xaiAccessNeedsRefresh = (entry: XaiAuthEntry, now = Date.now()): boolean => { + const access = asNonEmptyString(entry.access); + if (!access) return true; + + const refreshDeadline = now + XAI_REFRESH_SKEW_MS; + const storedExpiry = Number(entry.expires); + if (Number.isFinite(storedExpiry) && storedExpiry <= refreshDeadline) { + return true; + } + + const jwtExpiry = jwtExpiryMilliseconds(access); + return jwtExpiry !== null && jwtExpiry <= refreshDeadline; +}; + +const refreshXaiAuth = (entry: XaiAuthEntry): Promise => { + if (xaiRefreshPromise) return xaiRefreshPromise; + + const refreshToken = asNonEmptyString(entry.refresh); + if (!refreshToken) { + return Promise.reject(new Error('xAI OAuth refresh token is unavailable')); + } + + const pending = (async () => { + const response = await fetch(XAI_TOKEN_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: XAI_CLIENT_ID, + refresh_token: refreshToken, + grant_type: 'refresh_token', + }), + signal: AbortSignal.timeout(15_000), + }); + + const payload = await response.json().catch(() => null) as Record | null; + if (!response.ok) { + throw new Error(`xAI OAuth refresh failed: ${response.status}`); + } + + const responsePayload = payload ?? {}; + const access = asNonEmptyString(responsePayload.access_token); + if (!access) { + throw new Error('xAI OAuth refresh returned no access token'); + } + + const expiresIn = responsePayload.expires_in ?? XAI_DEFAULT_EXPIRES_IN_SECONDS; + if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn)) { + throw new Error('xAI OAuth refresh returned an invalid expiry'); + } + + const refreshed: XaiAuthEntry = { + ...entry, + type: 'oauth', + access, + refresh: asNonEmptyString(responsePayload.refresh_token) ?? refreshToken, + expires: Date.now() + expiresIn * 1000, + }; + + // Validate the new access token before updating the existing secure auth file. + updateProviderAuth('xai', refreshed); + return refreshed; + })(); + + const settled = pending.finally(() => { + if (xaiRefreshPromise === settled) { + xaiRefreshPromise = null; + } + }); + xaiRefreshPromise = settled; + return settled; +}; + +const getXaiAccessToken = async (entry: XaiAuthEntry): Promise => { + if (!xaiAccessNeedsRefresh(entry)) return entry.access!; + return (await refreshXaiAuth(entry)).access!; +}; + +type XaiFixed32Field = { path: number[]; value: number; order: number }; +type XaiVarintField = { path: number[]; value: bigint }; +type XaiProtobufScan = { + fixed32Fields: XaiFixed32Field[]; + varintFields: XaiVarintField[]; + nextOrder: number; +}; + +const readXaiVarint = (bytes: Uint8Array, index: { value: number }): bigint | null => { + let result = 0n; + for (let shift = 0n; index.value < bytes.length && shift < 64n; shift += 7n) { + const byte = bytes[index.value++]; + if (shift === 63n && (byte & 0x7e) !== 0) return null; + result |= BigInt(byte & 0x7f) << shift; + if ((byte & 0x80) === 0) return result; + } + return null; +}; + +const scanXaiProtobuf = ( + bytes: Uint8Array, + depth: number, + pathPrefix: number[], + scan: XaiProtobufScan, +): boolean => { + const index = { value: 0 }; + while (index.value < bytes.length) { + const fieldKey = readXaiVarint(bytes, index); + if (fieldKey === null || fieldKey === 0n) return false; + + const fieldNumber = Number(fieldKey >> 3n); + const wireType = Number(fieldKey & 0x07n); + if (fieldNumber < 1 || fieldNumber > 0x1fffffff) return false; + const fieldPath = [...pathPrefix, fieldNumber]; + + if (wireType === 0) { + const value = readXaiVarint(bytes, index); + if (value === null) return false; + scan.varintFields.push({ path: fieldPath, value }); + continue; + } + + if (wireType === 1) { + if (index.value + 8 > bytes.length) return false; + index.value += 8; + continue; + } + + if (wireType === 2) { + const length = readXaiVarint(bytes, index); + if (length === null || length > BigInt(bytes.length - index.value)) return false; + const start = index.value; + index.value += Number(length); + if (depth >= 4 && length !== 0n) return false; + if (depth < 4) { + const nestedScan: XaiProtobufScan = { + fixed32Fields: [], + varintFields: [], + nextOrder: scan.nextOrder, + }; + if (!scanXaiProtobuf(bytes.subarray(start, index.value), depth + 1, fieldPath, nestedScan)) return false; + scan.fixed32Fields.push(...nestedScan.fixed32Fields); + scan.varintFields.push(...nestedScan.varintFields); + scan.nextOrder = nestedScan.nextOrder; + } + continue; + } + + if (wireType === 5) { + if (index.value + 4 > bytes.length) return false; + const value = new DataView(bytes.buffer, bytes.byteOffset + index.value, 4).getFloat32(0, true); + scan.fixed32Fields.push({ path: fieldPath, value, order: scan.nextOrder++ }); + index.value += 4; + continue; + } + + return false; + } + + return true; +}; + +const looksLikeXaiProtobuf = (bytes: Uint8Array): boolean => { + if (!bytes.length) return false; + const fieldNumber = Math.floor(bytes[0] / 8); + const wireType = bytes[0] % 8; + return fieldNumber > 0 && [0, 1, 2, 5].includes(wireType); +}; + +const parseXaiGrpcTrailerStatus = (bytes: Uint8Array): number | null => { + let text: string; + try { + text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); + } catch { + return null; + } + + let status: number | null = null; + for (const line of text.split(/\r?\n/)) { + if (!line) continue; + const separator = line.indexOf(':'); + if (separator <= 0) return null; + const key = line.slice(0, separator).trim().toLowerCase(); + if (!key) return null; + if (key !== 'grpc-status') continue; + if (status !== null) return null; + const rawStatus = line.slice(separator + 1).trim(); + if (!/^\d+$/.test(rawStatus)) return null; + status = Number(rawStatus); + if (!Number.isSafeInteger(status)) return null; + } + return status; +}; + +const parseXaiGrpcFrames = (bytes: Uint8Array): { payloads: Uint8Array[]; trailerStatuses: number[] } | null | false => { + if (bytes.length < 5 || (bytes[0] & 0x7f) !== 0) return null; + const payloads: Uint8Array[] = []; + const trailerStatuses: number[] = []; + let index = 0; + let sawTrailer = false; + while (index < bytes.length) { + if (index + 5 > bytes.length) return false; + const flags = bytes[index++]; + if ((flags & 0x7f) !== 0) return false; + const length = (bytes[index++] * 0x1000000) + (bytes[index++] << 16) + (bytes[index++] << 8) + bytes[index++]; + if (length > bytes.length - index) return false; + const frame = bytes.subarray(index, index + length); + index += length; + if (flags & 0x80) { + sawTrailer = true; + const status = parseXaiGrpcTrailerStatus(frame); + if (status === null) return false; + trailerStatuses.push(status); + } else { + if (sawTrailer) return false; + payloads.push(frame); + } + } + return { payloads, trailerStatuses }; +}; + +const sameXaiPath = (left: number[], right: number[]) => ( + left.length === right.length && left.every((value, index) => value === right[index]) +); + +const XAI_USAGE_PERCENT_PATHS = [[1], [1, 1]]; +const isXaiUsagePercentPath = (path: number[]) => ( + XAI_USAGE_PERCENT_PATHS.some((candidate) => sameXaiPath(candidate, path)) +); + +const parseXaiUsage = (bytes: Uint8Array): { usedPercent: number; resetAt: number | null } => { + const frames = parseXaiGrpcFrames(bytes); + if (frames === false) throw new Error('xAI returned malformed gRPC-web framing'); + const payloads = frames === null + ? (looksLikeXaiProtobuf(bytes) ? [bytes] : []) + : frames.payloads; + if (frames) { + for (const status of frames.trailerStatuses) { + if (status !== 0) throw new Error(`xAI billing RPC failed with status ${status}`); + } + } + if (!payloads.length) throw new Error('xAI returned an empty protobuf response'); + + const scan: XaiProtobufScan = { fixed32Fields: [], varintFields: [], nextOrder: 0 }; + for (const payload of payloads) { + if (!scanXaiProtobuf(payload, 0, [], scan)) throw new Error('xAI returned malformed protobuf data'); + } + + const percentField = scan.fixed32Fields + .filter((field) => isXaiUsagePercentPath(field.path) && Number.isFinite(field.value) && field.value >= 0 && field.value <= 100) + .sort((left, right) => left.path.length - right.path.length || left.order - right.order)[0]; + const resetCandidates = scan.varintFields + .filter((field) => field.value >= 1_700_000_000n && field.value <= 2_100_000_000n) + .map((field) => ({ path: field.path, seconds: Number(field.value) })) + .map((field) => ({ path: field.path, timestamp: field.seconds * 1000 })) + .filter((field) => field.timestamp > Date.now()); + const preferredReset = resetCandidates + .filter((field) => sameXaiPath(field.path, [1, 5, 1])) + .sort((a, b) => a.timestamp - b.timestamp)[0]; + const resetAt = (preferredReset ?? resetCandidates.sort((a, b) => a.timestamp - b.timestamp)[0])?.timestamp ?? null; + const hasUsagePeriod = scan.varintFields.some((field) => ( + (field.path.length >= 2 && field.path[0] === 1 && field.path[1] === 6) + || (sameXaiPath(field.path, [1, 8, 1]) && (field.value === 1n || field.value === 2n)) + )); + const noUsageYet = !percentField && scan.fixed32Fields.length === 0 && resetAt !== null && hasUsagePeriod; + const usedPercent = percentField?.value ?? (noUsageYet ? 0 : null); + if (usedPercent === null) throw new Error('xAI billing response did not contain usable current-period data'); + return { usedPercent, resetAt }; +}; + const formatMoney = (value: number | null) => { if (value === null || !Number.isFinite(value)) return null; return value.toFixed(2); @@ -425,7 +739,12 @@ const durationToSeconds = (duration?: number, unit?: string) => { }; export const listConfiguredQuotaProviders = () => { - const auth = readAuthFile(); + let auth: AuthFile = {}; + try { + auth = readAuthFile(); + } catch { + // Managed credentials remain enumerable; unreadable auth cannot establish xAI configuration. + } const configured = new Set(); if (readCredential('opencode-go')) configured.add('opencode-go'); if (readCredential('ollama-cloud')) configured.add('ollama-cloud'); @@ -507,6 +826,16 @@ export const listConfiguredQuotaProviders = () => { configured.add('deepseek'); } + let xaiAuth: XaiAuthEntry | null = null; + try { + xaiAuth = resolveXaiAuth(); + } catch { + xaiAuth = null; + } + if (xaiAuth) { + configured.add('xai'); + } + return Array.from(configured); }; @@ -2303,6 +2632,78 @@ const fetchDeepseekQuota = async (): Promise => { } }; +const fetchXaiQuota = async (): Promise => { + try { + const entry = resolveXaiAuth(); + if (!entry) { + return buildResult({ + providerId: 'xai', + providerName: 'xAI', + ok: false, + configured: false, + error: 'Not configured', + }); + } + + const accessToken = await getXaiAccessToken(entry); + const response = await fetch(XAI_USAGE_ENDPOINT, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + Origin: 'https://grok.com', + Referer: 'https://grok.com/?_s=usage', + Accept: '*/*', + 'Content-Type': 'application/grpc-web+proto', + 'x-grpc-web': '1', + 'x-user-agent': 'connect-es/2.1.1', + 'User-Agent': 'OpenChamber', + }, + body: new Uint8Array([0, 0, 0, 0, 0]), + signal: AbortSignal.timeout(15_000), + }); + + const grpcStatus = response.headers.get('grpc-status'); + if (grpcStatus !== null) { + const rawStatus = grpcStatus.trim(); + if (!/^\d+$/.test(rawStatus)) throw new Error('xAI billing returned malformed gRPC status'); + const status = Number(rawStatus); + if (!Number.isSafeInteger(status)) throw new Error('xAI billing returned malformed gRPC status'); + if (status !== 0) { + throw new Error(`xAI billing RPC failed with status ${status}`); + } + } + + if (!response.ok) { + throw new Error(`xAI billing API error: ${response.status}`); + } + + const parsed = parseXaiUsage(new Uint8Array(await response.arrayBuffer())); + return buildResult({ + providerId: 'xai', + providerName: 'xAI', + ok: true, + configured: true, + usage: { + windows: { + billing_cycle: toUsageWindow({ + usedPercent: parsed.usedPercent, + windowSeconds: null, + resetAt: parsed.resetAt, + }), + }, + }, + }); + } catch (error) { + return buildResult({ + providerId: 'xai', + providerName: 'xAI', + ok: false, + configured: true, + error: error instanceof Error ? error.message : 'Request failed', + }); + } +}; + export const fetchQuotaForProvider = async (providerId: string): Promise => { switch (providerId) { case 'claude': @@ -2350,6 +2751,8 @@ export const fetchQuotaForProvider = async (providerId: string): Promise { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + return trimmed ? trimmed : null; +}; + +const readXaiAuth = () => { + try { + const entry = readAuthFile()?.xai; + if (!entry || typeof entry !== 'object' || entry.type !== 'oauth') { + return { entry: null, error: null }; + } + if (!nonEmptyString(entry.access) && !nonEmptyString(entry.refresh)) { + return { entry: null, error: null }; + } + return { entry, error: null }; + } catch { + return { entry: null, error: 'Failed to read xAI OAuth credentials' }; + } +}; + +const decodeJwtClaims = (token) => { + try { + const payload = token.split('.')[1]; + if (!payload) return null; + return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')); + } catch { + return null; + } +}; + +const tokenNeedsRefresh = (entry) => { + const access = nonEmptyString(entry.access); + if (!access) return true; + + const refreshDeadline = Date.now() + REFRESH_SKEW_MS; + const storedExpiry = Number(entry.expires); + if (Number.isFinite(storedExpiry) && storedExpiry <= refreshDeadline) return true; + + const jwtExpiry = Number(decodeJwtClaims(access)?.exp) * 1000; + return Number.isFinite(jwtExpiry) && jwtExpiry <= refreshDeadline; +}; + +const refreshXaiOauth = async (entry) => { + if (!refreshPromise) { + refreshPromise = (async () => { + const refreshToken = nonEmptyString(entry.refresh); + if (!refreshToken) { + throw new Error('xAI OAuth entry has no usable refresh token'); + } + + const response = await fetch(TOKEN_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + refresh_token: refreshToken, + grant_type: 'refresh_token' + }), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) + }); + + if (!response.ok) { + throw new Error(`xAI OAuth refresh failed with HTTP ${response.status}`); + } + + let payload; + try { + payload = await response.json(); + } catch { + throw new Error('xAI OAuth refresh returned invalid JSON'); + } + + const access = nonEmptyString(payload?.access_token); + if (!access) { + throw new Error('xAI OAuth refresh returned no access token'); + } + + const expiresIn = payload?.expires_in ?? 3600; + if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn)) { + throw new Error('xAI OAuth refresh returned an invalid expiry'); + } + const expires = Date.now() + expiresIn * 1000; + const refreshed = { + ...entry, + type: 'oauth', + access, + refresh: nonEmptyString(payload?.refresh_token) ?? refreshToken, + expires + }; + + const auth = readAuthFile(); + auth.xai = refreshed; + writeAuthFile(auth); + return refreshed; + })().finally(() => { + refreshPromise = null; + }); + } + + return refreshPromise; +}; + +const ensureFreshAccess = async (entry) => { + if (!tokenNeedsRefresh(entry)) return entry; + if (!nonEmptyString(entry.refresh)) { + throw new Error('xAI OAuth access token is expired and has no usable refresh token'); + } + return refreshXaiOauth(entry); +}; + +const readVarint = (bytes, state) => { + let value = 0n; + for (let shift = 0n; state.index < bytes.length && shift < 64n; shift += 7n) { + const byte = bytes[state.index++]; + if (shift === 63n && (byte & 0x7e) !== 0) return null; + value |= BigInt(byte & 0x7f) << shift; + if ((byte & 0x80) === 0) return value; + } + return null; +}; + +const samePath = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]); +// CodexBar observes both the flat billing message and the response envelope. +const USAGE_PERCENT_PATHS = [[1], [1, 1]]; +const hasPath = (paths, candidate) => paths.some((path) => samePath(path, candidate)); + +const scanProtobuf = (bytes, path = [], depth = 0, state = { index: 0, order: 0 }) => { + const fixed32Fields = []; + const varintFields = []; + + while (state.index < bytes.length) { + const key = readVarint(bytes, state); + if (key === null || key === 0n) return false; + const fieldNumber = Number(key >> 3n); + const wireType = Number(key & 0x07n); + if (!fieldNumber || fieldNumber > 0x1fffffff) return false; + const fieldPath = [...path, fieldNumber]; + + if (wireType === 0) { + const value = readVarint(bytes, state); + if (value === null) return false; + varintFields.push({ path: fieldPath, value }); + continue; + } + + if (wireType === 1) { + if (state.index + 8 > bytes.length) return false; + state.index += 8; + continue; + } + + if (wireType === 2) { + const length = readVarint(bytes, state); + if (length === null || length > BigInt(bytes.length - state.index)) return false; + const end = state.index + Number(length); + if (depth >= 4 && length !== 0n) return false; + if (depth < 4) { + const nestedState = { index: 0, order: state.order }; + const nested = scanProtobuf(bytes.slice(state.index, end), fieldPath, depth + 1, nestedState); + if (nested === false) return false; + fixed32Fields.push(...nested.fixed32Fields); + varintFields.push(...nested.varintFields); + state.order = nestedState.order; + } + state.index = end; + continue; + } + + if (wireType === 5) { + if (state.index + 4 > bytes.length) return false; + const value = Buffer.from(bytes.slice(state.index, state.index + 4)).readFloatLE(0); + fixed32Fields.push({ path: fieldPath, value, order: state.order++ }); + state.index += 4; + continue; + } + + return false; + } + + return { fixed32Fields, varintFields }; +}; + +const parseFrames = (bytes) => { + if (bytes.length < 5 || (bytes[0] & 0x7f) !== 0) return null; + const messages = []; + const trailerStatuses = []; + let trailerStarted = false; + let index = 0; + + while (index < bytes.length) { + if (index + 5 > bytes.length) return false; + const flags = bytes[index++]; + if ((flags & 0x7f) !== 0) return false; + const isTrailer = (flags & 0x80) !== 0; + if (trailerStarted && !isTrailer) return false; + const length = (bytes[index] * 0x1000000) + + (bytes[index + 1] << 16) + + (bytes[index + 2] << 8) + + bytes[index + 3]; + index += 4; + const end = index + length; + if (end > bytes.length) return false; + const payload = bytes.slice(index, end); + if (isTrailer) { + trailerStarted = true; + const status = parseGrpcTrailerStatus(payload); + if (status === null) return false; + trailerStatuses.push(status); + } else { + messages.push(payload); + } + index = end; + } + + return { messages, trailerStatuses }; +}; + +const parseGrpcTrailerStatus = (bytes) => { + let text; + try { + text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); + } catch { + return null; + } + + let status = null; + for (const line of text.split(/\r?\n/)) { + if (!line) continue; + const separator = line.indexOf(':'); + if (separator <= 0) return null; + const key = line.slice(0, separator).trim().toLowerCase(); + if (!key) return null; + if (key !== 'grpc-status') continue; + if (status !== null) return null; + const rawStatus = line.slice(separator + 1).trim(); + if (!/^\d+$/.test(rawStatus)) return null; + status = Number(rawStatus); + if (!Number.isSafeInteger(status)) return null; + } + return status; +}; + +const looksLikeProtobuf = (bytes) => { + if (!bytes.length) return false; + const fieldNumber = bytes[0] >> 3; + const wireType = bytes[0] & 0x07; + return fieldNumber > 0 && [0, 1, 2, 5].includes(wireType); +}; + +const parseUsage = (bytes) => { + const framed = parseFrames(bytes); + if (framed === false) throw new Error('xAI billing returned malformed gRPC-web framing'); + const payloads = framed ? framed.messages : (looksLikeProtobuf(bytes) ? [bytes] : []); + if (framed) { + for (const status of framed.trailerStatuses) { + if (status !== 0) throw new Error(`xAI billing RPC failed with status ${status}`); + } + } + if (payloads.length === 0) throw new Error('xAI billing returned an empty protobuf response'); + + const scan = { fixed32Fields: [], varintFields: [] }; + for (const payload of payloads) { + const result = scanProtobuf(payload); + if (result === false) throw new Error('xAI billing returned malformed protobuf'); + scan.fixed32Fields.push(...result.fixed32Fields); + scan.varintFields.push(...result.varintFields); + } + + const percentages = scan.fixed32Fields + .filter((field) => ( + hasPath(USAGE_PERCENT_PATHS, field.path) + && Number.isFinite(field.value) + && field.value >= 0 + && field.value <= 100 + )) + .sort((left, right) => left.path.length - right.path.length || left.order - right.order); + const usedPercent = percentages.length > 0 ? percentages[0].value : null; + + const resetCandidates = scan.varintFields + .filter((field) => field.value >= 1_700_000_000n && field.value <= 2_100_000_000n) + .map((field) => ({ ...field, seconds: Number(field.value) })) + .map((field) => ({ ...field, resetAt: field.seconds * 1000 })) + .filter((field) => field.resetAt > Date.now()); + const preferredReset = resetCandidates.filter((field) => samePath(field.path, [1, 5, 1])); + const resetAt = (preferredReset.length > 0 ? preferredReset : resetCandidates) + .sort((left, right) => left.resetAt - right.resetAt)[0]?.resetAt ?? null; + const hasUsagePeriod = scan.varintFields.some((field) => ( + (field.path.length >= 2 && field.path[0] === 1 && field.path[1] === 6) + || (samePath(field.path, [1, 8, 1]) && (field.value === 1n || field.value === 2n)) + )); + + if (usedPercent === null && scan.fixed32Fields.length === 0 && resetAt !== null && hasUsagePeriod) { + return { usedPercent: 0, resetAt }; + } + if (usedPercent === null) throw new Error('xAI billing response had no usable current-period usage'); + return { usedPercent, resetAt }; +}; + +const fetchUsage = async (accessToken) => { + const response = await fetch(USAGE_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + Origin: 'https://grok.com', + Referer: 'https://grok.com/?_s=usage', + Accept: '*/*', + 'Content-Type': 'application/grpc-web+proto', + 'x-grpc-web': '1', + 'x-user-agent': 'connect-es/2.1.1', + 'User-Agent': 'OpenChamber' + }, + body: EMPTY_GRPC_WEB_BODY, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) + }); + + const headerStatus = response.headers.get('grpc-status'); + if (headerStatus !== null) { + if (!/^\d+$/.test(headerStatus.trim())) throw new Error('xAI billing returned malformed gRPC status'); + const status = Number(headerStatus.trim()); + if (!Number.isSafeInteger(status)) throw new Error('xAI billing returned malformed gRPC status'); + if (status !== 0) throw new Error(`xAI billing RPC failed with status ${status}`); + } + if (!response.ok) throw new Error(`xAI billing request failed with HTTP ${response.status}`); + return parseUsage(new Uint8Array(await response.arrayBuffer())); +}; + +export const isConfigured = () => Boolean(readXaiAuth().entry); + +export const fetchQuota = async () => { + const { entry, error: authError } = readXaiAuth(); + if (authError) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: authError + }); + } + if (!entry) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: false, + error: 'Not configured' + }); + } + + try { + const freshEntry = await ensureFreshAccess(entry); + const accessToken = nonEmptyString(freshEntry.access); + if (!accessToken) throw new Error('xAI OAuth entry has no usable access token'); + const usage = await fetchUsage(accessToken); + + return buildResult({ + providerId, + providerName, + ok: true, + configured: true, + usage: { + windows: { + billing_cycle: toUsageWindow({ + usedPercent: usage.usedPercent, + windowSeconds: null, + resetAt: usage.resetAt + }) + } + } + }); + } catch (error) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: error instanceof Error ? error.message : 'Request failed' + }); + } +};