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

201 lines (200 loc) • 9.32 kB
/** * Reads token usage out of Copilot CLI's local SQLite store. * * Store: `~/.copilot/session-store.db`. Usage lives on the * `assistant_usage_events` table — one INSERT-only row per assistant turn, * with `model`, `input_tokens`, `output_tokens`, `cache_read_tokens`, * `cache_write_tokens`, `reasoning_tokens` and a `created_at` timestamp * (ISO-8601 with a `Z` suffix). Confirmed no duplicate `(session_id, * turn_index)` pair exists on a reference machine, so — like OpenCode's * `message` table — this needs no dedup logic beyond the table's own * uniqueness: every row is counted once, matching the `rowid-high-water-mark` * strategy already established for that reader. * * The same database also holds a JSONL-adjacent `session.shutdown` event with * its own `modelMetrics`/`tokenDetails` payload (written to * `~/.copilot/session-state/*\/events.jsonl`, not this database). It was * deliberately NOT used here: cross-checking real sessions confirmed the two * sources report identical totals for the sessions they both cover, so * combining them would double-count rather than add coverage. The SQLite * table is the sole source; the tradeoff is a disclosed gap for any history * that predates the table's introduction. * * `cache_read_tokens` and `cache_write_tokens` are both SUBSETS of * `input_tokens`, not disjoint — confirmed arithmetically against a real * `session.shutdown` record (session `c2c38d0b`): `tokenDetails.input.tokenCount * = 3`, `cache_write = 24047`, and `usage.inputTokens = 24050` — exactly * `3 + 24047`, with nothing left over for a separate additive interpretation. * So `inputTokens` here is `input_tokens - cache_read_tokens - * cache_write_tokens`, the same style of subtraction `codexReader.ts`, * `qwenCodeReader.ts` and `geminiCliReader.ts` make for the same reason: this * subsystem's `LocalUsageTotals.inputTokens` + `.cacheReadTokens` must sum to * the true prompt size without double-counting. `reasoning_tokens` folds into * output — Copilot's own SDK types (`ShutdownModelMetricUsage`) group it * alongside `outputTokens` as a further breakdown of output, not a separate * accounting bucket. * * Cost is deliberately `unavailable`. The table's own `request_multiplier` * and `total_nano_aiu` columns are Copilot's request-quota accounting, not a * USD figure — the SDK's own generated types tag the closest concept, * `ShutdownModelMetricUsage.cost`, `@experimental`, and every real sample row * on the reference machine had `request_multiplier: 0.0`. Modeling a * per-token dollar figure from public list prices would also ignore that * Copilot CLI is typically used under a flat-rate Copilot subscription, the * same reasoning that keeps Codex's confidence `unavailable`. This holds even * when the underlying model is a metered vendor's (e.g. `claude-haiku-4-5` on * the reference machine) — Copilot's telemetry layer normalizes across * backend vendors into its own accounting convention, and this reader has no * way to tell whether a given row was billed by the minute, by the token, or * not separately billed at all. */ import { stat } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; import { resolveScanCutoffMs } from "./scanWindow.js"; const CLI_ID = "copilot"; function databasePath() { return join(homedir(), ".copilot", "session-store.db"); } function emptyTotals() { return { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, costConfidence: "unavailable", unpricedRequests: 0, unpricedModels: [], }; } function num(value) { return typeof value === "number" && Number.isFinite(value) ? value : 0; } /** * `created_at` is an ISO-8601-with-`Z` text column, so the cutoff needs to be * the same shape to sort and compare correctly in SQL. `Infinity`/`undefined` * (no filter) becomes the epoch, matching `openCodeReader.ts`'s `?? 0` * convention of expressing "unbounded" as "since the earliest possible time" * rather than leaving the bound parameter out. */ function cutoffIso(sinceDays) { const cutoffMs = resolveScanCutoffMs(sinceDays) ?? 0; return new Date(cutoffMs).toISOString(); } export async function createCopilotCliReader() { return { descriptor: { id: CLI_ID, displayName: "Copilot CLI", verified: true, dedupStrategy: "rowid-high-water-mark", costConfidence: "unavailable", requiresSqlite: true, }, detect: async () => { try { const info = await stat(databasePath()); return info.isFile(); } catch { return false; } }, scan: async (options) => { const totals = emptyTotals(); const errors = []; const models = new Set(); const dbPath = databasePath(); // `node:sqlite` is experimental and may be absent or change shape — the // same lazy, validated import `openCodeReader.ts` uses, so one runtime // missing it degrades to a reported failure for this reader alone. let DatabaseSync; try { const sqlite = await import("node:sqlite"); if (typeof sqlite === "object" && sqlite !== null && "DatabaseSync" in sqlite && typeof sqlite.DatabaseSync === "function") { DatabaseSync = sqlite.DatabaseSync; } } catch (error) { errors.push({ cliId: CLI_ID, filePath: dbPath, message: `node:sqlite unavailable on this runtime: ${error instanceof Error ? error.message : String(error)}`, }); return { cliId: CLI_ID, totals, filesScanned: 0, errors }; } if (!DatabaseSync) { errors.push({ cliId: CLI_ID, filePath: dbPath, message: "node:sqlite did not expose a callable DatabaseSync — the experimental API has likely changed shape", }); return { cliId: CLI_ID, totals, filesScanned: 0, errors }; } const cutoff = cutoffIso(options?.sinceDays); let db; try { // Read-only: this is the user's live store and Copilot CLI may be // running. Bound parameter, not interpolation — never splice a // caller-influenced value into SQL text. db = new DatabaseSync(dbPath, { readOnly: true }); const rows = db .prepare("SELECT model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, created_at FROM assistant_usage_events WHERE created_at >= ?") .all(cutoff); for (const row of rows) { const inputTokens = num(row.input_tokens); const outputTokens = num(row.output_tokens); const cacheRead = num(row.cache_read_tokens); const cacheWrite = num(row.cache_write_tokens); const reasoning = num(row.reasoning_tokens); if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0 && reasoning === 0) { continue; } totals.requests += 1; totals.inputTokens += Math.max(0, inputTokens - cacheRead - cacheWrite); totals.outputTokens += outputTokens + reasoning; totals.cacheReadTokens += cacheRead; totals.cacheCreationTokens += cacheWrite; // Every counted row contributes a name. A null model still raised // unpricedRequests, so skipping it here produced a report saying N // turns went unpriced while naming fewer than N models — the operator // is then chasing a gap the report refuses to identify. models.add(row.model ?? "unknown"); } } catch (error) { errors.push({ cliId: CLI_ID, filePath: dbPath, message: error instanceof Error ? error.message : String(error), }); } finally { try { db?.close(); } catch { // Closing a database that failed to open is not a second failure. } } totals.unpricedRequests = totals.requests; totals.unpricedModels = [...models].sort(); return { cliId: CLI_ID, totals, filesScanned: totals.requests > 0 || errors.length === 0 ? 1 : 0, errors, }; }, }; }