@juspay/neurolink 11.19.0 → 11.20.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.
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads token usage out of Codex's own rollout transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Layout: `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-<iso>-<sessionId>.jsonl`.
|
|
5
|
+
* Each line is one of `session_meta`, `turn_context`, `response_item` or
|
|
6
|
+
* `event_msg`; usage lives on `event_msg` records whose `payload.type` is
|
|
7
|
+
* `token_count`.
|
|
8
|
+
*
|
|
9
|
+
* The counter semantics are the whole story here, and they are the opposite of
|
|
10
|
+
* the Claude Code reader's. Every `token_count` event carries BOTH a
|
|
11
|
+
* cumulative `total_token_usage` for the session and a per-turn
|
|
12
|
+
* `last_token_usage`. Summing the per-turn values is the obvious move and it is
|
|
13
|
+
* wrong: measured across all 108 sessions on a real machine, summing yields
|
|
14
|
+
* 9,191,613,238 tokens against a true 5,653,217,442 — an overstatement of
|
|
15
|
+
* 62.6%, and 195% on the worst single session. The per-turn value repeats
|
|
16
|
+
* across events within a turn, so it double-counts.
|
|
17
|
+
*
|
|
18
|
+
* The cumulative counter is authoritative and was monotonic in all 108
|
|
19
|
+
* sessions — it never resets mid-file — so the last one in the file is the
|
|
20
|
+
* session's true total. `Math.max` is still used rather than "last seen",
|
|
21
|
+
* because a counter that is only monotonic in every case observed is not the
|
|
22
|
+
* same as one that is guaranteed to be, and the max costs nothing.
|
|
23
|
+
*
|
|
24
|
+
* Cost is deliberately not computed. Codex is a ChatGPT subscription — the
|
|
25
|
+
* rollouts carry `rate_limits.plan_type` — so a per-token dollar figure would
|
|
26
|
+
* be an invention, not a measurement. The tokens are real; the cost is
|
|
27
|
+
* `unavailable`.
|
|
28
|
+
*/
|
|
29
|
+
import type { LocalUsageReader } from "../types/index.js";
|
|
30
|
+
export declare function createCodexReader(): Promise<LocalUsageReader>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads token usage out of Codex's own rollout transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Layout: `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-<iso>-<sessionId>.jsonl`.
|
|
5
|
+
* Each line is one of `session_meta`, `turn_context`, `response_item` or
|
|
6
|
+
* `event_msg`; usage lives on `event_msg` records whose `payload.type` is
|
|
7
|
+
* `token_count`.
|
|
8
|
+
*
|
|
9
|
+
* The counter semantics are the whole story here, and they are the opposite of
|
|
10
|
+
* the Claude Code reader's. Every `token_count` event carries BOTH a
|
|
11
|
+
* cumulative `total_token_usage` for the session and a per-turn
|
|
12
|
+
* `last_token_usage`. Summing the per-turn values is the obvious move and it is
|
|
13
|
+
* wrong: measured across all 108 sessions on a real machine, summing yields
|
|
14
|
+
* 9,191,613,238 tokens against a true 5,653,217,442 — an overstatement of
|
|
15
|
+
* 62.6%, and 195% on the worst single session. The per-turn value repeats
|
|
16
|
+
* across events within a turn, so it double-counts.
|
|
17
|
+
*
|
|
18
|
+
* The cumulative counter is authoritative and was monotonic in all 108
|
|
19
|
+
* sessions — it never resets mid-file — so the last one in the file is the
|
|
20
|
+
* session's true total. `Math.max` is still used rather than "last seen",
|
|
21
|
+
* because a counter that is only monotonic in every case observed is not the
|
|
22
|
+
* same as one that is guaranteed to be, and the max costs nothing.
|
|
23
|
+
*
|
|
24
|
+
* Cost is deliberately not computed. Codex is a ChatGPT subscription — the
|
|
25
|
+
* rollouts carry `rate_limits.plan_type` — so a per-token dollar figure would
|
|
26
|
+
* be an invention, not a measurement. The tokens are real; the cost is
|
|
27
|
+
* `unavailable`.
|
|
28
|
+
*/
|
|
29
|
+
import { createReadStream } from "fs";
|
|
30
|
+
import { createInterface } from "readline";
|
|
31
|
+
import { readdir, stat } from "fs/promises";
|
|
32
|
+
import { homedir } from "os";
|
|
33
|
+
import { join } from "path";
|
|
34
|
+
const CLI_ID = "codex";
|
|
35
|
+
const DEFAULT_SINCE_DAYS = 30;
|
|
36
|
+
function sessionsRoot() {
|
|
37
|
+
return join(homedir(), ".codex", "sessions");
|
|
38
|
+
}
|
|
39
|
+
function emptyTotals() {
|
|
40
|
+
return {
|
|
41
|
+
requests: 0,
|
|
42
|
+
inputTokens: 0,
|
|
43
|
+
outputTokens: 0,
|
|
44
|
+
cacheReadTokens: 0,
|
|
45
|
+
cacheCreationTokens: 0,
|
|
46
|
+
costUsd: 0,
|
|
47
|
+
costConfidence: "unavailable",
|
|
48
|
+
unpricedRequests: 0,
|
|
49
|
+
unpricedModels: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function collectRollouts(dir, out) {
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// One unreadable date directory must not cost the rest of the scan.
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const full = join(dir, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
await collectRollouts(full, out);
|
|
65
|
+
}
|
|
66
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
67
|
+
out.push(full);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function num(value) {
|
|
72
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fold one rollout into a single session-level rollup.
|
|
76
|
+
*
|
|
77
|
+
* `billableEvents` counts only the `token_count` events where the cumulative
|
|
78
|
+
* total actually advanced. Counting every event instead would inherit exactly
|
|
79
|
+
* the repetition that makes summing the per-turn values wrong.
|
|
80
|
+
*/
|
|
81
|
+
async function readRollout(filePath) {
|
|
82
|
+
const rl = createInterface({
|
|
83
|
+
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
84
|
+
crlfDelay: Infinity,
|
|
85
|
+
});
|
|
86
|
+
let model;
|
|
87
|
+
let bestTotal = -1;
|
|
88
|
+
let best;
|
|
89
|
+
let previousTotal = -1;
|
|
90
|
+
let billableEvents = 0;
|
|
91
|
+
try {
|
|
92
|
+
for await (const line of rl) {
|
|
93
|
+
// Cheap reject before JSON.parse: these files reach hundreds of MB and
|
|
94
|
+
// most lines are conversation content with no usage on them at all.
|
|
95
|
+
const isTokenCount = line.includes('"token_count"');
|
|
96
|
+
const isTurnContext = line.includes('"turn_context"');
|
|
97
|
+
if (!isTokenCount && !isTurnContext) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(line);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// A rollout being appended to while we read it ends mid-line.
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const record = parsed;
|
|
109
|
+
if (record.type === "turn_context" && record.payload?.model) {
|
|
110
|
+
// Last one wins: a session can switch models partway through, and the
|
|
111
|
+
// most recent is the better single label for it.
|
|
112
|
+
model = record.payload.model;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (record.payload?.type !== "token_count") {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const cumulative = record.payload.info?.total_token_usage;
|
|
119
|
+
if (!cumulative) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const total = num(cumulative.total_tokens);
|
|
123
|
+
if (total > previousTotal) {
|
|
124
|
+
billableEvents += 1;
|
|
125
|
+
}
|
|
126
|
+
previousTotal = total;
|
|
127
|
+
if (total > bestTotal) {
|
|
128
|
+
bestTotal = total;
|
|
129
|
+
best = {
|
|
130
|
+
input: num(cumulative.input_tokens),
|
|
131
|
+
output: num(cumulative.output_tokens),
|
|
132
|
+
cached: num(cumulative.cached_input_tokens),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
rl.close();
|
|
139
|
+
}
|
|
140
|
+
if (!best) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return { model, billableEvents, ...best };
|
|
144
|
+
}
|
|
145
|
+
export async function createCodexReader() {
|
|
146
|
+
return {
|
|
147
|
+
descriptor: {
|
|
148
|
+
id: CLI_ID,
|
|
149
|
+
displayName: "Codex",
|
|
150
|
+
verified: true,
|
|
151
|
+
dedupStrategy: "session-dag",
|
|
152
|
+
costConfidence: "unavailable",
|
|
153
|
+
requiresSqlite: false,
|
|
154
|
+
},
|
|
155
|
+
detect: async () => {
|
|
156
|
+
try {
|
|
157
|
+
const info = await stat(sessionsRoot());
|
|
158
|
+
return info.isDirectory();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
scan: async (options) => {
|
|
165
|
+
const totals = emptyTotals();
|
|
166
|
+
const errors = [];
|
|
167
|
+
const models = new Set();
|
|
168
|
+
const files = [];
|
|
169
|
+
await collectRollouts(sessionsRoot(), files);
|
|
170
|
+
const sinceDays = options?.sinceDays ?? DEFAULT_SINCE_DAYS;
|
|
171
|
+
const cutoff = Number.isFinite(sinceDays) && sinceDays > 0
|
|
172
|
+
? Date.now() - sinceDays * 86_400_000
|
|
173
|
+
: undefined;
|
|
174
|
+
let filesScanned = 0;
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
try {
|
|
177
|
+
if (cutoff !== undefined) {
|
|
178
|
+
const info = await stat(file);
|
|
179
|
+
if (info.mtimeMs < cutoff) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const rollup = await readRollout(file);
|
|
184
|
+
filesScanned += 1;
|
|
185
|
+
if (!rollup) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
totals.requests += rollup.billableEvents;
|
|
189
|
+
// `cached_input_tokens` is a SUBSET of `input_tokens` here — verified
|
|
190
|
+
// on all 108 sessions, where input + output == total exactly and
|
|
191
|
+
// cached <= input always. The other readers keep the two disjoint, so
|
|
192
|
+
// the cached portion is subtracted out rather than reported twice; a
|
|
193
|
+
// caller adding inputTokens + cacheReadTokens would otherwise
|
|
194
|
+
// over-count Codex and not the rest.
|
|
195
|
+
totals.inputTokens += Math.max(0, rollup.input - rollup.cached);
|
|
196
|
+
totals.cacheReadTokens += rollup.cached;
|
|
197
|
+
totals.outputTokens += rollup.output;
|
|
198
|
+
if (rollup.model) {
|
|
199
|
+
models.add(rollup.model);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
errors.push({
|
|
204
|
+
cliId: CLI_ID,
|
|
205
|
+
filePath: file,
|
|
206
|
+
message: error instanceof Error ? error.message : String(error),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Every turn is unpriced by construction, not by accident: see the note
|
|
211
|
+
// on this module about Codex being a subscription. Naming the models
|
|
212
|
+
// keeps that legible instead of looking like a lookup that failed.
|
|
213
|
+
totals.unpricedRequests = totals.requests;
|
|
214
|
+
totals.unpricedModels = [...models].sort();
|
|
215
|
+
return { cliId: CLI_ID, totals, filesScanned, errors };
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -38,3 +38,17 @@ registerLocalUsageReader({
|
|
|
38
38
|
return createClaudeCodeReader();
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
|
+
registerLocalUsageReader({
|
|
42
|
+
descriptor: {
|
|
43
|
+
id: "codex",
|
|
44
|
+
displayName: "Codex",
|
|
45
|
+
verified: true,
|
|
46
|
+
dedupStrategy: "session-dag",
|
|
47
|
+
costConfidence: "unavailable",
|
|
48
|
+
requiresSqlite: false,
|
|
49
|
+
},
|
|
50
|
+
factory: async () => {
|
|
51
|
+
const { createCodexReader } = await import("./codexReader.js");
|
|
52
|
+
return createCodexReader();
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -137,3 +137,17 @@ export type LocalUsageClaudeRawUsage = {
|
|
|
137
137
|
cache_read_input_tokens?: number;
|
|
138
138
|
cache_creation_input_tokens?: number;
|
|
139
139
|
};
|
|
140
|
+
/**
|
|
141
|
+
* One Codex rollout reduced to its session-level totals.
|
|
142
|
+
*
|
|
143
|
+
* The token figures here are the session's CUMULATIVE counter, not a sum of
|
|
144
|
+
* per-turn values — see `codexReader.ts` for why summing overstates by ~63%.
|
|
145
|
+
*/
|
|
146
|
+
export type LocalUsageCodexSessionRollup = {
|
|
147
|
+
model?: string;
|
|
148
|
+
input: number;
|
|
149
|
+
output: number;
|
|
150
|
+
cached: number;
|
|
151
|
+
/** token_count events where the cumulative total actually advanced. */
|
|
152
|
+
billableEvents: number;
|
|
153
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.20.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": {
|