@tokscale/cli 1.0.6 → 1.0.8

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,475 @@
1
+ /**
2
+ * TypeScript fallback report generators
3
+ *
4
+ * Used when native Rust module is not available.
5
+ */
6
+
7
+ import type { UnifiedMessage, TokenBreakdown, SourceType } from "./types.js";
8
+ import type { PricingEntry } from "../pricing.js";
9
+ import { normalizeModelName, isWordBoundaryMatch } from "../pricing.js";
10
+ import type { TokenContributionData } from "../graph-types.js";
11
+
12
+ export interface ModelUsage {
13
+ source: string;
14
+ model: string;
15
+ provider: string;
16
+ input: number;
17
+ output: number;
18
+ cacheRead: number;
19
+ cacheWrite: number;
20
+ reasoning: number;
21
+ messageCount: number;
22
+ cost: number;
23
+ }
24
+
25
+ export interface ModelReport {
26
+ entries: ModelUsage[];
27
+ totalInput: number;
28
+ totalOutput: number;
29
+ totalCacheRead: number;
30
+ totalCacheWrite: number;
31
+ totalMessages: number;
32
+ totalCost: number;
33
+ processingTimeMs: number;
34
+ }
35
+
36
+ export interface MonthlyUsage {
37
+ month: string;
38
+ models: string[];
39
+ input: number;
40
+ output: number;
41
+ cacheRead: number;
42
+ cacheWrite: number;
43
+ messageCount: number;
44
+ cost: number;
45
+ }
46
+
47
+ export interface MonthlyReport {
48
+ entries: MonthlyUsage[];
49
+ totalCost: number;
50
+ processingTimeMs: number;
51
+ }
52
+
53
+ interface PricingInfo {
54
+ inputCostPerToken: number;
55
+ outputCostPerToken: number;
56
+ cacheReadInputTokenCost?: number;
57
+ cacheCreationInputTokenCost?: number;
58
+ }
59
+
60
+ interface PricingLookup {
61
+ get(modelId: string): PricingInfo | undefined;
62
+ }
63
+
64
+ function buildPricingLookup(pricing: PricingEntry[]): PricingLookup {
65
+ const map = new Map<string, PricingInfo>();
66
+ for (const entry of pricing) {
67
+ map.set(entry.modelId, entry.pricing);
68
+ }
69
+
70
+ const sortedKeys = [...map.keys()].sort();
71
+ const prefixes = ["anthropic/", "openai/", "google/", "bedrock/"];
72
+
73
+ return {
74
+ get(modelId: string): PricingInfo | undefined {
75
+ if (map.has(modelId)) return map.get(modelId);
76
+
77
+ for (const prefix of prefixes) {
78
+ if (map.has(prefix + modelId)) return map.get(prefix + modelId);
79
+ }
80
+
81
+ const normalized = normalizeModelName(modelId);
82
+ if (normalized) {
83
+ if (map.has(normalized)) return map.get(normalized);
84
+ for (const prefix of prefixes) {
85
+ if (map.has(prefix + normalized)) return map.get(prefix + normalized);
86
+ }
87
+ }
88
+
89
+ const lowerModelId = modelId.toLowerCase();
90
+ const lowerNormalized = normalized?.toLowerCase();
91
+
92
+ for (const key of sortedKeys) {
93
+ const lowerKey = key.toLowerCase();
94
+ if (isWordBoundaryMatch(lowerKey, lowerModelId)) return map.get(key);
95
+ if (lowerNormalized && isWordBoundaryMatch(lowerKey, lowerNormalized)) return map.get(key);
96
+ }
97
+
98
+ for (const key of sortedKeys) {
99
+ const lowerKey = key.toLowerCase();
100
+ if (isWordBoundaryMatch(lowerModelId, lowerKey)) return map.get(key);
101
+ if (lowerNormalized && isWordBoundaryMatch(lowerNormalized, lowerKey)) return map.get(key);
102
+ }
103
+
104
+ return undefined;
105
+ },
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Calculate cost for a message based on token counts and pricing
111
+ */
112
+ function calculateCost(
113
+ tokens: TokenBreakdown,
114
+ pricing: PricingInfo | undefined
115
+ ): number {
116
+ if (!pricing) return 0;
117
+
118
+ let cost = 0;
119
+ cost += tokens.input * pricing.inputCostPerToken;
120
+ cost += tokens.output * pricing.outputCostPerToken;
121
+ cost += tokens.cacheRead * (pricing.cacheReadInputTokenCost || 0);
122
+ cost += tokens.cacheWrite * (pricing.cacheCreationInputTokenCost || 0);
123
+ // Note: reasoning tokens are typically charged at output rate
124
+ cost += tokens.reasoning * pricing.outputCostPerToken;
125
+
126
+ return cost;
127
+ }
128
+
129
+ /**
130
+ * Generate model report from parsed messages
131
+ */
132
+ export function generateModelReport(
133
+ messages: UnifiedMessage[],
134
+ pricing: PricingEntry[],
135
+ startTime: number
136
+ ): ModelReport {
137
+ const pricingMap = buildPricingLookup(pricing);
138
+
139
+ // Aggregate by source + model
140
+ const aggregated = new Map<string, ModelUsage>();
141
+
142
+ for (const msg of messages) {
143
+ const key = `${msg.source}:${msg.modelId}`;
144
+ let usage = aggregated.get(key);
145
+
146
+ if (!usage) {
147
+ usage = {
148
+ source: msg.source,
149
+ model: msg.modelId,
150
+ provider: msg.providerId,
151
+ input: 0,
152
+ output: 0,
153
+ cacheRead: 0,
154
+ cacheWrite: 0,
155
+ reasoning: 0,
156
+ messageCount: 0,
157
+ cost: 0,
158
+ };
159
+ aggregated.set(key, usage);
160
+ }
161
+
162
+ usage.input += msg.tokens.input;
163
+ usage.output += msg.tokens.output;
164
+ usage.cacheRead += msg.tokens.cacheRead;
165
+ usage.cacheWrite += msg.tokens.cacheWrite;
166
+ usage.reasoning += msg.tokens.reasoning;
167
+ usage.messageCount++;
168
+
169
+ const calculatedCost = calculateCost(msg.tokens, pricingMap.get(msg.modelId));
170
+ usage.cost += calculatedCost > 0 ? calculatedCost : msg.cost;
171
+ }
172
+
173
+ const entries = Array.from(aggregated.values()).sort((a, b) => b.cost - a.cost);
174
+
175
+ const totals = entries.reduce(
176
+ (acc, e) => ({
177
+ input: acc.input + e.input,
178
+ output: acc.output + e.output,
179
+ cacheRead: acc.cacheRead + e.cacheRead,
180
+ cacheWrite: acc.cacheWrite + e.cacheWrite,
181
+ messages: acc.messages + e.messageCount,
182
+ cost: acc.cost + e.cost,
183
+ }),
184
+ { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, messages: 0, cost: 0 }
185
+ );
186
+
187
+ return {
188
+ entries,
189
+ totalInput: totals.input,
190
+ totalOutput: totals.output,
191
+ totalCacheRead: totals.cacheRead,
192
+ totalCacheWrite: totals.cacheWrite,
193
+ totalMessages: totals.messages,
194
+ totalCost: totals.cost,
195
+ processingTimeMs: performance.now() - startTime,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Generate monthly report from parsed messages
201
+ */
202
+ export function generateMonthlyReport(
203
+ messages: UnifiedMessage[],
204
+ pricing: PricingEntry[],
205
+ startTime: number
206
+ ): MonthlyReport {
207
+ const pricingMap = buildPricingLookup(pricing);
208
+
209
+ // Aggregate by month
210
+ const aggregated = new Map<
211
+ string,
212
+ {
213
+ models: Set<string>;
214
+ input: number;
215
+ output: number;
216
+ cacheRead: number;
217
+ cacheWrite: number;
218
+ messageCount: number;
219
+ cost: number;
220
+ }
221
+ >();
222
+
223
+ for (const msg of messages) {
224
+ // Extract YYYY-MM from date
225
+ const month = msg.date.slice(0, 7);
226
+ let usage = aggregated.get(month);
227
+
228
+ if (!usage) {
229
+ usage = {
230
+ models: new Set(),
231
+ input: 0,
232
+ output: 0,
233
+ cacheRead: 0,
234
+ cacheWrite: 0,
235
+ messageCount: 0,
236
+ cost: 0,
237
+ };
238
+ aggregated.set(month, usage);
239
+ }
240
+
241
+ usage.models.add(msg.modelId);
242
+ usage.input += msg.tokens.input;
243
+ usage.output += msg.tokens.output;
244
+ usage.cacheRead += msg.tokens.cacheRead;
245
+ usage.cacheWrite += msg.tokens.cacheWrite;
246
+ usage.messageCount++;
247
+
248
+ const calculatedCost = calculateCost(msg.tokens, pricingMap.get(msg.modelId));
249
+ usage.cost += calculatedCost > 0 ? calculatedCost : msg.cost;
250
+ }
251
+
252
+ const entries: MonthlyUsage[] = Array.from(aggregated.entries())
253
+ .map(([month, data]) => ({
254
+ month,
255
+ models: Array.from(data.models),
256
+ input: data.input,
257
+ output: data.output,
258
+ cacheRead: data.cacheRead,
259
+ cacheWrite: data.cacheWrite,
260
+ messageCount: data.messageCount,
261
+ cost: data.cost,
262
+ }))
263
+ .sort((a, b) => b.month.localeCompare(a.month)); // Most recent first
264
+
265
+ const totalCost = entries.reduce((sum, e) => sum + e.cost, 0);
266
+
267
+ return {
268
+ entries,
269
+ totalCost,
270
+ processingTimeMs: performance.now() - startTime,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Generate graph data from parsed messages
276
+ */
277
+ export function generateGraphData(
278
+ messages: UnifiedMessage[],
279
+ pricing: PricingEntry[],
280
+ startTime: number
281
+ ): TokenContributionData {
282
+ const pricingMap = buildPricingLookup(pricing);
283
+
284
+ // Group messages by date
285
+ const byDate = new Map<
286
+ string,
287
+ {
288
+ tokens: TokenBreakdown;
289
+ cost: number;
290
+ messages: number;
291
+ sources: Map<
292
+ string,
293
+ {
294
+ modelId: string;
295
+ providerId: string;
296
+ tokens: TokenBreakdown;
297
+ cost: number;
298
+ messages: number;
299
+ }
300
+ >;
301
+ }
302
+ >();
303
+
304
+ const allSources = new Set<string>();
305
+ const allModels = new Set<string>();
306
+ let totalTokens = 0;
307
+ let totalCost = 0;
308
+ let maxCostInSingleDay = 0;
309
+
310
+ for (const msg of messages) {
311
+ allSources.add(msg.source);
312
+ allModels.add(msg.modelId);
313
+
314
+ let dayData = byDate.get(msg.date);
315
+ if (!dayData) {
316
+ dayData = {
317
+ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 },
318
+ cost: 0,
319
+ messages: 0,
320
+ sources: new Map(),
321
+ };
322
+ byDate.set(msg.date, dayData);
323
+ }
324
+
325
+ const calculatedCost = calculateCost(msg.tokens, pricingMap.get(msg.modelId));
326
+ const msgCost = calculatedCost > 0 ? calculatedCost : msg.cost;
327
+
328
+ dayData.tokens.input += msg.tokens.input;
329
+ dayData.tokens.output += msg.tokens.output;
330
+ dayData.tokens.cacheRead += msg.tokens.cacheRead;
331
+ dayData.tokens.cacheWrite += msg.tokens.cacheWrite;
332
+ dayData.tokens.reasoning += msg.tokens.reasoning;
333
+ dayData.cost += msgCost;
334
+ dayData.messages++;
335
+
336
+ // Source contribution
337
+ const sourceKey = `${msg.source}:${msg.modelId}`;
338
+ let sourceData = dayData.sources.get(sourceKey);
339
+ if (!sourceData) {
340
+ sourceData = {
341
+ modelId: msg.modelId,
342
+ providerId: msg.providerId,
343
+ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 },
344
+ cost: 0,
345
+ messages: 0,
346
+ };
347
+ dayData.sources.set(sourceKey, sourceData);
348
+ }
349
+ sourceData.tokens.input += msg.tokens.input;
350
+ sourceData.tokens.output += msg.tokens.output;
351
+ sourceData.tokens.cacheRead += msg.tokens.cacheRead;
352
+ sourceData.tokens.cacheWrite += msg.tokens.cacheWrite;
353
+ sourceData.tokens.reasoning += msg.tokens.reasoning;
354
+ sourceData.cost += msgCost;
355
+ sourceData.messages++;
356
+
357
+ const msgTokens =
358
+ msg.tokens.input +
359
+ msg.tokens.output +
360
+ msg.tokens.cacheRead +
361
+ msg.tokens.cacheWrite +
362
+ msg.tokens.reasoning;
363
+ totalTokens += msgTokens;
364
+ totalCost += msgCost;
365
+ }
366
+
367
+ // Calculate max cost per day for intensity calculation
368
+ for (const dayData of byDate.values()) {
369
+ if (dayData.cost > maxCostInSingleDay) {
370
+ maxCostInSingleDay = dayData.cost;
371
+ }
372
+ }
373
+
374
+ // Build contributions array
375
+ const contributions = Array.from(byDate.entries())
376
+ .map(([date, data]) => {
377
+ const dayTokens =
378
+ data.tokens.input +
379
+ data.tokens.output +
380
+ data.tokens.cacheRead +
381
+ data.tokens.cacheWrite +
382
+ data.tokens.reasoning;
383
+
384
+ // Calculate intensity (0-4 scale based on cost)
385
+ let intensity: 0 | 1 | 2 | 3 | 4 = 0;
386
+ if (maxCostInSingleDay > 0) {
387
+ const ratio = data.cost / maxCostInSingleDay;
388
+ if (ratio > 0.75) intensity = 4;
389
+ else if (ratio > 0.5) intensity = 3;
390
+ else if (ratio > 0.25) intensity = 2;
391
+ else if (ratio > 0) intensity = 1;
392
+ }
393
+
394
+ return {
395
+ date,
396
+ totals: {
397
+ tokens: dayTokens,
398
+ cost: data.cost,
399
+ messages: data.messages,
400
+ },
401
+ intensity,
402
+ tokenBreakdown: data.tokens,
403
+ sources: Array.from(data.sources.entries()).map(([key, src]) => ({
404
+ source: key.split(":")[0] as SourceType,
405
+ modelId: src.modelId,
406
+ providerId: src.providerId,
407
+ tokens: src.tokens,
408
+ cost: src.cost,
409
+ messages: src.messages,
410
+ })),
411
+ };
412
+ })
413
+ .sort((a, b) => a.date.localeCompare(b.date));
414
+
415
+ // Determine date range
416
+ const dates = Array.from(byDate.keys()).sort();
417
+ const rangeStart = dates[0] || new Date().toISOString().split("T")[0];
418
+ const rangeEnd = dates[dates.length - 1] || rangeStart;
419
+
420
+ // Group by year
421
+ const yearData = new Map<string, { tokens: number; cost: number; start: string; end: string }>();
422
+ for (const [date, data] of byDate.entries()) {
423
+ const year = date.slice(0, 4);
424
+ let yd = yearData.get(year);
425
+ if (!yd) {
426
+ yd = { tokens: 0, cost: 0, start: date, end: date };
427
+ yearData.set(year, yd);
428
+ }
429
+ const dayTokens =
430
+ data.tokens.input +
431
+ data.tokens.output +
432
+ data.tokens.cacheRead +
433
+ data.tokens.cacheWrite +
434
+ data.tokens.reasoning;
435
+ yd.tokens += dayTokens;
436
+ yd.cost += data.cost;
437
+ if (date < yd.start) yd.start = date;
438
+ if (date > yd.end) yd.end = date;
439
+ }
440
+
441
+ const years = Array.from(yearData.entries())
442
+ .map(([year, data]) => ({
443
+ year,
444
+ totalTokens: data.tokens,
445
+ totalCost: data.cost,
446
+ range: { start: data.start, end: data.end },
447
+ }))
448
+ .sort((a, b) => b.year.localeCompare(a.year));
449
+
450
+ const activeDays = byDate.size;
451
+ const totalDays =
452
+ activeDays > 0
453
+ ? Math.ceil((new Date(rangeEnd).getTime() - new Date(rangeStart).getTime()) / 86400000) + 1
454
+ : 0;
455
+
456
+ return {
457
+ meta: {
458
+ generatedAt: new Date().toISOString(),
459
+ version: "1.0.0-ts-fallback",
460
+ dateRange: { start: rangeStart, end: rangeEnd },
461
+ },
462
+ summary: {
463
+ totalTokens,
464
+ totalCost,
465
+ totalDays,
466
+ activeDays,
467
+ averagePerDay: activeDays > 0 ? totalCost / activeDays : 0,
468
+ maxCostInSingleDay,
469
+ sources: Array.from(allSources) as SourceType[],
470
+ models: Array.from(allModels),
471
+ },
472
+ years,
473
+ contributions,
474
+ };
475
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Unified message types for session parsers (matches Rust UnifiedMessage)
3
+ */
4
+
5
+ export interface TokenBreakdown {
6
+ input: number;
7
+ output: number;
8
+ cacheRead: number;
9
+ cacheWrite: number;
10
+ reasoning: number;
11
+ }
12
+
13
+ export interface UnifiedMessage {
14
+ source: string;
15
+ modelId: string;
16
+ providerId: string;
17
+ sessionId: string;
18
+ timestamp: number; // Unix milliseconds
19
+ date: string; // YYYY-MM-DD
20
+ tokens: TokenBreakdown;
21
+ cost: number;
22
+ }
23
+
24
+ export type SourceType = "opencode" | "claude" | "codex" | "gemini" | "cursor";
25
+
26
+ /**
27
+ * Convert Unix milliseconds timestamp to YYYY-MM-DD date string
28
+ */
29
+ export function timestampToDate(timestampMs: number): string {
30
+ const date = new Date(timestampMs);
31
+ const year = date.getUTCFullYear();
32
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
33
+ const day = String(date.getUTCDate()).padStart(2, "0");
34
+ return `${year}-${month}-${day}`;
35
+ }
36
+
37
+ /**
38
+ * Create a unified message
39
+ */
40
+ export function createUnifiedMessage(
41
+ source: string,
42
+ modelId: string,
43
+ providerId: string,
44
+ sessionId: string,
45
+ timestamp: number,
46
+ tokens: TokenBreakdown,
47
+ cost: number = 0
48
+ ): UnifiedMessage {
49
+ return {
50
+ source,
51
+ modelId,
52
+ providerId,
53
+ sessionId,
54
+ timestamp,
55
+ date: timestampToDate(timestamp),
56
+ tokens,
57
+ cost,
58
+ };
59
+ }