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

74 lines (73 loc) • 4.72 kB
/** * Reads token usage out of Gemini CLI's own session transcripts. * * Layout, confirmed on a real machine (18 files spanning June–August 2026): * `~/.gemini/tmp/<projectSlug>/chats/session-<timestamp>-<id>.jsonl`. The * sibling `~/.gemini/history/<projectSlug>/` directories are a git-based * shadow-history of edited files, not chat logs — no JSONL lives there. * * Each file is a small append-only patch log rather than one JSON object per * line of "the same shape": line 0 is a header (`sessionId`, `projectHash`, * `startTime`, `lastUpdated`, `kind`); every line after that is either * `{"$set": {...}}` (a partial merge — `messages` on session bootstrap, * scalar fields like `lastUpdated`/`summary` afterwards) or a bare message * object appended directly. Confirmed on the installed CLI's own bundled * source (`@google/gemini-cli` `ChatRecordingService.pushMessage`): a bare * append is `appendRecord(msg)` with no wrapper, which is why real transcripts * mix both shapes. This reader unwraps `$set.messages[]` (only ever seen once * per file, holding the single injected `<session_context>` bootstrap * message) and reads bare objects that carry `id`/`type` directly. * * The installed source also revealed a real double-write race, invisible in * this machine's own samples (0 duplicate ids across all 18 files) but * reachable on any machine: `recordMessage()` first pushes a `type: "gemini"` * message with whatever `tokens` value is already queued (often `null`, * before the response's usage metadata has arrived), and * `recordMessageTokens()` — called separately once usage arrives — re-pushes * the SAME message id with `tokens` now filled in if it finds the last * message still token-less. `pushMessage()` unconditionally appends a new * line every time it is called, even for an id it has already written. So the * same `id` can legitimately appear twice: once without tokens, once with. * Dedup here keeps, per id, whichever record has the larger `tokens.total` * (a record with no tokens contributes 0), which is correct for both that * race and an ordinary resumed-session replay. The map is per-file, mirroring * `claudeCodeReader.ts`. * * `tokens` is `{input, output, cached, thoughts, tool, total}`, and the source * (`recordMessageTokens`) maps it straight from the GenAI response's own * `usageMetadata` — `input = promptTokenCount`, `output = candidatesTokenCount`, * `cached = cachedContentTokenCount`, `thoughts = thoughtsTokenCount`, * `tool = toolUsePromptTokenCount`, `total = totalTokenCount`. Real data * confirms `total = input + output + thoughts + tool` and that `cached` is a * SUBSET of `input`: the one real record with nonzero cache had * `input: 12121, cached: 4073`, and `total (12344) = input (12121) + output * (1) + thoughts (222)` — cached tokens are not added on top of input, they * are already inside it. So `inputTokens` here is `input - cached`, the same * subtraction `codexReader.ts` and `qwenCodeReader.ts` make for the same * reason: this subsystem's `LocalUsageTotals.inputTokens` + * `.cacheReadTokens` must sum to the true prompt size without double-counting. * No cache-CREATION concept exists in this API family's usage metadata, so * `cacheCreationTokens` stays 0. * * The write path also nests subagent transcripts one level deeper — * `chats/<parentSessionId>/<subagentSessionId>.jsonl`, per the same source — * so this reader recurses under `chats/` rather than globbing one level, even * though no nested subagent file exists on the machine this was verified * against. * * Cost is deliberately `unavailable`. Gemini CLI supports three genuinely * different auth modes with unrelated billing (`gemini-api-key`: metered, and * the Flash models specifically have a real free tier; `oauth-personal`: free, * rate-limited; `vertex-ai`: billed to a GCP project at negotiated rates), and * no session record — not the header, not a message — carries which mode was * active when it was written. `settings.json` records only the CURRENT mode, * which cannot be projected onto historical sessions. Modeling a per-token * dollar figure from the public API price list would misrepresent every * free-tier or Vertex-billed session as if it were pay-as-you-go. Concretely, * on the reference machine even the majority model logged (`gemini-3.5-flash`, * 12 of 14 sampled turns) has no entry in `pricing.ts` at all — only the * minority model (`gemini-3-flash-preview`) does — so most real traffic would * be unpriced regardless. */ import type { LocalUsageReader } from "../types/index.js"; export declare function createGeminiCliReader(): Promise<LocalUsageReader>;