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

227 lines (226 loc) • 9.14 kB
/** * Reads token usage out of Codex's own rollout transcripts. * * Layout: `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-<iso>-<sessionId>.jsonl`. * Each line is one of `session_meta`, `turn_context`, `response_item` or * `event_msg`; usage lives on `event_msg` records whose `payload.type` is * `token_count`. * * The counter semantics are the whole story here, and they are the opposite of * the Claude Code reader's. Every `token_count` event carries BOTH a * cumulative `total_token_usage` for the session and a per-turn * `last_token_usage`. Summing the per-turn values is the obvious move and it is * wrong: measured across all 108 sessions on a real machine, summing yields * 9,191,613,238 tokens against a true 5,653,217,442 — an overstatement of * 62.6%, and 195% on the worst single session. The per-turn value repeats * across events within a turn, so it double-counts. * * The cumulative counter is authoritative and was monotonic in all 108 * sessions — it never resets mid-file — so the last one in the file is the * session's true total. `Math.max` is still used rather than "last seen", * because a counter that is only monotonic in every case observed is not the * same as one that is guaranteed to be, and the max costs nothing. * * Cost is deliberately not computed. Codex is a ChatGPT subscription — the * rollouts carry `rate_limits.plan_type` — so a per-token dollar figure would * be an invention, not a measurement. The tokens are real; the cost is * `unavailable`. */ 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 = "codex"; function sessionsRoot() { return join(homedir(), ".codex", "sessions"); } function emptyTotals() { return { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, costConfidence: "unavailable", unpricedRequests: 0, unpricedModels: [], }; } async function collectRollouts(dir, out) { let entries; try { entries = await readdir(dir, { withFileTypes: true }); } catch { // One unreadable date directory must not cost the rest of the scan. return; } for (const entry of entries) { const full = join(dir, entry.name); if (entry.isDirectory()) { await collectRollouts(full, out); } else if (entry.isFile() && entry.name.endsWith(".jsonl")) { out.push(full); } } } function num(value) { return typeof value === "number" && Number.isFinite(value) ? value : 0; } /** * Fold one rollout into a single session-level rollup. * * `billableEvents` counts only the `token_count` events where the cumulative * total actually advanced. Counting every event instead would inherit exactly * the repetition that makes summing the per-turn values wrong. */ async function readRollout(filePath) { const rl = createInterface({ input: createReadStream(filePath, { encoding: "utf8" }), crlfDelay: Infinity, }); let model; let bestTotal = -1; let best; let previousTotal = -1; let billableEvents = 0; try { for await (const line of rl) { // Cheap reject before JSON.parse: these files reach hundreds of MB and // most lines are conversation content with no usage on them at all. const isTokenCount = line.includes('"token_count"'); const isTurnContext = line.includes('"turn_context"'); if (!isTokenCount && !isTurnContext) { continue; } let parsed; try { parsed = JSON.parse(line); } catch { // A rollout being appended to while we read it ends mid-line. continue; } const record = parsed; if (record.type === "turn_context" && record.payload?.model) { // Last one wins: a session can switch models partway through, and the // most recent is the better single label for it. model = record.payload.model; continue; } if (record.payload?.type !== "token_count") { continue; } const cumulative = record.payload.info?.total_token_usage; if (!cumulative) { continue; } const total = num(cumulative.total_tokens); if (total > previousTotal) { billableEvents += 1; } previousTotal = total; if (total > bestTotal) { bestTotal = total; best = { input: num(cumulative.input_tokens), output: num(cumulative.output_tokens), cached: num(cumulative.cached_input_tokens), }; } } } finally { rl.close(); } if (!best) { return null; } return { model, billableEvents, ...best }; } export async function createCodexReader() { return { descriptor: { id: CLI_ID, displayName: "Codex", verified: true, dedupStrategy: "session-dag", costConfidence: "unavailable", requiresSqlite: false, }, detect: async () => { try { const info = await stat(sessionsRoot()); return info.isDirectory(); } catch { return false; } }, scan: async (options) => { const totals = emptyTotals(); const errors = []; const models = new Set(); const files = []; await collectRollouts(sessionsRoot(), files); // Only Infinity means "no time filter". A non-positive sinceDays used to // leave the cutoff undefined and read EVERYTHING — measured at 17,534 // files and 35.9s for `sinceDays: 0`, which is the widest possible scan // in answer to the narrowest possible request. Zero now means a // zero-length window, which is what it reads as. // NaN is not a window, and it is the case the previous fix missed: // Math.max(0, NaN) is NaN, every comparison against NaN is false, so the // filter passes EVERY file. Measured: sinceDays NaN read 17,537 files in // 32.8s — the same unbounded sweep this guard exists to prevent, reached // by a different door. The old guard caught it with Number.isFinite and // the replacement dropped that check. const cutoff = resolveScanCutoffMs(options?.sinceDays); let filesScanned = 0; for (const file of files) { try { if (cutoff !== undefined) { const info = await stat(file); if (info.mtimeMs < cutoff) { continue; } } const rollup = await readRollout(file); filesScanned += 1; if (!rollup) { continue; } totals.requests += rollup.billableEvents; // `cached_input_tokens` is a SUBSET of `input_tokens` here — verified // on all 108 sessions, where input + output == total exactly and // cached <= input always. The other readers keep the two disjoint, so // the cached portion is subtracted out rather than reported twice; a // caller adding inputTokens + cacheReadTokens would otherwise // over-count Codex and not the rest. totals.inputTokens += Math.max(0, rollup.input - rollup.cached); totals.cacheReadTokens += rollup.cached; totals.outputTokens += rollup.output; if (rollup.model) { models.add(rollup.model); } } catch (error) { errors.push({ cliId: CLI_ID, filePath: file, message: error instanceof Error ? error.message : String(error), }); } } // Every turn is unpriced by construction, not by accident: see the note // on this module about Codex being a subscription. Naming the models // keeps that legible instead of looking like a lookup that failed. totals.unpricedRequests = totals.requests; totals.unpricedModels = [...models].sort(); return { cliId: CLI_ID, totals, filesScanned, errors }; }, }; }