@roastcodes/ttdash 6.1.4 → 6.1.6
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 -4
- package/dist/assets/AutoImportModal-Dqbl8H04.js +2 -0
- package/dist/assets/CustomTooltip-DBPq6A_5.js +1 -0
- package/dist/assets/DrillDownModal-BKuxN4GY.js +1 -0
- package/dist/assets/index-D8uaHhGW.js +4 -0
- package/dist/assets/index-TppJ6Iqj.css +2 -0
- package/dist/index.html +4 -4
- package/package.json +18 -5
- package/server/model-normalization.json +28 -0
- package/server/report/charts.js +151 -64
- package/server/report/index.js +149 -109
- package/server/report/utils.js +482 -85
- package/server.js +305 -191
- package/src/locales/de/common.json +78 -1
- package/src/locales/en/common.json +78 -1
- package/usage-normalizer.js +44 -36
- package/dist/assets/AutoImportModal-Dig6ASar.js +0 -2
- package/dist/assets/CustomTooltip-YeXs5zcp.js +0 -1
- package/dist/assets/DrillDownModal-Ct7hxDzy.js +0 -1
- package/dist/assets/index-9cPAel40.js +0 -4
- package/dist/assets/index-DWoj-vpZ.css +0 -2
package/server/report/utils.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
const { version: APP_VERSION } = require('../../package.json');
|
|
2
2
|
const { getLanguage, getLocale, translate } = require('./i18n');
|
|
3
|
+
const modelNormalizationSpec = require('../model-normalization.json');
|
|
4
|
+
|
|
5
|
+
const DISPLAY_ALIASES = modelNormalizationSpec.displayAliases.map((alias) => ({
|
|
6
|
+
...alias,
|
|
7
|
+
matcher: new RegExp(alias.pattern, 'i'),
|
|
8
|
+
}));
|
|
9
|
+
const PROVIDER_MATCHERS = modelNormalizationSpec.providerMatchers.map((matcher) => ({
|
|
10
|
+
...matcher,
|
|
11
|
+
matcher: new RegExp(matcher.pattern, 'i'),
|
|
12
|
+
}));
|
|
3
13
|
|
|
4
14
|
const MODEL_COLORS = {
|
|
5
15
|
'Opus 4.6': 'rgb(175, 92, 224)',
|
|
@@ -10,8 +20,8 @@ const MODEL_COLORS = {
|
|
|
10
20
|
'GPT-5.4': 'rgb(230, 98, 56)',
|
|
11
21
|
'GPT-5': 'rgb(230, 98, 56)',
|
|
12
22
|
'Gemini 3 Flash Preview': 'rgb(237, 188, 8)',
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
Gemini: 'rgb(237, 188, 8)',
|
|
24
|
+
OpenCode: 'rgb(51, 181, 193)',
|
|
15
25
|
};
|
|
16
26
|
|
|
17
27
|
const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
|
@@ -23,58 +33,158 @@ function titleCaseSegment(segment) {
|
|
|
23
33
|
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (lower.includes('opencode')) return 'OpenCode';
|
|
38
|
-
if (lower.includes('haiku')) return 'Haiku';
|
|
39
|
-
|
|
40
|
-
const stripped = String(raw || '')
|
|
36
|
+
function capitalize(segment) {
|
|
37
|
+
if (!segment) return '';
|
|
38
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatVersion(version) {
|
|
42
|
+
return version.replace(/-/g, '.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function canonicalizeModelName(raw) {
|
|
46
|
+
const normalized = String(raw || '')
|
|
41
47
|
.trim()
|
|
42
|
-
.
|
|
43
|
-
.replace(/^(claude|anthropic|openai|google|vertex|models)-/i, '')
|
|
48
|
+
.toLowerCase()
|
|
44
49
|
.replace(/^model[:/ -]*/i, '')
|
|
50
|
+
.replace(/^(anthropic|openai|google|vertex|models)[/-]/i, '')
|
|
51
|
+
.replace(/\./g, '-')
|
|
45
52
|
.replace(/[_/]+/g, '-')
|
|
46
53
|
.replace(/\s+/g, '-')
|
|
47
54
|
.replace(/-{2,}/g, '-')
|
|
48
55
|
.replace(/^-|-$/g, '');
|
|
49
56
|
|
|
50
|
-
const
|
|
57
|
+
const suffixStart = normalized.lastIndexOf('-');
|
|
58
|
+
if (suffixStart > 0) {
|
|
59
|
+
const suffix = normalized.slice(suffixStart + 1);
|
|
60
|
+
if (suffix.length === 8 && suffix.startsWith('20') && /^\d+$/.test(suffix)) {
|
|
61
|
+
return normalized.slice(0, suffixStart);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseClaudeName(rest) {
|
|
69
|
+
const parts = rest.split('-', 2);
|
|
70
|
+
if (parts.length < 2) {
|
|
71
|
+
return `Claude ${capitalize(rest)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `${capitalize(parts[0] || '')} ${formatVersion(parts[1] || '')}`.trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseGptName(rest) {
|
|
78
|
+
const parts = rest.split('-');
|
|
79
|
+
const variant = parts[0] || '';
|
|
80
|
+
const minor = parts[1] || '';
|
|
81
|
+
|
|
82
|
+
if (minor && minor.length <= 2 && /^\d+$/.test(minor)) {
|
|
83
|
+
const version = `${variant}.${minor}`;
|
|
84
|
+
if (parts.length > 2) {
|
|
85
|
+
const suffix = parts.slice(2).map(capitalize).join(' ');
|
|
86
|
+
return `GPT-${version}${suffix ? ` ${suffix}` : ''}`;
|
|
87
|
+
}
|
|
88
|
+
return `GPT-${version}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (parts.length > 1) {
|
|
92
|
+
const suffix = parts.slice(1).map(capitalize).join(' ');
|
|
93
|
+
return `GPT-${variant}${suffix ? ` ${suffix}` : ''}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return `GPT-${rest}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseGeminiName(rest) {
|
|
100
|
+
const parts = rest.split('-');
|
|
101
|
+
if (parts.length < 2) {
|
|
102
|
+
return `Gemini ${rest}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const versionParts = [];
|
|
106
|
+
const tierParts = [];
|
|
107
|
+
|
|
108
|
+
for (const part of parts) {
|
|
109
|
+
if (/^\d+$/.test(part) && tierParts.length === 0) {
|
|
110
|
+
versionParts.push(part);
|
|
111
|
+
} else {
|
|
112
|
+
tierParts.push(capitalize(part));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const version = versionParts.join('.');
|
|
117
|
+
const tier = tierParts.join(' ');
|
|
118
|
+
|
|
119
|
+
return tier ? `Gemini ${version} ${tier}` : `Gemini ${version}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseCodexName(rest) {
|
|
123
|
+
const normalized = rest.replace(/-latest$/i, '');
|
|
124
|
+
if (!normalized) {
|
|
125
|
+
return 'Codex';
|
|
126
|
+
}
|
|
127
|
+
return `Codex ${normalized.split('-').map(capitalize).join(' ')}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseOSeries(name) {
|
|
131
|
+
const separatorIndex = name.indexOf('-');
|
|
132
|
+
if (separatorIndex === -1) {
|
|
133
|
+
return name;
|
|
134
|
+
}
|
|
135
|
+
return `${name.slice(0, separatorIndex)} ${capitalize(name.slice(separatorIndex + 1))}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeModelName(raw) {
|
|
139
|
+
const canonical = canonicalizeModelName(raw);
|
|
140
|
+
|
|
141
|
+
for (const alias of DISPLAY_ALIASES) {
|
|
142
|
+
if (alias.matcher.test(canonical)) return alias.name;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (canonical.startsWith('claude-')) {
|
|
146
|
+
return parseClaudeName(canonical.slice('claude-'.length));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (canonical.startsWith('gpt-')) {
|
|
150
|
+
return parseGptName(canonical.slice('gpt-'.length));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (canonical.startsWith('gemini-')) {
|
|
154
|
+
return parseGeminiName(canonical.slice('gemini-'.length));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (canonical.startsWith('codex-')) {
|
|
158
|
+
return parseCodexName(canonical.slice('codex-'.length));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (/^o\d/i.test(canonical)) {
|
|
162
|
+
return parseOSeries(canonical);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const familyMatch = canonical.match(
|
|
166
|
+
/^(gpt|opus|sonnet|haiku|gemini|codex|o\d|oai|grok|llama|mistral|command|deepseek|qwen)(?:-([a-z0-9-]+))?$/i,
|
|
167
|
+
);
|
|
51
168
|
if (familyMatch) {
|
|
52
169
|
const family = familyMatch[1];
|
|
53
|
-
|
|
170
|
+
if (/^codex$/i.test(family)) {
|
|
171
|
+
return parseCodexName(familyMatch[2] || '');
|
|
172
|
+
}
|
|
173
|
+
if (/^(o\d)$/i.test(family)) return parseOSeries(canonical);
|
|
174
|
+
|
|
175
|
+
const suffix = familyMatch[2] ? formatVersion(familyMatch[2]) : '';
|
|
54
176
|
if (/^gpt$/i.test(family) && suffix) return `GPT-${suffix.toUpperCase()}`;
|
|
55
|
-
if (/^(o\d)$/i.test(family)) return family.toUpperCase();
|
|
56
177
|
return `${titleCaseSegment(family)}${suffix ? ` ${suffix}` : ''}`.trim();
|
|
57
178
|
}
|
|
58
179
|
|
|
59
|
-
return
|
|
60
|
-
.split('-')
|
|
61
|
-
.filter(Boolean)
|
|
62
|
-
.map(titleCaseSegment)
|
|
63
|
-
.join(' ') || String(raw || '');
|
|
180
|
+
return canonical.split('-').filter(Boolean).map(titleCaseSegment).join(' ') || String(raw || '');
|
|
64
181
|
}
|
|
65
182
|
|
|
66
183
|
function getModelProvider(raw) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (lower.includes('grok') || lower.includes('xai')) return 'xAI';
|
|
72
|
-
if (lower.includes('llama') || lower.includes('meta-llama') || lower.includes('meta/')) return 'Meta';
|
|
73
|
-
if (lower.includes('command') || lower.includes('cohere')) return 'Cohere';
|
|
74
|
-
if (lower.includes('mistral')) return 'Mistral';
|
|
75
|
-
if (lower.includes('deepseek')) return 'DeepSeek';
|
|
76
|
-
if (lower.includes('qwen') || lower.includes('alibaba')) return 'Alibaba';
|
|
77
|
-
if (lower.includes('opencode')) return 'OpenCode';
|
|
184
|
+
const canonical = canonicalizeModelName(raw);
|
|
185
|
+
for (const matcher of PROVIDER_MATCHERS) {
|
|
186
|
+
if (matcher.matcher.test(canonical)) return matcher.provider;
|
|
187
|
+
}
|
|
78
188
|
return 'Other';
|
|
79
189
|
}
|
|
80
190
|
|
|
@@ -127,7 +237,8 @@ function recalculateDayFromBreakdowns(day, modelBreakdowns) {
|
|
|
127
237
|
thinkingTokens,
|
|
128
238
|
totalCost,
|
|
129
239
|
requestCount,
|
|
130
|
-
totalTokens:
|
|
240
|
+
totalTokens:
|
|
241
|
+
inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + thinkingTokens,
|
|
131
242
|
modelsUsed: modelBreakdowns.map((item) => item.modelName),
|
|
132
243
|
modelBreakdowns,
|
|
133
244
|
};
|
|
@@ -138,8 +249,12 @@ function filterByProviders(data, selectedProviders) {
|
|
|
138
249
|
const selected = new Set(selectedProviders);
|
|
139
250
|
return data
|
|
140
251
|
.map((day) => {
|
|
141
|
-
const filteredBreakdowns = day.modelBreakdowns.filter((entry) =>
|
|
142
|
-
|
|
252
|
+
const filteredBreakdowns = day.modelBreakdowns.filter((entry) =>
|
|
253
|
+
selected.has(getModelProvider(entry.modelName)),
|
|
254
|
+
);
|
|
255
|
+
return filteredBreakdowns.length > 0
|
|
256
|
+
? recalculateDayFromBreakdowns(day, filteredBreakdowns)
|
|
257
|
+
: null;
|
|
143
258
|
})
|
|
144
259
|
.filter(Boolean);
|
|
145
260
|
}
|
|
@@ -149,17 +264,19 @@ function filterByModels(data, selectedModels) {
|
|
|
149
264
|
const selected = new Set(selectedModels);
|
|
150
265
|
return data
|
|
151
266
|
.map((day) => {
|
|
152
|
-
const filteredBreakdowns = day.modelBreakdowns.filter((entry) =>
|
|
153
|
-
|
|
267
|
+
const filteredBreakdowns = day.modelBreakdowns.filter((entry) =>
|
|
268
|
+
selected.has(normalizeModelName(entry.modelName)),
|
|
269
|
+
);
|
|
270
|
+
return filteredBreakdowns.length > 0
|
|
271
|
+
? recalculateDayFromBreakdowns(day, filteredBreakdowns)
|
|
272
|
+
: null;
|
|
154
273
|
})
|
|
155
274
|
.filter(Boolean);
|
|
156
275
|
}
|
|
157
276
|
|
|
158
277
|
function aggregateToDailyFormat(data, viewMode) {
|
|
159
278
|
if (viewMode === 'daily') return data;
|
|
160
|
-
const groupKey = viewMode === 'monthly'
|
|
161
|
-
? (date) => date.slice(0, 7)
|
|
162
|
-
: (date) => date.slice(0, 4);
|
|
279
|
+
const groupKey = viewMode === 'monthly' ? (date) => date.slice(0, 7) : (date) => date.slice(0, 4);
|
|
163
280
|
const groups = new Map();
|
|
164
281
|
|
|
165
282
|
for (const day of data) {
|
|
@@ -239,7 +356,8 @@ function toWeekdayData(data) {
|
|
|
239
356
|
}
|
|
240
357
|
return WEEKDAYS.map((label, index) => {
|
|
241
358
|
const values = weekdayCosts[index];
|
|
242
|
-
const average =
|
|
359
|
+
const average =
|
|
360
|
+
values.length > 0 ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
243
361
|
return { day: label, cost: average };
|
|
244
362
|
});
|
|
245
363
|
}
|
|
@@ -338,7 +456,8 @@ function computeMetrics(data) {
|
|
|
338
456
|
totalCacheCreate += day.cacheCreationTokens;
|
|
339
457
|
totalThinking += day.thinkingTokens;
|
|
340
458
|
activeDays += day._aggregatedDays || 1;
|
|
341
|
-
if (day.requestCount > 0 || day.modelBreakdowns.some((entry) => entry.requestCount > 0))
|
|
459
|
+
if (day.requestCount > 0 || day.modelBreakdowns.some((entry) => entry.requestCount > 0))
|
|
460
|
+
hasRequestData = true;
|
|
342
461
|
if (day.totalCost > topDay.cost) topDay = { date: day.date, cost: day.totalCost };
|
|
343
462
|
if (day.totalCost < cheapestDay.cost) cheapestDay = { date: day.date, cost: day.totalCost };
|
|
344
463
|
|
|
@@ -416,7 +535,12 @@ function computeModelRows(data) {
|
|
|
416
535
|
_dates: new Set(),
|
|
417
536
|
};
|
|
418
537
|
current.cost += breakdown.cost;
|
|
419
|
-
current.tokens +=
|
|
538
|
+
current.tokens +=
|
|
539
|
+
breakdown.inputTokens +
|
|
540
|
+
breakdown.outputTokens +
|
|
541
|
+
breakdown.cacheCreationTokens +
|
|
542
|
+
breakdown.cacheReadTokens +
|
|
543
|
+
breakdown.thinkingTokens;
|
|
420
544
|
current.requests += breakdown.requestCount;
|
|
421
545
|
if (!current._dates.has(day.date)) {
|
|
422
546
|
current._dates.add(day.date);
|
|
@@ -454,7 +578,12 @@ function computeProviderRows(data) {
|
|
|
454
578
|
_dates: new Set(),
|
|
455
579
|
};
|
|
456
580
|
current.cost += breakdown.cost;
|
|
457
|
-
current.tokens +=
|
|
581
|
+
current.tokens +=
|
|
582
|
+
breakdown.inputTokens +
|
|
583
|
+
breakdown.outputTokens +
|
|
584
|
+
breakdown.cacheCreationTokens +
|
|
585
|
+
breakdown.cacheReadTokens +
|
|
586
|
+
breakdown.thinkingTokens;
|
|
458
587
|
current.requests += breakdown.requestCount;
|
|
459
588
|
if (!current._dates.has(day.date)) {
|
|
460
589
|
current._dates.add(day.date);
|
|
@@ -464,7 +593,13 @@ function computeProviderRows(data) {
|
|
|
464
593
|
}
|
|
465
594
|
}
|
|
466
595
|
return Array.from(rows.values())
|
|
467
|
-
.map((
|
|
596
|
+
.map((entry) => ({
|
|
597
|
+
name: entry.name,
|
|
598
|
+
cost: entry.cost,
|
|
599
|
+
tokens: entry.tokens,
|
|
600
|
+
requests: entry.requests,
|
|
601
|
+
days: entry.days,
|
|
602
|
+
}))
|
|
468
603
|
.sort((a, b) => b.cost - a.cost);
|
|
469
604
|
}
|
|
470
605
|
|
|
@@ -491,7 +626,12 @@ function formatDate(dateStr, mode = 'short', language = 'de') {
|
|
|
491
626
|
}
|
|
492
627
|
const date = new Date(`${dateStr}T00:00:00`);
|
|
493
628
|
if (mode === 'long') {
|
|
494
|
-
return date.toLocaleDateString(locale, {
|
|
629
|
+
return date.toLocaleDateString(locale, {
|
|
630
|
+
weekday: 'short',
|
|
631
|
+
day: '2-digit',
|
|
632
|
+
month: '2-digit',
|
|
633
|
+
year: 'numeric',
|
|
634
|
+
});
|
|
495
635
|
}
|
|
496
636
|
return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit' });
|
|
497
637
|
}
|
|
@@ -504,7 +644,10 @@ function formatDateAxis(dateStr, language = 'de') {
|
|
|
504
644
|
const date = new Date(Number(year), Number(month) - 1);
|
|
505
645
|
return date.toLocaleDateString(locale, { month: 'short', year: '2-digit' });
|
|
506
646
|
}
|
|
507
|
-
return new Date(`${dateStr}T00:00:00`).toLocaleDateString(locale, {
|
|
647
|
+
return new Date(`${dateStr}T00:00:00`).toLocaleDateString(locale, {
|
|
648
|
+
day: '2-digit',
|
|
649
|
+
month: '2-digit',
|
|
650
|
+
});
|
|
508
651
|
}
|
|
509
652
|
|
|
510
653
|
function formatFilterValue(value, language = 'de') {
|
|
@@ -527,16 +670,127 @@ function formatInteger(value, language = 'de') {
|
|
|
527
670
|
return Math.round(value || 0).toLocaleString(getLocale(language));
|
|
528
671
|
}
|
|
529
672
|
|
|
530
|
-
function
|
|
673
|
+
function formatPercent(value, language = 'de') {
|
|
674
|
+
return `${(value || 0).toLocaleString(getLocale(language), {
|
|
675
|
+
minimumFractionDigits: 1,
|
|
676
|
+
maximumFractionDigits: 1,
|
|
677
|
+
})}%`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function formatCompactNumber(value, language = 'de') {
|
|
531
681
|
if (!Number.isFinite(value)) return '0';
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
682
|
+
|
|
683
|
+
const abs = Math.abs(value);
|
|
684
|
+
const locale = getLocale(language);
|
|
685
|
+
|
|
686
|
+
if (abs >= 1e9) {
|
|
687
|
+
const suffix = language === 'en' ? 'B' : ' Mrd.';
|
|
688
|
+
return `${(value / 1e9).toLocaleString(locale, {
|
|
689
|
+
minimumFractionDigits: 1,
|
|
690
|
+
maximumFractionDigits: 1,
|
|
691
|
+
})}${suffix}`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (abs >= 1e6) {
|
|
695
|
+
const suffix = language === 'en' ? 'M' : ' Mio.';
|
|
696
|
+
return `${(value / 1e6).toLocaleString(locale, {
|
|
697
|
+
minimumFractionDigits: 1,
|
|
698
|
+
maximumFractionDigits: 1,
|
|
699
|
+
})}${suffix}`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (abs >= 1e3) {
|
|
703
|
+
const suffix = language === 'en' ? 'k' : ' Tsd.';
|
|
704
|
+
return `${(value / 1e3).toLocaleString(locale, {
|
|
705
|
+
minimumFractionDigits: 1,
|
|
706
|
+
maximumFractionDigits: 1,
|
|
707
|
+
})}${suffix}`;
|
|
708
|
+
}
|
|
709
|
+
|
|
535
710
|
return formatInteger(value, language);
|
|
536
711
|
}
|
|
537
712
|
|
|
538
|
-
function
|
|
539
|
-
return
|
|
713
|
+
function formatCompact(value, language = 'de') {
|
|
714
|
+
return formatCompactNumber(value, language);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function formatCompactAxis(value, language = 'de') {
|
|
718
|
+
return formatCompactNumber(value, language);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function summarizeSelection(
|
|
722
|
+
values,
|
|
723
|
+
language,
|
|
724
|
+
{ emptyKey, maxVisible = 3, normalize = (value) => value } = {},
|
|
725
|
+
) {
|
|
726
|
+
const normalized = (values || []).map(normalize).filter(Boolean);
|
|
727
|
+
|
|
728
|
+
if (normalized.length === 0) {
|
|
729
|
+
return translate(language, emptyKey);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const visible = normalized.slice(0, maxVisible);
|
|
733
|
+
const hidden = normalized.length - visible.length;
|
|
734
|
+
const suffix =
|
|
735
|
+
hidden > 0 ? ` ${translate(language, 'report.filters.andMore', { count: hidden })}` : '';
|
|
736
|
+
|
|
737
|
+
return `${visible.join(', ')}${suffix}`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function truncateLabel(value, maxLength = 28) {
|
|
741
|
+
const stringValue = String(value || '');
|
|
742
|
+
if (stringValue.length <= maxLength) return stringValue;
|
|
743
|
+
return `${stringValue.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function buildInsights(metrics, { filteredDaily, filtered, language }) {
|
|
747
|
+
const insights = [];
|
|
748
|
+
|
|
749
|
+
if (filteredDaily.length > 0 && filteredDaily.length < 7) {
|
|
750
|
+
insights.push({
|
|
751
|
+
tone: 'warn',
|
|
752
|
+
title: translate(language, 'report.insights.coverageTitle'),
|
|
753
|
+
body: translate(language, 'report.insights.coverageBody', {
|
|
754
|
+
days: formatInteger(filteredDaily.length, language),
|
|
755
|
+
periods: formatInteger(filtered.length, language),
|
|
756
|
+
}),
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (metrics.topProvider) {
|
|
761
|
+
insights.push({
|
|
762
|
+
tone: metrics.topProvider.share >= 60 ? 'warn' : 'accent',
|
|
763
|
+
title: translate(language, 'report.insights.providerTitle'),
|
|
764
|
+
body: translate(language, 'report.insights.providerBody', {
|
|
765
|
+
provider: metrics.topProvider.name,
|
|
766
|
+
share: formatPercent(metrics.topProvider.share, language),
|
|
767
|
+
}),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (metrics.cacheHitRate > 0) {
|
|
772
|
+
insights.push({
|
|
773
|
+
tone: metrics.cacheHitRate >= 20 ? 'good' : 'accent',
|
|
774
|
+
title: translate(language, 'report.insights.cacheTitle'),
|
|
775
|
+
body: translate(language, 'report.insights.cacheBody', {
|
|
776
|
+
share: formatPercent(metrics.cacheHitRate, language),
|
|
777
|
+
}),
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (metrics.busiestWeek) {
|
|
782
|
+
insights.push({
|
|
783
|
+
tone: 'accent',
|
|
784
|
+
title: translate(language, 'report.insights.peakWindowTitle'),
|
|
785
|
+
body: translate(language, 'report.insights.peakWindowBody', {
|
|
786
|
+
start: formatDate(metrics.busiestWeek.start, 'long', language),
|
|
787
|
+
end: formatDate(metrics.busiestWeek.end, 'long', language),
|
|
788
|
+
cost: formatCurrency(metrics.busiestWeek.cost, language),
|
|
789
|
+
}),
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return insights.slice(0, 4);
|
|
540
794
|
}
|
|
541
795
|
|
|
542
796
|
function periodUnit(viewMode, language = 'de') {
|
|
@@ -547,7 +801,10 @@ function periodUnit(viewMode, language = 'de') {
|
|
|
547
801
|
|
|
548
802
|
function applyReportFilters(allDailyData, filters) {
|
|
549
803
|
const sorted = sortByDate(allDailyData);
|
|
550
|
-
const preProvider = filterByMonth(
|
|
804
|
+
const preProvider = filterByMonth(
|
|
805
|
+
filterByDateRange(sorted, filters.startDate, filters.endDate),
|
|
806
|
+
filters.selectedMonth,
|
|
807
|
+
);
|
|
551
808
|
const preModel = filterByProviders(preProvider, filters.selectedProviders || []);
|
|
552
809
|
const filteredDaily = filterByModels(preModel, filters.selectedModels || []);
|
|
553
810
|
const filtered = aggregateToDailyFormat(filteredDaily, filters.viewMode || 'daily');
|
|
@@ -573,26 +830,99 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
573
830
|
const metrics = computeMetrics(filtered);
|
|
574
831
|
const modelRows = computeModelRows(filtered).slice(0, 12);
|
|
575
832
|
const providerRows = computeProviderRows(filtered).slice(0, 8);
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
833
|
+
const periodLabel = periodUnit(filters.viewMode, language);
|
|
834
|
+
const notAvailable = translate(language, 'report.common.notAvailable');
|
|
835
|
+
const selectedProvidersLabel = summarizeSelection(filters.selectedProviders, language, {
|
|
836
|
+
emptyKey: 'report.filters.all',
|
|
837
|
+
});
|
|
838
|
+
const selectedModelsLabel = summarizeSelection(filters.selectedModels, language, {
|
|
839
|
+
emptyKey: 'report.filters.all',
|
|
840
|
+
normalize: normalizeModelName,
|
|
841
|
+
});
|
|
842
|
+
const monthLabel =
|
|
843
|
+
formatFilterValue(filters.selectedMonth, language) || translate(language, 'report.filters.all');
|
|
844
|
+
const startDateLabel =
|
|
845
|
+
formatFilterValue(filters.startDate || null, language) ||
|
|
846
|
+
translate(language, 'report.filters.noFilter');
|
|
847
|
+
const endDateLabel =
|
|
848
|
+
formatFilterValue(filters.endDate || null, language) ||
|
|
849
|
+
translate(language, 'report.filters.noFilter');
|
|
850
|
+
const peakPeriodLabel = metrics.topDay
|
|
851
|
+
? formatDate(metrics.topDay.date, 'long', language)
|
|
852
|
+
: notAvailable;
|
|
853
|
+
const topModelValue = metrics.topModel ? metrics.topModel.name : notAvailable;
|
|
854
|
+
const topProviderValue = metrics.topProvider ? metrics.topProvider.name : notAvailable;
|
|
855
|
+
const insights = buildInsights(metrics, { filteredDaily, filtered, language });
|
|
856
|
+
const avgPeriodCost = filtered.length > 0 ? metrics.totalCost / filtered.length : 0;
|
|
857
|
+
const recentRows = sortByDate(filtered)
|
|
858
|
+
.slice(-12)
|
|
859
|
+
.reverse()
|
|
860
|
+
.map((entry) => ({
|
|
861
|
+
period: entry.date,
|
|
862
|
+
label: formatDate(entry.date, 'long', language),
|
|
863
|
+
cost: entry.totalCost,
|
|
864
|
+
costLabel: formatCurrency(entry.totalCost, language),
|
|
865
|
+
tokens: entry.totalTokens,
|
|
866
|
+
tokensLabel: formatCompact(entry.totalTokens, language),
|
|
867
|
+
requests: entry.requestCount,
|
|
868
|
+
requestsLabel: formatInteger(entry.requestCount, language),
|
|
869
|
+
}));
|
|
586
870
|
|
|
587
871
|
const summaryCards = [
|
|
588
|
-
{
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
872
|
+
{
|
|
873
|
+
label: translate(language, 'common.costs'),
|
|
874
|
+
value: formatCurrency(metrics.totalCost, language),
|
|
875
|
+
note: metrics.topProvider
|
|
876
|
+
? `${metrics.topProvider.name} ${formatPercent(metrics.topProvider.share, language)}`
|
|
877
|
+
: notAvailable,
|
|
878
|
+
tone: 'accent',
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
label: translate(language, 'common.tokens'),
|
|
882
|
+
value: formatCompact(metrics.totalTokens, language),
|
|
883
|
+
note: `CPM ${formatCurrency(metrics.costPerMillion, language)}`,
|
|
884
|
+
tone: 'accent',
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
label: translate(language, 'common.requests'),
|
|
888
|
+
value: formatInteger(metrics.totalRequests, language),
|
|
889
|
+
note: metrics.hasRequestData
|
|
890
|
+
? `${formatPercent(metrics.cacheHitRate, language)} Cache`
|
|
891
|
+
: notAvailable,
|
|
892
|
+
tone: 'good',
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
label: `Ø ${translate(language, 'common.cost')} / ${periodLabel}`,
|
|
896
|
+
value: formatCurrency(avgPeriodCost, language),
|
|
897
|
+
note: `${reportDataLabel(filters.viewMode, language)}`,
|
|
898
|
+
tone: 'accent',
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
label: translate(language, 'common.model'),
|
|
902
|
+
value: topModelValue,
|
|
903
|
+
note: metrics.topModel ? formatPercent(metrics.topModelShare, language) : notAvailable,
|
|
904
|
+
tone: 'warn',
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
label: translate(language, 'report.summary.peakPeriod'),
|
|
908
|
+
value: peakPeriodLabel,
|
|
909
|
+
note: metrics.topDay ? formatCurrency(metrics.topDay.cost, language) : notAvailable,
|
|
910
|
+
tone: 'warn',
|
|
911
|
+
},
|
|
594
912
|
];
|
|
595
913
|
|
|
914
|
+
const interpretationSummary = translate(language, 'report.interpretation.summary', {
|
|
915
|
+
days: formatInteger(filteredDaily.length, language),
|
|
916
|
+
periods: formatInteger(filtered.length, language),
|
|
917
|
+
peak: peakPeriodLabel,
|
|
918
|
+
topModel: topModelValue,
|
|
919
|
+
topProvider: topProviderValue,
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
const interpretationFooter = translate(language, 'report.interpretation.footer', {
|
|
923
|
+
version: APP_VERSION,
|
|
924
|
+
});
|
|
925
|
+
|
|
596
926
|
return {
|
|
597
927
|
meta: {
|
|
598
928
|
language,
|
|
@@ -607,20 +937,23 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
607
937
|
}),
|
|
608
938
|
reportTitle: options.reportTitle || translate(language, 'report.title'),
|
|
609
939
|
filterSummary: {
|
|
940
|
+
viewModeKey: filters.viewMode,
|
|
610
941
|
viewMode: translate(language, `viewModes.${filters.viewMode}`),
|
|
611
942
|
selectedMonth: filters.selectedMonth,
|
|
612
|
-
selectedMonthLabel:
|
|
943
|
+
selectedMonthLabel: monthLabel,
|
|
613
944
|
selectedProviders: filters.selectedProviders,
|
|
945
|
+
selectedProvidersLabel,
|
|
614
946
|
selectedModels: filters.selectedModels,
|
|
947
|
+
selectedModelsLabel,
|
|
615
948
|
startDate: filters.startDate || null,
|
|
616
|
-
startDateLabel
|
|
949
|
+
startDateLabel,
|
|
617
950
|
endDate: filters.endDate || null,
|
|
618
|
-
endDateLabel
|
|
951
|
+
endDateLabel,
|
|
619
952
|
},
|
|
620
953
|
dateRange,
|
|
621
954
|
periods: filtered.length,
|
|
622
955
|
days: filteredDaily.length,
|
|
623
|
-
periodUnit:
|
|
956
|
+
periodUnit: periodLabel,
|
|
624
957
|
},
|
|
625
958
|
metrics,
|
|
626
959
|
summaryCards,
|
|
@@ -643,13 +976,69 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
643
976
|
})),
|
|
644
977
|
recentPeriods: recentRows,
|
|
645
978
|
labels: {
|
|
646
|
-
dateRangeText: dateRange
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
979
|
+
dateRangeText: dateRange
|
|
980
|
+
? `${formatDate(dateRange.start, 'long', language)} - ${formatDate(dateRange.end, 'long', language)}`
|
|
981
|
+
: translate(language, 'common.noData'),
|
|
982
|
+
topModel: metrics.topModel
|
|
983
|
+
? `${metrics.topModel.name} (${formatPercent(metrics.topModelShare, language)})`
|
|
984
|
+
: notAvailable,
|
|
985
|
+
topProvider: metrics.topProvider
|
|
986
|
+
? `${metrics.topProvider.name} (${formatPercent(metrics.topProvider.share, language)})`
|
|
987
|
+
: notAvailable,
|
|
988
|
+
topDay: metrics.topDay
|
|
989
|
+
? `${formatDate(metrics.topDay.date, 'long', language)} (${formatCurrency(metrics.topDay.cost, language)})`
|
|
990
|
+
: notAvailable,
|
|
991
|
+
},
|
|
992
|
+
interpretation: {
|
|
993
|
+
summary: interpretationSummary,
|
|
994
|
+
footer: interpretationFooter,
|
|
995
|
+
},
|
|
996
|
+
insights: {
|
|
997
|
+
items: insights,
|
|
998
|
+
},
|
|
999
|
+
text: {
|
|
1000
|
+
headerEyebrow: translate(language, 'report.header.eyebrow'),
|
|
1001
|
+
sections: {
|
|
1002
|
+
overview: translate(language, 'report.sections.overview'),
|
|
1003
|
+
insights: translate(language, 'report.sections.insights'),
|
|
1004
|
+
filters: translate(language, 'report.sections.filters'),
|
|
1005
|
+
modelsProviders: translate(language, 'report.sections.modelsProviders'),
|
|
1006
|
+
recentPeriods: translate(language, 'report.sections.recentPeriods'),
|
|
1007
|
+
interpretation: translate(language, 'report.sections.interpretation'),
|
|
1008
|
+
},
|
|
1009
|
+
fields: {
|
|
1010
|
+
dateRange: translate(language, 'report.fields.dateRange'),
|
|
1011
|
+
view: translate(language, 'report.fields.view'),
|
|
1012
|
+
generated: translate(language, 'report.fields.generated'),
|
|
1013
|
+
month: translate(language, 'report.fields.month'),
|
|
1014
|
+
selectedProviders: translate(language, 'report.fields.selectedProviders'),
|
|
1015
|
+
selectedModels: translate(language, 'report.fields.selectedModels'),
|
|
1016
|
+
startDate: translate(language, 'report.fields.startDate'),
|
|
1017
|
+
endDate: translate(language, 'report.fields.endDate'),
|
|
1018
|
+
},
|
|
1019
|
+
tables: {
|
|
1020
|
+
topModels: translate(language, 'report.tables.topModels'),
|
|
1021
|
+
providers: translate(language, 'report.tables.providers'),
|
|
1022
|
+
columns: {
|
|
1023
|
+
model: translate(language, 'report.tables.columns.model'),
|
|
1024
|
+
provider: translate(language, 'report.tables.columns.provider'),
|
|
1025
|
+
cost: translate(language, 'report.tables.columns.cost'),
|
|
1026
|
+
tokens: translate(language, 'report.tables.columns.tokens'),
|
|
1027
|
+
requests: translate(language, 'report.tables.columns.requests'),
|
|
1028
|
+
period: translate(language, 'report.tables.columns.period'),
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
charts: {
|
|
1032
|
+
costTrend: translate(language, 'report.charts.costTrend'),
|
|
1033
|
+
topModels: translate(language, 'report.charts.topModels'),
|
|
1034
|
+
tokenTrend: translate(language, 'report.charts.tokenTrend'),
|
|
1035
|
+
},
|
|
650
1036
|
},
|
|
651
1037
|
formatting: {
|
|
652
|
-
axisDates: filtered.map((entry) => ({
|
|
1038
|
+
axisDates: filtered.map((entry) => ({
|
|
1039
|
+
date: entry.date,
|
|
1040
|
+
label: formatDateAxis(entry.date, language),
|
|
1041
|
+
})),
|
|
653
1042
|
},
|
|
654
1043
|
};
|
|
655
1044
|
}
|
|
@@ -668,7 +1057,15 @@ function reportDataLabel(viewMode, language = 'de') {
|
|
|
668
1057
|
module.exports = {
|
|
669
1058
|
applyReportFilters,
|
|
670
1059
|
buildReportData,
|
|
1060
|
+
formatCompact,
|
|
1061
|
+
formatCompactAxis,
|
|
1062
|
+
formatCurrency,
|
|
671
1063
|
formatDate,
|
|
672
1064
|
formatDateAxis,
|
|
673
1065
|
getModelColor,
|
|
1066
|
+
truncateLabel,
|
|
1067
|
+
__test__: {
|
|
1068
|
+
getModelProvider,
|
|
1069
|
+
normalizeModelName,
|
|
1070
|
+
},
|
|
674
1071
|
};
|