@zhangferry-dev/tokendash 1.0.0 → 1.1.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/bin/tokendash.js +2 -0
- package/dist/client/assets/{index-DOeZtR1c.js → index-BBDGQ0H0.js} +50 -50
- package/dist/client/assets/index-BaY47OM2.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/ccusage.d.ts +0 -1
- package/dist/server/ccusage.js +6 -10
- package/dist/server/claudeBlocksParser.d.ts +6 -0
- package/dist/server/claudeBlocksParser.js +150 -0
- package/dist/server/codexParser.d.ts +47 -0
- package/dist/server/codexParser.js +379 -0
- package/dist/server/codexPricing.d.ts +32 -0
- package/dist/server/codexPricing.js +43 -0
- package/dist/server/routes/blocks.js +28 -3
- package/dist/server/routes/daily.js +5 -7
- package/dist/server/routes/projects.js +5 -7
- package/dist/shared/schemas.d.ts +17 -17
- package/package.json +5 -4
- package/dist/client/assets/index-C9UxEhwo.css +0 -1
- package/dist/server/codexNormalizer.d.ts +0 -4
- package/dist/server/codexNormalizer.js +0 -50
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, accessSync, constants } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { calculateCost } from './codexPricing.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Zod schemas for JSONL event validation (format change detector)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const TokenUsageSchema = z.object({
|
|
10
|
+
input_tokens: z.number().default(0),
|
|
11
|
+
cached_input_tokens: z.number().default(0),
|
|
12
|
+
output_tokens: z.number().default(0),
|
|
13
|
+
reasoning_output_tokens: z.number().default(0),
|
|
14
|
+
total_tokens: z.number().default(0),
|
|
15
|
+
}).default({ input_tokens: 0, cached_input_tokens: 0, output_tokens: 0, reasoning_output_tokens: 0, total_tokens: 0 });
|
|
16
|
+
const TokenCountInfoSchema = z.object({
|
|
17
|
+
total_token_usage: TokenUsageSchema,
|
|
18
|
+
last_token_usage: TokenUsageSchema,
|
|
19
|
+
}).nullable().default(null);
|
|
20
|
+
const TokenCountPayloadSchema = z.object({
|
|
21
|
+
type: z.literal('token_count'),
|
|
22
|
+
info: TokenCountInfoSchema,
|
|
23
|
+
});
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function getSessionsDir() {
|
|
28
|
+
return join(homedir(), '.codex', 'sessions');
|
|
29
|
+
}
|
|
30
|
+
export function isSessionsDirAccessible() {
|
|
31
|
+
try {
|
|
32
|
+
accessSync(getSessionsDir(), constants.R_OK);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Recursively find all .jsonl files under ~/.codex/sessions/
|
|
41
|
+
*/
|
|
42
|
+
export function scanCodexSessions() {
|
|
43
|
+
const sessionsDir = getSessionsDir();
|
|
44
|
+
const results = [];
|
|
45
|
+
function walk(dir) {
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = readdirSync(dir);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const full = join(dir, entry);
|
|
55
|
+
let st;
|
|
56
|
+
try {
|
|
57
|
+
st = statSync(full);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (st.isDirectory()) {
|
|
63
|
+
walk(full);
|
|
64
|
+
}
|
|
65
|
+
else if (entry.endsWith('.jsonl')) {
|
|
66
|
+
results.push(full);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(sessionsDir);
|
|
71
|
+
return results.sort();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a single Codex session JSONL file.
|
|
75
|
+
*
|
|
76
|
+
* CRITICAL INVARIANT: Sum ALL token_count events without any deduplication.
|
|
77
|
+
* Each Codex turn emits TWO token_count events with identical last_token_usage
|
|
78
|
+
* values (~1-2s apart) — one for reasoning, one for output completion.
|
|
79
|
+
* Both are distinct billable events. Deduplicating would produce the wrong
|
|
80
|
+
* total (4.7M instead of the correct 9.2M).
|
|
81
|
+
*/
|
|
82
|
+
export function parseCodexSession(filepath) {
|
|
83
|
+
let content;
|
|
84
|
+
try {
|
|
85
|
+
content = readFileSync(filepath, 'utf-8');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const lines = content.split('\n');
|
|
91
|
+
let sessionId = '';
|
|
92
|
+
let cwd = '';
|
|
93
|
+
let model = '';
|
|
94
|
+
let createdAt = '';
|
|
95
|
+
const tokenEvents = [];
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (!trimmed)
|
|
99
|
+
continue;
|
|
100
|
+
let obj;
|
|
101
|
+
try {
|
|
102
|
+
obj = JSON.parse(trimmed);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const type = obj.type;
|
|
108
|
+
if (type === 'session_meta') {
|
|
109
|
+
const payload = obj.payload || {};
|
|
110
|
+
sessionId = payload.id || '';
|
|
111
|
+
cwd = payload.cwd || '';
|
|
112
|
+
createdAt = payload.timestamp || '';
|
|
113
|
+
}
|
|
114
|
+
if (type === 'turn_context') {
|
|
115
|
+
const payload = obj.payload || {};
|
|
116
|
+
if (!model && payload.model) {
|
|
117
|
+
model = payload.model;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Extract token counts from event_msg with nested token_count payload.
|
|
121
|
+
// NEVER deduplicate — see invariant comment above.
|
|
122
|
+
if (type === 'event_msg') {
|
|
123
|
+
const payload = obj.payload || {};
|
|
124
|
+
if (payload.type === 'token_count') {
|
|
125
|
+
const timestamp = obj.timestamp || '';
|
|
126
|
+
const parseResult = TokenCountPayloadSchema.safeParse(payload);
|
|
127
|
+
if (!parseResult.success) {
|
|
128
|
+
console.warn(`[codexParser] Schema validation failed in ${filepath}:`, parseResult.error.message);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const info = parseResult.data.info;
|
|
132
|
+
if (!info)
|
|
133
|
+
continue;
|
|
134
|
+
const last = info.last_token_usage;
|
|
135
|
+
tokenEvents.push({
|
|
136
|
+
timestamp,
|
|
137
|
+
inputTokens: last.input_tokens,
|
|
138
|
+
cachedInputTokens: last.cached_input_tokens,
|
|
139
|
+
outputTokens: last.output_tokens,
|
|
140
|
+
reasoningOutputTokens: last.reasoning_output_tokens,
|
|
141
|
+
totalTokens: last.total_tokens,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!sessionId)
|
|
147
|
+
return null;
|
|
148
|
+
return { id: sessionId, cwd, model, createdAt, tokenEvents };
|
|
149
|
+
}
|
|
150
|
+
/** Parse all Codex sessions. */
|
|
151
|
+
export function parseAllSessions() {
|
|
152
|
+
return scanCodexSessions()
|
|
153
|
+
.map(parseCodexSession)
|
|
154
|
+
.filter((s) => s !== null);
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Date/timezone helpers
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
const TZ_OFFSETS = {
|
|
160
|
+
'Asia/Shanghai': 8,
|
|
161
|
+
'Asia/Tokyo': 9,
|
|
162
|
+
'America/New_York': -5,
|
|
163
|
+
'America/Los_Angeles': -8,
|
|
164
|
+
'Europe/London': 0,
|
|
165
|
+
'UTC': 0,
|
|
166
|
+
};
|
|
167
|
+
function getTzOffsetHours(tz) {
|
|
168
|
+
return TZ_OFFSETS[tz] ?? 8; // Default Asia/Shanghai
|
|
169
|
+
}
|
|
170
|
+
function toLocalISO(ts, tz) {
|
|
171
|
+
const d = new Date(ts);
|
|
172
|
+
return new Date(d.getTime() + getTzOffsetHours(tz) * 3600_000);
|
|
173
|
+
}
|
|
174
|
+
function getDateKey(ts, tz) {
|
|
175
|
+
return toLocalISO(ts, tz).toISOString().slice(0, 10);
|
|
176
|
+
}
|
|
177
|
+
function getHourKey(ts, tz) {
|
|
178
|
+
const local = toLocalISO(ts, tz);
|
|
179
|
+
return local.toISOString().slice(0, 13).replace('T', ' ') + ':00';
|
|
180
|
+
}
|
|
181
|
+
function getMonthKey(ts, tz) {
|
|
182
|
+
return getDateKey(ts, tz).slice(0, 7);
|
|
183
|
+
}
|
|
184
|
+
function extractProjectName(cwd) {
|
|
185
|
+
if (!cwd)
|
|
186
|
+
return 'unknown';
|
|
187
|
+
const parts = cwd.replace(/\/+$/, '').split('/');
|
|
188
|
+
return parts[parts.length - 1] || 'unknown';
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Core aggregation
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
function emptyAcc() {
|
|
194
|
+
return { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0, totalTokens: 0 };
|
|
195
|
+
}
|
|
196
|
+
function addAcc(a, ev) {
|
|
197
|
+
a.inputTokens += ev.inputTokens;
|
|
198
|
+
a.cachedInputTokens += ev.cachedInputTokens;
|
|
199
|
+
a.outputTokens += ev.outputTokens;
|
|
200
|
+
a.reasoningOutputTokens += ev.reasoningOutputTokens;
|
|
201
|
+
a.totalTokens += ev.totalTokens;
|
|
202
|
+
}
|
|
203
|
+
function mergeAcc(a, b) {
|
|
204
|
+
a.inputTokens += b.inputTokens;
|
|
205
|
+
a.cachedInputTokens += b.cachedInputTokens;
|
|
206
|
+
a.outputTokens += b.outputTokens;
|
|
207
|
+
a.reasoningOutputTokens += b.reasoningOutputTokens;
|
|
208
|
+
a.totalTokens += b.totalTokens;
|
|
209
|
+
}
|
|
210
|
+
function accToEntry(date, acc, models) {
|
|
211
|
+
const cost = calculateCost(acc, models);
|
|
212
|
+
return {
|
|
213
|
+
date,
|
|
214
|
+
inputTokens: acc.inputTokens,
|
|
215
|
+
outputTokens: acc.outputTokens,
|
|
216
|
+
cacheCreationTokens: 0,
|
|
217
|
+
cacheReadTokens: acc.cachedInputTokens,
|
|
218
|
+
totalTokens: acc.totalTokens,
|
|
219
|
+
totalCost: cost,
|
|
220
|
+
modelsUsed: [...models],
|
|
221
|
+
modelBreakdowns: buildModelBreakdowns(acc, models, cost),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function buildModelBreakdowns(acc, models, totalCost) {
|
|
225
|
+
const modelList = [...models];
|
|
226
|
+
if (modelList.length === 0)
|
|
227
|
+
return [];
|
|
228
|
+
const costPerModel = totalCost / modelList.length;
|
|
229
|
+
return modelList.map(name => ({
|
|
230
|
+
modelName: name,
|
|
231
|
+
inputTokens: acc.inputTokens,
|
|
232
|
+
outputTokens: acc.outputTokens,
|
|
233
|
+
cacheCreationTokens: 0,
|
|
234
|
+
cacheReadTokens: acc.cachedInputTokens,
|
|
235
|
+
cost: costPerModel,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
function groupSessions(sessions, options) {
|
|
239
|
+
const tz = options.timezone || 'Asia/Shanghai';
|
|
240
|
+
const grouped = new Map();
|
|
241
|
+
for (const session of sessions) {
|
|
242
|
+
if (options.project && extractProjectName(session.cwd) !== options.project)
|
|
243
|
+
continue;
|
|
244
|
+
for (const ev of session.tokenEvents) {
|
|
245
|
+
const evDate = new Date(ev.timestamp);
|
|
246
|
+
if (options.since && evDate < options.since)
|
|
247
|
+
continue;
|
|
248
|
+
if (options.until && evDate > options.until)
|
|
249
|
+
continue;
|
|
250
|
+
let key;
|
|
251
|
+
switch (options.groupBy) {
|
|
252
|
+
case 'hour':
|
|
253
|
+
key = getHourKey(ev.timestamp, tz);
|
|
254
|
+
break;
|
|
255
|
+
case 'month':
|
|
256
|
+
key = getMonthKey(ev.timestamp, tz);
|
|
257
|
+
break;
|
|
258
|
+
case 'session':
|
|
259
|
+
key = session.id;
|
|
260
|
+
break;
|
|
261
|
+
case 'project':
|
|
262
|
+
key = extractProjectName(session.cwd);
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
key = getDateKey(ev.timestamp, tz);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
if (!grouped.has(key)) {
|
|
269
|
+
grouped.set(key, { acc: emptyAcc(), models: new Set() });
|
|
270
|
+
}
|
|
271
|
+
const entry = grouped.get(key);
|
|
272
|
+
addAcc(entry.acc, ev);
|
|
273
|
+
if (session.model)
|
|
274
|
+
entry.models.add(session.model);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return grouped;
|
|
278
|
+
}
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Public API — response builders for route handlers
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
/** Aggregate and return DailyResponse format (for /daily?agent=codex) */
|
|
283
|
+
export function getDailyResponse(options) {
|
|
284
|
+
const sessions = parseAllSessions();
|
|
285
|
+
const grouped = groupSessions(sessions, { groupBy: 'day', ...options });
|
|
286
|
+
const daily = [];
|
|
287
|
+
const totalsAcc = emptyAcc();
|
|
288
|
+
for (const [date, { acc, models }] of grouped) {
|
|
289
|
+
daily.push(accToEntry(date, acc, models));
|
|
290
|
+
mergeAcc(totalsAcc, acc);
|
|
291
|
+
}
|
|
292
|
+
// Sort by date ascending
|
|
293
|
+
daily.sort((a, b) => a.date.localeCompare(b.date));
|
|
294
|
+
const models = new Set();
|
|
295
|
+
for (const s of sessions)
|
|
296
|
+
if (s.model)
|
|
297
|
+
models.add(s.model);
|
|
298
|
+
const totalCost = calculateCost(totalsAcc, models);
|
|
299
|
+
return {
|
|
300
|
+
daily,
|
|
301
|
+
totals: {
|
|
302
|
+
inputTokens: totalsAcc.inputTokens,
|
|
303
|
+
outputTokens: totalsAcc.outputTokens,
|
|
304
|
+
cacheCreationTokens: 0,
|
|
305
|
+
cacheReadTokens: totalsAcc.cachedInputTokens,
|
|
306
|
+
totalTokens: totalsAcc.totalTokens,
|
|
307
|
+
totalCost,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Aggregate and return ProjectsResponse format (for /projects?agent=codex) */
|
|
312
|
+
export function getProjectsResponse(options) {
|
|
313
|
+
const sessions = parseAllSessions();
|
|
314
|
+
const tz = options?.timezone || 'Asia/Shanghai';
|
|
315
|
+
const projects = {};
|
|
316
|
+
for (const session of sessions) {
|
|
317
|
+
const projectName = extractProjectName(session.cwd);
|
|
318
|
+
// Per-project daily grouping
|
|
319
|
+
const dailyMap = new Map();
|
|
320
|
+
for (const ev of session.tokenEvents) {
|
|
321
|
+
const evDate = new Date(ev.timestamp);
|
|
322
|
+
if (options?.since && evDate < options.since)
|
|
323
|
+
continue;
|
|
324
|
+
if (options?.until && evDate > options.until)
|
|
325
|
+
continue;
|
|
326
|
+
const dayKey = getDateKey(ev.timestamp, tz);
|
|
327
|
+
if (!dailyMap.has(dayKey)) {
|
|
328
|
+
dailyMap.set(dayKey, { acc: emptyAcc(), models: new Set() });
|
|
329
|
+
}
|
|
330
|
+
addAcc(dailyMap.get(dayKey).acc, ev);
|
|
331
|
+
if (session.model)
|
|
332
|
+
dailyMap.get(dayKey).models.add(session.model);
|
|
333
|
+
}
|
|
334
|
+
if (!projects[projectName])
|
|
335
|
+
projects[projectName] = [];
|
|
336
|
+
for (const [date, { acc, models }] of dailyMap) {
|
|
337
|
+
projects[projectName].push(accToEntry(date, acc, models));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Sort each project's daily entries
|
|
341
|
+
for (const key of Object.keys(projects)) {
|
|
342
|
+
projects[key].sort((a, b) => a.date.localeCompare(b.date));
|
|
343
|
+
}
|
|
344
|
+
return { projects };
|
|
345
|
+
}
|
|
346
|
+
/** Aggregate and return BlocksResponse format (hourly, for /blocks?agent=codex) */
|
|
347
|
+
export function getBlocksResponse(options) {
|
|
348
|
+
const sessions = parseAllSessions();
|
|
349
|
+
const grouped = groupSessions(sessions, { groupBy: 'hour', ...options });
|
|
350
|
+
const blocks = [];
|
|
351
|
+
let idx = 0;
|
|
352
|
+
for (const [hourKey, { acc, models }] of grouped) {
|
|
353
|
+
const cost = calculateCost(acc, models);
|
|
354
|
+
const [datePart, timePart] = hourKey.split(' ');
|
|
355
|
+
const hour = timePart.split(':')[0];
|
|
356
|
+
blocks.push({
|
|
357
|
+
id: `codex-hour-${idx}`,
|
|
358
|
+
startTime: `${datePart}T${hour}:00:00`,
|
|
359
|
+
endTime: `${datePart}T${hour}:59:59`,
|
|
360
|
+
actualEndTime: null,
|
|
361
|
+
isActive: false,
|
|
362
|
+
isGap: false,
|
|
363
|
+
entries: acc.totalTokens > 0 ? 1 : 0,
|
|
364
|
+
tokenCounts: {
|
|
365
|
+
inputTokens: acc.inputTokens,
|
|
366
|
+
outputTokens: acc.outputTokens,
|
|
367
|
+
cacheCreationInputTokens: 0,
|
|
368
|
+
cacheReadInputTokens: acc.cachedInputTokens,
|
|
369
|
+
},
|
|
370
|
+
totalTokens: acc.totalTokens,
|
|
371
|
+
costUSD: cost,
|
|
372
|
+
models: [...models],
|
|
373
|
+
});
|
|
374
|
+
idx++;
|
|
375
|
+
}
|
|
376
|
+
// Sort by startTime
|
|
377
|
+
blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
378
|
+
return { blocks };
|
|
379
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex token pricing configuration.
|
|
3
|
+
*
|
|
4
|
+
* Pricing formula (confirmed by reverse-engineering @ccusage/codex):
|
|
5
|
+
* cost = (inputTokens - cachedInputTokens) * input_rate
|
|
6
|
+
* + cachedInputTokens * cached_rate
|
|
7
|
+
* + outputTokens * output_rate
|
|
8
|
+
*
|
|
9
|
+
* Reasoning tokens are NOT billed separately (included in outputTokens).
|
|
10
|
+
*
|
|
11
|
+
* Update rates from https://openai.com/api/pricing/ when models change.
|
|
12
|
+
* All prices are USD per 1M tokens.
|
|
13
|
+
*/
|
|
14
|
+
interface ModelPricing {
|
|
15
|
+
inputPer1M: number;
|
|
16
|
+
cachedInputPer1M: number;
|
|
17
|
+
outputPer1M: number;
|
|
18
|
+
}
|
|
19
|
+
interface TokenCounts {
|
|
20
|
+
inputTokens: number;
|
|
21
|
+
cachedInputTokens: number;
|
|
22
|
+
outputTokens: number;
|
|
23
|
+
reasoningOutputTokens: number;
|
|
24
|
+
totalTokens: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Calculate cost in USD from token counts and model pricing.
|
|
28
|
+
* Matches the @ccusage/codex calculateCostUSD function exactly.
|
|
29
|
+
*/
|
|
30
|
+
export declare function calculateCost(tokens: TokenCounts, models: Set<string>): number;
|
|
31
|
+
export declare function getModelPricing(model: string): ModelPricing;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex token pricing configuration.
|
|
3
|
+
*
|
|
4
|
+
* Pricing formula (confirmed by reverse-engineering @ccusage/codex):
|
|
5
|
+
* cost = (inputTokens - cachedInputTokens) * input_rate
|
|
6
|
+
* + cachedInputTokens * cached_rate
|
|
7
|
+
* + outputTokens * output_rate
|
|
8
|
+
*
|
|
9
|
+
* Reasoning tokens are NOT billed separately (included in outputTokens).
|
|
10
|
+
*
|
|
11
|
+
* Update rates from https://openai.com/api/pricing/ when models change.
|
|
12
|
+
* All prices are USD per 1M tokens.
|
|
13
|
+
*/
|
|
14
|
+
const MODEL_PRICING = {
|
|
15
|
+
'gpt-5.4': {
|
|
16
|
+
inputPer1M: 2.50,
|
|
17
|
+
cachedInputPer1M: 0.25,
|
|
18
|
+
outputPer1M: 15.00,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const DEFAULT_PRICING = {
|
|
22
|
+
inputPer1M: 2.50,
|
|
23
|
+
cachedInputPer1M: 0.25,
|
|
24
|
+
outputPer1M: 15.00,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Calculate cost in USD from token counts and model pricing.
|
|
28
|
+
* Matches the @ccusage/codex calculateCostUSD function exactly.
|
|
29
|
+
*/
|
|
30
|
+
export function calculateCost(tokens, models) {
|
|
31
|
+
const model = [...models][0] ?? '';
|
|
32
|
+
const pricing = MODEL_PRICING[model] ?? DEFAULT_PRICING;
|
|
33
|
+
const nonCachedInput = Math.max(tokens.inputTokens - tokens.cachedInputTokens, 0);
|
|
34
|
+
const cachedInput = Math.min(tokens.cachedInputTokens, tokens.inputTokens);
|
|
35
|
+
const outputTokens = tokens.outputTokens;
|
|
36
|
+
const inputCost = (nonCachedInput / 1_000_000) * pricing.inputPer1M;
|
|
37
|
+
const cachedCost = (cachedInput / 1_000_000) * pricing.cachedInputPer1M;
|
|
38
|
+
const outputCost = (outputTokens / 1_000_000) * pricing.outputPer1M;
|
|
39
|
+
return inputCost + cachedCost + outputCost;
|
|
40
|
+
}
|
|
41
|
+
export function getModelPricing(model) {
|
|
42
|
+
return MODEL_PRICING[model] ?? DEFAULT_PRICING;
|
|
43
|
+
}
|
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
import { runCcusage } from '../ccusage.js';
|
|
2
2
|
import { cache } from '../cache.js';
|
|
3
3
|
import { validateBlocks } from '../../shared/schemas.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getBlocksResponse } from '../codexParser.js';
|
|
5
|
+
import { getClaudeBlocksByProject } from '../claudeBlocksParser.js';
|
|
5
6
|
export async function getBlocks(req, res) {
|
|
6
7
|
const agent = req.query.agent || 'claude';
|
|
7
|
-
const
|
|
8
|
+
const project = req.query.project || undefined;
|
|
8
9
|
try {
|
|
9
10
|
if (agent === 'codex') {
|
|
10
|
-
|
|
11
|
+
const projectCacheKey = `blocks:${agent}:${project || 'all'}`;
|
|
12
|
+
const cached = cache.get(projectCacheKey);
|
|
13
|
+
if (cached) {
|
|
14
|
+
res.json(cached);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const data = getBlocksResponse({ project: project || null });
|
|
18
|
+
cache.set(projectCacheKey, data);
|
|
19
|
+
res.json(data);
|
|
11
20
|
return;
|
|
12
21
|
}
|
|
22
|
+
// Claude Code with project filter: use custom JSONL parser
|
|
23
|
+
if (project) {
|
|
24
|
+
const projectCacheKey = `blocks:claude:${project}`;
|
|
25
|
+
const cached = cache.get(projectCacheKey);
|
|
26
|
+
if (cached) {
|
|
27
|
+
res.json(cached);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const blocks = getClaudeBlocksByProject(project);
|
|
31
|
+
const data = { blocks };
|
|
32
|
+
cache.set(projectCacheKey, data);
|
|
33
|
+
res.json(data);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Claude Code without project filter: use ccusage blocks
|
|
37
|
+
const cacheKey = `blocks:${agent}`;
|
|
13
38
|
const cached = cache.get(cacheKey);
|
|
14
39
|
if (cached) {
|
|
15
40
|
res.json(cached);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { runCcusage
|
|
1
|
+
import { runCcusage } from '../ccusage.js';
|
|
2
2
|
import { cache } from '../cache.js';
|
|
3
3
|
import { validateDaily } from '../../shared/schemas.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getDailyResponse } from '../codexParser.js';
|
|
5
5
|
export async function getDaily(req, res) {
|
|
6
6
|
const agent = req.query.agent || 'claude';
|
|
7
7
|
const cacheKey = `daily:${agent}`;
|
|
@@ -12,11 +12,9 @@ export async function getDaily(req, res) {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
if (agent === 'codex') {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
cache.set(cacheKey, normalized);
|
|
19
|
-
res.json(normalized);
|
|
15
|
+
const data = getDailyResponse();
|
|
16
|
+
cache.set(cacheKey, data);
|
|
17
|
+
res.json(data);
|
|
20
18
|
}
|
|
21
19
|
else {
|
|
22
20
|
const stdout = await runCcusage(['daily', '--breakdown']);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { runCcusage
|
|
1
|
+
import { runCcusage } from '../ccusage.js';
|
|
2
2
|
import { cache } from '../cache.js';
|
|
3
3
|
import { validateProjects } from '../../shared/schemas.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getProjectsResponse } from '../codexParser.js';
|
|
5
5
|
export async function getProjects(req, res) {
|
|
6
6
|
const agent = req.query.agent || 'claude';
|
|
7
7
|
const cacheKey = `projects:${agent}`;
|
|
@@ -12,11 +12,9 @@ export async function getProjects(req, res) {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
if (agent === 'codex') {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
cache.set(cacheKey, normalized);
|
|
19
|
-
res.json(normalized);
|
|
15
|
+
const data = getProjectsResponse();
|
|
16
|
+
cache.set(cacheKey, data);
|
|
17
|
+
res.json(data);
|
|
20
18
|
}
|
|
21
19
|
else {
|
|
22
20
|
const stdout = await runCcusage(['daily', '--instances', '--breakdown']);
|