codexmeter 1.0.7 → 1.0.9

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.
@@ -60,9 +60,74 @@ export function isSubagent(agentRole) {
60
60
  return agentRole != null && agentRole !== 'default';
61
61
  }
62
62
 
63
+ export function parseThreadSource(rawSource) {
64
+ if (!rawSource) return null;
65
+ if (typeof rawSource === 'object') return rawSource;
66
+ const text = String(rawSource).trim();
67
+ if (!text) return null;
68
+ if (text.startsWith('{') || text.startsWith('[')) {
69
+ try {
70
+ return JSON.parse(text);
71
+ } catch {
72
+ return { kind: text };
73
+ }
74
+ }
75
+ return { kind: text };
76
+ }
77
+
78
+ export function deriveAgentRole(agentRole, rawSource, title = null) {
79
+ if (agentRole && agentRole !== 'default') return agentRole;
80
+ const source = parseThreadSource(rawSource);
81
+ if (source && typeof source === 'object' && typeof source.subagent === 'string') {
82
+ return source.subagent;
83
+ }
84
+ if (isReviewTaskTitle(title)) return 'review';
85
+ return null;
86
+ }
87
+
88
+ export function isReviewTaskTitle(title) {
89
+ if (typeof title !== 'string') return false;
90
+ const value = title.trim().toLowerCase();
91
+ return (
92
+ value.startsWith('review the code changes against the base branch ') ||
93
+ value.startsWith('you are reviewing a manually supplied diff artifact.') ||
94
+ value.startsWith('brief review for commit range ') ||
95
+ value.startsWith('independent brief review ') ||
96
+ value.startsWith('tight-scope integration review ') ||
97
+ value.startsWith('pr-scope review ') ||
98
+ value.startsWith('independent second-round pr review ') ||
99
+ value.startsWith('second independent pr-scope review ') ||
100
+ value.startsWith('implementation-review preflight ') ||
101
+ value.startsWith('implementation-review postflight ')
102
+ );
103
+ }
104
+
105
+ export function isReviewLauncherSession(session) {
106
+ if (session?.rollout_path && !session?.materialized) return false;
107
+ const source = parseThreadSource(session?.source_raw);
108
+ const sourceKind = source?.kind || (typeof source === 'string' ? source : null);
109
+ return isLauncherSourceKind(sourceKind) &&
110
+ isReviewTaskTitle(session?.title) &&
111
+ !session?.model_name &&
112
+ !session?.usage_total &&
113
+ !session?.has_usage_by_day &&
114
+ !session?.tokens_used;
115
+ }
116
+
117
+ function isLauncherSourceKind(sourceKind) {
118
+ const normalized = String(sourceKind || '').trim().toLowerCase();
119
+ return normalized === 'exec' ||
120
+ normalized === 'cli' ||
121
+ normalized.endsWith('-cli') ||
122
+ normalized.endsWith('_cli');
123
+ }
124
+
63
125
  const MODEL_ALIASES = {
64
126
  'codex-mini-latest': 'o4-mini',
65
- 'gpt-5.2': 'gpt-5.2-codex',
127
+ 'gpt-5 mini': 'gpt-5-mini',
128
+ 'gpt-5.4 mini': 'gpt-5.4-mini',
129
+ 'gpt-5 nano': 'gpt-5-nano',
130
+ 'gpt-5.4 nano': 'gpt-5.4-nano',
66
131
  };
67
132
 
