@roastcodes/ttdash 6.1.5 → 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/index.html +3 -3
- package/package.json +18 -5
- package/server/model-normalization.json +28 -0
- package/server/report/charts.js +127 -54
- package/server/report/index.js +25 -5
- package/server/report/utils.js +292 -86
- package/server.js +302 -188
- package/src/locales/de/common.json +14 -0
- package/src/locales/en/common.json +14 -0
- package/usage-normalizer.js +44 -36
- package/dist/assets/AutoImportModal-Dig6ASar.js +0 -2
- package/dist/assets/CustomTooltip-Be-rHcDB.js +0 -1
- package/dist/assets/DrillDownModal-DXP44-00.js +0 -1
- package/dist/assets/index-_318nw_j.js +0 -4
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') {
|
|
@@ -575,10 +718,12 @@ function formatCompactAxis(value, language = 'de') {
|
|
|
575
718
|
return formatCompactNumber(value, language);
|
|
576
719
|
}
|
|
577
720
|
|
|
578
|
-
function summarizeSelection(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
721
|
+
function summarizeSelection(
|
|
722
|
+
values,
|
|
723
|
+
language,
|
|
724
|
+
{ emptyKey, maxVisible = 3, normalize = (value) => value } = {},
|
|
725
|
+
) {
|
|
726
|
+
const normalized = (values || []).map(normalize).filter(Boolean);
|
|
582
727
|
|
|
583
728
|
if (normalized.length === 0) {
|
|
584
729
|
return translate(language, emptyKey);
|
|
@@ -586,9 +731,8 @@ function summarizeSelection(values, language, { emptyKey, maxVisible = 3, normal
|
|
|
586
731
|
|
|
587
732
|
const visible = normalized.slice(0, maxVisible);
|
|
588
733
|
const hidden = normalized.length - visible.length;
|
|
589
|
-
const suffix =
|
|
590
|
-
? ` ${translate(language, 'report.filters.andMore', { count: hidden })}`
|
|
591
|
-
: '';
|
|
734
|
+
const suffix =
|
|
735
|
+
hidden > 0 ? ` ${translate(language, 'report.filters.andMore', { count: hidden })}` : '';
|
|
592
736
|
|
|
593
737
|
return `${visible.join(', ')}${suffix}`;
|
|
594
738
|
}
|
|
@@ -657,7 +801,10 @@ function periodUnit(viewMode, language = 'de') {
|
|
|
657
801
|
|
|
658
802
|
function applyReportFilters(allDailyData, filters) {
|
|
659
803
|
const sorted = sortByDate(allDailyData);
|
|
660
|
-
const preProvider = filterByMonth(
|
|
804
|
+
const preProvider = filterByMonth(
|
|
805
|
+
filterByDateRange(sorted, filters.startDate, filters.endDate),
|
|
806
|
+
filters.selectedMonth,
|
|
807
|
+
);
|
|
661
808
|
const preModel = filterByProviders(preProvider, filters.selectedProviders || []);
|
|
662
809
|
const filteredDaily = filterByModels(preModel, filters.selectedModels || []);
|
|
663
810
|
const filtered = aggregateToDailyFormat(filteredDaily, filters.viewMode || 'daily');
|
|
@@ -692,32 +839,76 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
692
839
|
emptyKey: 'report.filters.all',
|
|
693
840
|
normalize: normalizeModelName,
|
|
694
841
|
});
|
|
695
|
-
const monthLabel =
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
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;
|
|
699
853
|
const topModelValue = metrics.topModel ? metrics.topModel.name : notAvailable;
|
|
700
854
|
const topProviderValue = metrics.topProvider ? metrics.topProvider.name : notAvailable;
|
|
701
855
|
const insights = buildInsights(metrics, { filteredDaily, filtered, language });
|
|
702
856
|
const avgPeriodCost = filtered.length > 0 ? metrics.totalCost / filtered.length : 0;
|
|
703
|
-
const recentRows = sortByDate(filtered)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
+
}));
|
|
713
870
|
|
|
714
871
|
const summaryCards = [
|
|
715
|
-
{
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
+
},
|
|
721
912
|
];
|
|
722
913
|
|
|
723
914
|
const interpretationSummary = translate(language, 'report.interpretation.summary', {
|
|
@@ -785,10 +976,18 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
785
976
|
})),
|
|
786
977
|
recentPeriods: recentRows,
|
|
787
978
|
labels: {
|
|
788
|
-
dateRangeText: dateRange
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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,
|
|
792
991
|
},
|
|
793
992
|
interpretation: {
|
|
794
993
|
summary: interpretationSummary,
|
|
@@ -836,7 +1035,10 @@ function buildReportData(allDailyData, options = {}) {
|
|
|
836
1035
|
},
|
|
837
1036
|
},
|
|
838
1037
|
formatting: {
|
|
839
|
-
axisDates: filtered.map((entry) => ({
|
|
1038
|
+
axisDates: filtered.map((entry) => ({
|
|
1039
|
+
date: entry.date,
|
|
1040
|
+
label: formatDateAxis(entry.date, language),
|
|
1041
|
+
})),
|
|
840
1042
|
},
|
|
841
1043
|
};
|
|
842
1044
|
}
|
|
@@ -862,4 +1064,8 @@ module.exports = {
|
|
|
862
1064
|
formatDateAxis,
|
|
863
1065
|
getModelColor,
|
|
864
1066
|
truncateLabel,
|
|
1067
|
+
__test__: {
|
|
1068
|
+
getModelProvider,
|
|
1069
|
+
normalizeModelName,
|
|
1070
|
+
},
|
|
865
1071
|
};
|