@juspay/neurolink 11.18.4 → 11.19.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 +5 -1
- package/dist/browser/neurolink.min.js +360 -373
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/localUsage/claudeCodeReader.d.ts +25 -0
- package/dist/localUsage/claudeCodeReader.js +222 -0
- package/dist/localUsage/index.d.ts +18 -0
- package/dist/localUsage/index.js +46 -0
- package/dist/localUsage/localUsageReaderRegistry.d.ts +14 -0
- package/dist/localUsage/localUsageReaderRegistry.js +40 -0
- package/dist/proxy/geminiFormat.js +25 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/localUsage.d.ts +139 -0
- package/dist/types/localUsage.js +13 -0
- package/dist/utils/messageBuilder.js +0 -76
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -445,3 +445,4 @@ export { TripwireEvaluator, createDefaultTripwireEvaluator, commonTripwires, } f
|
|
|
445
445
|
export { Agent } from "./agent/agent.js";
|
|
446
446
|
export { AgentNetwork } from "./agent/agentNetwork.js";
|
|
447
447
|
export { AgentCoordinator, TaskDistributor, MessageBus, NetworkOrchestrator, NetworkTopology, TopologyBuilder, } from "./agent/index.js";
|
|
448
|
+
export { createLocalUsageReader, getLocalUsageDescriptors, getRegisteredLocalUsageCliIds, readAllLocalUsage, registerLocalUsageReader, } from "./localUsage/index.js";
|
package/dist/index.js
CHANGED
|
@@ -745,3 +745,10 @@ AgentCoordinator, TaskDistributor,
|
|
|
745
745
|
MessageBus,
|
|
746
746
|
// Orchestration
|
|
747
747
|
NetworkOrchestrator, NetworkTopology, TopologyBuilder, } from "./agent/index.js";
|
|
748
|
+
// ============================================================================
|
|
749
|
+
// Local usage exports
|
|
750
|
+
// ============================================================================
|
|
751
|
+
// Token spend read from each CLI's own session logs, rather than from proxy
|
|
752
|
+
// traffic. Exported because the proxy's ledger only sees CLIs that route
|
|
753
|
+
// through it; this covers the rest, and covers history predating the proxy.
|
|
754
|
+
export { createLocalUsageReader, getLocalUsageDescriptors, getRegisteredLocalUsageCliIds, readAllLocalUsage, registerLocalUsageReader, } from "./localUsage/index.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads token usage out of Claude Code's own session transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Layout, confirmed on a real machine rather than from documentation:
|
|
5
|
+
* `~/.claude/projects/<sanitized-cwd>/<sessionId>.jsonl`, where the sanitized
|
|
6
|
+
* cwd is the absolute working-directory path with every `/` replaced by `-`.
|
|
7
|
+
*
|
|
8
|
+
* The load-bearing detail is that top-level session transcripts are a tiny
|
|
9
|
+
* minority of the files. On the machine this was written against, 17,439
|
|
10
|
+
* transcripts totalling 9.7 GB broke down as roughly 103 top-level sessions
|
|
11
|
+
* against 17,116 subagent-task transcripts nested under
|
|
12
|
+
* `<sessionId>/subagents/`. A reader that globbed only `<project>/*.jsonl`
|
|
13
|
+
* would miss essentially all of a subagent-heavy user's real spend, so this
|
|
14
|
+
* one recurses.
|
|
15
|
+
*
|
|
16
|
+
* The files are not homogeneous message streams. A single 14,747-line
|
|
17
|
+
* transcript carried eleven distinct `type` values — `assistant`, `user`,
|
|
18
|
+
* `attachment`, `pr-link`, `last-prompt`, `mode`, `permission-mode`,
|
|
19
|
+
* `queue-operation`, `system`, `file-history-delta`, `file-history-snapshot` —
|
|
20
|
+
* several of which have no `message` key at all. Every one of its 6,028
|
|
21
|
+
* `type: "assistant"` lines carried a `message.usage` object, so that filter is
|
|
22
|
+
* safe, but everything else must be skipped rather than treated as malformed.
|
|
23
|
+
*/
|
|
24
|
+
import type { LocalUsageReader } from "../types/index.js";
|
|
25
|
+
export declare function createClaudeCodeReader(): Promise<LocalUsageReader>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads token usage out of Claude Code's own session transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Layout, confirmed on a real machine rather than from documentation:
|
|
5
|
+
* `~/.claude/projects/<sanitized-cwd>/<sessionId>.jsonl`, where the sanitized
|
|
6
|
+
* cwd is the absolute working-directory path with every `/` replaced by `-`.
|
|
7
|
+
*
|
|
8
|
+
* The load-bearing detail is that top-level session transcripts are a tiny
|
|
9
|
+
* minority of the files. On the machine this was written against, 17,439
|
|
10
|
+
* transcripts totalling 9.7 GB broke down as roughly 103 top-level sessions
|
|
11
|
+
* against 17,116 subagent-task transcripts nested under
|
|
12
|
+
* `<sessionId>/subagents/`. A reader that globbed only `<project>/*.jsonl`
|
|
13
|
+
* would miss essentially all of a subagent-heavy user's real spend, so this
|
|
14
|
+
* one recurses.
|
|
15
|
+
*
|
|
16
|
+
* The files are not homogeneous message streams. A single 14,747-line
|
|
17
|
+
* transcript carried eleven distinct `type` values — `assistant`, `user`,
|
|
18
|
+
* `attachment`, `pr-link`, `last-prompt`, `mode`, `permission-mode`,
|
|
19
|
+
* `queue-operation`, `system`, `file-history-delta`, `file-history-snapshot` —
|
|
20
|
+
* several of which have no `message` key at all. Every one of its 6,028
|
|
21
|
+
* `type: "assistant"` lines carried a `message.usage` object, so that filter is
|
|
22
|
+
* safe, but everything else must be skipped rather than treated as malformed.
|
|
23
|
+
*/
|
|
24
|
+
import { createReadStream } from "fs";
|
|
25
|
+
import { createInterface } from "readline";
|
|
26
|
+
import { readdir, stat } from "fs/promises";
|
|
27
|
+
import { homedir } from "os";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
import { calculateCost, hasPricing } from "../utils/pricing.js";
|
|
30
|
+
const CLI_ID = "claude-code";
|
|
31
|
+
const DEFAULT_SINCE_DAYS = 30;
|
|
32
|
+
const PROVIDER = "anthropic";
|
|
33
|
+
function projectsRoot() {
|
|
34
|
+
return join(homedir(), ".claude", "projects");
|
|
35
|
+
}
|
|
36
|
+
function emptyTotals() {
|
|
37
|
+
return {
|
|
38
|
+
requests: 0,
|
|
39
|
+
inputTokens: 0,
|
|
40
|
+
outputTokens: 0,
|
|
41
|
+
cacheReadTokens: 0,
|
|
42
|
+
cacheCreationTokens: 0,
|
|
43
|
+
costUsd: 0,
|
|
44
|
+
costConfidence: "modeled",
|
|
45
|
+
unpricedRequests: 0,
|
|
46
|
+
unpricedModels: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Every `.jsonl` under the projects root, at any depth.
|
|
51
|
+
*
|
|
52
|
+
* Depth matters here — see the module header. `withFileTypes` avoids a stat
|
|
53
|
+
* per entry, and a directory that cannot be read is skipped rather than
|
|
54
|
+
* aborting the walk: one unreadable project must not cost the other eleven
|
|
55
|
+
* thousand files.
|
|
56
|
+
*/
|
|
57
|
+
async function collectTranscripts(dir, out) {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const full = join(dir, entry.name);
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
await collectTranscripts(full, out);
|
|
69
|
+
}
|
|
70
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
71
|
+
out.push(full);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function num(value) {
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Fold one transcript into `totals`.
|
|
80
|
+
*
|
|
81
|
+
* Read line by line rather than into a string: these files reach tens of
|
|
82
|
+
* megabytes, and a full-history scan opens thousands of them.
|
|
83
|
+
*
|
|
84
|
+
* Dedup is by `message.id`, keeping the LARGEST output count seen for that id.
|
|
85
|
+
* A resumed session re-logs turns it has already written, and the re-logged
|
|
86
|
+
* copy can carry a higher output count than the first — taking the max rather
|
|
87
|
+
* than the first or last is what makes a resumed session total correct instead
|
|
88
|
+
* of either under- or double-counted. The map is per-file and discarded after,
|
|
89
|
+
* so it stays bounded no matter how many files a scan covers.
|
|
90
|
+
*/
|
|
91
|
+
async function foldTranscript(filePath, totals, unpriced) {
|
|
92
|
+
const seen = new Map();
|
|
93
|
+
const rl = createInterface({
|
|
94
|
+
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
95
|
+
crlfDelay: Infinity,
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
for await (const line of rl) {
|
|
99
|
+
if (!line || line.charCodeAt(0) !== 123 /* '{' */) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(line);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A transcript being appended to while we read it ends in a partial
|
|
108
|
+
// line. That is normal, not corruption — skip it.
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const record = parsed;
|
|
112
|
+
if (record.type !== "assistant" || !record.message?.usage) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const usage = record.message.usage;
|
|
116
|
+
const id = record.message.id;
|
|
117
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const candidate = {
|
|
121
|
+
model: record.message.model ?? "unknown",
|
|
122
|
+
input: num(usage.input_tokens),
|
|
123
|
+
output: num(usage.output_tokens),
|
|
124
|
+
read: num(usage.cache_read_input_tokens),
|
|
125
|
+
create: num(usage.cache_creation_input_tokens),
|
|
126
|
+
};
|
|
127
|
+
const existing = seen.get(id);
|
|
128
|
+
if (!existing || candidate.output > existing.output) {
|
|
129
|
+
seen.set(id, candidate);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
rl.close();
|
|
135
|
+
}
|
|
136
|
+
for (const turn of seen.values()) {
|
|
137
|
+
totals.requests += 1;
|
|
138
|
+
totals.inputTokens += turn.input;
|
|
139
|
+
totals.outputTokens += turn.output;
|
|
140
|
+
totals.cacheReadTokens += turn.read;
|
|
141
|
+
totals.cacheCreationTokens += turn.create;
|
|
142
|
+
if (hasPricing(PROVIDER, turn.model)) {
|
|
143
|
+
totals.costUsd += calculateCost(PROVIDER, turn.model, {
|
|
144
|
+
input: turn.input,
|
|
145
|
+
output: turn.output,
|
|
146
|
+
total: turn.input + turn.output,
|
|
147
|
+
cacheReadTokens: turn.read,
|
|
148
|
+
cacheCreationTokens: turn.create,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
totals.unpricedRequests += 1;
|
|
153
|
+
unpriced.add(turn.model);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function createClaudeCodeReader() {
|
|
158
|
+
return {
|
|
159
|
+
descriptor: {
|
|
160
|
+
id: CLI_ID,
|
|
161
|
+
displayName: "Claude Code",
|
|
162
|
+
verified: true,
|
|
163
|
+
dedupStrategy: "message-id-keep-max",
|
|
164
|
+
costConfidence: "modeled",
|
|
165
|
+
requiresSqlite: false,
|
|
166
|
+
},
|
|
167
|
+
detect: async () => {
|
|
168
|
+
try {
|
|
169
|
+
const info = await stat(projectsRoot());
|
|
170
|
+
return info.isDirectory();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
scan: async (options) => {
|
|
177
|
+
const totals = emptyTotals();
|
|
178
|
+
const errors = [];
|
|
179
|
+
const unpriced = new Set();
|
|
180
|
+
const files = [];
|
|
181
|
+
await collectTranscripts(projectsRoot(), files);
|
|
182
|
+
const sinceDays = options?.sinceDays ?? DEFAULT_SINCE_DAYS;
|
|
183
|
+
const cutoff = Number.isFinite(sinceDays) && sinceDays > 0
|
|
184
|
+
? Date.now() - sinceDays * 86_400_000
|
|
185
|
+
: undefined;
|
|
186
|
+
let filesScanned = 0;
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
try {
|
|
189
|
+
if (cutoff !== undefined) {
|
|
190
|
+
const info = await stat(file);
|
|
191
|
+
if (info.mtimeMs < cutoff) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
await foldTranscript(file, totals, unpriced);
|
|
196
|
+
filesScanned += 1;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// One unreadable transcript is a reportable fact, not a reason to
|
|
200
|
+
// lose the totals from every other file in the scan.
|
|
201
|
+
errors.push({
|
|
202
|
+
cliId: CLI_ID,
|
|
203
|
+
filePath: file,
|
|
204
|
+
message: error instanceof Error ? error.message : String(error),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
totals.unpricedModels = [...unpriced].sort();
|
|
209
|
+
// Confidence stays "modeled" even when some turns went unpriced, and the
|
|
210
|
+
// distinction matters. "heuristic" means the number itself was estimated
|
|
211
|
+
// — Kiro's byte-count figure, say. Here every dollar in `costUsd` came
|
|
212
|
+
// from a real rate table; what is missing is turns, not accuracy. That
|
|
213
|
+
// incompleteness is already reported precisely by `unpricedRequests` and
|
|
214
|
+
// `unpricedModels` (on this machine: Claude Code's internal
|
|
215
|
+
// "<synthetic>" model), so downgrading the whole row would overstate the
|
|
216
|
+
// doubt and make a genuinely estimated total indistinguishable from this
|
|
217
|
+
// one.
|
|
218
|
+
totals.costUsd = Math.round(totals.costUsd * 1_000_000) / 1_000_000;
|
|
219
|
+
return { cliId: CLI_ID, totals, filesScanned, errors };
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local usage: token spend read from each CLI's own session logs.
|
|
3
|
+
*
|
|
4
|
+
* The proxy's ledger can only account for traffic that went through it, which
|
|
5
|
+
* caps coverage at the CLIs that expose a base-URL override. Every CLI writes
|
|
6
|
+
* a local transcript regardless, so reading those covers the rest — and covers
|
|
7
|
+
* history from before the proxy existed.
|
|
8
|
+
*/
|
|
9
|
+
import type { LocalUsageAggregateReport, LocalUsageScanOptions } from "../types/index.js";
|
|
10
|
+
export { createLocalUsageReader, getLocalUsageDescriptors, getRegisteredLocalUsageCliIds, registerLocalUsageReader, } from "./localUsageReaderRegistry.js";
|
|
11
|
+
/**
|
|
12
|
+
* Scan every registered reader whose CLI is actually present on this machine.
|
|
13
|
+
*
|
|
14
|
+
* "Not installed" and "failed" are reported separately and deliberately: a CLI
|
|
15
|
+
* the user never installed is not an error, and collapsing the two would make
|
|
16
|
+
* a broken reader indistinguishable from an absent one.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readAllLocalUsage(options?: LocalUsageScanOptions): Promise<LocalUsageAggregateReport>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local usage: token spend read from each CLI's own session logs.
|
|
3
|
+
*
|
|
4
|
+
* The proxy's ledger can only account for traffic that went through it, which
|
|
5
|
+
* caps coverage at the CLIs that expose a base-URL override. Every CLI writes
|
|
6
|
+
* a local transcript regardless, so reading those covers the rest — and covers
|
|
7
|
+
* history from before the proxy existed.
|
|
8
|
+
*/
|
|
9
|
+
import { createLocalUsageReader, getRegisteredLocalUsageCliIds, } from "./localUsageReaderRegistry.js";
|
|
10
|
+
export { createLocalUsageReader, getLocalUsageDescriptors, getRegisteredLocalUsageCliIds, registerLocalUsageReader, } from "./localUsageReaderRegistry.js";
|
|
11
|
+
/**
|
|
12
|
+
* Scan every registered reader whose CLI is actually present on this machine.
|
|
13
|
+
*
|
|
14
|
+
* "Not installed" and "failed" are reported separately and deliberately: a CLI
|
|
15
|
+
* the user never installed is not an error, and collapsing the two would make
|
|
16
|
+
* a broken reader indistinguishable from an absent one.
|
|
17
|
+
*/
|
|
18
|
+
export async function readAllLocalUsage(options) {
|
|
19
|
+
const totals = {};
|
|
20
|
+
const failures = [];
|
|
21
|
+
const notInstalled = [];
|
|
22
|
+
for (const cliId of getRegisteredLocalUsageCliIds()) {
|
|
23
|
+
try {
|
|
24
|
+
const reader = await createLocalUsageReader(cliId);
|
|
25
|
+
if (!(await reader.detect())) {
|
|
26
|
+
notInstalled.push(cliId);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const result = await reader.scan(options);
|
|
30
|
+
totals[cliId] = result.totals;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// One reader throwing must not lose the others' results.
|
|
34
|
+
failures.push({
|
|
35
|
+
cliId,
|
|
36
|
+
message: error instanceof Error ? error.message : String(error),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
generatedAt: new Date().toISOString(),
|
|
42
|
+
totals,
|
|
43
|
+
failures,
|
|
44
|
+
notInstalled,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of local-usage readers, one per CLI.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `providerRegistry.ts`: descriptors are static and always available,
|
|
5
|
+
* while the reader itself arrives through a dynamic import inside its factory.
|
|
6
|
+
* Same reason as the provider registry — a static import graph across a dozen
|
|
7
|
+
* readers is how circular dependencies start, and it would also force every
|
|
8
|
+
* reader's cost of loading onto a caller who only wanted one of them.
|
|
9
|
+
*/
|
|
10
|
+
import type { LocalUsageCliId, LocalUsageReader, LocalUsageReaderDescriptor, LocalUsageReaderRegistration } from "../types/index.js";
|
|
11
|
+
export declare function registerLocalUsageReader(registration: LocalUsageReaderRegistration): void;
|
|
12
|
+
export declare function getLocalUsageDescriptors(): LocalUsageReaderDescriptor[];
|
|
13
|
+
export declare function getRegisteredLocalUsageCliIds(): LocalUsageCliId[];
|
|
14
|
+
export declare function createLocalUsageReader(cliId: LocalUsageCliId): Promise<LocalUsageReader>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of local-usage readers, one per CLI.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `providerRegistry.ts`: descriptors are static and always available,
|
|
5
|
+
* while the reader itself arrives through a dynamic import inside its factory.
|
|
6
|
+
* Same reason as the provider registry — a static import graph across a dozen
|
|
7
|
+
* readers is how circular dependencies start, and it would also force every
|
|
8
|
+
* reader's cost of loading onto a caller who only wanted one of them.
|
|
9
|
+
*/
|
|
10
|
+
const registry = new Map();
|
|
11
|
+
export function registerLocalUsageReader(registration) {
|
|
12
|
+
registry.set(registration.descriptor.id, registration);
|
|
13
|
+
}
|
|
14
|
+
export function getLocalUsageDescriptors() {
|
|
15
|
+
return [...registry.values()].map((entry) => entry.descriptor);
|
|
16
|
+
}
|
|
17
|
+
export function getRegisteredLocalUsageCliIds() {
|
|
18
|
+
return [...registry.keys()];
|
|
19
|
+
}
|
|
20
|
+
export async function createLocalUsageReader(cliId) {
|
|
21
|
+
const registration = registry.get(cliId);
|
|
22
|
+
if (!registration) {
|
|
23
|
+
throw new Error(`No local-usage reader registered for "${cliId}". Registered: ${getRegisteredLocalUsageCliIds().join(", ") || "(none)"}`);
|
|
24
|
+
}
|
|
25
|
+
return registration.factory();
|
|
26
|
+
}
|
|
27
|
+
registerLocalUsageReader({
|
|
28
|
+
descriptor: {
|
|
29
|
+
id: "claude-code",
|
|
30
|
+
displayName: "Claude Code",
|
|
31
|
+
verified: true,
|
|
32
|
+
dedupStrategy: "message-id-keep-max",
|
|
33
|
+
costConfidence: "modeled",
|
|
34
|
+
requiresSqlite: false,
|
|
35
|
+
},
|
|
36
|
+
factory: async () => {
|
|
37
|
+
const { createClaudeCodeReader } = await import("./claudeCodeReader.js");
|
|
38
|
+
return createClaudeCodeReader();
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
*/
|
|
21
21
|
/** Google's role for assistant turns. */
|
|
22
22
|
const MODEL_ROLE = "model";
|
|
23
|
+
/**
|
|
24
|
+
* Sent as `input.text` when a request's final turn is a model turn.
|
|
25
|
+
*
|
|
26
|
+
* Google lets a client continue generation from the assistant's own last turn;
|
|
27
|
+
* the chat-completions wire format the engine targets has no way to say that,
|
|
28
|
+
* and an empty prompt is rejected before any provider is reached.
|
|
29
|
+
*/
|
|
30
|
+
const CONTINUATION_PROMPT = "Continue.";
|
|
23
31
|
function partsToText(parts) {
|
|
24
32
|
if (!Array.isArray(parts)) {
|
|
25
33
|
return "";
|
|
@@ -91,11 +99,24 @@ export function parseGeminiRequest(model, body, stream) {
|
|
|
91
99
|
// it, which is the same lost-turn bug one case further along.
|
|
92
100
|
//
|
|
93
101
|
// A terminal placeholder restores the invariant: the slice removes this
|
|
94
|
-
// instead of the model turn.
|
|
95
|
-
//
|
|
96
|
-
//
|
|
102
|
+
// instead of the model turn.
|
|
103
|
+
//
|
|
104
|
+
// The prompt needs its own answer, and leaving it "" was a 500. A
|
|
105
|
+
// model-final request carries no user turn to send as `input.text`, and
|
|
106
|
+
// NeuroLink's stream() rejects an empty one outright — "Stream options must
|
|
107
|
+
// include either input.text, input.audio, or stt.audio" — so every continue
|
|
108
|
+
// from a model turn failed at the door rather than reaching a provider. The
|
|
109
|
+
// placeholder above fixed the history slice but never exercised this path,
|
|
110
|
+
// because nothing had sent the request.
|
|
111
|
+
//
|
|
112
|
+
// Google's semantics for a model-final `contents` are "keep going", and the
|
|
113
|
+
// chat-completions shape the engine translates into has no assistant-prefill
|
|
114
|
+
// to express that. An explicit continuation instruction is the closest
|
|
115
|
+
// faithful equivalent: the full conversation still arrives as history, and
|
|
116
|
+
// the model is told to continue it rather than being handed an empty turn.
|
|
97
117
|
if (turns.length > 0 && turns[turns.length - 1].role !== "user") {
|
|
98
|
-
conversationMessages.push({ role: "user", content:
|
|
118
|
+
conversationMessages.push({ role: "user", content: CONTINUATION_PROMPT });
|
|
119
|
+
prompt = CONTINUATION_PROMPT;
|
|
99
120
|
}
|
|
100
121
|
const numeric = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
101
122
|
const stops = generationConfig.stopSequences;
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for `src/lib/localUsage/` — reading token usage out of each AI CLI's
|
|
3
|
+
* own local session logs.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists alongside the proxy's ledger: the proxy can only account for
|
|
6
|
+
* traffic that went through it, which caps coverage at the CLIs that expose a
|
|
7
|
+
* base-URL override. Every CLI writes its own local transcript regardless, so
|
|
8
|
+
* reading those recovers the rest — and recovers history from before the proxy
|
|
9
|
+
* was ever installed. It is also an independent source of truth: a pricing
|
|
10
|
+
* defect in the proxy's own accounting cannot hide from a reader that derives
|
|
11
|
+
* cost from a different input.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Stable identifier for one CLI this subsystem can read local usage from.
|
|
15
|
+
* Kebab-case, matching `CliProxyClientConfigurator.id`'s convention — a
|
|
16
|
+
* different registry, but the same repo-wide convention for CLI identifiers.
|
|
17
|
+
*/
|
|
18
|
+
export type LocalUsageCliId = "claude-code" | "codex" | "gemini-cli" | "opencode" | "qwen-code" | "copilot-cli" | "cursor" | "amp" | "hermes" | "kiro" | "antigravity" | "grok";
|
|
19
|
+
/**
|
|
20
|
+
* How much to trust a computed cost figure.
|
|
21
|
+
*
|
|
22
|
+
* Not decoration: some CLIs are flat-rate subscriptions where a per-request
|
|
23
|
+
* cost is meaningless, and at least one publishes a byte heuristic rather than
|
|
24
|
+
* a real number. A caller must never render "heuristic" with the same
|
|
25
|
+
* confidence as "modeled", so the distinction travels with the number.
|
|
26
|
+
*/
|
|
27
|
+
export type LocalUsageCostConfidence = "modeled" | "unavailable" | "heuristic";
|
|
28
|
+
/**
|
|
29
|
+
* How a reader avoids counting the same turn twice.
|
|
30
|
+
*
|
|
31
|
+
* Metadata on the descriptor, for introspection and for the person writing the
|
|
32
|
+
* next reader — the aggregator does not branch on it.
|
|
33
|
+
*/
|
|
34
|
+
export type LocalUsageDedupStrategy = "message-id-keep-max" | "last-write-wins" | "rowid-high-water-mark" | "session-dag";
|
|
35
|
+
/** Aggregated totals for one CLI, one scan. */
|
|
36
|
+
export type LocalUsageTotals = {
|
|
37
|
+
requests: number;
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
cacheReadTokens: number;
|
|
41
|
+
cacheCreationTokens: number;
|
|
42
|
+
costUsd: number;
|
|
43
|
+
/**
|
|
44
|
+
* The weakest confidence contributing to `costUsd`. A totals row mixing
|
|
45
|
+
* modeled and heuristic entries must report the weaker one, otherwise the
|
|
46
|
+
* aggregate looks better-sourced than its worst input.
|
|
47
|
+
*/
|
|
48
|
+
costConfidence: LocalUsageCostConfidence;
|
|
49
|
+
/** Turns whose model had no pricing entry, so contributed 0 to costUsd. */
|
|
50
|
+
unpricedRequests: number;
|
|
51
|
+
/** Distinct model ids behind `unpricedRequests`, for diagnosis. */
|
|
52
|
+
unpricedModels: string[];
|
|
53
|
+
};
|
|
54
|
+
/** A non-fatal per-file problem, surfaced instead of aborting the scan. */
|
|
55
|
+
export type LocalUsageScanError = {
|
|
56
|
+
cliId: LocalUsageCliId;
|
|
57
|
+
filePath: string;
|
|
58
|
+
message: string;
|
|
59
|
+
};
|
|
60
|
+
/** What one reader's `scan()` returns. */
|
|
61
|
+
export type LocalUsageScanResult = {
|
|
62
|
+
cliId: LocalUsageCliId;
|
|
63
|
+
totals: LocalUsageTotals;
|
|
64
|
+
/** Files opened during this scan, after any time filter. */
|
|
65
|
+
filesScanned: number;
|
|
66
|
+
errors: LocalUsageScanError[];
|
|
67
|
+
};
|
|
68
|
+
/** Static metadata, available without constructing a reader. */
|
|
69
|
+
export type LocalUsageReaderDescriptor = {
|
|
70
|
+
id: LocalUsageCliId;
|
|
71
|
+
displayName: string;
|
|
72
|
+
/**
|
|
73
|
+
* True only for readers checked against real data on a real machine. An
|
|
74
|
+
* honesty marker, not a completeness claim — an unverified reader may still
|
|
75
|
+
* be correct, it just has not been shown to be.
|
|
76
|
+
*/
|
|
77
|
+
verified: boolean;
|
|
78
|
+
dedupStrategy: LocalUsageDedupStrategy;
|
|
79
|
+
costConfidence: LocalUsageCostConfidence;
|
|
80
|
+
/** Whether reading this CLI's store needs a SQLite binding. */
|
|
81
|
+
requiresSqlite: boolean;
|
|
82
|
+
};
|
|
83
|
+
/** Options accepted by every reader's `scan()` and by the aggregator. */
|
|
84
|
+
export type LocalUsageScanOptions = {
|
|
85
|
+
/**
|
|
86
|
+
* Only read files modified within this many days. Defaults to 30.
|
|
87
|
+
*
|
|
88
|
+
* This is a real constraint rather than a convenience: one developer machine
|
|
89
|
+
* held 17,439 transcripts totalling 9.7 GB, and an unbounded scan reads all
|
|
90
|
+
* of it on every call. Pass `Infinity` for a deliberate full history sweep.
|
|
91
|
+
*/
|
|
92
|
+
sinceDays?: number;
|
|
93
|
+
};
|
|
94
|
+
/** The contract every reader implements — one per CLI. */
|
|
95
|
+
export type LocalUsageReader = {
|
|
96
|
+
descriptor: LocalUsageReaderDescriptor;
|
|
97
|
+
/**
|
|
98
|
+
* Whether this CLI's local store appears to exist on this machine at all —
|
|
99
|
+
* the same "do not report on something never installed" discipline the proxy
|
|
100
|
+
* client configurators use before writing a config.
|
|
101
|
+
*/
|
|
102
|
+
detect: () => Promise<boolean>;
|
|
103
|
+
scan: (options?: LocalUsageScanOptions) => Promise<LocalUsageScanResult>;
|
|
104
|
+
};
|
|
105
|
+
/** Async factory stored in the registry — a reader needs no credentials, only
|
|
106
|
+
* the filesystem, so this takes no arguments. */
|
|
107
|
+
export type LocalUsageReaderFactoryFn = () => Promise<LocalUsageReader>;
|
|
108
|
+
/** One entry in the registry map. */
|
|
109
|
+
export type LocalUsageReaderRegistration = {
|
|
110
|
+
descriptor: LocalUsageReaderDescriptor;
|
|
111
|
+
factory: LocalUsageReaderFactoryFn;
|
|
112
|
+
};
|
|
113
|
+
/** A whole reader failing — not installed, or threw — so the aggregate report
|
|
114
|
+
* can carry successes and failures side by side rather than losing both. */
|
|
115
|
+
export type LocalUsageReaderFailure = {
|
|
116
|
+
cliId: LocalUsageCliId;
|
|
117
|
+
message: string;
|
|
118
|
+
};
|
|
119
|
+
/** Top-level output of scanning every registered, detected reader. */
|
|
120
|
+
export type LocalUsageAggregateReport = {
|
|
121
|
+
generatedAt: string;
|
|
122
|
+
/** Only CLIs whose store was detected AND scanned appear here. */
|
|
123
|
+
totals: Partial<Record<LocalUsageCliId, LocalUsageTotals>>;
|
|
124
|
+
/** CLIs that were registered but produced nothing, and why. */
|
|
125
|
+
failures: LocalUsageReaderFailure[];
|
|
126
|
+
/** CLIs with no local store on this machine — absent, not failed. */
|
|
127
|
+
notInstalled: LocalUsageCliId[];
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The `message.usage` object exactly as Claude Code writes it into a
|
|
131
|
+
* transcript line — snake_case, and every field optional because older
|
|
132
|
+
* transcripts predate some of them.
|
|
133
|
+
*/
|
|
134
|
+
export type LocalUsageClaudeRawUsage = {
|
|
135
|
+
input_tokens?: number;
|
|
136
|
+
output_tokens?: number;
|
|
137
|
+
cache_read_input_tokens?: number;
|
|
138
|
+
cache_creation_input_tokens?: number;
|
|
139
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for `src/lib/localUsage/` — reading token usage out of each AI CLI's
|
|
3
|
+
* own local session logs.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists alongside the proxy's ledger: the proxy can only account for
|
|
6
|
+
* traffic that went through it, which caps coverage at the CLIs that expose a
|
|
7
|
+
* base-URL override. Every CLI writes its own local transcript regardless, so
|
|
8
|
+
* reading those recovers the rest — and recovers history from before the proxy
|
|
9
|
+
* was ever installed. It is also an independent source of truth: a pricing
|
|
10
|
+
* defect in the proxy's own accounting cannot hide from a reader that derives
|
|
11
|
+
* cost from a different input.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|