68
133
  export function normalizeModelName(rawModel) {
@@ -1,43 +1,47 @@
1
1
  // Local pricing catalog for models used in codex-cli/codex app lifetime.
2
2
  const FALLBACK = {
3
- 'gpt-5.4': { input: 2.50, output: 15.00, cached_input: 0.25 },
4
- 'gpt-5.3-codex': { input: 2.00, output: 10.00, cached_input: 0.20 },
5
- 'gpt-5.3-codex-spark': { input: 2.00, output: 10.00, cached_input: 0.20 },
6
- 'gpt-5.2-codex': { input: 2.00, output: 10.00, cached_input: 0.20 },
7
- 'gpt-5.2': { input: 2.00, output: 10.00, cached_input: 0.20 },
8
- 'gpt-5.1-codex-mini': { input: 0.25, output: 2.00, cached_input: 0.025},
9
- 'gpt-4.1': { input: 2.00, output: 8.00, cached_input: 0.50 },
10
- 'gpt-4.1-mini': { input: 0.40, output: 1.60, cached_input: 0.10 },
11
- 'gpt-4.1-nano': { input: 0.10, output: 0.40, cached_input: 0.025 },
12
- 'o3': { input: 2.00, output: 8.00, cached_input: 0.50 },
13
- 'o3-mini': { input: 1.10, output: 4.40, cached_input: 0.275},
14
- 'o4-mini': { input: 1.10, output: 4.40, cached_input: 0.275},
15
- 'gpt-4o': { input: 2.50, output: 10.00, cached_input: 1.25 },
16
- 'gpt-4o-mini': { input: 0.15, output: 0.60, cached_input: 0.075},
17
- 'gpt-4-turbo': { input: 10.00, output: 30.00, cached_input: 1.00 },
18
- 'gpt-4': { input: 30.00, output: 60.00, cached_input: 3.00 },
19
- 'gpt-3.5-turbo': { input: 0.50, output: 1.50, cached_input: 0.05 },
20
- };
21
-
3
+ 'gpt-5.4': { input: 2.50, output: 15.00, cached_input: 0.25 },
4
+ 'gpt-5-mini': { input: 0.75, output: 4.50, cached_input: 0.075},
5
+ 'gpt-5.4-mini': { input: 0.75, output: 4.50, cached_input: 0.075},
6
+ 'gpt-5-nano': { input: 0.15, output: 0.60, cached_input: 0.015},
7
+ 'gpt-5.4-nano': { input: 0.15, output: 0.60, cached_input: 0.015},
8
+ 'gpt-5.3-codex': { input: 2.00, output: 10.00, cached_input: 0.20 },
9
+ 'gpt-5.3-codex-spark': { input: 2.00, output: 10.00, cached_input: 0.20 },
10
+ 'gpt-5.2-codex': { input: 2.00, output: 10.00, cached_input: 0.20 },
11
+ 'gpt-5.2': { input: 2.00, output: 10.00, cached_input: 0.20 },
12
+ 'gpt-5.1-codex-mini': { input: 0.25, output: 2.00, cached_input: 0.025},
13
+ 'gpt-4.1': { input: 2.00, output: 8.00, cached_input: 0.50 },
14
+ 'gpt-4.1-mini': { input: 0.40, output: 1.60, cached_input: 0.10 },
15
+ 'gpt-4.1-nano': { input: 0.10, output: 0.40, cached_input: 0.025 },
16
+ 'o3': { input: 2.00, output: 8.00, cached_input: 0.50 },
17
+ 'o3-mini': { input: 1.10, output: 4.40, cached_input: 0.275},
18
+ 'o4-mini': { input: 1.10, output: 4.40, cached_input: 0.275},
19
+ 'gpt-4o': { input: 2.50, output: 10.00, cached_input: 1.25 },
20
+ 'gpt-4o-mini': { input: 0.15, output: 0.60, cached_input: 0.075},
21
+ 'gpt-4-turbo': { input: 10.00, output: 30.00, cached_input: 1.00 },
22
+ 'gpt-4': { input: 30.00, output: 60.00, cached_input: 3.00 },
23
+ 'gpt-3.5-turbo': { input: 0.50, output: 1.50, cached_input: 0.05 },
24
+ };
25
+
22
26
  const PRICING_URL = process.env.CODEXMETER_PRICING_URL || null;
