@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
TypeScript
/**
* 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>;