@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
357 lines (356 loc) • 14.3 kB
TypeScript
/**
* Types for `src/lib/localUsage/` — reading token usage out of each AI CLI's
* own local session logs.
*
* Why this exists alongside the proxy's ledger: the proxy can only account for
* traffic that went through it, which caps coverage at the CLIs that expose a
* base-URL override. Every CLI writes its own local transcript regardless, so
* reading those recovers the rest — and recovers history from before the proxy
* was ever installed. It is also an independent source of truth: a pricing
* defect in the proxy's own accounting cannot hide from a reader that derives
* cost from a different input.
*/
/**
* Stable identifier for one CLI this subsystem can read local usage from.
* Kebab-case, matching `CliProxyClientConfigurator.id`'s convention — a
* different registry, but the same repo-wide convention for CLI identifiers.
*/
export type LocalUsageCliId = "claude-code" | "codex" | "gemini-cli" | "opencode" | "qwen-code" | "copilot"
/**
* @deprecated Use "copilot". Retained because this literal is already part
* of the published `LocalUsageCliId`, so dropping it would break any caller
* that names it. `usage local --cli copilot-cli` still resolves, normalised
* to "copilot" at the input boundary.
*/
| "copilot-cli" | "cursor" | "grok" | "hermes" | "amp" | "kiro" | "antigravity";
/**
* How much to trust a computed cost figure.
*
* Not decoration: some CLIs are flat-rate subscriptions where a per-request
* cost is meaningless, and at least one publishes a byte heuristic rather than
* a real number. A caller must never render "heuristic" with the same
* confidence as "modeled", so the distinction travels with the number.
*/
export type LocalUsageCostConfidence = "modeled" | "unavailable" | "heuristic";
/**
* How a reader avoids counting the same turn twice.
*
* Metadata on the descriptor, for introspection and for the person writing the
* next reader — the aggregator does not branch on it.
*/
export type LocalUsageDedupStrategy = "message-id-keep-max" | "last-write-wins" | "rowid-high-water-mark" | "session-dag";
/**
* What one unit of `LocalUsageTotals.requests` actually counts.
*
* Not cosmetic. Every reader but one counts assistant turns, so the CLI could
* safely print "turns" for all of them. Cursor persists no per-turn record at
* all — only a snapshot of the current context, one per session however many
* turns that session ran — so printing "turns 1" for a two-hundred-turn
* session states something false. The unit travels with the number for the
* same reason `costConfidence` does: a figure rendered without saying what it
* counts is the failure this whole subsystem exists to avoid.
*
* Optional on the descriptor, defaulting to "turn", so adding it does not
* break an existing caller constructing a descriptor of its own.
*/
export type LocalUsageRequestUnit = "turn" | "session-snapshot";
/** Aggregated totals for one CLI, one scan. */
export type LocalUsageTotals = {
requests: number;
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
costUsd: number;
/**
* The weakest confidence contributing to `costUsd`. A totals row mixing
* modeled and heuristic entries must report the weaker one, otherwise the
* aggregate looks better-sourced than its worst input.
*/
costConfidence: LocalUsageCostConfidence;
/** Turns whose model had no pricing entry, so contributed 0 to costUsd. */
unpricedRequests: number;
/** Distinct model ids behind `unpricedRequests`, for diagnosis. */
unpricedModels: string[];
};
/** A non-fatal per-file problem, surfaced instead of aborting the scan. */
export type LocalUsageScanError = {
cliId: LocalUsageCliId;
filePath: string;
message: string;
};
/** What one reader's `scan()` returns. */
export type LocalUsageScanResult = {
cliId: LocalUsageCliId;
totals: LocalUsageTotals;
/** Files opened during this scan, after any time filter. */
filesScanned: number;
errors: LocalUsageScanError[];
};
/** Static metadata, available without constructing a reader. */
export type LocalUsageReaderDescriptor = {
id: LocalUsageCliId;
displayName: string;
/**
* True only for readers checked against real data on a real machine. An
* honesty marker, not a completeness claim — an unverified reader may still
* be correct, it just has not been shown to be.
*/
verified: boolean;
dedupStrategy: LocalUsageDedupStrategy;
costConfidence: LocalUsageCostConfidence;
/** Whether reading this CLI's store needs a SQLite binding. */
requiresSqlite: boolean;
/**
* What `LocalUsageTotals.requests` counts for this reader. Absent means
* "turn", which is what every reader except Cursor records.
*/
requestUnit?: LocalUsageRequestUnit;
};
/** Options accepted by every reader's `scan()` and by the aggregator. */
export type LocalUsageScanOptions = {
/**
* Only read files modified within this many days. Defaults to 30.
*
* This is a real constraint rather than a convenience: one developer machine
* held 17,439 transcripts totalling 9.7 GB, and an unbounded scan reads all
* of it on every call. Pass `Infinity` for a deliberate full history sweep.
*/
sinceDays?: number;
};
/** The contract every reader implements — one per CLI. */
export type LocalUsageReader = {
descriptor: LocalUsageReaderDescriptor;
/**
* Whether this CLI's local store appears to exist on this machine at all —
* the same "do not report on something never installed" discipline the proxy
* client configurators use before writing a config.
*/
detect: () => Promise<boolean>;
scan: (options?: LocalUsageScanOptions) => Promise<LocalUsageScanResult>;
};
/** Async factory stored in the registry — a reader needs no credentials, only
* the filesystem, so this takes no arguments. */
export type LocalUsageReaderFactoryFn = () => Promise<LocalUsageReader>;
/** One entry in the registry map. */
export type LocalUsageReaderRegistration = {
descriptor: LocalUsageReaderDescriptor;
factory: LocalUsageReaderFactoryFn;
};
/** A whole reader failing — not installed, or threw — so the aggregate report
* can carry successes and failures side by side rather than losing both. */
export type LocalUsageReaderFailure = {
cliId: LocalUsageCliId;
message: string;
};
/** Top-level output of scanning every registered, detected reader. */
export type LocalUsageAggregateReport = {
generatedAt: string;
/** Only CLIs whose store was detected AND scanned appear here. */
totals: Partial<Record<LocalUsageCliId, LocalUsageTotals>>;
/** CLIs whose reader could not be created, detected, or scanned, and why. */
failures: LocalUsageReaderFailure[];
/**
* Per-file problems from readers that otherwise succeeded.
*
* Distinct from `failures`, which is a reader that threw. A scan can read
* nine of ten transcripts and still be wrong by the tenth; without this the
* shortfall is invisible and the totals look authoritative. Readers have
* always collected these — nothing consumed them until now.
*/
scanErrors: LocalUsageScanError[];
/** CLIs with no local store on this machine — absent, not failed. */
notInstalled: LocalUsageCliId[];
};
/**
* The `message.usage` object exactly as Claude Code writes it into a
* transcript line — snake_case, and every field optional because older
* transcripts predate some of them.
*/
export type LocalUsageClaudeRawUsage = {
input_tokens?: number;
output_tokens?: number;
cache_read_input_tokens?: number;
cache_creation_input_tokens?: number;
};
/**
* One Codex rollout reduced to its session-level totals.
*
* The token figures here are the session's CUMULATIVE counter, not a sum of
* per-turn values — see `codexReader.ts` for why summing overstates by ~63%.
*/
export type LocalUsageCodexSessionRollup = {
model?: string;
input: number;
output: number;
cached: number;
/** token_count events where the cumulative total actually advanced. */
billableEvents: number;
};
/**
* The `usageMetadata` object exactly as Qwen Code writes it into a transcript
* line — Google GenAI's `usageMetadata` shape, camelCase, every field
* optional because not every assistant record carries one. See
* `qwenCodeReader.ts` for why `cachedContentTokenCount` is subtracted out of
* `promptTokenCount` rather than added.
*/
export type LocalUsageQwenRawUsage = {
promptTokenCount?: number;
candidatesTokenCount?: number;
thoughtsTokenCount?: number;
totalTokenCount?: number;
cachedContentTokenCount?: number;
};
/**
* The `tokens` object exactly as Gemini CLI writes it onto a `type: "gemini"`
* message record — mapped straight from the GenAI response's own
* `usageMetadata` by the CLI's own `recordMessageTokens()`. See
* `geminiCliReader.ts` for why `cached` is subtracted out of `input` rather
* than added, and why `thoughts`/`tool` fold into output.
*/
export type LocalUsageGeminiCliTokens = {
input?: number;
output?: number;
cached?: number;
thoughts?: number;
tool?: number;
total?: number;
};
/**
* A `type: "gemini"` message record as read out of a chat transcript line —
* whether it arrived bare-appended or unwrapped from a `$set.messages[]`
* bootstrap entry. See `geminiCliReader.ts` for both shapes.
*/
export type LocalUsageGeminiMessageRecord = {
id?: string;
type?: string;
model?: string;
tokens?: LocalUsageGeminiCliTokens;
};
/**
* One row of Copilot CLI's `assistant_usage_events` SQLite table, restricted
* to the columns `copilotCliReader.ts` actually reads. `cache_read_tokens`
* and `cache_write_tokens` are both subsets of `input_tokens` — see that
* reader's module header for the arithmetic proof.
*/
export type LocalUsageCopilotUsageRow = {
model: string | null;
input_tokens: number | null;
output_tokens: number | null;
cache_read_tokens: number | null;
cache_write_tokens: number | null;
reasoning_tokens: number | null;
created_at: string | null;
};
/**
* One usage row as this subsystem reads it out of Hermes Agent's `state.db` —
* either a `session_model_usage` row, or a `sessions` row projected onto the
* same columns for a session that predates that table. Every column but the
* first five may be absent from an older schema and is selected with a
* default, so they are optional here. `at` is the row's last activity in
* epoch SECONDS, coalesced from whichever timestamp the schema has.
*/
export type LocalUsageHermesUsageRow = {
session_id: string;
model: string | null;
api_call_count: number | null;
input_tokens: number | null;
output_tokens: number | null;
cache_read_tokens?: number | null;
cache_write_tokens?: number | null;
reasoning_tokens?: number | null;
estimated_cost_usd?: number | null;
actual_cost_usd?: number | null;
cost_status?: string | null;
at?: number | null;
};
/**
* The `usage` object on a Grok Build `turn_completed` session update, as
* appended to a session's `updates.jsonl`. camelCase, from the CLI's own
* serde definitions and confirmed on a real run. `modelUsage` holds the same
* shape per model id; `numTurns` is the process ledger's turn counter, which
* is how a reader tells a cumulative run from a fresh one — see
* `grokReader.ts`.
*/
export type LocalUsageGrokTurnUsage = {
inputTokens?: number;
outputTokens?: number;
cachedReadTokens?: number;
cacheCreationTokens?: number;
reasoningTokens?: number;
modelCalls?: number;
numTurns?: number;
modelUsage?: Record<string, unknown>;
};
/**
* One Grok Build completed turn after validation: every count a finite,
* non-negative safe integer, and the `modelUsage` keys collected. `turns` is
* the ledger's `numTurns`, which decides whether the next record continues
* this process run or starts a fresh one — see `grokReader.ts`.
*/
export type LocalUsageGrokTurn = {
input: number;
output: number;
cacheRead: number;
cacheCreation: number;
calls: number;
turns: number;
models: string[];
};
/**
* The slice of `node:sqlite`'s `DatabaseSync` the OpenCode reader uses.
*
* Deliberately minimal. `node:sqlite` is still flagged experimental and may
* change shape between Node releases, so the reader validates this much at
* runtime rather than trusting a type assertion — naming only what is actually
* called keeps that check small and honest.
*/
/**
* One decoded protobuf field from a Cursor root blob. Wire types 0 (varint)
* and 2 (length-delimited) only — the two Cursor actually uses; fixed-width
* fields are skipped by the decoder rather than represented.
*/
export type LocalUsageWireField = {
field: number;
kind: "varint";
value: number;
} | {
field: number;
kind: "bytes";
value: Uint8Array;
};
/**
* One entry in Cursor's context breakdown — `system_prompt`, `tools`, `rules`
* and friends — carrying the token count that entry occupies in context.
*/
export type LocalUsageContextPart = {
name: string;
tokens: number;
};
export type LocalUsageSqliteDatabase = {
prepare: (sql: string) => {
all: (...params: unknown[]) => unknown[];
};
close: () => void;
};
/** Constructor shape for the same. */
export type LocalUsageSqliteDatabaseCtor = new (path: string, options?: {
readOnly?: boolean;
}) => LocalUsageSqliteDatabase;
/** Arguments for the `neurolink usage local` command. */
export type LocalUsageCommandArgs = {
since: number;
json: boolean;
cli?: string;
};
/**
* Options for scanning every registered reader at once.
*
* `only` is not a convenience filter applied to the results — it decides which
* readers are constructed and run at all. Scanning everything and discarding
* the rest turned a 10s single-CLI query into 28s of reading two other stores
* nobody asked for, one of them 742 MB.
*/
export type LocalUsageAggregateOptions = LocalUsageScanOptions & {
only?: LocalUsageCliId[];
};