lumencode 0.4.3

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,626 @@
1
+ import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { getCachedFileRecords } from './cache.js';
4
+ import { aggregateScenarios } from './scenario.js';
5
+ import { parseSubagentFiles } from './parser.js';
6
+ import { getInputTokens, getOutputTokens, getCacheRead, getCacheCreate, getModel, isAssistantRecord } from './record-utils.js';
7
+ import { resolveModelPricing } from './pricing-loader.js';
8
+
9
+ // 重新导出以保持向后兼容(测试和其他模块从 aggregate.js 引用)
10
+ export { resolveModelPricing };
11
+
12
+ export function normalizeProjectPath(path) {
13
+ return path.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, '');
14
+ }
15
+
16
+ function getProjectBaseName(projectPath) {
17
+ if (!projectPath) return '';
18
+ const normalized = projectPath.replace(/\\/g, '/').replace(/\/$/, '');
19
+ const parts = normalized.split('/');
20
+ return parts[parts.length - 1] || '';
21
+ }
22
+
23
+ function encodeProjectPath(projectPath) {
24
+ return projectPath
25
+ .replace(/:[\\/]/, '--') // D:/ → D--
26
+ .replace(/[\\/]/g, '-'); // 剩余 / 或 \ → -
27
+ }
28
+
29
+ export function collectAllRecords(claudeDir, excludeProjects = [], includeProjects = null) {
30
+ const projectsDir = join(claudeDir, 'projects');
31
+ const allRecords = [];
32
+ const projectStats = {};
33
+
34
+ if (!statSync(projectsDir).isDirectory()) {
35
+ return { records: [], projects: {} };
36
+ }
37
+
38
+ const dirs = readdirSync(projectsDir).filter(d => {
39
+ const full = join(projectsDir, d);
40
+ return statSync(full).isDirectory() && !excludeProjects.includes(d);
41
+ });
42
+
43
+ // 反向编码匹配:把 includeProjects 编码为目录名
44
+ const encodedIncludes = includeProjects
45
+ ? new Set(includeProjects.map(p => encodeProjectPath(normalizeProjectPath(p))))
46
+ : null;
47
+
48
+ for (const projDir of dirs) {
49
+ const projPath = join(projectsDir, projDir);
50
+ const projName = decodeProjectName(projDir);
51
+
52
+ if (encodedIncludes) {
53
+ if (!encodedIncludes.has(projDir)) {
54
+ continue;
55
+ }
56
+ }
57
+
58
+ const allEntries = readdirSync(projPath);
59
+ const files = allEntries.filter(f => f.endsWith('.jsonl') && !f.includes('subagents'));
60
+
61
+ let projRequests = 0;
62
+ let projSessions = new Set();
63
+
64
+ for (const file of files) {
65
+ const filePath = join(projPath, file);
66
+ const sessionIdFromFile = file.replace(/\.jsonl$/, '');
67
+ try {
68
+ const records = getCachedFileRecords(filePath);
69
+ for (const r of records) {
70
+ if (!r.sessionId) r.sessionId = sessionIdFromFile;
71
+ r.project = projName;
72
+ allRecords.push(r);
73
+ }
74
+ const sessionRecords = records.filter(r => r.sessionId);
75
+ sessionRecords.forEach(r => projSessions.add(r.sessionId));
76
+ projRequests += records.filter(r => isAssistantRecord(r)).length;
77
+ } catch {}
78
+
79
+ // 解析子 agent 日志
80
+ try {
81
+ const sessionDir = dirname(filePath);
82
+ const subRecords = parseSubagentFiles(sessionDir);
83
+ for (const r of subRecords) {
84
+ if (!r.sessionId) r.sessionId = sessionIdFromFile;
85
+ r.project = projName;
86
+ allRecords.push(r);
87
+ }
88
+ projRequests += subRecords.filter(r => isAssistantRecord(r)).length;
89
+ subRecords.filter(r => r.sessionId).forEach(r => projSessions.add(r.sessionId));
90
+ } catch {}
91
+ }
92
+
93
+ // 当无主 JSONL 文件时,扫描 sessions-index.json 和 UUID 子目录
94
+ if (files.length === 0) {
95
+ // 解析 sessions-index.json
96
+ const indexPath = join(projPath, 'sessions-index.json');
97
+ if (existsSync(indexPath)) {
98
+ try {
99
+ const raw = JSON.parse(readFileSync(indexPath, 'utf-8'));
100
+ for (const entry of raw?.entries || []) {
101
+ if (!entry.sessionId) continue;
102
+ const jsonlPath = entry.fullPath || join(projPath, entry.sessionId + '.jsonl');
103
+ if (existsSync(jsonlPath)) continue;
104
+ const ts = entry.created || entry.modified || '';
105
+ const userRec = {
106
+ type: 'user', role: 'user', timestamp: ts, model: '',
107
+ text: entry.firstPrompt || entry.summary || '', toolCalls: [],
108
+ sessionId: entry.sessionId, cwd: entry.projectPath || '',
109
+ gitBranch: entry.gitBranch || '', project: projName,
110
+ tokens: { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
111
+ isSidechain: false, isSubagent: false, messageId: '', requestId: '',
112
+ costUSD: null, isApiError: false, speed: 'standard', _fromIndex: true,
113
+ };
114
+ allRecords.push(userRec);
115
+ projSessions.add(entry.sessionId);
116
+ if (entry.messageCount > 0) {
117
+ allRecords.push({
118
+ type: 'assistant', role: 'assistant',
119
+ timestamp: entry.modified || ts, model: '',
120
+ text: '', toolCalls: [], sessionId: entry.sessionId,
121
+ cwd: entry.projectPath || '', gitBranch: entry.gitBranch || '',
122
+ project: projName,
123
+ tokens: { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
124
+ isSidechain: false, isSubagent: false, messageId: '', requestId: '',
125
+ costUSD: null, isApiError: false, speed: 'standard', _fromIndex: true,
126
+ });
127
+ projRequests++;
128
+ }
129
+ }
130
+ } catch {}
131
+ }
132
+
133
+ // 扫描 UUID 子目录中的 subagent 文件
134
+ const uuidDirs = allEntries.filter(d => {
135
+ const full = join(projPath, d);
136
+ try { return statSync(full).isDirectory() && /^[0-9a-f]{8}-/i.test(d); } catch { return false; }
137
+ });
138
+ for (const uuidDir of uuidDirs) {
139
+ const sessionDir = join(projPath, uuidDir);
140
+ try {
141
+ const subRecords = parseSubagentFiles(sessionDir);
142
+ for (const r of subRecords) {
143
+ if (!r.sessionId) r.sessionId = uuidDir;
144
+ r.project = projName;
145
+ allRecords.push(r);
146
+ }
147
+ projRequests += subRecords.filter(r => isAssistantRecord(r)).length;
148
+ subRecords.filter(r => r.sessionId).forEach(r => projSessions.add(r.sessionId));
149
+ } catch {}
150
+ }
151
+ }
152
+
153
+ if (projSessions.size > 0 || projRequests > 0) {
154
+ projectStats[projName] = { sessions: projSessions.size, requests: projRequests };
155
+ }
156
+ }
157
+
158
+ const deduped = deduplicateRecords(allRecords);
159
+ return { records: deduped, projects: projectStats };
160
+ }
161
+
162
+ export function deduplicateRecords(records) {
163
+ const seen = new Map();
164
+ const nonDedupable = [];
165
+
166
+ for (const r of records) {
167
+ if (!isAssistantRecord(r) || (!r.messageId && !r.metadata?.messageId) || (!r.requestId && !r.metadata?.requestId)) {
168
+ nonDedupable.push(r);
169
+ continue;
170
+ }
171
+
172
+ const key = `${r.messageId || r.metadata?.messageId}:${r.requestId || r.metadata?.requestId}`;
173
+ const tokenCount = getInputTokens(r) + getOutputTokens(r) + getCacheRead(r) + getCacheCreate(r);
174
+ const existing = seen.get(key);
175
+
176
+ if (!existing || tokenCount > existing.tokenCount) {
177
+ seen.set(key, { record: r, tokenCount });
178
+ }
179
+ }
180
+
181
+ const deduped = [...nonDedupable];
182
+ for (const { record } of seen.values()) {
183
+ deduped.push(record);
184
+ }
185
+
186
+ deduped.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
187
+ return deduped;
188
+ }
189
+
190
+ function decodeProjectName(dirName) {
191
+ let decoded = dirName
192
+ .replace(/^([A-Z])-/, '$1:/')
193
+ .replace(/--/g, '/')
194
+ .replace(/-/g, '/');
195
+
196
+ if (dirName.startsWith('...')) {
197
+ decoded = '.../' + decoded.slice(3);
198
+ }
199
+
200
+ // 去掉尾部多余斜杠
201
+ decoded = decoded.replace(/\/+$/, '');
202
+
203
+ if (/^[A-Z]:$/.test(decoded)) {
204
+ decoded = decoded + ' [空路径]';
205
+ }
206
+
207
+ return decoded || '[未知项目]';
208
+ }
209
+
210
+ export function computeUsageStats(records, scenarioKeywords, costMode = 'auto') {
211
+ const stats = {
212
+ sessionCount: new Set(records.filter(r => !(r.metadata?.isSubagent || r.isSubagent)).map(r => r.sessionId).filter(Boolean)).size,
213
+ requestCount: records.filter(r => isAssistantRecord(r)).length,
214
+ userMessageCount: records.filter(r => {
215
+ const text = r.metadata?.text || r.text || '';
216
+ const type = r.metadata?.type || r.type;
217
+ return type === 'user' && text && !text.startsWith('<system-reminder');
218
+ }).length,
219
+ activeDays: new Set(records.filter(r => r.timestamp).map(r => r.timestamp.slice(0, 10))).size,
220
+ inputTokens: 0,
221
+ outputTokens: 0,
222
+ cacheRead: 0,
223
+ cacheCreate: 0,
224
+ totalTokens: 0,
225
+ subagentTokens: 0,
226
+ models: {},
227
+ tools: {},
228
+ scenarios: {},
229
+ projects: {},
230
+ dailyStats: {},
231
+ toolBreakdown: {}, // 新增:各工具数据分布
232
+ };
233
+
234
+ for (const r of records) {
235
+ const inputTokens = getInputTokens(r);
236
+ const outputTokens = getOutputTokens(r);
237
+ const cacheRead = getCacheRead(r);
238
+ const cacheCreate = getCacheCreate(r);
239
+ const model = getModel(r);
240
+ const isAssistant = isAssistantRecord(r);
241
+
242
+ if (isAssistant) {
243
+ stats.inputTokens += inputTokens;
244
+ stats.outputTokens += outputTokens;
245
+ stats.cacheRead += cacheRead;
246
+ stats.cacheCreate += cacheCreate;
247
+ const tokenTotal = inputTokens + outputTokens + cacheRead + cacheCreate;
248
+ stats.totalTokens += tokenTotal;
249
+ if (r.metadata?.isSubagent || r.isSubagent) stats.subagentTokens += tokenTotal;
250
+
251
+ // Model
252
+ if (model) {
253
+ if (!stats.models[model]) stats.models[model] = { count: 0, outputTokens: 0, inputTokens: 0, cacheRead: 0, cost: 0, costMode: 'unknown' };
254
+ stats.models[model].count++;
255
+ stats.models[model].outputTokens += outputTokens;
256
+ stats.models[model].inputTokens += inputTokens;
257
+ stats.models[model].cacheRead += cacheRead;
258
+ }
259
+ }
260
+
261
+ // Tool breakdown(新增)
262
+ const toolName = r.tool || 'claude';
263
+ if (!stats.toolBreakdown[toolName]) {
264
+ stats.toolBreakdown[toolName] = { inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheCreate: 0, count: 0 };
265
+ }
266
+ if (isAssistant) {
267
+ stats.toolBreakdown[toolName].inputTokens += inputTokens;
268
+ stats.toolBreakdown[toolName].outputTokens += outputTokens;
269
+ stats.toolBreakdown[toolName].cacheRead += cacheRead;
270
+ stats.toolBreakdown[toolName].cacheCreate += cacheCreate;
271
+ stats.toolBreakdown[toolName].count++;
272
+ }
273
+
274
+ // Projects - 使用 basename 规范化,避免不同工具路径差异导致同一项目分裂
275
+ const projectName = getProjectBaseName(r.project);
276
+ if (projectName) {
277
+ if (!stats.projects[projectName]) {
278
+ stats.projects[projectName] = { sessions: new Set(), requests: 0 };
279
+ }
280
+ if (isAssistant) {
281
+ stats.projects[projectName].requests++;
282
+ }
283
+ if (r.sessionId) {
284
+ stats.projects[projectName].sessions.add(r.sessionId);
285
+ }
286
+ }
287
+
288
+ // Tools (toolCalls) - 保持现有逻辑
289
+ const toolCalls = r.metadata?.toolCalls || r.toolCalls || [];
290
+ for (const tc of toolCalls) {
291
+ stats.tools[tc.name] = (stats.tools[tc.name] || 0) + 1;
292
+ }
293
+
294
+ // Daily stats
295
+ if (r.timestamp) {
296
+ const date = r.timestamp.slice(0, 10);
297
+ if (!stats.dailyStats[date]) {
298
+ stats.dailyStats[date] = { requests: 0, userMessages: 0, inputTokens: 0, outputTokens: 0 };
299
+ }
300
+ if (isAssistant) {
301
+ stats.dailyStats[date].requests++;
302
+ stats.dailyStats[date].inputTokens += inputTokens;
303
+ stats.dailyStats[date].outputTokens += outputTokens;
304
+ }
305
+ if (r.metadata?.type === 'user' || r.type === 'user') {
306
+ const text = r.metadata?.text || r.text || '';
307
+ if (text && !text.startsWith('<system-reminder')) {
308
+ stats.dailyStats[date].userMessages++;
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ // Scenarios
315
+ stats.scenarios = aggregateScenarios(records, scenarioKeywords);
316
+
317
+ // Cost estimation
318
+ const estimatedCost = computeCostFromRecords(records, costMode);
319
+ stats.estimatedCost = Math.round(estimatedCost * 100) / 100;
320
+
321
+ // Cost accuracy metadata
322
+ const modelPricingStatus = {};
323
+ const uniqueModels = Object.keys(stats.models);
324
+ for (const m of uniqueModels) {
325
+ const p = resolveModelPricing(m);
326
+ modelPricingStatus[m] = p.unknown ? 'unknown' : 'estimated';
327
+ }
328
+ // Check if any records have actual costUSD (Claude API mode)
329
+ const hasActualCost = records.some(r => {
330
+ if (!isAssistantRecord(r)) return false;
331
+ return r.costUSD != null && r.costUSD > 0;
332
+ });
333
+ if (hasActualCost) {
334
+ for (const m of uniqueModels) {
335
+ // Models with actual costUSD are accurate, not estimated
336
+ const hasModelActualCost = records.some(r => {
337
+ if (!isAssistantRecord(r) || getModel(r) !== m) return false;
338
+ return r.costUSD != null && r.costUSD > 0;
339
+ });
340
+ if (hasModelActualCost) modelPricingStatus[m] = 'actual';
341
+ }
342
+ }
343
+ const unknownModels = uniqueModels.filter(m => modelPricingStatus[m] === 'unknown');
344
+ stats.costMeta = {
345
+ accuracy: hasActualCost ? 'mixed' : (unknownModels.length > 0 ? 'partial' : 'estimated'),
346
+ modelPricingStatus,
347
+ unknownModels,
348
+ hasActualCost,
349
+ };
350
+
351
+ // Per-model cost calculation
352
+ for (const m of uniqueModels) {
353
+ stats.models[m].costMode = modelPricingStatus[m] || 'unknown';
354
+ stats.models[m].cost = 0;
355
+ }
356
+ for (const r of records) {
357
+ if (!isAssistantRecord(r)) continue;
358
+ const model = getModel(r);
359
+ if (!model || !stats.models[model]) continue;
360
+ const costUSD = r.costUSD ?? null;
361
+ if (costMode === 'display') {
362
+ stats.models[model].cost += costUSD || 0;
363
+ } else if (costMode === 'calculate' || costUSD == null || costUSD <= 0) {
364
+ const pricing = resolveModelPricing(model);
365
+ stats.models[model].cost += calculateRecordCost(r, pricing);
366
+ } else {
367
+ stats.models[model].cost += costUSD;
368
+ }
369
+ }
370
+ for (const m of uniqueModels) {
371
+ stats.models[m].cost = Math.round(stats.models[m].cost * 100) / 100;
372
+ }
373
+
374
+ // Convert project session Sets to sizes
375
+ for (const proj of Object.keys(stats.projects)) {
376
+ stats.projects[proj] = {
377
+ sessions: stats.projects[proj].sessions.size,
378
+ requests: stats.projects[proj].requests,
379
+ };
380
+ }
381
+
382
+ return stats;
383
+ }
384
+
385
+ export function filterRecordsByPeriod(records, period, refDate) {
386
+ const d = new Date(refDate);
387
+ let start, end;
388
+
389
+ switch (period) {
390
+ case 'daily':
391
+ start = formatDate(d);
392
+ end = start;
393
+ break;
394
+ case 'weekly': {
395
+ const day = d.getDay();
396
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday start
397
+ const monday = new Date(d);
398
+ monday.setDate(diff);
399
+ const sunday = new Date(monday);
400
+ sunday.setDate(monday.getDate() + 6);
401
+ start = formatDate(monday);
402
+ end = formatDate(sunday);
403
+ break;
404
+ }
405
+ case 'monthly':
406
+ start = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`;
407
+ end = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate()).padStart(2, '0')}`;
408
+ break;
409
+ default:
410
+ start = formatDate(d);
411
+ end = start;
412
+ }
413
+
414
+ const filtered = records.filter(r => {
415
+ if (!r.timestamp) return false;
416
+ const date = r.timestamp.slice(0, 10);
417
+ return date >= start && date <= end;
418
+ });
419
+
420
+ return { filtered, start, end };
421
+ }
422
+
423
+ function formatDate(d) {
424
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
425
+ }
426
+
427
+ export function computeTrendData(allRecords, period, refDate) {
428
+ const d = new Date(refDate);
429
+ let trendDays;
430
+ switch (period) {
431
+ case 'daily': trendDays = 7; break;
432
+ case 'weekly': trendDays = 28; break;
433
+ case 'monthly': trendDays = 180; break;
434
+ default: trendDays = 7;
435
+ }
436
+
437
+ const trendStartDate = new Date(d);
438
+ trendStartDate.setDate(trendStartDate.getDate() - trendDays + 1);
439
+
440
+ const trendStart = formatDate(trendStartDate);
441
+ const trendEnd = formatDate(d);
442
+
443
+ const dailyStats = {};
444
+ for (const r of allRecords) {
445
+ if (!r.timestamp) continue;
446
+ const date = r.timestamp.slice(0, 10);
447
+ if (date < trendStart || date > trendEnd) continue;
448
+ if (!dailyStats[date]) dailyStats[date] = { requests: 0, inputTokens: 0, outputTokens: 0 };
449
+ if (isAssistantRecord(r)) {
450
+ dailyStats[date].requests++;
451
+ dailyStats[date].inputTokens += getInputTokens(r);
452
+ dailyStats[date].outputTokens += getOutputTokens(r);
453
+ }
454
+ }
455
+
456
+ return { dailyStats, start: trendStart, end: trendEnd };
457
+ }
458
+
459
+ export function groupBySessions(records) {
460
+ const sessions = {};
461
+ const addUnique = (arr, value) => {
462
+ if (value && !arr.includes(value)) arr.push(value);
463
+ };
464
+ const collectPathValues = (value, out = []) => {
465
+ if (!value) return out;
466
+ if (typeof value === 'string') return out;
467
+ if (Array.isArray(value)) {
468
+ for (const item of value) collectPathValues(item, out);
469
+ return out;
470
+ }
471
+ if (typeof value !== 'object') return out;
472
+ for (const [key, val] of Object.entries(value)) {
473
+ if (typeof val === 'string' && /(?:file|path|filename|filepath)$/i.test(key)) {
474
+ addUnique(out, val);
475
+ } else if (val && typeof val === 'object') {
476
+ collectPathValues(val, out);
477
+ }
478
+ }
479
+ return out;
480
+ };
481
+ for (const r of records) {
482
+ if (!r.sessionId) continue;
483
+ if (!sessions[r.sessionId]) {
484
+ sessions[r.sessionId] = {
485
+ id: r.sessionId,
486
+ project: r.project,
487
+ startTime: r.timestamp,
488
+ endTime: r.timestamp,
489
+ requests: 0,
490
+ userMessages: 0,
491
+ inputTokens: 0,
492
+ outputTokens: 0,
493
+ models: new Set(),
494
+ sampleTexts: [],
495
+ toolSequence: [],
496
+ touchedFiles: [],
497
+ shellCommands: [],
498
+ gitCommitTimestamps: [],
499
+ commits: [],
500
+ toolCounts: {},
501
+ };
502
+ }
503
+ const s = sessions[r.sessionId];
504
+ if (r.timestamp && r.timestamp < s.startTime) s.startTime = r.timestamp;
505
+ if (r.timestamp && r.timestamp > s.endTime) s.endTime = r.timestamp;
506
+ const isAssistant = isAssistantRecord(r);
507
+ // 统计工具归属(仅当记录有明确 tool 字段时)
508
+ if (r.tool) {
509
+ s.toolCounts[r.tool] = (s.toolCounts[r.tool] || 0) + 1;
510
+ }
511
+ if (isAssistant) {
512
+ s.requests++;
513
+ s.inputTokens += getInputTokens(r);
514
+ s.outputTokens += getOutputTokens(r);
515
+ if (getModel(r)) s.models.add(getModel(r));
516
+ // 收集工具调用序列
517
+ const toolCalls = r.metadata?.toolCalls || r.toolCalls || [];
518
+ for (const tc of toolCalls) {
519
+ s.toolSequence.push({
520
+ name: tc.name,
521
+ input: tc.input || {},
522
+ timestamp: r.timestamp,
523
+ });
524
+ for (const p of collectPathValues(tc.input || {})) addUnique(s.touchedFiles, p);
525
+ if (tc.name === 'Bash' && tc.input?.command) {
526
+ addUnique(s.shellCommands, tc.input.command);
527
+ if (/\bgit\s+commit\b/i.test(tc.input.command)) addUnique(s.gitCommitTimestamps, r.timestamp);
528
+ }
529
+ }
530
+ }
531
+ const recordType = r.metadata?.type || r.type;
532
+ const recordText = r.metadata?.text || r.text;
533
+ if (recordType === 'user' && recordText && !recordText.startsWith('<system-reminder')) {
534
+ s.userMessages++;
535
+ if (s.sampleTexts.length < 3) s.sampleTexts.push(recordText.slice(0, 100));
536
+ }
537
+ }
538
+ return Object.values(sessions).map(s => {
539
+ // 推导 primaryTool:出现次数最多的 tool
540
+ let primaryTool = null;
541
+ let maxCount = 0;
542
+ for (const [tool, count] of Object.entries(s.toolCounts)) {
543
+ if (count > maxCount) {
544
+ maxCount = count;
545
+ primaryTool = tool;
546
+ }
547
+ }
548
+ const { toolCounts, ...rest } = s;
549
+ return {
550
+ ...rest,
551
+ models: [...s.models],
552
+ primaryTool,
553
+ };
554
+ }).sort((a, b) => b.endTime.localeCompare(a.endTime));
555
+ }
556
+
557
+ export function computePrevPeriodRange(period, refDate) {
558
+ const d = new Date(refDate);
559
+ switch (period) {
560
+ case 'daily':
561
+ d.setDate(d.getDate() - 1);
562
+ return { start: formatDate(d), end: formatDate(d) };
563
+ case 'weekly': {
564
+ const day = d.getDay();
565
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
566
+ d.setDate(diff - 7);
567
+ const prevMonday = new Date(d);
568
+ const prevSunday = new Date(d);
569
+ prevSunday.setDate(prevMonday.getDate() + 6);
570
+ return { start: formatDate(prevMonday), end: formatDate(prevSunday) };
571
+ }
572
+ case 'monthly': {
573
+ d.setMonth(d.getMonth() - 1);
574
+ const y = d.getFullYear(), m = d.getMonth();
575
+ return { start: `${y}-${String(m + 1).padStart(2, '0')}-01`, end: `${y}-${String(m + 1).padStart(2, '0')}-${String(new Date(y, m + 1, 0).getDate()).padStart(2, '0')}` };
576
+ }
577
+ default:
578
+ d.setDate(d.getDate() - 1);
579
+ return { start: formatDate(d), end: formatDate(d) };
580
+ }
581
+ }
582
+
583
+ // ── Pricing Engine ──
584
+
585
+ function calculateRecordCost(record, pricing) {
586
+ if (pricing.unknown) return 0;
587
+
588
+ const input = getInputTokens(record);
589
+ const output = getOutputTokens(record);
590
+ const cacheRead = getCacheRead(record);
591
+ const cacheCreate = getCacheCreate(record);
592
+ const totalContext = input + cacheRead + cacheCreate;
593
+ const tier = pricing.tier && totalContext > pricing.tier.threshold ? pricing.tier : null;
594
+
595
+ const inputRate = tier ? pricing.input * tier.multiplier : pricing.input;
596
+ const outputRate = tier ? pricing.output * tier.multiplier : pricing.output;
597
+ const cacheReadRate = tier ? pricing.cacheRead * tier.multiplier : pricing.cacheRead;
598
+ const cacheCreateRate = tier ? pricing.cacheCreate * tier.multiplier : pricing.cacheCreate;
599
+
600
+ const speed = record.speed || record.metadata?.speed || 'standard';
601
+ const fastMul = speed === 'fast' ? (pricing.fastMultiplier || 1) : 1;
602
+
603
+ return (
604
+ (input / 1_000_000) * inputRate +
605
+ (output / 1_000_000) * outputRate * fastMul +
606
+ (cacheRead / 1_000_000) * cacheReadRate +
607
+ (cacheCreate / 1_000_000) * cacheCreateRate
608
+ );
609
+ }
610
+
611
+ export function computeCostFromRecords(records, costMode = 'auto') {
612
+ let total = 0;
613
+ for (const r of records) {
614
+ if (!isAssistantRecord(r)) continue;
615
+ const costUSD = r.costUSD ?? null;
616
+ if (costMode === 'display') {
617
+ total += costUSD || 0;
618
+ } else if (costMode === 'calculate' || costUSD == null || costUSD <= 0) {
619
+ const pricing = resolveModelPricing(getModel(r));
620
+ total += calculateRecordCost(r, pricing);
621
+ } else {
622
+ total += costUSD;
623
+ }
624
+ }
625
+ return total;
626
+ }