codexmeter 1.0.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/README.md +60 -0
- package/bin/codexmeter.js +57 -0
- package/dist/assets/index-DJWqyRDh.css +1 -0
- package/dist/assets/index-DZKogILW.js +113 -0
- package/dist/index.html +16 -0
- package/package.json +43 -0
- package/server/aggregator.js +428 -0
- package/server/cost-catalog.js +85 -0
- package/server/day-key.js +16 -0
- package/server/index.js +134 -0
- package/server/ingest.js +595 -0
- package/server/live-state.js +464 -0
- package/server/normalize.js +72 -0
- package/server/pricing-fetch.js +59 -0
- package/server/rollout-reader.js +108 -0
- package/server/rollout-worker-pool.js +159 -0
- package/server/rollout-worker.js +21 -0
- package/server/sqlite-reader.js +59 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>CodexMeter</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-DZKogILW.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DJWqyRDh.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codexmeter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local telemetry dashboard for Codex CLI usage",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"codexmeter": "./bin/codexmeter.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"server",
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"package.json"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "node dev-runner.js",
|
|
18
|
+
"build": "vite build",
|
|
19
|
+
"start": "node bin/codexmeter.js",
|
|
20
|
+
"preview": "npm run build && node bin/codexmeter.js",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"better-sqlite3": "^11.8.1",
|
|
25
|
+
"commander": "^13.1.0",
|
|
26
|
+
"express": "^5.1.0",
|
|
27
|
+
"fast-glob": "^3.3.3",
|
|
28
|
+
"open": "^10.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@tanstack/react-table": "^8.21.3",
|
|
32
|
+
"@tanstack/react-virtual": "^3.13.6",
|
|
33
|
+
"@types/react": "^19.0.0",
|
|
34
|
+
"@types/react-dom": "^19.0.0",
|
|
35
|
+
"@vitejs/plugin-react": "^4.4.1",
|
|
36
|
+
"echarts": "^5.6.0",
|
|
37
|
+
"echarts-for-react": "^3.0.2",
|
|
38
|
+
"nodemon": "^3.1.14",
|
|
39
|
+
"react": "^19.1.0",
|
|
40
|
+
"react-dom": "^19.1.0",
|
|
41
|
+
"vite": "^6.3.5"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { CACHE_ASSUMPTIONS } from './cost-catalog.js';
|
|
2
|
+
import { createDayKeyFormatter } from './day-key.js';
|
|
3
|
+
|
|
4
|
+
function normalizeEffortKey(effort) {
|
|
5
|
+
if (!effort) return 'unknown';
|
|
6
|
+
const k = String(effort).toLowerCase().trim().replace(/-/g, '');
|
|
7
|
+
if (k === 'low') return 'low';
|
|
8
|
+
if (k === 'medium') return 'medium';
|
|
9
|
+
if (k === 'high') return 'high';
|
|
10
|
+
if (k === 'xhigh') return 'xhigh';
|
|
11
|
+
return k || 'unknown';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildAggregates(sessions, tz, sessionView = null) {
|
|
15
|
+
const now = Date.now() / 1000;
|
|
16
|
+
const d7 = now - 7 * 86400;
|
|
17
|
+
const d30 = now - 30 * 86400;
|
|
18
|
+
const groupedSessions = sessionView || buildSessionView(sessions);
|
|
19
|
+
|
|
20
|
+
const rawBuckets = { total: sessions, d7: [], d30: [] };
|
|
21
|
+
for (const s of sessions) {
|
|
22
|
+
if (overlapsLowerBound(s, d7)) rawBuckets.d7.push(s);
|
|
23
|
+
if (overlapsLowerBound(s, d30)) rawBuckets.d30.push(s);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const overview = buildOverview(sessions, groupedSessions, d7, d30);
|
|
27
|
+
const repos = {
|
|
28
|
+
total: buildRepos(rawBuckets.total),
|
|
29
|
+
d7: buildRepos(rawBuckets.d7),
|
|
30
|
+
d30: buildRepos(rawBuckets.d30),
|
|
31
|
+
};
|
|
32
|
+
const models = {
|
|
33
|
+
total: buildModels(rawBuckets.total),
|
|
34
|
+
d7: buildModels(rawBuckets.d7),
|
|
35
|
+
d30: buildModels(rawBuckets.d30),
|
|
36
|
+
};
|
|
37
|
+
const families = {
|
|
38
|
+
total: buildFamilies(rawBuckets.total),
|
|
39
|
+
d7: buildFamilies(rawBuckets.d7),
|
|
40
|
+
d30: buildFamilies(rawBuckets.d30),
|
|
41
|
+
};
|
|
42
|
+
const daily = buildDaily(sessions, groupedSessions, tz);
|
|
43
|
+
const heatmap = buildHeatmap(sessions, groupedSessions, tz);
|
|
44
|
+
|
|
45
|
+
return { overview, repos, models, daily, heatmap, families };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildSessionView(sessions, allSessions = sessions) {
|
|
49
|
+
const grouped = new Map();
|
|
50
|
+
const rootLookup = new Map(allSessions.map(session => [session.thread_id, session]));
|
|
51
|
+
|
|
52
|
+
for (const session of sessions) {
|
|
53
|
+
const key = session.root_thread_id || session.thread_id;
|
|
54
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
55
|
+
grouped.get(key).push(session);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return [...grouped.entries()]
|
|
59
|
+
.map(([rootThreadId, group]) => collapseSessionGroup(rootThreadId, group, rootLookup))
|
|
60
|
+
.sort((a, b) => (b.started_at || 0) - (a.started_at || 0));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildOverview(rawSessions, groupedSessions, d7, d30) {
|
|
64
|
+
const rawBuckets = { total: rawSessions, d7: [], d30: [] };
|
|
65
|
+
for (const session of rawSessions) {
|
|
66
|
+
if (overlapsLowerBound(session, d7)) rawBuckets.d7.push(session);
|
|
67
|
+
if (overlapsLowerBound(session, d30)) rawBuckets.d30.push(session);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const groupedBuckets = {
|
|
71
|
+
total: groupedSessions,
|
|
72
|
+
d7: groupedSessions.filter(session => overlapsLowerBound(session, d7)),
|
|
73
|
+
d30: groupedSessions.filter(session => overlapsLowerBound(session, d30)),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const calc = (arr, groupedArr) => {
|
|
77
|
+
let tokens = 0, cost = 0, elapsed = 0, priced = 0, exactPriced = 0, heuristicPriced = 0, unpriced = 0, timeValid = 0, enriched = 0;
|
|
78
|
+
const repoSet = new Set(), modelSet = new Set();
|
|
79
|
+
let earliest = Infinity, latest = -Infinity;
|
|
80
|
+
|
|
81
|
+
for (const s of arr) {
|
|
82
|
+
tokens += s.tokens_used;
|
|
83
|
+
if (s.cost !== null) {
|
|
84
|
+
cost += s.cost;
|
|
85
|
+
priced++;
|
|
86
|
+
if (s.cost_source === 'exact') exactPriced++;
|
|
87
|
+
if (s.cost_source === 'heuristic') heuristicPriced++;
|
|
88
|
+
} else {
|
|
89
|
+
unpriced++;
|
|
90
|
+
}
|
|
91
|
+
if (s.elapsed_seconds != null && s.elapsed_seconds > 0) { timeValid++; }
|
|
92
|
+
if (s.model_name) { enriched++; modelSet.add(s.model_name); }
|
|
93
|
+
repoSet.add(s.repo_label);
|
|
94
|
+
if (s.started_at < earliest) earliest = s.started_at;
|
|
95
|
+
if (s.ended_at > latest) latest = s.ended_at;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const s of groupedArr) {
|
|
99
|
+
if (s.elapsed_seconds != null && s.elapsed_seconds > 0) {
|
|
100
|
+
elapsed += s.elapsed_seconds;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
total_tokens: tokens,
|
|
106
|
+
total_cost: cost,
|
|
107
|
+
total_sessions: groupedArr.length,
|
|
108
|
+
active_repos: repoSet.size,
|
|
109
|
+
active_models: modelSet.size,
|
|
110
|
+
total_elapsed_seconds: elapsed,
|
|
111
|
+
date_range: { from: earliest === Infinity ? null : earliest, to: latest === -Infinity ? null : latest },
|
|
112
|
+
coverage: {
|
|
113
|
+
total: arr.length,
|
|
114
|
+
thread_rows: arr.length,
|
|
115
|
+
root_sessions: groupedArr.length,
|
|
116
|
+
enriched,
|
|
117
|
+
priced,
|
|
118
|
+
priced_exact: exactPriced,
|
|
119
|
+
priced_fallback: heuristicPriced,
|
|
120
|
+
unpriced,
|
|
121
|
+
time_valid: timeValid,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
total: calc(rawBuckets.total, groupedBuckets.total),
|
|
128
|
+
d7: calc(rawBuckets.d7, groupedBuckets.d7),
|
|
129
|
+
d30: calc(rawBuckets.d30, groupedBuckets.d30),
|
|
130
|
+
cost_assumptions: CACHE_ASSUMPTIONS,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildRepos(sessions) {
|
|
135
|
+
const map = new Map();
|
|
136
|
+
for (const s of sessions) {
|
|
137
|
+
const key = s.repo_label;
|
|
138
|
+
if (!map.has(key)) {
|
|
139
|
+
map.set(key, {
|
|
140
|
+
repo_key: s.repo_key,
|
|
141
|
+
repo_label: key,
|
|
142
|
+
tokens: 0,
|
|
143
|
+
cost: 0,
|
|
144
|
+
cost_known: 0,
|
|
145
|
+
exact_priced: 0,
|
|
146
|
+
heuristic_priced: 0,
|
|
147
|
+
sessions: 0,
|
|
148
|
+
by_model: {},
|
|
149
|
+
by_family: {},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const r = map.get(key);
|
|
153
|
+
r.tokens += s.tokens_used;
|
|
154
|
+
if (s.cost !== null) {
|
|
155
|
+
r.cost += s.cost;
|
|
156
|
+
r.cost_known++;
|
|
157
|
+
if (s.cost_source === 'exact') r.exact_priced++;
|
|
158
|
+
if (s.cost_source === 'heuristic') r.heuristic_priced++;
|
|
159
|
+
}
|
|
160
|
+
r.sessions++;
|
|
161
|
+
|
|
162
|
+
const mKey = s.model_name || 'unknown';
|
|
163
|
+
if (!r.by_model[mKey]) r.by_model[mKey] = { tokens: 0, cost: 0, sessions: 0, exact_priced: 0, heuristic_priced: 0 };
|
|
164
|
+
r.by_model[mKey].tokens += s.tokens_used;
|
|
165
|
+
if (s.cost !== null) {
|
|
166
|
+
r.by_model[mKey].cost += s.cost;
|
|
167
|
+
if (s.cost_source === 'exact') r.by_model[mKey].exact_priced++;
|
|
168
|
+
if (s.cost_source === 'heuristic') r.by_model[mKey].heuristic_priced++;
|
|
169
|
+
}
|
|
170
|
+
r.by_model[mKey].sessions++;
|
|
171
|
+
|
|
172
|
+
const fKey = s.agent_family;
|
|
173
|
+
if (!r.by_family[fKey]) r.by_family[fKey] = { tokens: 0, cost: 0, sessions: 0, exact_priced: 0, heuristic_priced: 0 };
|
|
174
|
+
r.by_family[fKey].tokens += s.tokens_used;
|
|
175
|
+
if (s.cost !== null) {
|
|
176
|
+
r.by_family[fKey].cost += s.cost;
|
|
177
|
+
if (s.cost_source === 'exact') r.by_family[fKey].exact_priced++;
|
|
178
|
+
if (s.cost_source === 'heuristic') r.by_family[fKey].heuristic_priced++;
|
|
179
|
+
}
|
|
180
|
+
r.by_family[fKey].sessions++;
|
|
181
|
+
}
|
|
182
|
+
return [...map.values()].sort((a, b) => b.tokens - a.tokens);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildModels(sessions) {
|
|
186
|
+
const map = new Map();
|
|
187
|
+
for (const s of sessions) {
|
|
188
|
+
const key = s.model_name || 'unknown';
|
|
189
|
+
if (!map.has(key)) map.set(key, { model_name: key, tokens: 0, cost: 0, cost_known: 0, exact_priced: 0, heuristic_priced: 0, sessions: 0, by_effort: {} });
|
|
190
|
+
const m = map.get(key);
|
|
191
|
+
m.tokens += s.tokens_used;
|
|
192
|
+
if (s.cost !== null) {
|
|
193
|
+
m.cost += s.cost;
|
|
194
|
+
m.cost_known++;
|
|
195
|
+
if (s.cost_source === 'exact') m.exact_priced++;
|
|
196
|
+
if (s.cost_source === 'heuristic') m.heuristic_priced++;
|
|
197
|
+
}
|
|
198
|
+
m.sessions++;
|
|
199
|
+
const eKey = normalizeEffortKey(s.reasoning_effort);
|
|
200
|
+
if (!m.by_effort[eKey]) m.by_effort[eKey] = { tokens: 0, cost: 0, sessions: 0, exact_priced: 0, heuristic_priced: 0 };
|
|
201
|
+
m.by_effort[eKey].tokens += s.tokens_used;
|
|
202
|
+
if (s.cost !== null) {
|
|
203
|
+
m.by_effort[eKey].cost += s.cost;
|
|
204
|
+
if (s.cost_source === 'exact') m.by_effort[eKey].exact_priced++;
|
|
205
|
+
if (s.cost_source === 'heuristic') m.by_effort[eKey].heuristic_priced++;
|
|
206
|
+
}
|
|
207
|
+
m.by_effort[eKey].sessions++;
|
|
208
|
+
}
|
|
209
|
+
return [...map.values()].sort((a, b) => b.tokens - a.tokens);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildFamilies(sessions) {
|
|
213
|
+
const map = new Map();
|
|
214
|
+
for (const s of sessions) {
|
|
215
|
+
const key = s.agent_family;
|
|
216
|
+
if (!map.has(key)) map.set(key, { family: key, tokens: 0, cost: 0, exact_priced: 0, heuristic_priced: 0, sessions: 0 });
|
|
217
|
+
const f = map.get(key);
|
|
218
|
+
f.tokens += s.tokens_used;
|
|
219
|
+
if (s.cost !== null) {
|
|
220
|
+
f.cost += s.cost;
|
|
221
|
+
if (s.cost_source === 'exact') f.exact_priced++;
|
|
222
|
+
if (s.cost_source === 'heuristic') f.heuristic_priced++;
|
|
223
|
+
}
|
|
224
|
+
f.sessions++;
|
|
225
|
+
}
|
|
226
|
+
return [...map.values()].sort((a, b) => b.tokens - a.tokens);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildDaily(rawSessions, groupedSessions, tz) {
|
|
230
|
+
const toDayKeyInTz = createDayKeyFormatter(tz);
|
|
231
|
+
const dayMap = new Map();
|
|
232
|
+
for (const s of rawSessions) {
|
|
233
|
+
if (!s.started_at || !s.ended_at) continue;
|
|
234
|
+
|
|
235
|
+
const startMs = s.started_at * 1000;
|
|
236
|
+
const endMs = s.ended_at * 1000;
|
|
237
|
+
const totalDur = endMs - startMs;
|
|
238
|
+
if (totalDur <= 0) continue;
|
|
239
|
+
|
|
240
|
+
const startDay = toDayKeyInTz(startMs);
|
|
241
|
+
const endDay = toDayKeyInTz(endMs - 1);
|
|
242
|
+
|
|
243
|
+
if (startDay === endDay) {
|
|
244
|
+
addToDay(dayMap, startDay, s, 1.0);
|
|
245
|
+
} else {
|
|
246
|
+
let cursor = dayStartMs(startDay);
|
|
247
|
+
while (cursor < endMs) {
|
|
248
|
+
const nextDay = cursor + 86400000;
|
|
249
|
+
const overlapStart = Math.max(cursor, startMs);
|
|
250
|
+
const overlapEnd = Math.min(nextDay, endMs);
|
|
251
|
+
const fraction = (overlapEnd - overlapStart) / totalDur;
|
|
252
|
+
if (fraction > 0) addToDay(dayMap, toDayKeyInTz(cursor), s, fraction);
|
|
253
|
+
cursor = nextDay;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const s of groupedSessions) {
|
|
259
|
+
if (!s.active_by_day) continue;
|
|
260
|
+
for (const [dayKey, seconds] of Object.entries(s.active_by_day)) {
|
|
261
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {} });
|
|
262
|
+
dayMap.get(dayKey).elapsed_seconds += seconds;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return [...dayMap.entries()]
|
|
267
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
268
|
+
.map(([date, d]) => ({
|
|
269
|
+
date,
|
|
270
|
+
tokens: Math.round(d.tokens),
|
|
271
|
+
cost: d.cost,
|
|
272
|
+
elapsed_seconds: Math.round(d.elapsed_seconds),
|
|
273
|
+
sessions: d.sessions,
|
|
274
|
+
by_model: Object.fromEntries(
|
|
275
|
+
Object.entries(d.by_model).map(([k, v]) => [k, { tokens: Math.round(v.tokens), cost: v.cost, elapsed_seconds: Math.round(v.elapsed_seconds) }])
|
|
276
|
+
),
|
|
277
|
+
by_family: Object.fromEntries(
|
|
278
|
+
Object.entries(d.by_family).map(([k, v]) => [k, { tokens: Math.round(v.tokens), cost: v.cost, sessions: v.sessions }])
|
|
279
|
+
),
|
|
280
|
+
by_repo: Object.fromEntries(
|
|
281
|
+
Object.entries(d.by_repo || {}).map(([k, v]) => [k, { tokens: Math.round(v.tokens), cost: v.cost, sessions: v.sessions }])
|
|
282
|
+
),
|
|
283
|
+
approximate: true,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function addToDay(dayMap, dayKey, session, fraction) {
|
|
288
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {}, by_repo: {} });
|
|
289
|
+
const d = dayMap.get(dayKey);
|
|
290
|
+
d.tokens += session.tokens_used * fraction;
|
|
291
|
+
if (session.cost !== null) d.cost += session.cost * fraction;
|
|
292
|
+
if (fraction > 0.001) d.sessions++;
|
|
293
|
+
|
|
294
|
+
const mKey = session.model_name || 'unknown';
|
|
295
|
+
if (!d.by_model[mKey]) d.by_model[mKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
296
|
+
d.by_model[mKey].tokens += session.tokens_used * fraction;
|
|
297
|
+
if (session.cost !== null) d.by_model[mKey].cost += session.cost * fraction;
|
|
298
|
+
|
|
299
|
+
const fKey = session.agent_family;
|
|
300
|
+
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
301
|
+
d.by_family[fKey].tokens += session.tokens_used * fraction;
|
|
302
|
+
if (session.cost !== null) d.by_family[fKey].cost += session.cost * fraction;
|
|
303
|
+
if (fraction > 0.001) d.by_family[fKey].sessions++;
|
|
304
|
+
|
|
305
|
+
const rKey = session.repo_label || 'unknown';
|
|
306
|
+
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
307
|
+
d.by_repo[rKey].tokens += session.tokens_used * fraction;
|
|
308
|
+
if (session.cost !== null) d.by_repo[rKey].cost += session.cost * fraction;
|
|
309
|
+
if (fraction > 0.001) d.by_repo[rKey].sessions++;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildHeatmap(rawSessions, groupedSessions, tz) {
|
|
313
|
+
const toDayKeyInTz = createDayKeyFormatter(tz);
|
|
314
|
+
const dayMap = new Map();
|
|
315
|
+
for (const s of rawSessions) {
|
|
316
|
+
if (!s.started_at) continue;
|
|
317
|
+
const dk = toDayKeyInTz(s.started_at * 1000);
|
|
318
|
+
if (!dayMap.has(dk)) dayMap.set(dk, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
319
|
+
const d = dayMap.get(dk);
|
|
320
|
+
d.tokens += s.tokens_used;
|
|
321
|
+
if (s.cost !== null) d.cost += s.cost;
|
|
322
|
+
d.sessions++;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const s of groupedSessions) {
|
|
326
|
+
if (!s.active_by_day) continue;
|
|
327
|
+
for (const [dayKey, seconds] of Object.entries(s.active_by_day)) {
|
|
328
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
329
|
+
dayMap.get(dayKey).elapsed += seconds;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return Object.fromEntries(dayMap);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function dayStartMs(dayKey) {
|
|
336
|
+
const [y, m, d] = dayKey.split('-').map(Number);
|
|
337
|
+
return new Date(y, m - 1, d).getTime();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function overlapsLowerBound(session, lowerBound) {
|
|
341
|
+
const startedAt = session.started_at || 0;
|
|
342
|
+
const endedAt = session.ended_at || startedAt;
|
|
343
|
+
return endedAt >= lowerBound;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function collapseSessionGroup(rootThreadId, group, rootLookup) {
|
|
347
|
+
const sorted = [...group].sort((a, b) => (a.started_at || 0) - (b.started_at || 0));
|
|
348
|
+
const root = rootLookup.get(rootThreadId) || group.find(s => s.thread_id === rootThreadId) || sorted[0];
|
|
349
|
+
const rootIncluded = group.some(session => session.thread_id === root.thread_id);
|
|
350
|
+
const repoLabels = new Set(group.map(s => s.repo_label).filter(Boolean));
|
|
351
|
+
const modelNames = new Set(group.map(s => s.model_name).filter(Boolean));
|
|
352
|
+
const efforts = new Set(group.map(s => s.reasoning_effort).filter(Boolean));
|
|
353
|
+
const agentRoles = new Set(group.map(s => s.agent_role).filter(Boolean));
|
|
354
|
+
const agentNicknames = new Set(group.map(s => s.agent_nickname).filter(Boolean));
|
|
355
|
+
const titles = new Set(group.map(s => s.title).filter(Boolean));
|
|
356
|
+
|
|
357
|
+
let startedAt = Infinity;
|
|
358
|
+
let endedAt = -Infinity;
|
|
359
|
+
let tokensUsed = 0;
|
|
360
|
+
let elapsedSeconds = 0;
|
|
361
|
+
let cost = 0;
|
|
362
|
+
let hasCost = false;
|
|
363
|
+
const agentFamilySet = new Set();
|
|
364
|
+
const activeByDay = new Map();
|
|
365
|
+
|
|
366
|
+
for (const session of group) {
|
|
367
|
+
if (session.started_at && session.started_at < startedAt) startedAt = session.started_at;
|
|
368
|
+
if (session.ended_at && session.ended_at > endedAt) endedAt = session.ended_at;
|
|
369
|
+
tokensUsed += session.tokens_used || 0;
|
|
370
|
+
elapsedSeconds += session.elapsed_seconds || 0;
|
|
371
|
+
mergeActiveByDay(activeByDay, session.active_by_day);
|
|
372
|
+
if (session.cost !== null) {
|
|
373
|
+
cost += session.cost;
|
|
374
|
+
hasCost = true;
|
|
375
|
+
}
|
|
376
|
+
if (session.agent_family) agentFamilySet.add(session.agent_family);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const rootStartedAt = startedAt === Infinity ? root.started_at : startedAt;
|
|
380
|
+
const rootEndedAt = endedAt === -Infinity ? root.ended_at : endedAt;
|
|
381
|
+
const exactPriced = group.filter(session => session.cost_source === 'exact').length;
|
|
382
|
+
const heuristicPriced = group.filter(session => session.cost_source === 'heuristic').length;
|
|
383
|
+
const availablePriced = exactPriced + heuristicPriced;
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
thread_id: root.thread_id,
|
|
387
|
+
root_thread_id: rootThreadId,
|
|
388
|
+
repo_label: repoLabels.size === 1 ? [...repoLabels][0] : (root.repo_label || 'mixed'),
|
|
389
|
+
model_name: root.model_name || pickSummaryValue(modelNames),
|
|
390
|
+
reasoning_effort: root.reasoning_effort || pickSummaryValue(efforts),
|
|
391
|
+
agent_role: root.agent_role,
|
|
392
|
+
agent_nickname: root.agent_nickname,
|
|
393
|
+
agent_family: root.agent_family,
|
|
394
|
+
is_subagent: false,
|
|
395
|
+
started_at: rootStartedAt,
|
|
396
|
+
ended_at: rootEndedAt,
|
|
397
|
+
elapsed_seconds: elapsedSeconds || null,
|
|
398
|
+
active_by_day: activeByDay.size > 0 ? Object.fromEntries(activeByDay) : null,
|
|
399
|
+
tokens_used: tokensUsed,
|
|
400
|
+
cost: hasCost ? cost : null,
|
|
401
|
+
title: root.title,
|
|
402
|
+
thread_count: group.length,
|
|
403
|
+
subagent_count: group.length - (rootIncluded ? 1 : 0),
|
|
404
|
+
cost_source:
|
|
405
|
+
availablePriced === 0 ? 'unavailable'
|
|
406
|
+
: heuristicPriced === 0 ? 'exact'
|
|
407
|
+
: exactPriced === 0 ? 'heuristic'
|
|
408
|
+
: 'mixed',
|
|
409
|
+
descendant_models: [...modelNames],
|
|
410
|
+
descendant_families: [...agentFamilySet],
|
|
411
|
+
descendant_roles: [...agentRoles],
|
|
412
|
+
descendant_nicknames: [...agentNicknames],
|
|
413
|
+
related_titles: [...titles].slice(0, 5),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function pickSummaryValue(values) {
|
|
418
|
+
if (values.size === 0) return null;
|
|
419
|
+
if (values.size === 1) return [...values][0];
|
|
420
|
+
return `${[...values][0]} +${values.size - 1}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function mergeActiveByDay(target, source) {
|
|
424
|
+
if (!source) return;
|
|
425
|
+
for (const [dayKey, seconds] of Object.entries(source)) {
|
|
426
|
+
target.set(dayKey, (target.get(dayKey) || 0) + (seconds || 0));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { fetchPricing } from './pricing-fetch.js';
|
|
2
|
+
|
|
3
|
+
// Pricing map: populated by initPricing() from online lookup with local fallback
|
|
4
|
+
let PRICING = null;
|
|
5
|
+
|
|
6
|
+
/** Initialize pricing from online source; falls back to local catalog on timeout/failure. Call before cost calculations. */
|
|
7
|
+
export async function initPricing() {
|
|
8
|
+
PRICING = await fetchPricing();
|
|
9
|
+
return PRICING;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getPricing() {
|
|
13
|
+
if (!PRICING) throw new Error('Cost catalog not initialized. Call initPricing() before cost calculations.');
|
|
14
|
+
return PRICING;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Codex sessions: ~75% input (mostly cached), ~25% output
|
|
18
|
+
// With ~95% prompt cache hit rate (from Codex Monitor)
|
|
19
|
+
const INPUT_FRACTION = 0.75;
|
|
20
|
+
const OUTPUT_FRACTION = 0.25;
|
|
21
|
+
const CACHE_HIT_RATE = 0.95;
|
|
22
|
+
|
|
23
|
+
export function getCacheAwareRate(modelName) {
|
|
24
|
+
if (!modelName) return null;
|
|
25
|
+
const entry = getPricing()[modelName];
|
|
26
|
+
if (!entry) return null;
|
|
27
|
+
|
|
28
|
+
const effectiveInputRate =
|
|
29
|
+
(1 - CACHE_HIT_RATE) * entry.input +
|
|
30
|
+
CACHE_HIT_RATE * entry.cached_input;
|
|
31
|
+
|
|
32
|
+
return INPUT_FRACTION * effectiveInputRate + OUTPUT_FRACTION * entry.output;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function calculateCostFromUsage(modelName, usage) {
|
|
36
|
+
if (!modelName || !usage) return null;
|
|
37
|
+
const entry = getPricing()[modelName];
|
|
38
|
+
if (!entry) return null;
|
|
39
|
+
|
|
40
|
+
const inputTokens = usage.input_tokens || 0;
|
|
41
|
+
const cachedInputTokens = usage.cached_input_tokens || 0;
|
|
42
|
+
const outputTokens = usage.output_tokens || 0;
|
|
43
|
+
const uncachedInputTokens = Math.max(inputTokens - cachedInputTokens, 0);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
(uncachedInputTokens * entry.input) +
|
|
47
|
+
(cachedInputTokens * entry.cached_input) +
|
|
48
|
+
(outputTokens * entry.output)
|
|
49
|
+
) / 1_000_000;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function estimateCostFromTotalTokens(modelName, tokensUsed) {
|
|
53
|
+
const rate = getCacheAwareRate(modelName);
|
|
54
|
+
if (rate === null) return null;
|
|
55
|
+
return (tokensUsed / 1_000_000) * rate;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function priceSession(modelName, { totalTokens = 0, usageBuckets = null } = {}) {
|
|
59
|
+
const exactCost = calculateCostFromUsage(modelName, usageBuckets);
|
|
60
|
+
if (exactCost !== null) {
|
|
61
|
+
return { cost: exactCost, source: 'exact' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const heuristicCost = estimateCostFromTotalTokens(modelName, totalTokens);
|
|
65
|
+
if (heuristicCost !== null) {
|
|
66
|
+
return { cost: heuristicCost, source: 'heuristic' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { cost: null, source: 'unpriced' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isModelPriced(modelName) {
|
|
73
|
+
return modelName != null && getPricing()[modelName] != null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getModelPricing(modelName) {
|
|
77
|
+
return getPricing()[modelName] || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const CATALOG_VERSION = '2026-03-13';
|
|
81
|
+
export const CACHE_ASSUMPTIONS = {
|
|
82
|
+
input_fraction: INPUT_FRACTION,
|
|
83
|
+
output_fraction: OUTPUT_FRACTION,
|
|
84
|
+
cache_hit_rate: CACHE_HIT_RATE,
|
|
85
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createDayKeyFormatter(tz) {
|
|
2
|
+
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
3
|
+
timeZone: tz,
|
|
4
|
+
year: 'numeric',
|
|
5
|
+
month: '2-digit',
|
|
6
|
+
day: '2-digit',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return (ms) => {
|
|
10
|
+
const parts = formatter.formatToParts(new Date(ms));
|
|
11
|
+
const year = parts.find((part) => part.type === 'year')?.value;
|
|
12
|
+
const month = parts.find((part) => part.type === 'month')?.value;
|
|
13
|
+
const day = parts.find((part) => part.type === 'day')?.value;
|
|
14
|
+
return `${year}-${month}-${day}`;
|
|
15
|
+
};
|
|
16
|
+
}
|