@juspay/neurolink 11.20.0 → 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 +2 -2
- 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/server/routes/claudeProxyRoutes.d.ts +14 -0
- package/dist/server/routes/claudeProxyRoutes.js +3 -3
- 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
|
+
}
|
|
@@ -187,6 +187,20 @@ declare function executeClaudeFallbackTranslation(args: {
|
|
|
187
187
|
}) => void;
|
|
188
188
|
options: Parameters<ServerContext["neurolink"]["stream"]>[0];
|
|
189
189
|
providerLabel: string;
|
|
190
|
+
/**
|
|
191
|
+
* Idle timeout for the fallback stream, in ms. Defaults to
|
|
192
|
+
* FALLBACK_STREAM_IDLE_TIMEOUT_MS.
|
|
193
|
+
*
|
|
194
|
+
* Injectable purely so a test can drive the timeout path in milliseconds
|
|
195
|
+
* instead of two minutes. The alternative the coverage used before was
|
|
196
|
+
* patching `globalThis.setTimeout` to fire every timer at 0ms for the
|
|
197
|
+
* duration of an await — which rewrites the delay of ANY timer created in
|
|
198
|
+
* that window, not just this one's, inside a 280-case suite sharing a single
|
|
199
|
+
* process. That is not a hypothetical: an attempt to measure the patched
|
|
200
|
+
* case with its own `setTimeout`-based watchdog had the watchdog rewritten
|
|
201
|
+
* out from under it and reported an instant false hang.
|
|
202
|
+
*/
|
|
203
|
+
idleTimeoutMs?: number;
|
|
190
204
|
}): Promise<unknown>;
|
|
191
205
|
declare function executeClaudeFallbackWithRetry(args: Parameters<typeof executeClaudeFallbackTranslation>[0]): Promise<unknown>;
|
|
192
206
|
declare function buildClaudeAnthropicFailureResponse(args: {
|
|
@@ -2690,14 +2690,14 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
2690
2690
|
};
|
|
2691
2691
|
}
|
|
2692
2692
|
async function executeClaudeFallbackTranslation(args) {
|
|
2693
|
-
const { ctx, body, tracer, requestStartTime, logProxyBody, logFinalRequest, options, providerLabel, } = args;
|
|
2693
|
+
const { ctx, body, tracer, requestStartTime, logProxyBody, logFinalRequest, options, providerLabel, idleTimeoutMs = FALLBACK_STREAM_IDLE_TIMEOUT_MS, } = args;
|
|
2694
2694
|
const fallbackAbortController = new AbortController();
|
|
2695
2695
|
let streamResult;
|
|
2696
2696
|
try {
|
|
2697
2697
|
streamResult = await withTimeout(ctx.neurolink.stream({
|
|
2698
2698
|
...options,
|
|
2699
2699
|
abortSignal: fallbackAbortController.signal,
|
|
2700
|
-
}),
|
|
2700
|
+
}), idleTimeoutMs, `Fallback ${providerLabel} initialization timed out after ${idleTimeoutMs}ms`);
|
|
2701
2701
|
}
|
|
2702
2702
|
catch (error) {
|
|
2703
2703
|
fallbackAbortController.abort(error);
|
|
@@ -2708,7 +2708,7 @@ async function executeClaudeFallbackTranslation(args) {
|
|
|
2708
2708
|
let collectedText = "";
|
|
2709
2709
|
try {
|
|
2710
2710
|
while (true) {
|
|
2711
|
-
const { value: chunk, done } = await withTimeout(iterator.next(),
|
|
2711
|
+
const { value: chunk, done } = await withTimeout(iterator.next(), idleTimeoutMs, `Fallback ${providerLabel} stream timed out after ${idleTimeoutMs}ms of inactivity`);
|
|
2712
2712
|
if (done) {
|
|
2713
2713
|
return collectedText;
|
|
2714
2714
|
}
|
|
@@ -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": {
|