@juspay/neurolink 12.8.0 → 12.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@ import { ProxyRuntimeConfigStore } from "../../proxy/runtimeConfig.js";
26
26
  import { startProxyLogCleanupScheduler } from "../../proxy/logCleanupScheduler.js";
27
27
  import { anthropicAccountKeysEqual, createAccountAllowlist, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
28
28
  import { resolveProxyStatusAccountIdentity } from "../../proxy/codexAccountUsage.js";
29
- import { beginProxyRequest, getProxyActivitySnapshot, trackProxyResponse, } from "../../proxy/proxyActivity.js";
29
+ import { beginProxyRequest, getProxyActivitySnapshot, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
30
30
  import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../proxy/proxyLifecycle.js";
31
31
  import { describeInstallFailure, getGlobalInstallArgs, isTransientInstallFailure, resolveGlobalInstaller, validateInstalledVersion, } from "../../proxy/globalInstaller.js";
32
32
  import { startUpdaterWorkerSupervisor } from "../../proxy/updaterSupervisor.js";
@@ -1120,6 +1120,27 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1120
1120
  responseStatus,
1121
1121
  elapsedMs: performance.now() - startedMonotonicMs,
1122
1122
  });
1123
+ const routeResponseObservers = takeProxyResponseObservers(metadata);
1124
+ const notifyRouteFirstChunk = (details) => {
1125
+ for (const observer of routeResponseObservers) {
1126
+ try {
1127
+ observer.onFirstChunk?.(details);
1128
+ }
1129
+ catch {
1130
+ // Route-level accounting must never interfere with the relay.
1131
+ }
1132
+ }
1133
+ };
1134
+ const notifyRouteTerminal = (details) => {
1135
+ for (const observer of routeResponseObservers) {
1136
+ try {
1137
+ observer.onTerminal?.(details);
1138
+ }
1139
+ catch {
1140
+ // Route-level accounting must never interfere with the relay.
1141
+ }
1142
+ }
1143
+ };
1123
1144
  c.res = trackProxyResponse(c.res, finish, {
1124
1145
  onFirstChunk: ({ observedBodyBytes, responseChunks }) => {
1125
1146
  logProxyLifecycleEvent({
@@ -1137,6 +1158,10 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1137
1158
  responseChunks,
1138
1159
  elapsedMs: performance.now() - startedMonotonicMs,
1139
1160
  });
1161
+ notifyRouteFirstChunk({
1162
+ observedBodyBytes,
1163
+ responseChunks,
1164
+ });
1140
1165
  },
1141
1166
  onTerminal: ({ outcome, observedBodyBytes, responseChunks }) => {
1142
1167
  logProxyLifecycleEvent({
@@ -1157,6 +1182,11 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1157
1182
  errorType: metadata.terminalErrorType,
1158
1183
  errorCode: metadata.terminalErrorCode,
1159
1184
  });
1185
+ notifyRouteTerminal({
1186
+ outcome,
1187
+ observedBodyBytes,
1188
+ responseChunks,
1189
+ });
1160
1190
  },
1161
1191
  });
1162
1192
  }
