UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

365 lines (364 loc) • 14.7 kB
/** * Reads token usage out of Grok Build's own session streams. * * Grok Build is xAI's official terminal coding agent (github.com/xai-org/ * grok-build, Rust, installed from x.ai/cli/install.sh as `grok`). An earlier * note in this folder said "grok" named no product, only eight competing npm * packages; it had searched npm for a Rust binary that is not distributed * there, and is retracted here. * * Layout, confirmed by running the real binary against a redirected home: * `$GROK_HOME/sessions/<url-encoded cwd>/<session-id>/`, defaulting to * `~/.grok`. Each session directory holds `updates.jsonl` — the CLI's own * documentation calls it the authoritative conversation log — plus * `summary.json`, `chat_history.jsonl`, `signals.json` and others. The walk * here reads exactly `<group>/<session>/updates.jsonl` and nothing deeper: * a session directory can also hold `compaction_checkpoints/`, and a * checkpoint that snapshots the stream would otherwise be counted twice. * * Each line is a JSON-RPC style record. The ones that matter are * `method: "_x.ai/session/update"` with `params.update.sessionUpdate === * "turn_completed"`; a completed turn that reached a model carries a `usage` * object with `inputTokens`, `outputTokens`, `cachedReadTokens`, * `cacheCreationTokens`, `reasoningTokens`, `modelCalls`, a per-model * `modelUsage` map and `numTurns`. A turn that failed before any model call * carries no `usage` at all, so it excludes itself. `stop_reason` is not an * eligibility test: a truncated or interrupted turn with a usage object was * still billed. A `usage` that is present but is not that ledger (empty, an * array, none of the counters) is a changed format: it is reported, not billed * as a request of zero tokens. * * What the numbers MEAN took a second real turn to settle, and the answer is * neither "per turn" nor "cumulative" but both, by process: * * turn 1 (fresh process) inputTokens 10141 numTurns 1 * turn 2 (`grok -r`, new one) inputTokens 11034 numTurns 1 * * The second record is not 21175, so the usage is not a session-lifetime * counter. But the CLI's own persistence code says the figure is the * process's live ledger, and computes a turn's cost as live minus the * previous live value when the ledger has only grown — a running total within * one process, reset when a new process resumes the session. A reader has no * process boundary to look at; what it has is `numTurns`, the ledger's own * turn counter. Strictly increasing, with every bucket at least as large, * means the same ledger, and the turn's usage is the difference from the * previous record. Anything else means a fresh ledger, and the whole record * counts. Both real records above have numTurns 1, so both count whole, which * is the measured truth. The in-process cumulative branch is taken from the * CLI's source, not from a measurement — headless `-p` runs are one prompt * per process, so no real stream on this machine exercises it. * * Duplicates: a prompt's terminal record can be re-emitted (the persistence * layer folds a late re-emission into the same turn). Dedup is by * `prompt_id`, scoped to the session directory — prompt ids are not * documented as globally unique — keeping the last record seen. A record * with no prompt id is keyed by its line. * * Cost is `unavailable`: the turn record carries no price, and Grok Build is * a subscription product whose custom-model configurations can point at any * OpenAI- or Anthropic-compatible endpoint — the real run here used DeepSeek. * Cache and reasoning counts are reported as stored; every real sample so far * has them at zero, so whether `cachedReadTokens` is a subset of * `inputTokens` has not been measured and nothing is subtracted or folded * until it has. Reasoning tokens in particular are NOT added to output: on the * OpenAI wire they are a subset of completion tokens, and adding them would * double count. * * Not read: the newer per-session `usage.json` the CLI writes from the same * ledger. No real run on this machine produced one — the 1.0.13 binary's * `grok usage` reports "No usage recorded" for these sessions — and a reader * written against a struct definition alone is the guessed-format failure * this folder keeps paying for. When a real file exists it belongs here as a * fallback for a session with no readable stream, never as an addition. */ import { createReadStream } from "fs"; import { createInterface } from "readline"; import { readdir, stat } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; import { resolveScanCutoffMs } from "./scanWindow.js"; const CLI_ID = "grok"; function grokHome() { const env = process.env.GROK_HOME; return env !== undefined && env.trim().length > 0 ? env : join(homedir(), ".grok"); } function sessionsRoot() { return join(grokHome(), "sessions"); } function emptyTotals() { return { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, costConfidence: "unavailable", unpricedRequests: 0, unpricedModels: [], }; } /** A finite, non-negative safe integer, or 0. */ function count(value) { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0; } function isMissing(error) { return (typeof error === "object" && error !== null && error.code === "ENOENT"); } /** * Every `<group>/<session>/updates.jsonl`, exactly two levels down. Symlinks * are not followed: a linked session directory is either a duplicate of one * already in the tree or something outside it, and neither should be read. */ async function collectStreams(root, errors) { let groups; try { groups = await readdir(root, { withFileTypes: true }); } catch (error) { if (!isMissing(error)) { errors.push({ cliId: CLI_ID, filePath: root, message: error instanceof Error ? error.message : String(error), }); } return []; } const out = []; for (const group of groups) { if (!group.isDirectory()) { continue; } const groupDir = join(root, group.name); let sessions; try { sessions = await readdir(groupDir, { withFileTypes: true }); } catch (error) { if (!isMissing(error)) { errors.push({ cliId: CLI_ID, filePath: groupDir, message: error instanceof Error ? error.message : String(error), }); } continue; } for (const session of sessions) { if (!session.isDirectory()) { continue; } const stream = join(groupDir, session.name, "updates.jsonl"); try { if ((await stat(stream)).isFile()) { out.push(stream); } } catch { // A session directory with no stream yet. } } } return out; } function readTurn(usage) { const models = typeof usage.modelUsage === "object" && usage.modelUsage !== null ? Object.keys(usage.modelUsage).filter((k) => k.length > 0) : []; return { input: count(usage.inputTokens), output: count(usage.outputTokens), cacheRead: count(usage.cachedReadTokens), cacheCreation: count(usage.cacheCreationTokens), calls: count(usage.modelCalls), turns: count(usage.numTurns), models, }; } /** Whether `next` is the same process ledger as `prev`, grown by a turn. */ function continuesRun(prev, next) { return (next.turns > prev.turns && next.input >= prev.input && next.output >= prev.output && next.cacheRead >= prev.cacheRead && next.cacheCreation >= prev.cacheCreation && next.calls >= prev.calls); } const USAGE_COUNTERS = ["inputTokens", "outputTokens", "modelCalls"]; /** * Whether `usage` is shaped like the ledger Grok writes: an object, not an * array, with at least one of the counters a turn that reached a model always * carries. `count()` reads any missing field as 0, so without this a `{}`, a * `[]` or the same counters under renamed keys passed as a turn of zero tokens * and was billed as one request. */ function isTurnUsage(usage) { if (typeof usage !== "object" || usage === null || Array.isArray(usage)) { return false; } const record = usage; // `1e999` parses as Infinity, which is a number but not a count: `count()` // reads it as 0, so one such field made a malformed turn a zero-token request. const counters = USAGE_COUNTERS.map((key) => record[key]).filter((value) => typeof value === "number"); return counters.length > 0 && counters.every(Number.isFinite); } function completedTurn(parsed) { if (typeof parsed !== "object" || parsed === null) { return null; } const record = parsed; if (record.method !== "_x.ai/session/update") { return null; } if (typeof record.params !== "object" || record.params === null) { return null; } const update = record.params.update; if (typeof update !== "object" || update === null) { return null; } const u = update; if (u.sessionUpdate !== "turn_completed") { return null; } if (u.usage === undefined || u.usage === null) { // Completed without reaching a model — an error before the first call. return null; } return { promptId: typeof u.prompt_id === "string" ? u.prompt_id : undefined, usage: u.usage, }; } async function foldStream(filePath, totals, unpriced, errors) { // Insertion-ordered, so the delta rule below sees turns in the order Grok // wrote them, with a re-emitted prompt replacing its earlier record. const turns = new Map(); let unrecognised = 0; const rl = createInterface({ input: createReadStream(filePath, { encoding: "utf8" }), crlfDelay: Infinity, }); try { let lineNo = 0; for await (const line of rl) { lineNo += 1; if (!line || line.charCodeAt(0) !== 123 /* '{' */) { continue; } let parsed; try { parsed = JSON.parse(line); } catch { // A stream being appended to ends in a partial line — normal. continue; } const turn = completedTurn(parsed); if (turn === null) { continue; } if (!isTurnUsage(turn.usage)) { unrecognised += 1; // The later record for a prompt replaces the earlier one, so one that // cannot be read must not leave the earlier reading in the totals. if (turn.promptId !== undefined) { turns.delete(turn.promptId); } continue; } turns.set(turn.promptId ?? `line:${lineNo}`, readTurn(turn.usage)); } } finally { rl.close(); } if (unrecognised > 0) { // One entry per stream: a changed format hits every turn in the file, and // an entry each would bury everything else a scan has to report. errors.push({ cliId: CLI_ID, filePath, message: `${unrecognised} completed turn(s) carried a usage record with no recognisable token or call counter — Grok Build's stream format has likely changed`, }); } let prev; for (const turn of turns.values()) { const delta = prev !== undefined && continuesRun(prev, turn) ? { input: turn.input - prev.input, output: turn.output - prev.output, cacheRead: turn.cacheRead - prev.cacheRead, cacheCreation: turn.cacheCreation - prev.cacheCreation, calls: turn.calls - prev.calls, } : turn; prev = turn; const calls = delta.calls > 0 ? delta.calls : 1; totals.requests += calls; totals.inputTokens += delta.input; totals.outputTokens += delta.output; totals.cacheReadTokens += delta.cacheRead; totals.cacheCreationTokens += delta.cacheCreation; totals.unpricedRequests += calls; for (const model of turn.models.length > 0 ? turn.models : ["unknown"]) { unpriced.add(model); } } } export async function createGrokReader() { return { descriptor: { id: CLI_ID, displayName: "Grok Build", verified: true, // Per prompt id within a session, last record wins — a re-emitted // terminal replaces the earlier one rather than adding to it. dedupStrategy: "last-write-wins", costConfidence: "unavailable", requiresSqlite: false, }, detect: async () => { try { return (await stat(sessionsRoot())).isDirectory(); } catch { return false; } }, scan: async (options) => { const totals = emptyTotals(); const errors = []; const unpriced = new Set(); const cutoff = resolveScanCutoffMs(options?.sinceDays); let filesScanned = 0; for (const file of await collectStreams(sessionsRoot(), errors)) { try { // One stream is one session, and its mtime is the session's last // activity: a session is in or out of the window whole. if (cutoff !== undefined && (await stat(file)).mtimeMs < cutoff) { continue; } await foldStream(file, totals, unpriced, errors); filesScanned += 1; } catch (error) { errors.push({ cliId: CLI_ID, filePath: file, message: error instanceof Error ? error.message : String(error), }); } } totals.unpricedModels = [...unpriced].sort(); return { cliId: CLI_ID, totals, filesScanned, errors }; }, }; }