@roastcodes/ttdash 6.1.8 → 6.2.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.
@@ -0,0 +1,19 @@
1
+ import type { DailyUsage, DashboardMetrics, ViewMode } from './dashboard-types'
2
+
3
+ export function aggregateToDailyFormat(data: DailyUsage[], viewMode: ViewMode): DailyUsage[]
4
+ export function computeBusiestWeek(
5
+ data: DailyUsage[],
6
+ ): { start: string; end: string; cost: number } | null
7
+ export function computeMetrics(data: DailyUsage[]): DashboardMetrics
8
+ export function computeMovingAverage(
9
+ values: Array<number | undefined>,
10
+ window?: number,
11
+ ): Array<number | undefined>
12
+ export function computeWeekOverWeekChange(data: DailyUsage[]): number | null
13
+ export function filterByDateRange(data: DailyUsage[], start?: string, end?: string): DailyUsage[]
14
+ export function filterByModels(data: DailyUsage[], selectedModels: string[]): DailyUsage[]
15
+ export function filterByMonth(data: DailyUsage[], month: string | null): DailyUsage[]
16
+ export function filterByProviders(data: DailyUsage[], selectedProviders: string[]): DailyUsage[]
17
+ export function getModelProvider(raw: string): string
18
+ export function normalizeModelName(raw: string): string
19
+ export function sortByDate(data: DailyUsage[]): DailyUsage[]
@@ -0,0 +1,615 @@
1
+ const modelNormalizationSpec = require('../server/model-normalization.json')
2
+
3
+ const DISPLAY_ALIASES = modelNormalizationSpec.displayAliases.map((alias) => ({
4
+ ...alias,
5
+ matcher: new RegExp(alias.pattern, 'i'),
6
+ }))
7
+
8
+ const PROVIDER_MATCHERS = modelNormalizationSpec.providerMatchers.map((matcher) => ({
9
+ ...matcher,
10
+ matcher: new RegExp(matcher.pattern, 'i'),
11
+ }))
12
+
13
+ function titleCaseSegment(segment) {
14
+ if (!segment) return segment
15
+ if (/^\d+([.-]\d+)*$/.test(segment)) return segment.replace(/-/g, '.')
16
+ if (/^[a-z]{1,4}\d+$/i.test(segment)) return segment.toUpperCase()
17
+ return segment.charAt(0).toUpperCase() + segment.slice(1)
18
+ }
19
+
20
+ function capitalize(segment) {
21
+ if (!segment) return ''
22
+ return segment.charAt(0).toUpperCase() + segment.slice(1)
23
+ }
24
+
25
+ function formatVersion(version) {
26
+ return version.replace(/-/g, '.')
27
+ }
28
+
29
+ function canonicalizeModelName(raw) {
30
+ const normalized = String(raw || '')
31
+ .trim()
32
+ .toLowerCase()
33
+ .replace(/^model[:/ -]*/i, '')
34
+ .replace(/^(anthropic|openai|google|vertex|models)[/-]/i, '')
35
+ .replace(/\./g, '-')
36
+ .replace(/[_/]+/g, '-')
37
+ .replace(/\s+/g, '-')
38
+ .replace(/-{2,}/g, '-')
39
+ .replace(/^-|-$/g, '')
40
+
41
+ const suffixStart = normalized.lastIndexOf('-')
42
+ if (suffixStart > 0) {
43
+ const suffix = normalized.slice(suffixStart + 1)
44
+ if (suffix.length === 8 && suffix.startsWith('20') && /^\d+$/.test(suffix)) {
45
+ return normalized.slice(0, suffixStart)
46
+ }
47
+ }
48
+
49
+ return normalized
50
+ }
51
+
52
+ function parseClaudeName(rest) {
53
+ const parts = rest.split('-')
54
+ if (parts.length < 2) {
55
+ return `Claude ${capitalize(rest)}`
56
+ }
57
+
58
+ const family = capitalize(parts[0] || '')
59
+ const secondPart = parts[1] || ''
60
+
61
+ if (/^\d+$/.test(secondPart)) {
62
+ const version = formatVersion(parts.slice(1).join('-'))
63
+ return ['Claude', family, version].filter(Boolean).join(' ').trim()
64
+ }
65
+
66
+ const model = capitalize(secondPart)
67
+ const version = formatVersion(parts.slice(2).join('-'))
68
+
69
+ return ['Claude', family, model, version].filter(Boolean).join(' ').trim()
70
+ }
71
+
72
+ function parseGptName(rest) {
73
+ const parts = rest.split('-')
74
+ const variant = parts[0] || ''
75
+ const minor = parts[1] || ''
76
+
77
+ if (minor && minor.length <= 2 && /^\d+$/.test(minor)) {
78
+ const version = `${variant}.${minor}`
79
+ if (parts.length > 2) {
80
+ const suffix = parts.slice(2).map(capitalize).join(' ')
81
+ return `GPT-${version}${suffix ? ` ${suffix}` : ''}`
82
+ }
83
+ return `GPT-${version}`
84
+ }
85
+
86
+ if (parts.length > 1) {
87
+ const suffix = parts.slice(1).map(capitalize).join(' ')
88
+ return `GPT-${variant}${suffix ? ` ${suffix}` : ''}`
89
+ }
90
+
91
+ return `GPT-${rest}`
92
+ }
93
+
94
+ function parseGeminiName(rest) {
95
+ const parts = rest.split('-')
96
+ if (parts.length < 2) {
97
+ return `Gemini ${rest}`
98
+ }
99
+
100
+ const versionParts = []
101
+ const tierParts = []
102
+
103
+ for (const part of parts) {
104
+ if (/^\d+$/.test(part) && tierParts.length === 0) {
105
+ versionParts.push(part)
106
+ } else {
107
+ tierParts.push(capitalize(part))
108
+ }
109
+ }
110
+
111
+ const version = versionParts.join('.')
112
+ const tier = tierParts.join(' ')
113
+
114
+ return tier ? `Gemini ${version} ${tier}` : `Gemini ${version}`
115
+ }
116
+
117
+ function parseCodexName(rest) {
118
+ const normalized = rest.replace(/-latest$/i, '')
119
+ if (!normalized) {
120
+ return 'Codex'
121
+ }
122
+
123
+ return `Codex ${normalized.split('-').map(capitalize).join(' ')}`
124
+ }
125
+
126
+ function parseOSeries(name) {
127
+ const separatorIndex = name.indexOf('-')
128
+ if (separatorIndex === -1) {
129
+ return name
130
+ }
131
+
132
+ return `${name.slice(0, separatorIndex)} ${capitalize(name.slice(separatorIndex + 1))}`
133
+ }
134
+
135
+ function normalizeModelName(raw) {
136
+ const canonical = canonicalizeModelName(raw)
137
+
138
+ if (canonical.startsWith('claude-')) {
139
+ return parseClaudeName(canonical.slice('claude-'.length))
140
+ }
141
+
142
+ for (const alias of DISPLAY_ALIASES) {
143
+ if (alias.matcher.test(canonical)) return alias.name
144
+ }
145
+
146
+ if (canonical.startsWith('gpt-')) {
147
+ return parseGptName(canonical.slice('gpt-'.length))
148
+ }
149
+
150
+ if (canonical.startsWith('gemini-')) {
151
+ return parseGeminiName(canonical.slice('gemini-'.length))
152
+ }
153
+
154
+ if (canonical.startsWith('codex-')) {
155
+ return parseCodexName(canonical.slice('codex-'.length))
156
+ }
157
+
158
+ if (/^o\d/i.test(canonical)) {
159
+ return parseOSeries(canonical)
160
+ }
161
+
162
+ const familyMatch = canonical.match(
163
+ /^(gpt|opus|sonnet|haiku|gemini|codex|o\d|oai|grok|llama|mistral|command|deepseek|qwen)(?:-([a-z0-9-]+))?$/i,
164
+ )
165
+ if (familyMatch) {
166
+ const family = familyMatch[1]
167
+ if (/^codex$/i.test(family)) {
168
+ return parseCodexName(familyMatch[2] || '')
169
+ }
170
+
171
+ if (/^(o\d)$/i.test(family)) return parseOSeries(canonical)
172
+
173
+ const suffix = familyMatch[2] ? formatVersion(familyMatch[2]) : ''
174
+ if (/^gpt$/i.test(family) && suffix) return `GPT-${suffix.toUpperCase()}`
175
+ return `${titleCaseSegment(family)}${suffix ? ` ${suffix}` : ''}`.trim()
176
+ }
177
+
178
+ return canonical.split('-').filter(Boolean).map(titleCaseSegment).join(' ') || String(raw || '')
179
+ }
180
+
181
+ function getModelProvider(raw) {
182
+ const canonical = canonicalizeModelName(raw)
183
+ for (const matcher of PROVIDER_MATCHERS) {
184
+ if (matcher.matcher.test(canonical)) return matcher.provider
185
+ }
186
+ return 'Other'
187
+ }
188
+
189
+ function recalculateDayFromBreakdowns(day, filteredBreakdowns) {
190
+ let totalCost = 0
191
+ let inputTokens = 0
192
+ let outputTokens = 0
193
+ let cacheCreationTokens = 0
194
+ let cacheReadTokens = 0
195
+ let thinkingTokens = 0
196
+ let requestCount = 0
197
+
198
+ for (const breakdown of filteredBreakdowns) {
199
+ totalCost += breakdown.cost
200
+ inputTokens += breakdown.inputTokens
201
+ outputTokens += breakdown.outputTokens
202
+ cacheCreationTokens += breakdown.cacheCreationTokens
203
+ cacheReadTokens += breakdown.cacheReadTokens
204
+ thinkingTokens += breakdown.thinkingTokens
205
+ requestCount += breakdown.requestCount
206
+ }
207
+
208
+ return {
209
+ ...day,
210
+ totalCost,
211
+ totalTokens:
212
+ inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + thinkingTokens,
213
+ inputTokens,
214
+ outputTokens,
215
+ cacheCreationTokens,
216
+ cacheReadTokens,
217
+ thinkingTokens,
218
+ requestCount,
219
+ modelBreakdowns: filteredBreakdowns,
220
+ modelsUsed: [
221
+ ...new Set(filteredBreakdowns.map((breakdown) => normalizeModelName(breakdown.modelName))),
222
+ ],
223
+ }
224
+ }
225
+
226
+ function filterByDateRange(data, start, end) {
227
+ return data.filter((entry) => {
228
+ if (start && entry.date < start) return false
229
+ if (end && entry.date > end) return false
230
+ return true
231
+ })
232
+ }
233
+
234
+ function filterByModels(data, selectedModels) {
235
+ if (!selectedModels || selectedModels.length === 0) return data
236
+ const selected = new Set(selectedModels)
237
+
238
+ return data
239
+ .map((entry) => {
240
+ const filteredBreakdowns = entry.modelBreakdowns.filter((breakdown) =>
241
+ selected.has(normalizeModelName(breakdown.modelName)),
242
+ )
243
+
244
+ if (filteredBreakdowns.length === 0) return null
245
+ return recalculateDayFromBreakdowns(entry, filteredBreakdowns)
246
+ })
247
+ .filter(Boolean)
248
+ }
249
+
250
+ function filterByProviders(data, selectedProviders) {
251
+ if (!selectedProviders || selectedProviders.length === 0) return data
252
+ const selected = new Set(selectedProviders)
253
+
254
+ return data
255
+ .map((entry) => {
256
+ const filteredBreakdowns = entry.modelBreakdowns.filter((breakdown) =>
257
+ selected.has(getModelProvider(breakdown.modelName)),
258
+ )
259
+
260
+ if (filteredBreakdowns.length === 0) return null
261
+ return recalculateDayFromBreakdowns(entry, filteredBreakdowns)
262
+ })
263
+ .filter(Boolean)
264
+ }
265
+
266
+ function filterByMonth(data, month) {
267
+ if (!month) return data
268
+ return data.filter((entry) => entry.date.startsWith(month))
269
+ }
270
+
271
+ function sortByDate(data) {
272
+ return [...data].sort((left, right) => left.date.localeCompare(right.date))
273
+ }
274
+
275
+ function aggregateToDailyFormat(data, viewMode) {
276
+ if (viewMode === 'daily') return data
277
+
278
+ const getGroupKey =
279
+ viewMode === 'monthly' ? (date) => date.slice(0, 7) : (date) => date.slice(0, 4)
280
+ const groups = new Map()
281
+
282
+ for (const day of data) {
283
+ const key = getGroupKey(day.date)
284
+ const existing = groups.get(key)
285
+ const aggregatedDays = day._aggregatedDays || 1
286
+
287
+ if (!existing) {
288
+ groups.set(key, {
289
+ ...day,
290
+ date: key,
291
+ _aggregatedDays: aggregatedDays,
292
+ })
293
+ continue
294
+ }
295
+
296
+ existing.totalCost += day.totalCost
297
+ existing.totalTokens += day.totalTokens
298
+ existing.inputTokens += day.inputTokens
299
+ existing.outputTokens += day.outputTokens
300
+ existing.cacheCreationTokens += day.cacheCreationTokens
301
+ existing.cacheReadTokens += day.cacheReadTokens
302
+ existing.thinkingTokens += day.thinkingTokens
303
+ existing.requestCount += day.requestCount
304
+ existing._aggregatedDays += aggregatedDays
305
+ existing.modelBreakdowns = existing.modelBreakdowns.concat(day.modelBreakdowns)
306
+ existing.modelsUsed = Array.from(new Set(existing.modelsUsed.concat(day.modelsUsed)))
307
+ }
308
+
309
+ return Array.from(groups.values()).sort((left, right) => left.date.localeCompare(right.date))
310
+ }
311
+
312
+ function computeMovingAverage(values, window = 7) {
313
+ const result = Array(values.length)
314
+ let sum = 0
315
+ let definedCount = 0
316
+
317
+ for (let index = 0; index < values.length; index += 1) {
318
+ const currentValue = values[index]
319
+ if (currentValue !== undefined) {
320
+ sum += currentValue
321
+ definedCount += 1
322
+ }
323
+
324
+ if (index >= window) {
325
+ const outgoingValue = values[index - window]
326
+ if (outgoingValue !== undefined) {
327
+ sum -= outgoingValue
328
+ definedCount -= 1
329
+ }
330
+ }
331
+
332
+ result[index] =
333
+ index < window - 1 ? undefined : definedCount > 0 ? sum / definedCount : undefined
334
+ }
335
+
336
+ return result
337
+ }
338
+
339
+ function stdDev(values) {
340
+ if (!values.length) return 0
341
+ const mean = values.reduce((sum, value) => sum + value, 0) / values.length
342
+ const variance = values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / values.length
343
+ return Math.sqrt(variance)
344
+ }
345
+
346
+ function computeBusiestWeek(data) {
347
+ const sorted = data
348
+ .filter((entry) => /^\d{4}-\d{2}-\d{2}$/.test(entry.date))
349
+ .sort((left, right) => left.date.localeCompare(right.date))
350
+
351
+ if (sorted.length < 3) return null
352
+
353
+ let bestWindow = null
354
+
355
+ for (let start = 0; start < sorted.length; start += 1) {
356
+ const startEntry = sorted[start]
357
+ if (!startEntry) continue
358
+
359
+ const startDate = new Date(`${startEntry.date}T00:00:00`)
360
+ const endLimit = new Date(startDate)
361
+ endLimit.setDate(endLimit.getDate() + 6)
362
+ let windowCost = 0
363
+ let end = start
364
+
365
+ while (end < sorted.length) {
366
+ const endEntry = sorted[end]
367
+ if (!endEntry) break
368
+ if (new Date(`${endEntry.date}T00:00:00`) > endLimit) break
369
+ windowCost += endEntry.totalCost
370
+ end += 1
371
+ }
372
+
373
+ const finalEntry = sorted[end - 1]
374
+ if (finalEntry && (!bestWindow || windowCost > bestWindow.cost)) {
375
+ bestWindow = {
376
+ start: startEntry.date,
377
+ end: finalEntry.date,
378
+ cost: windowCost,
379
+ }
380
+ }
381
+ }
382
+
383
+ return bestWindow
384
+ }
385
+
386
+ function computeWeekOverWeekChange(data) {
387
+ if (data.some((entry) => !/^\d{4}-\d{2}-\d{2}$/.test(entry.date))) return null
388
+ if (data.length < 14) return null
389
+ const sorted = sortByDate(data)
390
+ const last7 = sorted.slice(-7)
391
+ const prev7 = sorted.slice(-14, -7)
392
+ const lastSum = last7.reduce((sum, day) => sum + day.totalCost, 0)
393
+ const prevSum = prev7.reduce((sum, day) => sum + day.totalCost, 0)
394
+ if (prevSum === 0) return null
395
+ return ((lastSum - prevSum) / prevSum) * 100
396
+ }
397
+
398
+ function computeMetrics(data) {
399
+ if (data.length === 0) {
400
+ return {
401
+ totalCost: 0,
402
+ totalTokens: 0,
403
+ activeDays: 0,
404
+ topModel: null,
405
+ topRequestModel: null,
406
+ topTokenModel: null,
407
+ topModelShare: 0,
408
+ topThreeModelsShare: 0,
409
+ topProvider: null,
410
+ providerCount: 0,
411
+ hasRequestData: false,
412
+ cacheHitRate: 0,
413
+ costPerMillion: 0,
414
+ avgTokensPerRequest: 0,
415
+ avgCostPerRequest: 0,
416
+ avgModelsPerEntry: 0,
417
+ avgDailyCost: 0,
418
+ avgRequestsPerDay: 0,
419
+ topDay: null,
420
+ cheapestDay: null,
421
+ busiestWeek: null,
422
+ weekendCostShare: null,
423
+ totalInput: 0,
424
+ totalOutput: 0,
425
+ totalCacheRead: 0,
426
+ totalCacheCreate: 0,
427
+ totalThinking: 0,
428
+ totalRequests: 0,
429
+ weekOverWeekChange: null,
430
+ requestVolatility: 0,
431
+ modelConcentrationIndex: 0,
432
+ providerConcentrationIndex: 0,
433
+ }
434
+ }
435
+
436
+ const firstDay = data[0]
437
+ let topDay = { date: firstDay.date, cost: firstDay.totalCost }
438
+ let cheapestDay = { date: firstDay.date, cost: firstDay.totalCost }
439
+ let totalCost = 0
440
+ let totalTokens = 0
441
+ let totalInput = 0
442
+ let totalOutput = 0
443
+ let totalCacheRead = 0
444
+ let totalCacheCreate = 0
445
+ let totalThinking = 0
446
+ let totalRequests = 0
447
+ let activeDays = 0
448
+ let hasRequestData = false
449
+ let totalModelsUsed = 0
450
+ let weekendCost = 0
451
+ let weekendEligible = 0
452
+ const modelCosts = new Map()
453
+ const modelTokens = new Map()
454
+ const modelRequests = new Map()
455
+ const providerCosts = new Map()
456
+
457
+ for (const day of data) {
458
+ totalCost += day.totalCost
459
+ totalTokens += day.totalTokens
460
+ totalInput += day.inputTokens
461
+ totalOutput += day.outputTokens
462
+ totalCacheRead += day.cacheReadTokens
463
+ totalCacheCreate += day.cacheCreationTokens
464
+ totalThinking += day.thinkingTokens
465
+ totalRequests += day.requestCount
466
+ if (
467
+ day.requestCount > 0 ||
468
+ day.modelBreakdowns.some((breakdown) => breakdown.requestCount > 0)
469
+ ) {
470
+ hasRequestData = true
471
+ }
472
+ activeDays += day._aggregatedDays || 1
473
+ totalModelsUsed += day.modelsUsed.length
474
+
475
+ if (/^\d{4}-\d{2}-\d{2}$/.test(day.date)) {
476
+ const weekday = new Date(`${day.date}T00:00:00`).getDay()
477
+ if (weekday === 0 || weekday === 6) weekendCost += day.totalCost
478
+ weekendEligible += day.totalCost
479
+ }
480
+
481
+ if (day.totalCost > topDay.cost) topDay = { date: day.date, cost: day.totalCost }
482
+ if (day.totalCost < cheapestDay.cost) cheapestDay = { date: day.date, cost: day.totalCost }
483
+
484
+ for (const breakdown of day.modelBreakdowns) {
485
+ const normalizedName = normalizeModelName(breakdown.modelName)
486
+ const totalBreakdownTokens =
487
+ breakdown.inputTokens +
488
+ breakdown.outputTokens +
489
+ breakdown.cacheCreationTokens +
490
+ breakdown.cacheReadTokens +
491
+ breakdown.thinkingTokens
492
+
493
+ modelCosts.set(normalizedName, (modelCosts.get(normalizedName) || 0) + breakdown.cost)
494
+ modelTokens.set(normalizedName, (modelTokens.get(normalizedName) || 0) + totalBreakdownTokens)
495
+ modelRequests.set(
496
+ normalizedName,
497
+ (modelRequests.get(normalizedName) || 0) + breakdown.requestCount,
498
+ )
499
+
500
+ const provider = getModelProvider(breakdown.modelName)
501
+ providerCosts.set(provider, (providerCosts.get(provider) || 0) + breakdown.cost)
502
+ }
503
+ }
504
+
505
+ const avgDailyCost = totalCost / activeDays
506
+ const avgRequestsPerDay = hasRequestData && activeDays > 0 ? totalRequests / activeDays : 0
507
+ const costPerMillion = totalTokens > 0 ? totalCost / (totalTokens / 1_000_000) : 0
508
+ const avgTokensPerRequest = hasRequestData && totalRequests > 0 ? totalTokens / totalRequests : 0
509
+ const avgCostPerRequest = hasRequestData && totalRequests > 0 ? totalCost / totalRequests : 0
510
+ const avgModelsPerEntry = data.length > 0 ? totalModelsUsed / data.length : 0
511
+ const cacheBase = totalCacheRead + totalCacheCreate + totalInput + totalOutput + totalThinking
512
+ const cacheHitRate = cacheBase > 0 ? (totalCacheRead / cacheBase) * 100 : 0
513
+
514
+ let topModel = null
515
+ for (const [name, cost] of modelCosts) {
516
+ if (!topModel || cost > topModel.cost) topModel = { name, cost }
517
+ }
518
+
519
+ let topRequestModel = null
520
+ for (const [name, requests] of modelRequests) {
521
+ if (!topRequestModel || requests > topRequestModel.requests) {
522
+ topRequestModel = { name, requests }
523
+ }
524
+ }
525
+
526
+ let topTokenModel = null
527
+ for (const [name, tokens] of modelTokens) {
528
+ if (!topTokenModel || tokens > topTokenModel.tokens) topTokenModel = { name, tokens }
529
+ }
530
+
531
+ const topModelShare = topModel && totalCost > 0 ? (topModel.cost / totalCost) * 100 : 0
532
+ const topThreeModelsShare =
533
+ totalCost > 0
534
+ ? ([...modelCosts.values()]
535
+ .sort((left, right) => right - left)
536
+ .slice(0, 3)
537
+ .reduce((sum, value) => sum + value, 0) /
538
+ totalCost) *
539
+ 100
540
+ : 0
541
+
542
+ let topProvider = null
543
+ for (const [name, cost] of providerCosts) {
544
+ if (!topProvider || cost > topProvider.cost) {
545
+ topProvider = { name, cost, share: totalCost > 0 ? (cost / totalCost) * 100 : 0 }
546
+ }
547
+ }
548
+
549
+ const requestValues = data.map((entry) => entry.requestCount)
550
+ const requestVolatility = stdDev(requestValues)
551
+ const modelConcentrationIndex =
552
+ totalCost > 0
553
+ ? [...modelCosts.values()].reduce((sum, cost) => {
554
+ const share = cost / totalCost
555
+ return sum + share * share
556
+ }, 0)
557
+ : 0
558
+ const providerConcentrationIndex =
559
+ totalCost > 0
560
+ ? [...providerCosts.values()].reduce((sum, cost) => {
561
+ const share = cost / totalCost
562
+ return sum + share * share
563
+ }, 0)
564
+ : 0
565
+
566
+ return {
567
+ totalCost,
568
+ totalTokens,
569
+ activeDays,
570
+ topModel,
571
+ topRequestModel,
572
+ topTokenModel,
573
+ topModelShare,
574
+ topThreeModelsShare,
575
+ topProvider,
576
+ providerCount: providerCosts.size,
577
+ hasRequestData,
578
+ cacheHitRate,
579
+ costPerMillion,
580
+ avgTokensPerRequest,
581
+ avgCostPerRequest,
582
+ avgModelsPerEntry,
583
+ avgDailyCost,
584
+ avgRequestsPerDay,
585
+ topDay,
586
+ cheapestDay,
587
+ busiestWeek: computeBusiestWeek(data),
588
+ weekendCostShare: weekendEligible > 0 ? (weekendCost / weekendEligible) * 100 : null,
589
+ totalInput,
590
+ totalOutput,
591
+ totalCacheRead,
592
+ totalCacheCreate,
593
+ totalThinking,
594
+ totalRequests,
595
+ weekOverWeekChange: computeWeekOverWeekChange(data),
596
+ requestVolatility,
597
+ modelConcentrationIndex,
598
+ providerConcentrationIndex,
599
+ }
600
+ }
601
+
602
+ module.exports = {
603
+ aggregateToDailyFormat,
604
+ computeBusiestWeek,
605
+ computeMetrics,
606
+ computeMovingAverage,
607
+ computeWeekOverWeekChange,
608
+ filterByDateRange,
609
+ filterByModels,
610
+ filterByMonth,
611
+ filterByProviders,
612
+ getModelProvider,
613
+ normalizeModelName,
614
+ sortByDate,
615
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "datePresets": ["all", "7d", "30d", "month", "year"],
3
+ "viewModes": ["daily", "monthly", "yearly"],
4
+ "sectionDefinitions": [
5
+ { "id": "insights", "domId": "insights", "labelKey": "helpPanel.sectionLabels.insights" },
6
+ { "id": "metrics", "domId": "metrics", "labelKey": "helpPanel.sectionLabels.metrics" },
7
+ { "id": "today", "domId": "today", "labelKey": "helpPanel.sectionLabels.today" },
8
+ {
9
+ "id": "currentMonth",
10
+ "domId": "current-month",
11
+ "labelKey": "helpPanel.sectionLabels.currentMonth"
12
+ },
13
+ { "id": "activity", "domId": "activity", "labelKey": "helpPanel.sectionLabels.activity" },
14
+ {
15
+ "id": "forecastCache",
16
+ "domId": "forecast-cache",
17
+ "labelKey": "helpPanel.sectionLabels.forecastCache"
18
+ },
19
+ { "id": "limits", "domId": "limits", "labelKey": "helpPanel.sectionLabels.limits" },
20
+ { "id": "costAnalysis", "domId": "charts", "labelKey": "helpPanel.sectionLabels.costAnalysis" },
21
+ {
22
+ "id": "tokenAnalysis",
23
+ "domId": "token-analysis",
24
+ "labelKey": "helpPanel.sectionLabels.tokenAnalysis"
25
+ },
26
+ {
27
+ "id": "requestAnalysis",
28
+ "domId": "request-analysis",
29
+ "labelKey": "helpPanel.sectionLabels.requestAnalysis"
30
+ },
31
+ {
32
+ "id": "advancedAnalysis",
33
+ "domId": "advanced-analysis",
34
+ "labelKey": "helpPanel.sectionLabels.advancedAnalysis"
35
+ },
36
+ {
37
+ "id": "comparisons",
38
+ "domId": "comparisons",
39
+ "labelKey": "helpPanel.sectionLabels.comparisons"
40
+ },
41
+ { "id": "tables", "domId": "tables", "labelKey": "helpPanel.sectionLabels.tables" }
42
+ ]
43
+ }