23
-
24
- function normalizeEntry(entry) {
25
- if (!entry || typeof entry !== 'object' || entry.input == null || entry.output == null) return null;
26
- const cached = entry.cached_input ?? entry.cachedInput ?? entry.input * 0.1;
27
- return { input: entry.input, output: entry.output, cached_input: cached };
28
- }
29
-
30
- function normalizePricing(raw) {
31
- const models = raw?.models && typeof raw.models === 'object' ? raw.models : raw;
32
- if (!models || typeof models !== 'object') return null;
33
- const out = {};
34
- for (const [name, entry] of Object.entries(models)) {
35
- const norm = normalizeEntry(entry);
36
- if (norm) out[name] = norm;
37
- }
38
- return Object.keys(out).length > 0 ? out : null;
39
- }
40
-
27
+
28
+ function normalizeEntry(entry) {
29
+ if (!entry || typeof entry !== 'object' || entry.input == null || entry.output == null) return null;
30
+ const cached = entry.cached_input ?? entry.cachedInput ?? entry.input * 0.1;
31
+ return { input: entry.input, output: entry.output, cached_input: cached };
32
+ }
33
+
34
+ function normalizePricing(raw) {
35
+ const models = raw?.models && typeof raw.models === 'object' ? raw.models : raw;
36
+ if (!models || typeof models !== 'object') return null;
37
+ const out = {};
38
+ for (const [name, entry] of Object.entries(models)) {
39
+ const norm = normalizeEntry(entry);
40
+ if (norm) out[name] = norm;
41
+ }
42
+ return Object.keys(out).length > 0 ? out : null;
43
+ }
44
+
41
45
  export async function fetchPricing() {
42
46
  if (!PRICING_URL) return FALLBACK;
43
47
 
@@ -45,15 +49,15 @@ export async function fetchPricing() {
45
49
  const ctrl = new AbortController();
46
50
  const t = setTimeout(() => ctrl.abort(), 3000);
47
51
  const res = await fetch(PRICING_URL, { signal: ctrl.signal });
48
- clearTimeout(t);
49
- if (!res.ok) return FALLBACK;
50
- const data = await res.json();
51
- const normalized = normalizePricing(data);
52
- if (normalized) {
53
- return { ...FALLBACK, ...normalized };
54
- }
55
- } catch {
56
- // timeout, network error, invalid JSON - use fallback
57
- }
58
- return FALLBACK;
59
- }
52
+ clearTimeout(t);
53
+ if (!res.ok) return FALLBACK;
54
+ const data = await res.json();
55
+ const normalized = normalizePricing(data);
56
+ if (normalized) {
57
+ return { ...FALLBACK, ...normalized };
58
+ }
59
+ } catch {
60
+ // timeout, network error, invalid JSON - use fallback
61
+ }
62
+ return FALLBACK;
63
+ }
@@ -21,7 +21,9 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
21
21
  active_by_day: null,
22
22
  usage_by_day: null,
23
23
  parent_thread_id: null,
24
+ forked_from_id: null,
24
25
  usage_total: null,
26
+ usage_reset_detected: false,
25
27
  };
26
28
 
