@juspay/neurolink 11.20.1 → 11.21.0

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.
@@ -52,3 +52,17 @@ registerLocalUsageReader({
52
52
  return createCodexReader();
53
53
  },
54
54
  });
55
+ registerLocalUsageReader({
56
+ descriptor: {
57
+ id: "opencode",
58
+ displayName: "OpenCode",
59
+ verified: true,
60
+ dedupStrategy: "rowid-high-water-mark",
61
+ costConfidence: "unavailable",
62
+ requiresSqlite: true,
63
+ },
64
+ factory: async () => {
65
+ const { createOpenCodeReader } = await import("./openCodeReader.js");
66
+ return createOpenCodeReader();
67
+ },
68
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Reads token usage out of OpenCode's local SQLite store.
3
+ *
4
+ * Store: `~/.local/share/opencode/opencode.db` (NOT `~/.config/opencode`,
5
+ * which holds only configuration). Usage lives on the `message` table, whose
6
+ * `data` column is a JSON blob carrying `role`, `modelID`, `providerID`,
7
+ * `cost` and a `tokens` object.
8
+ *
9
+ * Two things the real data settled, both of which would corrupt totals
10
+ * silently if assumed instead of checked.
11
+ *
12
+ * **Cache is DISJOINT from input here.** Measured across all 4,674
13
+ * usage-bearing messages on a reference machine, `total` equals
14
+ * `input + output + cache.read + cache.write` — never `input + output` alone
15
+ * unless cache happened to be zero. This is the opposite of Codex, where
16
+ * `cached_input_tokens` is a SUBSET of `input_tokens` and has to be subtracted
17
+ * back out. Three readers now, three conventions; none of them is safe to
18
+ * infer from the others.
19
+ *
20
+ * **OpenCode's own `cost` field is not usable.** It was 0 in all 4,674
21
+ * messages, while the provider mix spanned `github-copilot` (a subscription),
22
+ * `openai` (metered) and `neurolink` (this proxy). A single cost figure for
23
+ * that mixture would be wrong whichever way it was computed, so tokens are
24
+ * reported and cost is declared `unavailable` rather than invented or
25
+ * quietly zeroed.
26
+ *
27
+ * Worth knowing for anyone summing sources: traffic OpenCode sent through the
28
+ * NeuroLink proxy appears under `providerID: "neurolink"` here AND in the
29
+ * proxy's own ledger. The two are independent measurements of the same
30
+ * requests, not additive. On the reference machine that was 41 messages and
31
+ * ~772k tokens against 458M total, so it is small — but it is not zero, and a
32
+ * dashboard adding local usage to proxy usage double-counts exactly that
33
+ * slice.
34
+ */
35
+ import type { LocalUsageReader } from "../types/index.js";
36
+ export declare function createOpenCodeReader(): Promise<LocalUsageReader>;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Reads token usage out of OpenCode's local SQLite store.
3
+ *
4
+ * Store: `~/.local/share/opencode/opencode.db` (NOT `~/.config/opencode`,
5
+ * which holds only configuration). Usage lives on the `message` table, whose
6
+ * `data` column is a JSON blob carrying `role`, `modelID`, `providerID`,
7
+ * `cost` and a `tokens` object.
8
+ *
9
+ * Two things the real data settled, both of which would corrupt totals
10
+ * silently if assumed instead of checked.
11
+ *
12
+ * **Cache is DISJOINT from input here.** Measured across all 4,674
13
+ * usage-bearing messages on a reference machine, `total` equals
14
+ * `input + output + cache.read + cache.write` — never `input + output` alone
15
+ * unless cache happened to be zero. This is the opposite of Codex, where
16
+ * `cached_input_tokens` is a SUBSET of `input_tokens` and has to be subtracted
17
+ * back out. Three readers now, three conventions; none of them is safe to
18
+ * infer from the others.
19
+ *
20
+ * **OpenCode's own `cost` field is not usable.** It was 0 in all 4,674
21
+ * messages, while the provider mix spanned `github-copilot` (a subscription),
22
+ * `openai` (metered) and `neurolink` (this proxy). A single cost figure for
23
+ * that mixture would be wrong whichever way it was computed, so tokens are
24
+ * reported and cost is declared `unavailable` rather than invented or
25
+ * quietly zeroed.
26
+ *
27
+ * Worth knowing for anyone summing sources: traffic OpenCode sent through the
28
+ * NeuroLink proxy appears under `providerID: "neurolink"` here AND in the
29
+ * proxy's own ledger. The two are independent measurements of the same
30
+ * requests, not additive. On the reference machine that was 41 messages and
31
+ * ~772k tokens against 458M total, so it is small — but it is not zero, and a
32
+ * dashboard adding local usage to proxy usage double-counts exactly that
33
+ * slice.
34
+ */
35
+ import { stat } from "fs/promises";
36
+ import { homedir } from "os";
37
+ import { join } from "path";
38
+ const CLI_ID = "opencode";
39
+ const DEFAULT_SINCE_DAYS = 30;
40
+ function databasePath() {
41
+ return join(homedir(), ".local", "share", "opencode", "opencode.db");
42
+ }
43
+ function emptyTotals() {
44
+ return {
45
+ requests: 0,
46
+ inputTokens: 0,
47
+ outputTokens: 0,
48
+ cacheReadTokens: 0,
49
+ cacheCreationTokens: 0,
50
+ costUsd: 0,
51
+ costConfidence: "unavailable",
52
+ unpricedRequests: 0,
53
+ unpricedModels: [],
54
+ };
55
+ }
56
+ function num(value) {
57
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
58
+ }
59
+ export async function createOpenCodeReader() {
60
+ return {
61
+ descriptor: {
62
+ id: CLI_ID,
63
+ displayName: "OpenCode",
64
+ verified: true,
65
+ dedupStrategy: "rowid-high-water-mark",
66
+ costConfidence: "unavailable",
67
+ requiresSqlite: true,
68
+ },
69
+ detect: async () => {
70
+ try {
71
+ const info = await stat(databasePath());
72
+ return info.isFile();
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ },
78
+ scan: async (options) => {
79
+ const totals = emptyTotals();
80
+ const errors = [];
81
+ const models = new Set();
82
+ const dbPath = databasePath();
83
+ // `node:sqlite` is built in from Node 22 but still flagged experimental,
84
+ // so it can be absent or change shape. Imported lazily and behind a
85
+ // try/catch: a runtime without it must degrade to a reported failure for
86
+ // this one reader, not take down a scan of all the others.
87
+ let DatabaseSync;
88
+ try {
89
+ const sqlite = await import("node:sqlite");
90
+ // Validated, not asserted. The module is experimental and may change
91
+ // shape between Node releases; a cast would let a changed export sail
92
+ // through and fail later as an unrelated TypeError deep in the scan.
93
+ if (typeof sqlite === "object" &&
94
+ sqlite !== null &&
95
+ "DatabaseSync" in sqlite &&
96
+ typeof sqlite.DatabaseSync ===
97
+ "function") {
98
+ DatabaseSync = sqlite.DatabaseSync;
99
+ }
100
+ }
101
+ catch (error) {
102
+ errors.push({
103
+ cliId: CLI_ID,
104
+ filePath: dbPath,
105
+ message: `node:sqlite unavailable on this runtime: ${error instanceof Error ? error.message : String(error)}`,
106
+ });
107
+ return { cliId: CLI_ID, totals, filesScanned: 0, errors };
108
+ }
109
+ if (!DatabaseSync) {
110
+ errors.push({
111
+ cliId: CLI_ID,
112
+ filePath: dbPath,
113
+ message: "node:sqlite did not expose a callable DatabaseSync — the experimental API has likely changed shape",
114
+ });
115
+ return { cliId: CLI_ID, totals, filesScanned: 0, errors };
116
+ }
117
+ const sinceDays = options?.sinceDays ?? DEFAULT_SINCE_DAYS;
118
+ const cutoffMs = Number.isFinite(sinceDays) && sinceDays > 0
119
+ ? Date.now() - sinceDays * 86_400_000
120
+ : 0;
121
+ let db;
122
+ try {
123
+ // Read-only: this is the user's live store and OpenCode may be running.
124
+ db = new DatabaseSync(dbPath, { readOnly: true });
125
+ // Filtered in SQL rather than in JS. `time_created` is epoch ms, and
126
+ // the table holds thousands of rows whose `data` blobs are large — the
127
+ // point of the time window is not reading them at all.
128
+ const rows = db
129
+ .prepare(`SELECT data FROM message WHERE time_created >= ${cutoffMs}`)
130
+ .all();
131
+ for (const row of rows) {
132
+ if (typeof row.data !== "string") {
133
+ continue;
134
+ }
135
+ let parsed;
136
+ try {
137
+ parsed = JSON.parse(row.data);
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ const message = parsed;
143
+ const tokens = message.tokens;
144
+ if (!tokens || message.role !== "assistant") {
145
+ continue;
146
+ }
147
+ const input = num(tokens.input);
148
+ const output = num(tokens.output);
149
+ const cacheRead = num(tokens.cache?.read);
150
+ const cacheWrite = num(tokens.cache?.write);
151
+ if (input === 0 &&
152
+ output === 0 &&
153
+ cacheRead === 0 &&
154
+ cacheWrite === 0) {
155
+ continue;
156
+ }
157
+ totals.requests += 1;
158
+ // Added as-is: cache is disjoint from input in this store. See the
159
+ // note on this module — Codex is the other way round.
160
+ totals.inputTokens += input;
161
+ totals.outputTokens += output;
162
+ totals.cacheReadTokens += cacheRead;
163
+ totals.cacheCreationTokens += cacheWrite;
164
+ if (message.modelID) {
165
+ models.add(message.modelID);
166
+ }
167
+ }
168
+ }
169
+ catch (error) {
170
+ errors.push({
171
+ cliId: CLI_ID,
172
+ filePath: dbPath,
173
+ message: error instanceof Error ? error.message : String(error),
174
+ });
175
+ }
176
+ finally {
177
+ try {
178
+ db?.close();
179
+ }
180
+ catch {
181
+ // Closing a database that failed to open is not a second failure.
182
+ }
183
+ }
184
+ // Unpriced by construction rather than by a failed lookup — see the note
185
+ // on this module about the mixed subscription/metered provider set.
186
+ totals.unpricedRequests = totals.requests;
187
+ totals.unpricedModels = [...models].sort();
188
+ return {
189
+ cliId: CLI_ID,
190
+ totals,
191
+ filesScanned: totals.requests > 0 || errors.length === 0 ? 1 : 0,
192
+ errors,
193
+ };
194
+ },
195
+ };
196
+ }
@@ -151,3 +151,21 @@ export type LocalUsageCodexSessionRollup = {
151
151
  /** token_count events where the cumulative total actually advanced. */
152
152
  billableEvents: number;
153
153
  };
154
+ /**
155
+ * The slice of `node:sqlite`'s `DatabaseSync` the OpenCode reader uses.
156
+ *
157
+ * Deliberately minimal. `node:sqlite` is still flagged experimental and may
158
+ * change shape between Node releases, so the reader validates this much at
159
+ * runtime rather than trusting a type assertion — naming only what is actually
160
+ * called keeps that check small and honest.
161
+ */
162
+ export type LocalUsageSqliteDatabase = {
163
+ prepare: (sql: string) => {
164
+ all: () => unknown[];
165
+ };
166
+ close: () => void;
167
+ };
168
+ /** Constructor shape for the same. */
169
+ export type LocalUsageSqliteDatabaseCtor = new (path: string, options?: {
170
+ readOnly?: boolean;
171
+ }) => LocalUsageSqliteDatabase;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.20.1",
3
+ "version": "11.21.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {