@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.
- package/README.md +14 -5
- package/dist/assets/AutoImportModal-C8gA0_mL.js +3 -0
- package/dist/assets/CustomTooltip-CdIOw3Ep.js +1 -0
- package/dist/assets/DrillDownModal-d6hcut-I.js +1 -0
- package/dist/assets/button-B26tLVFw.js +1 -0
- package/dist/assets/dialog-CA-ZSHjK.js +1 -0
- package/dist/assets/{icons-vendor-DFoaijFJ.js → icons-vendor-z59La6A4.js} +1 -1
- package/dist/assets/index-BkGSNAne.css +2 -0
- package/dist/assets/index-CMtAn7c8.js +4 -0
- package/dist/index.html +6 -6
- package/package.json +3 -1
- package/server/http-utils.js +165 -0
- package/server/report/chart-labels.js +12 -0
- package/server/report/charts.js +4 -8
- package/server/report/index.js +73 -16
- package/server/report/utils.js +76 -478
- package/server/runtime.js +78 -0
- package/server.js +280 -165
- package/shared/dashboard-domain.d.ts +19 -0
- package/shared/dashboard-domain.js +615 -0
- package/shared/dashboard-preferences.json +43 -0
- package/shared/dashboard-types.d.ts +62 -0
- package/shared/model-colors.d.ts +17 -0
- package/shared/model-colors.js +241 -0
- package/src/locales/de/common.json +198 -124
- package/src/locales/en/common.json +78 -4
- package/dist/assets/AutoImportModal-Dqbl8H04.js +0 -2
- package/dist/assets/CustomTooltip-BxopDd3O.js +0 -1
- package/dist/assets/DrillDownModal-B7ZU15xQ.js +0 -1
- package/dist/assets/button-D7Ib8H7t.js +0 -1
- package/dist/assets/dialog-Cn1m7WhC.js +0 -1
- package/dist/assets/index-DDw3UUhU.js +0 -4
- package/dist/assets/index-g2F-z39N.css +0 -2
|
@@ -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
|
+
}
|