@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.
- package/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +395 -395
- package/dist/localUsage/localUsageReaderRegistry.js +14 -0
- package/dist/localUsage/openCodeReader.d.ts +36 -0
- package/dist/localUsage/openCodeReader.js +196 -0
- package/dist/types/localUsage.d.ts +18 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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": {
|