@@ -1484,7 +1514,10 @@ export async function createProxyStartApp(params) {
1484
1514
  neurolink: params.neurolink,
1485
1515
  toolRegistry: params.neurolink.getToolRegistry(),
1486
1516
  timestamp: Date.now(),
1487
- metadata: {},
1517
+ // Keep route terminal observers on the runtime request metadata so the
1518
+ // outer response tracker can notify them without adding a second body
1519
+ // wrapper to streaming routes.
1520
+ metadata: (metadata ?? {}),
1488
1521
  // Route handlers publish limit/quota headers here. Only the streaming
1489
1522
  // paths build their own Response (and set headers directly); every
1490
1523
  // JSON and error path returns a plain object, so without this the
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Reads token usage out of Grok Build's own session streams.
3
+ *
4
+ * Grok Build is xAI's official terminal coding agent (github.com/xai-org/
5
+ * grok-build, Rust, installed from x.ai/cli/install.sh as `grok`). An earlier
6
+ * note in this folder said "grok" named no product, only eight competing npm
7
+ * packages; it had searched npm for a Rust binary that is not distributed
8
+ * there, and is retracted here.
9
+ *
10
+ * Layout, confirmed by running the real binary against a redirected home:
11
+ * `$GROK_HOME/sessions/<url-encoded cwd>/<session-id>/`, defaulting to
12
+ * `~/.grok`. Each session directory holds `updates.jsonl` — the CLI's own
13
+ * documentation calls it the authoritative conversation log — plus
14
+ * `summary.json`, `chat_history.jsonl`, `signals.json` and others. The walk
15
+ * here reads exactly `<group>/<session>/updates.jsonl` and nothing deeper:
16
+ * a session directory can also hold `compaction_checkpoints/`, and a
17
+ * checkpoint that snapshots the stream would otherwise be counted twice.
18
+ *
19
+ * Each line is a JSON-RPC style record. The ones that matter are
20
+ * `method: "_x.ai/session/update"` with `params.update.sessionUpdate ===
21
+ * "turn_completed"`; a completed turn that reached a model carries a `usage`
22
+ * object with `inputTokens`, `outputTokens`, `cachedReadTokens`,
23
+ * `cacheCreationTokens`, `reasoningTokens`, `modelCalls`, a per-model
24
+ * `modelUsage` map and `numTurns`. A turn that failed before any model call
25
+ * carries no `usage` at all, so it excludes itself. `stop_reason` is not an
26
+ * eligibility test: a truncated or interrupted turn with a usage object was
27
+ * still billed.
28
+ *
29
+ * What the numbers MEAN took a second real turn to settle, and the answer is
30
+ * neither "per turn" nor "cumulative" but both, by process:
31
+ *
32
+ * turn 1 (fresh process) inputTokens 10141 numTurns 1
33
+ * turn 2 (`grok -r`, new one) inputTokens 11034 numTurns 1
34
+ *
35
+ * The second record is not 21175, so the usage is not a session-lifetime
36
+ * counter. But the CLI's own persistence code says the figure is the
37
+ * process's live ledger, and computes a turn's cost as live minus the
38
+ * previous live value when the ledger has only grown — a running total within
39
+ * one process, reset when a new process resumes the session. A reader has no
40
+ * process boundary to look at; what it has is `numTurns`, the ledger's own
41
+ * turn counter. Strictly increasing, with every bucket at least as large,
42
+ * means the same ledger, and the turn's usage is the difference from the
43
+ * previous record. Anything else means a fresh ledger, and the whole record
44
+ * counts. Both real records above have numTurns 1, so both count whole, which
45
+ * is the measured truth. The in-process cumulative branch is taken from the
46
+ * CLI's source, not from a measurement — headless `-p` runs are one prompt
47
+ * per process, so no real stream on this machine exercises it.
48
+ *
49
+ * Duplicates: a prompt's terminal record can be re-emitted (the persistence
50
+ * layer folds a late re-emission into the same turn). Dedup is by
51
+ * `prompt_id`, scoped to the session directory — prompt ids are not
52
+ * documented as globally unique — keeping the last record seen. A record
53
+ * with no prompt id is keyed by its line.
54
+ *
55
+ * Cost is `unavailable`: the turn record carries no price, and Grok Build is
56
+ * a subscription product whose custom-model configurations can point at any
57
+ * OpenAI- or Anthropic-compatible endpoint — the real run here used DeepSeek.
58
+ * Cache and reasoning counts are reported as stored; every real sample so far
59
+ * has them at zero, so whether `cachedReadTokens` is a subset of
60
+ * `inputTokens` has not been measured and nothing is subtracted or folded
61
+ * until it has. Reasoning tokens in particular are NOT added to output: on the
62
+ * OpenAI wire they are a subset of completion tokens, and adding them would
63
+ * double count.
64
+ *
65
+ * Not read: the newer per-session `usage.json` the CLI writes from the same
66
+ * ledger. No real run on this machine produced one — the 1.0.13 binary's
67
+ * `grok usage` reports "No usage recorded" for these sessions — and a reader
68
+ * written against a struct definition alone is the guessed-format failure
69
+ * this folder keeps paying for. When a real file exists it belongs here as a
70
+ * fallback for a session with no readable stream, never as an addition.
71
+ */
72
+ import type { LocalUsageReader } from "../types/index.js";
73
+ export declare function createGrokReader(): Promise<LocalUsageReader>;
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Reads token usage out of Grok Build's own session streams.
3
+ *
4
+ * Grok Build is xAI's official terminal coding agent (github.com/xai-org/
5
+ * grok-build, Rust, installed from x.ai/cli/install.sh as `grok`). An earlier
6
+ * note in this folder said "grok" named no product, only eight competing npm
7
+ * packages; it had searched npm for a Rust binary that is not distributed
8
+ * there, and is retracted here.
9
+ *
10
+ * Layout, confirmed by running the real binary against a redirected home:
11
+ * `$GROK_HOME/sessions/<url-encoded cwd>/<session-id>/`, defaulting to
12
+ * `~/.grok`. Each session directory holds `updates.jsonl` — the CLI's own
13
+ * documentation calls it the authoritative conversation log — plus
14
+ * `summary.json`, `chat_history.jsonl`, `signals.json` and others. The walk
15
+ * here reads exactly `<group>/<session>/updates.jsonl` and nothing deeper:
16
+ * a session directory can also hold `compaction_checkpoints/`, and a
17
+ * checkpoint that snapshots the stream would otherwise be counted twice.
18
+ *
19
+ * Each line is a JSON-RPC style record. The ones that matter are
20
+ * `method: "_x.ai/session/update"` with `params.update.sessionUpdate ===
21
+ * "turn_completed"`; a completed turn that reached a model carries a `usage`
22
+ * object with `inputTokens`, `outputTokens`, `cachedReadTokens`,
23
+ * `cacheCreationTokens`, `reasoningTokens`, `modelCalls`, a per-model
24
+ * `modelUsage` map and `numTurns`. A turn that failed before any model call
25
+ * carries no `usage` at all, so it excludes itself. `stop_reason` is not an
26
+ * eligibility test: a truncated or interrupted turn with a usage object was
27
+ * still billed.
28
+ *
29
+ * What the numbers MEAN took a second real turn to settle, and the answer is
30
+ * neither "per turn" nor "cumulative" but both, by process:
31
+ *
32
+ * turn 1 (fresh process) inputTokens 10141 numTurns 1
33
+ * turn 2 (`grok -r`, new one) inputTokens 11034 numTurns 1
34
+ *
35
+ * The second record is not 21175, so the usage is not a session-lifetime
36
+ * counter. But the CLI's own persistence code says the figure is the
37
+ * process's live ledger, and computes a turn's cost as live minus the
38
+ * previous live value when the ledger has only grown — a running total within
39
+ * one process, reset when a new process resumes the session. A reader has no
40
+ * process boundary to look at; what it has is `numTurns`, the ledger's own
41
+ * turn counter. Strictly increasing, with every bucket at least as large,
42
+ * means the same ledger, and the turn's usage is the difference from the
43
+ * previous record. Anything else means a fresh ledger, and the whole record
44
+ * counts. Both real records above have numTurns 1, so both count whole, which
45
+ * is the measured truth. The in-process cumulative branch is taken from the
46
+ * CLI's source, not from a measurement — headless `-p` runs are one prompt
47
+ * per process, so no real stream on this machine exercises it.
48
+ *
49
+ * Duplicates: a prompt's terminal record can be re-emitted (the persistence
50
+ * layer folds a late re-emission into the same turn). Dedup is by
51
+ * `prompt_id`, scoped to the session directory — prompt ids are not
52
+ * documented as globally unique — keeping the last record seen. A record
53
+ * with no prompt id is keyed by its line.
54
+ *
55
+ * Cost is `unavailable`: the turn record carries no price, and Grok Build is
56
+ * a subscription product whose custom-model configurations can point at any
57
+ * OpenAI- or Anthropic-compatible endpoint — the real run here used DeepSeek.
58
+ * Cache and reasoning counts are reported as stored; every real sample so far
59
+ * has them at zero, so whether `cachedReadTokens` is a subset of
60
+ * `inputTokens` has not been measured and nothing is subtracted or folded
61
+ * until it has. Reasoning tokens in particular are NOT added to output: on the
62
+ * OpenAI wire they are a subset of completion tokens, and adding them would
63
+ * double count.
64
+ *
65
+ * Not read: the newer per-session `usage.json` the CLI writes from the same
66
+ * ledger. No real run on this machine produced one — the 1.0.13 binary's
67
+ * `grok usage` reports "No usage recorded" for these sessions — and a reader
68
+ * written against a struct definition alone is the guessed-format failure
69
+ * this folder keeps paying for. When a real file exists it belongs here as a
70
+ * fallback for a session with no readable stream, never as an addition.
71
+ */
72
+ import { createReadStream } from "fs";
73
+ import { createInterface } from "readline";
74
+ import { readdir, stat } from "fs/promises";
75
+ import { homedir } from "os";
76
+ import { join } from "path";
77
+ import { resolveScanCutoffMs } from "./scanWindow.js";
78
+ const CLI_ID = "grok";
79
+ function grokHome() {
80
+ const env = process.env.GROK_HOME;
81
+ return env !== undefined && env.trim().length > 0
82
+ ? env
83
+ : join(homedir(), ".grok");
84
+ }
85
+ function sessionsRoot() {
86
+ return join(grokHome(), "sessions");
87
+ }
88
+ function emptyTotals() {
89
+ return {
90
+ requests: 0,
91
+ inputTokens: 0,
92
+ outputTokens: 0,
93
+ cacheReadTokens: 0,
94
+ cacheCreationTokens: 0,
95
+ costUsd: 0,
96
+ costConfidence: "unavailable",
97
+ unpricedRequests: 0,
98
+ unpricedModels: [],
99
+ };
100
+ }
101
+ /** A finite, non-negative safe integer, or 0. */
102
+ function count(value) {
103
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
104
+ ? value
105
+ : 0;
106
+ }
107
+ function isMissing(error) {
108
+ return (typeof error === "object" &&
109
+ error !== null &&
110
+ error.code === "ENOENT");
111
+ }
112
+ /**
113
+ * Every `<group>/<session>/updates.jsonl`, exactly two levels down. Symlinks
114
+ * are not followed: a linked session directory is either a duplicate of one
115
+ * already in the tree or something outside it, and neither should be read.
116
+ */
117
+ async function collectStreams(root, errors) {
118
+ let groups;
119
+ try {
120
+ groups = await readdir(root, { withFileTypes: true });
121
+ }
122
+ catch (error) {
123
+ if (!isMissing(error)) {
124
+ errors.push({
125
+ cliId: CLI_ID,
126
+ filePath: root,
127
+ message: error instanceof Error ? error.message : String(error),
128
+ });
129
+ }
130
+ return [];
131
+ }
132
+ const out = [];
133
+ for (const group of groups) {
134
+ if (!group.isDirectory()) {
135
+ continue;
136
+ }
137
+ const groupDir = join(root, group.name);
138
+ let sessions;
139
+ try {
140
+ sessions = await readdir(groupDir, { withFileTypes: true });
141
+ }
142
+ catch (error) {
143
+ if (!isMissing(error)) {
144
+ errors.push({
145
+ cliId: CLI_ID,
146
+ filePath: groupDir,
147
+ message: error instanceof Error ? error.message : String(error),
148
+ });
149
+ }
150
+ continue;
151
+ }
152
+ for (const session of sessions) {
153
+ if (!session.isDirectory()) {
154
+ continue;
155
+ }
156
+ const stream = join(groupDir, session.name, "updates.jsonl");
157
+ try {
158
+ if ((await stat(stream)).isFile()) {
159
+ out.push(stream);
160
+ }
161
+ }
162
+ catch {
163
+ // A session directory with no stream yet.
164
+ }
165
+ }
166
+ }
167
+ return out;
168
+ }
169
+ function readTurn(usage) {
170
+ const models = typeof usage.modelUsage === "object" && usage.modelUsage !== null
171
+ ? Object.keys(usage.modelUsage).filter((k) => k.length > 0)
172
+ : [];
173
+ return {
174
+ input: count(usage.inputTokens),
175
+ output: count(usage.outputTokens),
176
+ cacheRead: count(usage.cachedReadTokens),
177
+ cacheCreation: count(usage.cacheCreationTokens),
178
+ calls: count(usage.modelCalls),
179
+ turns: count(usage.numTurns),
180
+ models,
181
+ };
182
+ }
183
+ /** Whether `next` is the same process ledger as `prev`, grown by a turn. */
184
+ function continuesRun(prev, next) {
185
+ return (next.turns > prev.turns &&
186
+ next.input >= prev.input &&
187
+ next.output >= prev.output &&
188
+ next.cacheRead >= prev.cacheRead &&
189
+ next.cacheCreation >= prev.cacheCreation &&
190
+ next.calls >= prev.calls);
191
+ }
192
+ function completedTurn(parsed) {
193
+ if (typeof parsed !== "object" || parsed === null) {
194
+ return null;
195
+ }
196
+ const record = parsed;
197
+ if (record.method !== "_x.ai/session/update") {
198
+ return null;
199
+ }
200
+ if (typeof record.params !== "object" || record.params === null) {
201
+ return null;
202
+ }
203
+ const update = record.params.update;
204
+ if (typeof update !== "object" || update === null) {
205
+ return null;
206
+ }
207
+ const u = update;
208
+ if (u.sessionUpdate !== "turn_completed") {
209
+ return null;
210
+ }
211
+ if (typeof u.usage !== "object" || u.usage === null) {
212
+ // Completed without reaching a model — an error before the first call.
213
+ return null;
214
+ }
215
+ return {
216
+ promptId: typeof u.prompt_id === "string" ? u.prompt_id : undefined,
217
+ usage: u.usage,
218
+ };
219
+ }
220
+ async function foldStream(filePath, totals, unpriced) {
221
+ // Insertion-ordered, so the delta rule below sees turns in the order Grok
222
+ // wrote them, with a re-emitted prompt replacing its earlier record.
223
+ const turns = new Map();
224
+ const rl = createInterface({
225
+ input: createReadStream(filePath, { encoding: "utf8" }),
226
+ crlfDelay: Infinity,
227
+ });
228
+ try {
229
+ let lineNo = 0;
230
+ for await (const line of rl) {
231
+ lineNo += 1;
232
+ if (!line || line.charCodeAt(0) !== 123 /* '{' */) {
233
+ continue;
234
+ }
235
+ let parsed;
236
+ try {
237
+ parsed = JSON.parse(line);
238
+ }
239
+ catch {
240
+ // A stream being appended to ends in a partial line — normal.
241
+ continue;
242
+ }
243
+ const turn = completedTurn(parsed);
244
+ if (turn === null) {
245
+ continue;
246
+ }
247
+ turns.set(turn.promptId ?? `line:${lineNo}`, readTurn(turn.usage));
248
+ }
249
+ }
250
+ finally {
251
+ rl.close();
252
+ }
253
+ let prev;
254
+ for (const turn of turns.values()) {
255
+ const delta = prev !== undefined && continuesRun(prev, turn)
256
+ ? {
257
+ input: turn.input - prev.input,
258
+ output: turn.output - prev.output,
259
+ cacheRead: turn.cacheRead - prev.cacheRead,
260
+ cacheCreation: turn.cacheCreation - prev.cacheCreation,
261
+ calls: turn.calls - prev.calls,
262
+ }
263
+ : turn;
264
+ prev = turn;
265
+ const calls = delta.calls > 0 ? delta.calls : 1;
266
+ totals.requests += calls;
267
+ totals.inputTokens += delta.input;
268
+ totals.outputTokens += delta.output;
269
+ totals.cacheReadTokens += delta.cacheRead;
270
+ totals.cacheCreationTokens += delta.cacheCreation;
271
+ totals.unpricedRequests += calls;
272
+ for (const model of turn.models.length > 0 ? turn.models : ["unknown"]) {
273
+ unpriced.add(model);
274
+ }
275
+ }
276
+ }
277
+ export async function createGrokReader() {
278
+ return {
279
+ descriptor: {
280
+ id: CLI_ID,
281
+ displayName: "Grok Build",
282
+ verified: true,
283
+ // Per prompt id within a session, last record wins — a re-emitted
284
+ // terminal replaces the earlier one rather than adding to it.
285
+ dedupStrategy: "last-write-wins",
286
+ costConfidence: "unavailable",
287
+ requiresSqlite: false,
288
+ },
289
+ detect: async () => {
290
+ try {
291
+ return (await stat(sessionsRoot())).isDirectory();
292
+ }
293
+ catch {
294
+ return false;
295
+ }
296
+ },
297
+ scan: async (options) => {
298
+ const totals = emptyTotals();
299
+ const errors = [];
300
+ const unpriced = new Set();
301
+ const cutoff = resolveScanCutoffMs(options?.sinceDays);
302
+ let filesScanned = 0;
303
+ for (const file of await collectStreams(sessionsRoot(), errors)) {
304
+ try {
305
+ // One stream is one session, and its mtime is the session's last
306
+ // activity: a session is in or out of the window whole.
307
+ if (cutoff !== undefined && (await stat(file)).mtimeMs < cutoff) {
308
+ continue;
309
+ }
310
+ await foldStream(file, totals, unpriced);
311
+ filesScanned += 1;
312
+ }
313
+ catch (error) {
314
+ errors.push({
315
+ cliId: CLI_ID,
316
+ filePath: file,
317
+ message: error instanceof Error ? error.message : String(error),
318
+ });
319
+ }
320
+ }
321
+ totals.unpricedModels = [...unpriced].sort();
322
+ return { cliId: CLI_ID, totals, filesScanned, errors };
323
+ },
324
+ };
325
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Reads token usage out of Hermes Agent's SQLite state store.
3
+ *
4
+ * Hermes Agent is Nous Research's official CLI agent (github.com/NousResearch/
5
+ * hermes-agent), a Python program installed from its own install script — not
6
+ * an npm package. An earlier note in this folder said the opposite ("no
7
+ * official CLI, only an unofficial npm bridge"); it had searched npm for a
8
+ * product that is not distributed there, and is retracted here.
9
+ *
10
+ * Store: `$HERMES_HOME/state.db`, defaulting to `~/.hermes/state.db`, plus one
11
+ * `state.db` per profile under `profiles/<name>/`. SQLite, `schema_version`
12
+ * table (26 when this was written; `PRAGMA user_version` stays 0, so it is
13
+ * not the version to read). Confirmed on a real store produced by running the
14
+ * CLI itself, not inferred from documentation.
15
+ *
16
+ * Two tables carry usage, and they are NOT additive:
17
+ *
18
+ * sessions one row per session, with cumulative token columns
19
+ * for the PRIMARY task only.
20
+ * session_model_usage one row per (session, model, billing, task), each
21
+ * with its own `api_call_count`, token columns and cost.
22
+ *
23
+ * Measured on the real store: a one-prompt session held a `sessions` row of
24
+ * 10,568 input / 1 output / 1 call, and TWO usage rows — the primary task
25
+ * (`task = ''`, identical numbers) and a `title_generation` task of 248 / 8 /
26
+ * 1 call that the `sessions` aggregate does not include. The usage rows are
27
+ * therefore the complete record of what Hermes actually sent to a provider,
28
+ * and this reader sums them. The `sessions` aggregate is read only for a
29
+ * session that has no usage rows at all (a store older than the migration
30
+ * that introduced the table), and never in addition to them.
31
+ *
32
+ * Cost: Hermes records `estimated_cost_usd` with a `cost_status` of
33
+ * `estimated`, from its own pricing snapshot. That is a modeled figure and is
34
+ * reported as such. `actual_cost_usd` is `NOT NULL DEFAULT 0` on usage rows,
35
+ * so a zero there is a schema default, not evidence of a free call — it is
36
+ * used only when `cost_status` explicitly says the figure is actual. A row
37
+ * with no trustworthy cost is counted as unpriced and its model named.
38
+ *
39
+ * Cache and reasoning columns are reported as stored. Every real sample so far
40
+ * has them at zero, so whether `cache_read_tokens` is a subset of
41
+ * `input_tokens` (OpenAI convention) or disjoint from it (Anthropic
42
+ * convention) has not been measured, and no subtraction or folding is applied
43
+ * until it has. Reasoning tokens are not added to output for the same reason.
44
+ *
45
+ * The time window is a snapshot filter, not an attribution: every row is a
46
+ * cumulative counter, so a session that spans the cutoff is either included
47
+ * whole or excluded whole, keyed on its last activity. Timestamps are epoch
48
+ * SECONDS stored as REAL.
49
+ */
50
+ import type { LocalUsageReader } from "../types/index.js";
51
+ export declare function createHermesReader(): Promise<LocalUsageReader>;