@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.19.0",
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": {