27
29
  try {
@@ -34,6 +36,8 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
34
36
  let lastInputTokens = 0;
35
37
  let lastCachedInputTokens = 0;
36
38
  let lastOutputTokens = 0;
39
+ let lastReasoningOutputTokens = 0;
40
+ let lastTotalTokens = 0;
37
41
  let hasSeenUsage = false;
38
42
  for (const line of content.split(/\r?\n/)) {
39
43
  if (!line.trim()) continue;
@@ -79,30 +83,54 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
79
83
  total_tokens: totalTokens,
80
84
  };
81
85
 
86
+ const hasReset =
87
+ hasSeenUsage &&
88
+ totalTokens < lastTotalTokens;
89
+
90
+ if (hasReset) {
91
+ usageByDay.clear();
92
+ result.usage_reset_detected = true;
93
+ result.first_usage_timestamp = null;
94
+ hasSeenUsage = false;
95
+ lastInputTokens = 0;
96
+ lastCachedInputTokens = 0;
97
+ lastOutputTokens = 0;
98
+ lastReasoningOutputTokens = 0;
99
+ lastTotalTokens = 0;
100
+ }
101
+
82
102
  const usageDelta = hasSeenUsage
83
103
  ? {
84
104
  input_tokens: Math.max(inputTokens - lastInputTokens, 0),
85
105
  cached_input_tokens: Math.max(cachedInputTokens - lastCachedInputTokens, 0),
86
106
  output_tokens: Math.max(outputTokens - lastOutputTokens, 0),
107
+ reasoning_output_tokens: Math.max(reasoningOutputTokens - lastReasoningOutputTokens, 0),
108
+ total_tokens: Math.max(totalTokens - lastTotalTokens, 0),
87
109
  }
88
110
  : {
89
111
  input_tokens: inputTokens,
90
112
  cached_input_tokens: cachedInputTokens,
91
113
  output_tokens: outputTokens,
114
+ reasoning_output_tokens: reasoningOutputTokens,
115
+ total_tokens: totalTokens,
92
116
  };
93
117
 
94
118
  hasSeenUsage = true;
95
119
  lastInputTokens = inputTokens;
96
120
  lastCachedInputTokens = cachedInputTokens;
97
121
  lastOutputTokens = outputTokens;
98
- if (obj.timestamp && hasUsage(usageDelta)) {
122
+ lastReasoningOutputTokens = reasoningOutputTokens;
123
+ lastTotalTokens = totalTokens;
124
+ if (obj.timestamp && hasUsageBoundarySignal(usageDelta)) {
99
125
  const ts = new Date(obj.timestamp).getTime();
100
126
  if (!isNaN(ts)) {
101
127
  if (!result.first_usage_timestamp || ts < result.first_usage_timestamp) {
102
128
  result.first_usage_timestamp = ts;
103
129
  }
104
- const dayKey = toDayKey(ts);
105
- mergeUsageTotals(usageByDay, dayKey, usageDelta);
130
+ if (hasUsage(usageDelta)) {
131
+ const dayKey = toDayKey(ts);
132
+ mergeUsageTotals(usageByDay, dayKey, usageDelta);
133
+ }
106
134
  }
107
135
  }
108
136
  }
@@ -121,6 +149,7 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
121
149
 
122
150
  if (obj.type === 'session_meta' && obj.payload) {
123
151
  if (obj.payload.model && !result.model_name) result.model_name = obj.payload.model;
152
+ if (!result.forked_from_id) result.forked_from_id = obj.payload?.forked_from_id || null;
124
153
  if (!result.parent_thread_id) {
125
154
  result.parent_thread_id =
126
155
  obj.payload?.source?.subagent?.thread_spawn?.parent_thread_id ||
@@ -150,6 +179,68 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
150
179
  return result;
151
180
  }
152
181
 
182
+ export async function readUsageTimeline(rolloutPath) {
183
+ if (!rolloutPath || !existsSync(rolloutPath)) {
184
+ return [];
185
+ }
186
+
187
+ try {
188
+ const content = await readFile(rolloutPath, 'utf8');
189
+ const timeline = [];
190
+ let segmentId = 0;
191
+ let lastTotalTokens = 0;
192
+ let hasSeenUsage = false;
193
+ for (const line of content.split(/\r?\n/)) {
194
+ if (!line.trim()) continue;
195
+ try {
196
+ const obj = JSON.parse(line);
197
+ if (obj.type !== 'event_msg' || obj.payload?.type !== 'token_count') continue;
198
+ const usage = obj.payload.info?.total_token_usage;
199
+ if (!usage || !obj.timestamp) continue;
200
+ const ts = new Date(obj.timestamp).getTime();
201
+ if (isNaN(ts)) continue;
202
+ const normalizedUsage = normalizeUsageTotals(usage);
203
+ if (hasSeenUsage && normalizedUsage.total_tokens < lastTotalTokens) {
204
+ segmentId += 1;
205
+ }
206
+ timeline.push({
207
+ timestamp: ts,
208
+ segment_id: segmentId,
209
+ usage: normalizedUsage,
210
+ });
211
+ hasSeenUsage = true;
212
+ lastTotalTokens = normalizedUsage.total_tokens;
213
+ } catch {
214
+ // malformed line
215
+ }
216
+ }
217
+ timeline.sort((left, right) => left.timestamp - right.timestamp);
218
+ return timeline;
219
+ } catch {
220
+ return [];
221
+ }
222
+ }
223
+
224
+ export function findUsageEntryAtOrBefore(timeline, timestampMs) {
225
+ if (!Array.isArray(timeline) || !timeline.length || !timestampMs) return null;
226
+ const orderedTimeline = timeline.every((entry, index) => index === 0 || timeline[index - 1].timestamp <= entry.timestamp)
227
+ ? timeline
228
+ : [...timeline].sort((left, right) => left.timestamp - right.timestamp);
229
+ let best = null;
230
+ for (const entry of orderedTimeline) {
231
+ if ((entry?.timestamp || 0) <= timestampMs) {
232
+ best = entry;
233
+ continue;
234
+ }
235
+ break;
236
+ }
237
+ return best || null;
238
+ }
239
+
240
+ export function findUsageAtOrBefore(timeline, timestampMs) {
241
+ return findUsageEntryAtOrBefore(timeline, timestampMs)?.usage || null;
242
+ }
243
+
153
244
  function normalizeUsageTotals(usage) {
154
245
  return {
155
246
  input_tokens: usage.input_tokens || 0,
@@ -160,16 +251,50 @@ function normalizeUsageTotals(usage) {
160
251
  };
161
252
  }
162
253
 
163
- function subtractUsageTotals(current, previous) {
254
+ function splitUsageTotals(usage) {
255
+ const normalized = normalizeUsageTotals(usage || {});
256
+ const cachedInputTokens = Math.min(normalized.cached_input_tokens, normalized.input_tokens);
257
+ return {
258
+ uncached_input_tokens: Math.max(normalized.input_tokens - cachedInputTokens, 0),
259
+ cached_input_tokens: cachedInputTokens,
260
+ output_tokens: normalized.output_tokens,
261
+ reasoning_output_tokens: normalized.reasoning_output_tokens,
262
+ };
263
+ }
264
+
265
+ function combineUsageTotals(parts, currentTotalTokens = 0, previousTotalTokens = 0) {
266
+ const inputTokens = (parts.uncached_input_tokens || 0) + (parts.cached_input_tokens || 0);
267
+ const outputTokens = parts.output_tokens || 0;
164
268
  return {
165
- input_tokens: Math.max((current.input_tokens || 0) - (previous.input_tokens || 0), 0),
166
- cached_input_tokens: Math.max((current.cached_input_tokens || 0) - (previous.cached_input_tokens || 0), 0),
167
- output_tokens: Math.max((current.output_tokens || 0) - (previous.output_tokens || 0), 0),
168
- reasoning_output_tokens: Math.max((current.reasoning_output_tokens || 0) - (previous.reasoning_output_tokens || 0), 0),
169
- total_tokens: Math.max((current.total_tokens || 0) - (previous.total_tokens || 0), 0),
269
+ input_tokens: inputTokens,
270
+ cached_input_tokens: parts.cached_input_tokens || 0,
271
+ output_tokens: outputTokens,
272
+ reasoning_output_tokens: parts.reasoning_output_tokens || 0,
273
+ total_tokens: Math.max(inputTokens + outputTokens, Math.max(currentTotalTokens - previousTotalTokens, 0)),
170
274
  };
171
275
  }
172
276
 
277
+ export function subtractUsageTotals(current, previous) {
278
+ const currentParts = splitUsageTotals(current);
279
+ const previousParts = splitUsageTotals(previous);
280
+ return combineUsageTotals({
281
+ uncached_input_tokens: Math.max(currentParts.uncached_input_tokens - previousParts.uncached_input_tokens, 0),
282
+ cached_input_tokens: Math.max(currentParts.cached_input_tokens - previousParts.cached_input_tokens, 0),
283
+ output_tokens: Math.max(currentParts.output_tokens - previousParts.output_tokens, 0),
284
+ reasoning_output_tokens: Math.max(currentParts.reasoning_output_tokens - previousParts.reasoning_output_tokens, 0),
285
+ }, current?.total_tokens || 0, previous?.total_tokens || 0);
286
+ }
287
+
288
+ export function hasUsageTotals(usage) {
289
+ return !!usage && (
290
+ (usage.input_tokens || 0) > 0 ||
291
+ (usage.cached_input_tokens || 0) > 0 ||
292
+ (usage.output_tokens || 0) > 0 ||
293
+ (usage.reasoning_output_tokens || 0) > 0 ||
294
+ (usage.total_tokens || 0) > 0
295
+ );
296
+ }
297
+
173
298
  function hasUsage(usage) {
174
299
  return !!usage && (
175
300
  usage.input_tokens > 0 ||
@@ -178,6 +303,10 @@ function hasUsage(usage) {
178
303
  );
179
304
  }
180
305
 
306
+ function hasUsageBoundarySignal(usage) {
307
+ return hasUsage(usage) || (usage?.reasoning_output_tokens || 0) > 0 || (usage?.total_tokens || 0) > 0;
308
+ }
309
+
181
310
  function mergeUsageTotals(target, dayKey, usage) {
182
311
  let previous = target.get(dayKey);
183
312
  if (!previous) {
@@ -35,7 +35,7 @@ export function readThreads(codexHome, onProgress) {
35
35
  rollout_path: row.rollout_path,
36
36
  created_at: row.created_at,
37
37
  updated_at: row.updated_at,
38
- source: row.source,
38
+ source_raw: row.source,
39
39
  model_provider: row.model_provider,
40
40
  cwd_raw: row.cwd,
41
41
  title: row.title ? row.title.slice(0, 200) : '',