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

365 lines (364 loc) • 15.2 kB
/** * Reads token usage out of Hermes Agent's SQLite state store. * * Hermes Agent is Nous Research's official CLI agent (github.com/NousResearch/ * hermes-agent), a Python program installed from its own install script — not * an npm package. An earlier note in this folder said the opposite ("no * official CLI, only an unofficial npm bridge"); it had searched npm for a * product that is not distributed there, and is retracted here. * * Store: `$HERMES_HOME/state.db`, defaulting to `~/.hermes/state.db`, plus one * `state.db` per profile under `profiles/<name>/`. SQLite, `schema_version` * table (26 when this was written; `PRAGMA user_version` stays 0, so it is * not the version to read). Confirmed on a real store produced by running the * CLI itself, not inferred from documentation. * * Two tables carry usage, and they are NOT additive: * * sessions one row per session, with cumulative token columns * for the PRIMARY task only. * session_model_usage one row per (session, model, billing, task), each * with its own `api_call_count`, token columns and cost. * * Measured on the real store: a one-prompt session held a `sessions` row of * 10,568 input / 1 output / 1 call, and TWO usage rows — the primary task * (`task = ''`, identical numbers) and a `title_generation` task of 248 / 8 / * 1 call that the `sessions` aggregate does not include. The usage rows are * therefore the complete record of what Hermes actually sent to a provider, * and this reader sums them. The `sessions` aggregate is read only for a * session that has no usage rows at all (a store older than the migration * that introduced the table), and never in addition to them. A usage table * that exists but lacks a required column is a changed schema, and is reported * rather than read around. * * Cost: Hermes records `estimated_cost_usd` with a `cost_status` of * `estimated`, from its own pricing snapshot. That is a modeled figure and is * reported as such. `actual_cost_usd` is `NOT NULL DEFAULT 0` on usage rows, * so a zero there is a schema default, not evidence of a free call — it is * used only when `cost_status` explicitly says the figure is actual. A row * with no trustworthy cost is counted as unpriced and its model named. * * Cache and reasoning columns are reported as stored. Every real sample so far * has them at zero, so whether `cache_read_tokens` is a subset of * `input_tokens` (OpenAI convention) or disjoint from it (Anthropic * convention) has not been measured, and no subtraction or folding is applied * until it has. Reasoning tokens are not added to output for the same reason. * * The time window is a snapshot filter, not an attribution: every row is a * cumulative counter, so a session that spans the cutoff is either included * whole or excluded whole, keyed on its last activity. Timestamps are epoch * SECONDS stored as REAL. */ import { readdir, stat } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; import { resolveScanCutoffMs } from "./scanWindow.js"; const CLI_ID = "hermes"; function hermesHome() { const env = process.env.HERMES_HOME; return env !== undefined && env.trim().length > 0 ? env : join(homedir(), ".hermes"); } function emptyTotals() { return { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, costConfidence: "modeled", unpricedRequests: 0, unpricedModels: [], }; } /** A finite, non-negative number, or 0. NULL and garbage both read as 0. */ function count(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; } /** A finite, non-negative number, or null — for costs, where 0 is a value. */ function amount(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null; } /** The root store plus one per profile. Missing pieces are simply absent. */ async function findStateDatabases() { const root = hermesHome(); const out = []; const rootDb = join(root, "state.db"); try { if ((await stat(rootDb)).isFile()) { out.push(rootDb); } } catch { // No root store. } let profiles; try { profiles = (await readdir(join(root, "profiles"), { withFileTypes: true })) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name); } catch { return out; } for (const name of profiles) { const db = join(root, "profiles", name, "state.db"); try { if ((await stat(db)).isFile()) { out.push(db); } } catch { // Profile without a store yet. } } return out; } function columnsOf(db, table) { // PRAGMA table_info cannot take a bound parameter; the table names here are // fixed identifiers, never user input. const rows = db.prepare(`PRAGMA table_info(${table})`).all(); return new Set(rows .map((row) => row.name) .filter((name) => typeof name === "string")); } const USAGE_REQUIRED = [ "session_id", "model", "api_call_count", "input_tokens", "output_tokens", ]; const SESSIONS_REQUIRED = [ "id", "started_at", "input_tokens", "output_tokens", ]; const OPTIONAL_COUNTS = [ "cache_read_tokens", "cache_write_tokens", "reasoning_tokens", ]; const OPTIONAL_COST = [ "estimated_cost_usd", "actual_cost_usd", "cost_status", ]; /** `col` if the table has it, else a typed default under the same alias. */ function select(have, col, fallback, alias = col) { return have.has(col) ? `${alias === col ? col : `${col} AS ${alias}`}` : `${fallback} AS ${alias}`; } /** * Which cost a row is allowed to claim. * * `cost_status` is provenance, and it governs which column is trusted: an * explicit actual/billed status admits `actual_cost_usd`; an explicit * estimated status admits `estimated_cost_usd`. Without a status, a POSITIVE * estimate is still a computed value rather than a schema default and is * accepted; a bare zero is not, because `actual_cost_usd` defaults to 0 on * every usage row and would otherwise price every call at nothing. */ function rowCost(row) { const status = typeof row.cost_status === "string" ? row.cost_status.toLowerCase() : ""; const actual = amount(row.actual_cost_usd); const estimated = amount(row.estimated_cost_usd); if (status === "actual" || status === "billed") { return actual; } if (status === "estimated") { return estimated; } return estimated !== null && estimated > 0 ? estimated : null; } /** * `COALESCE` over the timestamp columns a schema actually has. SQLite rejects * a one-argument COALESCE outright, and a minimal schema (or a fixture * modelled on one) can have exactly one — `started_at` is the only required * timestamp — so a single column is emitted bare. */ function newestOf(cols) { const [only] = cols; return cols.length === 1 && only !== undefined ? only : `COALESCE(${cols.join(", ")})`; } function foldRow(row, totals, unpriced) { const calls = count(row.api_call_count); const input = count(row.input_tokens); const output = count(row.output_tokens); if (calls === 0 && input + output === 0) { // A failed one-shot leaves a session with no calls and no tokens. It is // not usage, and counting it as a zero-token request would inflate the // request count with attempts that never reached a model. return; } totals.requests += calls > 0 ? calls : 1; totals.inputTokens += input; totals.outputTokens += output; totals.cacheReadTokens += count(row.cache_read_tokens); totals.cacheCreationTokens += count(row.cache_write_tokens); const cost = rowCost(row); if (cost === null) { totals.unpricedRequests += calls > 0 ? calls : 1; unpriced.add(typeof row.model === "string" && row.model ? row.model : "unknown"); } else { totals.costUsd += cost; } } function readStore(db, dbPath, cutoffSeconds, totals, unpriced, errors) { const sessionCols = columnsOf(db, "sessions"); if (sessionCols.size === 0) { errors.push({ cliId: CLI_ID, filePath: dbPath, message: "no sessions table — not a Hermes state store, or one this reader does not understand", }); return; } const missingSessions = SESSIONS_REQUIRED.filter((c) => !sessionCols.has(c)); if (missingSessions.length > 0) { // Fail closed. Reading a partial schema and reporting a clean zero would // look like "no usage" when the truth is "unreadable". errors.push({ cliId: CLI_ID, filePath: dbPath, message: `sessions table lacks required column(s) ${missingSessions.join(", ")} — Hermes' schema has changed`, }); return; } const usageCols = columnsOf(db, "session_model_usage"); const missingUsage = USAGE_REQUIRED.filter((c) => !usageCols.has(c)); if (usageCols.size > 0 && missingUsage.length > 0) { // Fail closed, for the same reason as above. The table exists, so this is // not a store older than the migration; falling back to the sessions // aggregate would silently drop every non-primary task. errors.push({ cliId: CLI_ID, filePath: dbPath, message: `session_model_usage table lacks required column(s) ${missingUsage.join(", ")} — Hermes' schema has changed`, }); return; } const hasUsage = usageCols.size > 0; // Last activity, in seconds. Sessions know theirs; usage rows fall back to // their session's when they carry no `last_seen` of their own. const sessionAt = [ sessionCols.has("last_activity_at") ? "s.last_activity_at" : null, sessionCols.has("ended_at") ? "s.ended_at" : null, "s.started_at", ].filter((c) => c !== null); if (hasUsage) { const at = [ usageCols.has("last_seen") ? "u.last_seen" : null, usageCols.has("first_seen") ? "u.first_seen" : null, ...sessionAt, ].filter((c) => c !== null); const sql = `SELECT u.session_id, u.model, u.api_call_count, u.input_tokens, u.output_tokens, ${OPTIONAL_COUNTS.map((c) => (usageCols.has(c) ? `u.${c}` : `0 AS ${c}`)).join(", ")}, ${OPTIONAL_COST.map((c) => usageCols.has(c) ? `u.${c}` : `NULL AS ${c}`).join(", ")}, ${newestOf(at)} AS at FROM session_model_usage u LEFT JOIN sessions s ON s.id = u.session_id`; const rows = db.prepare(sql).all(); for (const row of rows) { if (cutoffSeconds !== undefined && count(row.at) < cutoffSeconds) { continue; } foldRow(row, totals, unpriced); } } // Sessions with no usage rows: only the aggregate exists for them. const orphanFilter = hasUsage ? " WHERE NOT EXISTS (SELECT 1 FROM session_model_usage u WHERE u.session_id = s.id)" : ""; const sessionSql = `SELECT s.id AS session_id, ${select(sessionCols, "model", "NULL")}, ${select(sessionCols, "api_call_count", "0")}, s.input_tokens, s.output_tokens, ${OPTIONAL_COUNTS.map((c) => sessionCols.has(c) ? `s.${c}` : `0 AS ${c}`).join(", ")}, ${OPTIONAL_COST.map((c) => sessionCols.has(c) ? `s.${c}` : `NULL AS ${c}`).join(", ")}, ${newestOf(sessionAt)} AS at FROM sessions s${orphanFilter}`; const sessions = db.prepare(sessionSql).all(); for (const row of sessions) { if (cutoffSeconds !== undefined && count(row.at) < cutoffSeconds) { continue; } foldRow(row, totals, unpriced); } } export async function createHermesReader() { return { descriptor: { id: CLI_ID, displayName: "Hermes Agent", verified: true, // One row per (session, model, task): the store already holds each call // exactly once, so the only thing to avoid is reading a session's // aggregate on top of its rows. dedupStrategy: "last-write-wins", costConfidence: "modeled", requiresSqlite: true, }, detect: async () => (await findStateDatabases()).length > 0, scan: async (options) => { const totals = emptyTotals(); const errors = []; const unpriced = new Set(); 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: hermesHome(), 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: hermesHome(), 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 cutoffMs = resolveScanCutoffMs(options?.sinceDays); const cutoffSeconds = cutoffMs === undefined ? undefined : cutoffMs / 1000; let filesScanned = 0; for (const dbPath of await findStateDatabases()) { let db; try { // Read-only: Hermes may be writing to this file right now. db = new DatabaseSync(dbPath, { readOnly: true }); filesScanned += 1; readStore(db, dbPath, cutoffSeconds, totals, unpriced, errors); } catch (error) { errors.push({ cliId: CLI_ID, filePath: dbPath, message: error instanceof Error ? error.message : String(error), }); } finally { try { db?.close(); } catch { // Already closed, or never opened. } } } totals.unpricedModels = [...unpriced].sort(); return { cliId: CLI_ID, totals, filesScanned, errors }; }, }; }