token-speed-tester 1.5.0 → 1.7.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.en.md +50 -78
- package/README.md +3 -0
- package/dist/index.js +744 -52
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,167 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync } from "fs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { writeFile as fsWriteFile } from "fs/promises";
|
|
6
|
+
import { dirname as dirname2, join } from "path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
8
|
import { Command } from "commander";
|
|
8
9
|
import chalk from "chalk";
|
|
10
|
+
import open from "open";
|
|
11
|
+
|
|
12
|
+
// src/i18n.ts
|
|
13
|
+
var SUPPORTED_LANGS = ["zh", "en"];
|
|
14
|
+
var DEFAULT_LANG = "zh";
|
|
15
|
+
var zhMessages = {
|
|
16
|
+
defaultPrompt: "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587",
|
|
17
|
+
appTitle: "\u{1F680} Token \u901F\u5EA6\u6D4B\u8BD5\u5DE5\u5177",
|
|
18
|
+
runningTests: "\u23F3 \u6B63\u5728\u8FD0\u884C\u6D4B\u8BD5...",
|
|
19
|
+
streamingOutput: "\u6A21\u578B\u8F93\u51FA (\u6D41\u5F0F):",
|
|
20
|
+
testComplete: "\u2705 \u6D4B\u8BD5\u5B8C\u6210!",
|
|
21
|
+
errorPrefix: "\u274C \u9519\u8BEF",
|
|
22
|
+
unknownError: "\u274C \u53D1\u751F\u672A\u77E5\u9519\u8BEF",
|
|
23
|
+
configLabels: {
|
|
24
|
+
provider: "Provider",
|
|
25
|
+
model: "Model",
|
|
26
|
+
maxTokens: "Max Tokens",
|
|
27
|
+
runs: "Runs",
|
|
28
|
+
prompt: "Prompt"
|
|
29
|
+
},
|
|
30
|
+
runLabel: (index) => `[\u8FD0\u884C ${index}]`,
|
|
31
|
+
runProgressLabel: (current, total) => `[\u8FD0\u884C ${current}/${total}]`,
|
|
32
|
+
reportTitle: "Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A",
|
|
33
|
+
speedChartTitle: "Token \u901F\u5EA6\u8D8B\u52BF\u56FE (TPS)",
|
|
34
|
+
tpsHistogramTitle: "TPS \u5206\u5E03",
|
|
35
|
+
noChartData: "\u6CA1\u6709\u53EF\u7528\u4E8E\u56FE\u8868\u7684\u6570\u636E",
|
|
36
|
+
noTpsData: "\u6CA1\u6709 TPS \u6570\u636E\u53EF\u7528",
|
|
37
|
+
statsSummaryTitle: (sampleSize) => `\u7EDF\u8BA1\u6C47\u603B (N=${sampleSize})`,
|
|
38
|
+
statsHeaders: {
|
|
39
|
+
metric: "\u6307\u6807",
|
|
40
|
+
mean: "\u5747\u503C",
|
|
41
|
+
min: "\u6700\u5C0F\u503C",
|
|
42
|
+
max: "\u6700\u5927\u503C",
|
|
43
|
+
stdDev: "\u6807\u51C6\u5DEE"
|
|
44
|
+
},
|
|
45
|
+
statsLabels: {
|
|
46
|
+
ttft: "TTFT (ms)",
|
|
47
|
+
totalTime: "\u603B\u8017\u65F6 (ms)",
|
|
48
|
+
totalTokens: "\u603B Token \u6570",
|
|
49
|
+
averageSpeed: "\u5E73\u5747\u901F\u5EA6",
|
|
50
|
+
peakSpeed: "\u5CF0\u503C\u901F\u5EA6",
|
|
51
|
+
peakTps: "\u5CF0\u503C TPS"
|
|
52
|
+
},
|
|
53
|
+
resultLabels: {
|
|
54
|
+
ttft: "TTFT",
|
|
55
|
+
totalTime: "\u603B\u8017\u65F6",
|
|
56
|
+
totalTokens: "\u603B Token \u6570",
|
|
57
|
+
averageSpeed: "\u5E73\u5747\u901F\u5EA6",
|
|
58
|
+
peakSpeed: "\u5CF0\u503C\u901F\u5EA6",
|
|
59
|
+
peakTps: "\u5CF0\u503C TPS"
|
|
60
|
+
},
|
|
61
|
+
htmlTitle: "Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A",
|
|
62
|
+
htmlReportTitle: "LLM API Token \u6D41\u5F0F\u6027\u80FD\u6D4B\u8BD5\u62A5\u544A",
|
|
63
|
+
htmlGenerated: (file) => `\u2713 HTML \u62A5\u544A\u5DF2\u751F\u6210: ${file}`,
|
|
64
|
+
htmlOpenError: (file) => `\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u62A5\u544A\uFF0C\u8BF7\u624B\u52A8\u6253\u5F00: ${file}`,
|
|
65
|
+
htmlConfigSection: "\u6D4B\u8BD5\u914D\u7F6E",
|
|
66
|
+
htmlSummarySection: "\u7EDF\u8BA1\u6C47\u603B",
|
|
67
|
+
htmlChartsSection: "\u56FE\u8868\u5206\u6790",
|
|
68
|
+
htmlDetailsSection: "\u8BE6\u7EC6\u6570\u636E",
|
|
69
|
+
htmlTestTime: "\u6D4B\u8BD5\u65F6\u95F4",
|
|
70
|
+
htmlMetric: "\u6307\u6807",
|
|
71
|
+
htmlValue: "\u6570\u503C",
|
|
72
|
+
htmlRun: "\u8FD0\u884C",
|
|
73
|
+
htmlTokens: "Token \u6570",
|
|
74
|
+
htmlDuration: "\u8017\u65F6",
|
|
75
|
+
htmlSpeed: "\u901F\u5EA6",
|
|
76
|
+
htmlTps: "TPS",
|
|
77
|
+
htmlAverageTps: "\u5E73\u5747 TPS",
|
|
78
|
+
htmlTpsDistribution: "TPS \u5206\u5E03",
|
|
79
|
+
htmlTimeUnit: "\u79D2",
|
|
80
|
+
htmlTpsChartHover: "\u6B21"
|
|
81
|
+
};
|
|
82
|
+
var enMessages = {
|
|
83
|
+
defaultPrompt: "Write a short essay about AI",
|
|
84
|
+
appTitle: "\u{1F680} Token Speed Test",
|
|
85
|
+
runningTests: "\u23F3 Running tests...",
|
|
86
|
+
streamingOutput: "Model output (streaming):",
|
|
87
|
+
testComplete: "\u2705 Tests complete!",
|
|
88
|
+
errorPrefix: "\u274C Error",
|
|
89
|
+
unknownError: "\u274C An unknown error occurred",
|
|
90
|
+
configLabels: {
|
|
91
|
+
provider: "Provider",
|
|
92
|
+
model: "Model",
|
|
93
|
+
maxTokens: "Max Tokens",
|
|
94
|
+
runs: "Runs",
|
|
95
|
+
prompt: "Prompt"
|
|
96
|
+
},
|
|
97
|
+
runLabel: (index) => `[Run ${index}]`,
|
|
98
|
+
runProgressLabel: (current, total) => `[Run ${current}/${total}]`,
|
|
99
|
+
reportTitle: "Token Speed Test Report",
|
|
100
|
+
speedChartTitle: "Token Speed Trend (TPS)",
|
|
101
|
+
tpsHistogramTitle: "TPS Distribution",
|
|
102
|
+
noChartData: "No data available for chart",
|
|
103
|
+
noTpsData: "No TPS data available",
|
|
104
|
+
statsSummaryTitle: (sampleSize) => `Summary (N=${sampleSize})`,
|
|
105
|
+
statsHeaders: {
|
|
106
|
+
metric: "Metric",
|
|
107
|
+
mean: "Mean",
|
|
108
|
+
min: "Min",
|
|
109
|
+
max: "Max",
|
|
110
|
+
stdDev: "Std Dev"
|
|
111
|
+
},
|
|
112
|
+
statsLabels: {
|
|
113
|
+
ttft: "TTFT (ms)",
|
|
114
|
+
totalTime: "Total Time (ms)",
|
|
115
|
+
totalTokens: "Total Tokens",
|
|
116
|
+
averageSpeed: "Avg Speed",
|
|
117
|
+
peakSpeed: "Peak Speed",
|
|
118
|
+
peakTps: "Peak TPS"
|
|
119
|
+
},
|
|
120
|
+
resultLabels: {
|
|
121
|
+
ttft: "TTFT",
|
|
122
|
+
totalTime: "Total Time",
|
|
123
|
+
totalTokens: "Total Tokens",
|
|
124
|
+
averageSpeed: "Avg Speed",
|
|
125
|
+
peakSpeed: "Peak Speed",
|
|
126
|
+
peakTps: "Peak TPS"
|
|
127
|
+
},
|
|
128
|
+
htmlTitle: "Token Speed Test Report",
|
|
129
|
+
htmlReportTitle: "LLM API Token Streaming Performance Report",
|
|
130
|
+
htmlGenerated: (file) => `\u2713 HTML report generated: ${file}`,
|
|
131
|
+
htmlOpenError: (file) => `Could not auto-open report, please open manually: ${file}`,
|
|
132
|
+
htmlConfigSection: "Test Configuration",
|
|
133
|
+
htmlSummarySection: "Summary Statistics",
|
|
134
|
+
htmlChartsSection: "Chart Analysis",
|
|
135
|
+
htmlDetailsSection: "Detailed Data",
|
|
136
|
+
htmlTestTime: "Test Time",
|
|
137
|
+
htmlMetric: "Metric",
|
|
138
|
+
htmlValue: "Value",
|
|
139
|
+
htmlRun: "Run",
|
|
140
|
+
htmlTokens: "Tokens",
|
|
141
|
+
htmlDuration: "Duration",
|
|
142
|
+
htmlSpeed: "Speed",
|
|
143
|
+
htmlTps: "TPS",
|
|
144
|
+
htmlAverageTps: "Average TPS",
|
|
145
|
+
htmlTpsDistribution: "TPS Distribution",
|
|
146
|
+
htmlTimeUnit: "s",
|
|
147
|
+
htmlTpsChartHover: "count"
|
|
148
|
+
};
|
|
149
|
+
function isSupportedLang(value) {
|
|
150
|
+
return SUPPORTED_LANGS.includes(value);
|
|
151
|
+
}
|
|
152
|
+
function resolveLang(value) {
|
|
153
|
+
if (!value) {
|
|
154
|
+
return DEFAULT_LANG;
|
|
155
|
+
}
|
|
156
|
+
const normalized = value.toLowerCase();
|
|
157
|
+
if (!isSupportedLang(normalized)) {
|
|
158
|
+
throw new Error(`Invalid lang: ${value}. Must be 'zh' or 'en'.`);
|
|
159
|
+
}
|
|
160
|
+
return normalized;
|
|
161
|
+
}
|
|
162
|
+
function getMessages(lang) {
|
|
163
|
+
return lang === "en" ? enMessages : zhMessages;
|
|
164
|
+
}
|
|
9
165
|
|
|
10
166
|
// src/config.ts
|
|
11
167
|
var DEFAULT_MODELS = {
|
|
@@ -14,7 +170,6 @@ var DEFAULT_MODELS = {
|
|
|
14
170
|
};
|
|
15
171
|
var DEFAULT_MAX_TOKENS = 1024;
|
|
16
172
|
var DEFAULT_RUNS = 3;
|
|
17
|
-
var DEFAULT_PROMPT = "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587";
|
|
18
173
|
function parseConfig(args) {
|
|
19
174
|
const {
|
|
20
175
|
apiKey,
|
|
@@ -23,8 +178,12 @@ function parseConfig(args) {
|
|
|
23
178
|
model,
|
|
24
179
|
maxTokens = DEFAULT_MAX_TOKENS,
|
|
25
180
|
runs = DEFAULT_RUNS,
|
|
26
|
-
prompt
|
|
181
|
+
prompt,
|
|
182
|
+
lang: langInput
|
|
27
183
|
} = args;
|
|
184
|
+
const lang = resolveLang(langInput);
|
|
185
|
+
const messages = getMessages(lang);
|
|
186
|
+
const finalPrompt = prompt ?? messages.defaultPrompt;
|
|
28
187
|
if (!apiKey || apiKey.trim() === "") {
|
|
29
188
|
throw new Error("API Key is required. Use --api-key or -k to provide it.");
|
|
30
189
|
}
|
|
@@ -45,7 +204,8 @@ function parseConfig(args) {
|
|
|
45
204
|
model: finalModel,
|
|
46
205
|
maxTokens,
|
|
47
206
|
runCount: runs,
|
|
48
|
-
prompt:
|
|
207
|
+
prompt: finalPrompt.trim(),
|
|
208
|
+
lang
|
|
49
209
|
};
|
|
50
210
|
}
|
|
51
211
|
|
|
@@ -202,10 +362,11 @@ async function streamTest(config) {
|
|
|
202
362
|
}
|
|
203
363
|
async function runMultipleTests(config) {
|
|
204
364
|
const results = [];
|
|
365
|
+
const messages = getMessages(config.lang);
|
|
205
366
|
for (let i = 0; i < config.runCount; i++) {
|
|
206
367
|
if (config.runCount > 1) {
|
|
207
368
|
const label = `
|
|
208
|
-
|
|
369
|
+
${messages.runProgressLabel(i + 1, config.runCount)}`;
|
|
209
370
|
console.log(label);
|
|
210
371
|
console.log("-".repeat(label.length - 1));
|
|
211
372
|
}
|
|
@@ -366,9 +527,10 @@ function padStartWidth(text, width) {
|
|
|
366
527
|
}
|
|
367
528
|
return " ".repeat(width - currentWidth) + text;
|
|
368
529
|
}
|
|
369
|
-
function renderSpeedChart(tps, maxSpeed) {
|
|
530
|
+
function renderSpeedChart(tps, maxSpeed, lang = DEFAULT_LANG) {
|
|
531
|
+
const messages = getMessages(lang);
|
|
370
532
|
if (tps.length === 0) {
|
|
371
|
-
return
|
|
533
|
+
return messages.noChartData;
|
|
372
534
|
}
|
|
373
535
|
const actualMax = maxSpeed ?? Math.max(...tps, 1);
|
|
374
536
|
const maxVal = Math.max(actualMax, 1);
|
|
@@ -377,7 +539,7 @@ function renderSpeedChart(tps, maxSpeed) {
|
|
|
377
539
|
const chartWidth = stringWidth(emptyRow) - 2;
|
|
378
540
|
const axisPrefix = `\u2502 ${padStartWidth("", Y_LABEL_WIDTH)} \u253C`;
|
|
379
541
|
const lines = [];
|
|
380
|
-
lines.push(
|
|
542
|
+
lines.push(messages.speedChartTitle);
|
|
381
543
|
lines.push("\u250C" + "\u2500".repeat(chartWidth) + "\u2510");
|
|
382
544
|
for (let row = CHART_HEIGHT - 1; row >= 0; row--) {
|
|
383
545
|
const value = row / (CHART_HEIGHT - 1) * maxVal;
|
|
@@ -423,12 +585,13 @@ function generateXLabels(dataPoints, maxLabels) {
|
|
|
423
585
|
}
|
|
424
586
|
return labels;
|
|
425
587
|
}
|
|
426
|
-
function renderTPSHistogram(tps) {
|
|
588
|
+
function renderTPSHistogram(tps, lang = DEFAULT_LANG) {
|
|
589
|
+
const messages = getMessages(lang);
|
|
427
590
|
if (tps.length === 0) {
|
|
428
|
-
return
|
|
591
|
+
return messages.noTpsData;
|
|
429
592
|
}
|
|
430
593
|
const lines = [];
|
|
431
|
-
lines.push(
|
|
594
|
+
lines.push(messages.tpsHistogramTitle);
|
|
432
595
|
const maxTps = Math.max(...tps, 1);
|
|
433
596
|
const buckets = 10;
|
|
434
597
|
const bucketSize = maxTps / buckets;
|
|
@@ -454,18 +617,19 @@ function renderTPSHistogram(tps) {
|
|
|
454
617
|
}
|
|
455
618
|
return lines.join("\n");
|
|
456
619
|
}
|
|
457
|
-
function renderStatsTable(stats) {
|
|
620
|
+
function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
621
|
+
const messages = getMessages(lang);
|
|
458
622
|
const lines = [];
|
|
459
623
|
lines.push("");
|
|
460
|
-
lines.push(
|
|
461
|
-
const headerRow = "\u2502 " + padEndWidth(
|
|
624
|
+
lines.push(messages.statsSummaryTitle(stats.sampleSize));
|
|
625
|
+
const headerRow = "\u2502 " + padEndWidth(messages.statsHeaders.metric, STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.mean, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.min, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.max, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.stdDev, STAT_VALUE_WIDTH) + " \u2502";
|
|
462
626
|
const tableWidth = stringWidth(headerRow) - 2;
|
|
463
627
|
lines.push("\u250C" + "\u2500".repeat(tableWidth) + "\u2510");
|
|
464
628
|
lines.push(headerRow);
|
|
465
629
|
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
466
630
|
lines.push(
|
|
467
631
|
formatStatRow(
|
|
468
|
-
|
|
632
|
+
messages.statsLabels.ttft,
|
|
469
633
|
stats.mean.ttft,
|
|
470
634
|
stats.min.ttft,
|
|
471
635
|
stats.max.ttft,
|
|
@@ -476,7 +640,7 @@ function renderStatsTable(stats) {
|
|
|
476
640
|
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
477
641
|
lines.push(
|
|
478
642
|
formatStatRow(
|
|
479
|
-
|
|
643
|
+
messages.statsLabels.totalTime,
|
|
480
644
|
stats.mean.totalTime,
|
|
481
645
|
stats.min.totalTime,
|
|
482
646
|
stats.max.totalTime,
|
|
@@ -487,7 +651,7 @@ function renderStatsTable(stats) {
|
|
|
487
651
|
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
488
652
|
lines.push(
|
|
489
653
|
formatStatRow(
|
|
490
|
-
|
|
654
|
+
messages.statsLabels.totalTokens,
|
|
491
655
|
stats.mean.totalTokens,
|
|
492
656
|
stats.min.totalTokens,
|
|
493
657
|
stats.max.totalTokens,
|
|
@@ -498,7 +662,7 @@ function renderStatsTable(stats) {
|
|
|
498
662
|
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
499
663
|
lines.push(
|
|
500
664
|
formatStatRow(
|
|
501
|
-
|
|
665
|
+
messages.statsLabels.averageSpeed,
|
|
502
666
|
stats.mean.averageSpeed,
|
|
503
667
|
stats.min.averageSpeed,
|
|
504
668
|
stats.max.averageSpeed,
|
|
@@ -509,7 +673,7 @@ function renderStatsTable(stats) {
|
|
|
509
673
|
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
510
674
|
lines.push(
|
|
511
675
|
formatStatRow(
|
|
512
|
-
|
|
676
|
+
messages.statsLabels.peakSpeed,
|
|
513
677
|
stats.mean.peakSpeed,
|
|
514
678
|
stats.min.peakSpeed,
|
|
515
679
|
stats.max.peakSpeed,
|
|
@@ -520,7 +684,7 @@ function renderStatsTable(stats) {
|
|
|
520
684
|
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
521
685
|
lines.push(
|
|
522
686
|
formatStatRow(
|
|
523
|
-
|
|
687
|
+
messages.statsLabels.peakTps,
|
|
524
688
|
stats.mean.peakTps,
|
|
525
689
|
stats.min.peakTps,
|
|
526
690
|
stats.max.peakTps,
|
|
@@ -541,39 +705,531 @@ function formatTimeWithDecimals(ms) {
|
|
|
541
705
|
}
|
|
542
706
|
return `${ms.toFixed(2)}ms`;
|
|
543
707
|
}
|
|
544
|
-
function renderSingleResult(metrics, runIndex) {
|
|
708
|
+
function renderSingleResult(metrics, runIndex, lang = DEFAULT_LANG) {
|
|
709
|
+
const messages = getMessages(lang);
|
|
545
710
|
const lines = [];
|
|
546
711
|
lines.push(`
|
|
547
|
-
|
|
548
|
-
lines.push(`
|
|
549
|
-
lines.push(`
|
|
550
|
-
lines.push(`
|
|
551
|
-
lines.push(
|
|
552
|
-
|
|
553
|
-
|
|
712
|
+
${messages.runLabel(runIndex + 1)}`);
|
|
713
|
+
lines.push(` ${messages.resultLabels.ttft}: ${formatTimeWithDecimals(metrics.ttft)}`);
|
|
714
|
+
lines.push(` ${messages.resultLabels.totalTime}: ${formatTimeWithDecimals(metrics.totalTime)}`);
|
|
715
|
+
lines.push(` ${messages.resultLabels.totalTokens}: ${metrics.totalTokens}`);
|
|
716
|
+
lines.push(
|
|
717
|
+
` ${messages.resultLabels.averageSpeed}: ${metrics.averageSpeed.toFixed(2)} tokens/s`
|
|
718
|
+
);
|
|
719
|
+
lines.push(` ${messages.resultLabels.peakSpeed}: ${metrics.peakSpeed.toFixed(2)} tokens/s`);
|
|
720
|
+
lines.push(` ${messages.resultLabels.peakTps}: ${metrics.peakTps.toFixed(2)} tokens/s`);
|
|
554
721
|
return lines.join("\n");
|
|
555
722
|
}
|
|
556
|
-
function renderReport(stats) {
|
|
723
|
+
function renderReport(stats, lang = DEFAULT_LANG) {
|
|
724
|
+
const messages = getMessages(lang);
|
|
557
725
|
const lines = [];
|
|
558
726
|
lines.push("\n" + "\u2550".repeat(72));
|
|
559
|
-
lines.push(
|
|
727
|
+
lines.push(messages.reportTitle);
|
|
560
728
|
lines.push("\u2550".repeat(72));
|
|
561
|
-
lines.push(renderStatsTable(stats));
|
|
729
|
+
lines.push(renderStatsTable(stats, lang));
|
|
562
730
|
if (stats.mean.tps.length > 0) {
|
|
563
|
-
lines.push("\n" + renderSpeedChart(stats.mean.tps));
|
|
731
|
+
lines.push("\n" + renderSpeedChart(stats.mean.tps, void 0, lang));
|
|
564
732
|
}
|
|
565
733
|
if (stats.mean.tps.length > 0) {
|
|
566
|
-
lines.push("\n" + renderTPSHistogram(stats.mean.tps));
|
|
734
|
+
lines.push("\n" + renderTPSHistogram(stats.mean.tps, lang));
|
|
567
735
|
}
|
|
568
736
|
return lines.join("\n");
|
|
569
737
|
}
|
|
570
738
|
|
|
739
|
+
// src/html-report.ts
|
|
740
|
+
import { readFileSync } from "fs";
|
|
741
|
+
import { dirname, resolve } from "path";
|
|
742
|
+
import { fileURLToPath } from "url";
|
|
743
|
+
var CHART_COLORS = [
|
|
744
|
+
"#00f5ff",
|
|
745
|
+
// cyan
|
|
746
|
+
"#ff00aa",
|
|
747
|
+
// magenta
|
|
748
|
+
"#ffcc00",
|
|
749
|
+
// yellow
|
|
750
|
+
"#00ff88",
|
|
751
|
+
// green
|
|
752
|
+
"#ff6600",
|
|
753
|
+
// orange
|
|
754
|
+
"#aa00ff"
|
|
755
|
+
// purple
|
|
756
|
+
];
|
|
757
|
+
var PALETTE = {
|
|
758
|
+
bg: "#0a0a0f",
|
|
759
|
+
bgSecondary: "#12121a",
|
|
760
|
+
bgCard: "#1a1a24",
|
|
761
|
+
border: "#2a2a3a",
|
|
762
|
+
text: "#e4e4eb",
|
|
763
|
+
textMuted: "#6a6a7a",
|
|
764
|
+
accent: "#00f5ff",
|
|
765
|
+
accentSecondary: "#ff00aa",
|
|
766
|
+
accentTertiary: "#ffcc00"
|
|
767
|
+
};
|
|
768
|
+
function formatNumber(num, decimals = 2) {
|
|
769
|
+
return num.toFixed(decimals);
|
|
770
|
+
}
|
|
771
|
+
function formatTime(ms) {
|
|
772
|
+
if (ms < 1e3) {
|
|
773
|
+
return `${ms.toFixed(0)}ms`;
|
|
774
|
+
}
|
|
775
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
776
|
+
}
|
|
777
|
+
function escapeHtml(text) {
|
|
778
|
+
const map = {
|
|
779
|
+
"&": "&",
|
|
780
|
+
"<": "<",
|
|
781
|
+
">": ">",
|
|
782
|
+
'"': """,
|
|
783
|
+
"'": "'"
|
|
784
|
+
};
|
|
785
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
786
|
+
}
|
|
787
|
+
function loadTemplate() {
|
|
788
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
789
|
+
const __dirname = dirname(__filename);
|
|
790
|
+
return readFileSync(resolve(__dirname, "template.html"), "utf-8");
|
|
791
|
+
}
|
|
792
|
+
function replaceTemplate(template, data) {
|
|
793
|
+
let result = template;
|
|
794
|
+
for (const [key, value] of Object.entries(data)) {
|
|
795
|
+
result = result.replaceAll(new RegExp(`{{${key}}}`, "g"), value);
|
|
796
|
+
}
|
|
797
|
+
return result;
|
|
798
|
+
}
|
|
799
|
+
function generateSpeedChart(results, messages) {
|
|
800
|
+
const allTps = results.flatMap((r) => r.tps);
|
|
801
|
+
if (allTps.length === 0) {
|
|
802
|
+
return `<div class="no-data">${messages.noChartData || "No data available"}</div>`;
|
|
803
|
+
}
|
|
804
|
+
const maxTps = Math.max(...allTps, 1);
|
|
805
|
+
const maxDuration = Math.max(...results.map((r) => r.tps.length));
|
|
806
|
+
const width = 800;
|
|
807
|
+
const height = 320;
|
|
808
|
+
const padding = { top: 30, right: 30, bottom: 45, left: 55 };
|
|
809
|
+
const chartWidth = width - padding.left - padding.right;
|
|
810
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
811
|
+
const avgTps = [];
|
|
812
|
+
for (let i = 0; i < maxDuration; i++) {
|
|
813
|
+
const values = results.map((r) => r.tps[i] ?? 0);
|
|
814
|
+
avgTps.push(values.reduce((a, b) => a + b, 0) / values.length);
|
|
815
|
+
}
|
|
816
|
+
const polylines = results.map((result, idx) => {
|
|
817
|
+
const color = CHART_COLORS[idx % CHART_COLORS.length];
|
|
818
|
+
let points = "";
|
|
819
|
+
let areaPoints = `${padding.left},${height - padding.bottom} `;
|
|
820
|
+
result.tps.forEach((tps, i) => {
|
|
821
|
+
const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
|
|
822
|
+
const y = padding.top + chartHeight - tps / maxTps * chartHeight;
|
|
823
|
+
points += `${x},${y} `;
|
|
824
|
+
areaPoints += `${x},${y} `;
|
|
825
|
+
});
|
|
826
|
+
areaPoints += `${padding.left + chartWidth},${height - padding.bottom}`;
|
|
827
|
+
return `
|
|
828
|
+
<defs>
|
|
829
|
+
<linearGradient id="grad-${idx}" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
830
|
+
<stop offset="0%" style="stop-color:${color};stop-opacity:0.3"/>
|
|
831
|
+
<stop offset="100%" style="stop-color:${color};stop-opacity:0"/>
|
|
832
|
+
</linearGradient>
|
|
833
|
+
</defs>
|
|
834
|
+
<polygon
|
|
835
|
+
points="${areaPoints.trim()}"
|
|
836
|
+
fill="url(#grad-${idx})"
|
|
837
|
+
class="area-${idx}"
|
|
838
|
+
/>
|
|
839
|
+
<polyline
|
|
840
|
+
fill="none"
|
|
841
|
+
stroke="${color}"
|
|
842
|
+
stroke-width="2.5"
|
|
843
|
+
points="${points.trim()}"
|
|
844
|
+
class="line line-${idx}"
|
|
845
|
+
data-run="${idx + 1}"
|
|
846
|
+
>
|
|
847
|
+
<animate
|
|
848
|
+
attributeName="stroke-dasharray"
|
|
849
|
+
from="0,2000"
|
|
850
|
+
to="2000,0"
|
|
851
|
+
dur="1.5s"
|
|
852
|
+
fill="freeze"
|
|
853
|
+
calcMode="spline"
|
|
854
|
+
keySplines="0.4 0 0.2 1"
|
|
855
|
+
/>
|
|
856
|
+
</polyline>
|
|
857
|
+
${result.tps.map((tps, i) => {
|
|
858
|
+
const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
|
|
859
|
+
const y = padding.top + chartHeight - tps / maxTps * chartHeight;
|
|
860
|
+
return `<circle cx="${x}" cy="${y}" r="4" fill="${PALETTE.bg}" stroke="${color}" stroke-width="2" class="dot-${idx}" opacity="0"><title>${messages.htmlAverageTps || "Avg TPS"} ${i}s: ${tps.toFixed(1)}</title>
|
|
861
|
+
<animate attributeName="opacity" from="0" to="1" begin="${0.5 + i * 0.05}s" dur="0.3s" fill="freeze"/>
|
|
862
|
+
</circle>`;
|
|
863
|
+
}).join("")}
|
|
864
|
+
`;
|
|
865
|
+
}).join("\n ");
|
|
866
|
+
let avgPoints = "";
|
|
867
|
+
avgTps.forEach((tps, i) => {
|
|
868
|
+
const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
|
|
869
|
+
const y = padding.top + chartHeight - tps / maxTps * chartHeight;
|
|
870
|
+
avgPoints += `${x},${y} `;
|
|
871
|
+
});
|
|
872
|
+
const yLabels = [];
|
|
873
|
+
for (let i = 0; i <= 5; i++) {
|
|
874
|
+
const value = Math.round(maxTps * i / 5);
|
|
875
|
+
const y = padding.top + chartHeight - i / 5 * chartHeight;
|
|
876
|
+
yLabels.push(
|
|
877
|
+
`<text x="${padding.left - 12}" y="${y + 4}" text-anchor="end" font-size="11" fill="${PALETTE.textMuted}">${value}</text>`
|
|
878
|
+
);
|
|
879
|
+
if (i > 0) {
|
|
880
|
+
const yLine = padding.top + chartHeight - i / 5 * chartHeight;
|
|
881
|
+
yLabels.push(
|
|
882
|
+
`<line x1="${padding.left}" y1="${yLine}" x2="${width - padding.right}" y2="${yLine}" stroke="${PALETTE.border}" stroke-width="1" opacity="0.5"/>`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const xLabels = [];
|
|
887
|
+
const xSteps = Math.min(maxDuration, 10);
|
|
888
|
+
for (let i = 0; i < xSteps; i++) {
|
|
889
|
+
const x = padding.left + i / Math.max(xSteps - 1, 1) * chartWidth;
|
|
890
|
+
const label = i.toString();
|
|
891
|
+
xLabels.push(
|
|
892
|
+
`<text x="${x}" y="${height - padding.bottom + 20}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">${label}${messages.htmlTimeUnit}</text>`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
return `
|
|
896
|
+
<svg viewBox="0 0 ${width} ${height}" class="chart" id="speedChart">
|
|
897
|
+
<style>
|
|
898
|
+
#speedChart .line { stroke-dasharray: 2000; stroke-dashoffset: 0; }
|
|
899
|
+
#speedChart .line:hover { stroke-width: 4; filter: drop-shadow(0 0 8px currentColor); }
|
|
900
|
+
#speedChart circle { transition: all 0.2s ease; cursor: pointer; }
|
|
901
|
+
#speedChart circle:hover { r: 6; stroke-width: 3; }
|
|
902
|
+
</style>
|
|
903
|
+
<rect x="${padding.left}" y="${padding.top}" width="${chartWidth}" height="${chartHeight}" fill="${PALETTE.bgCard}" rx="4"/>
|
|
904
|
+
${yLabels.join("\n ")}
|
|
905
|
+
${xLabels.join("\n ")}
|
|
906
|
+
<line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
|
|
907
|
+
<line x1="${padding.left}" y1="${height - padding.bottom}" x2="${width - padding.right}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
|
|
908
|
+
${polylines}
|
|
909
|
+
<polyline
|
|
910
|
+
fill="none"
|
|
911
|
+
stroke="${PALETTE.text}"
|
|
912
|
+
stroke-width="2"
|
|
913
|
+
stroke-dasharray="6,4"
|
|
914
|
+
opacity="0.7"
|
|
915
|
+
points="${avgPoints.trim()}"
|
|
916
|
+
/>
|
|
917
|
+
<text x="${padding.left + chartWidth / 2}" y="${height - 8}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">TIME (${messages.htmlTimeUnit})</text>
|
|
918
|
+
<text x="12" y="${padding.top + chartHeight / 2}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}" transform="rotate(-90, 12, ${padding.top + chartHeight / 2})">TPS</text>
|
|
919
|
+
</svg>
|
|
920
|
+
<div class="chart-legend">
|
|
921
|
+
<div class="legend-item">
|
|
922
|
+
<span class="legend-line avg"></span>
|
|
923
|
+
<span>${messages.htmlSummarySection}</span>
|
|
924
|
+
</div>
|
|
925
|
+
${results.map((_, idx) => {
|
|
926
|
+
const color = CHART_COLORS[idx % CHART_COLORS.length];
|
|
927
|
+
return `
|
|
928
|
+
<div class="legend-item">
|
|
929
|
+
<span class="legend-line" style="background: ${color};"></span>
|
|
930
|
+
<span>${messages.htmlRun} ${idx + 1}</span>
|
|
931
|
+
</div>`;
|
|
932
|
+
}).join("")}
|
|
933
|
+
</div>
|
|
934
|
+
`;
|
|
935
|
+
}
|
|
936
|
+
function generateTPSHistogram(stats, messages) {
|
|
937
|
+
const allTps = stats.mean.tps;
|
|
938
|
+
if (allTps.length === 0) {
|
|
939
|
+
return `<div class="no-data">${messages.noTpsData || "No TPS data available"}</div>`;
|
|
940
|
+
}
|
|
941
|
+
const maxTps = Math.max(...allTps);
|
|
942
|
+
const width = 400;
|
|
943
|
+
const height = 280;
|
|
944
|
+
const padding = { top: 25, right: 20, bottom: 40, left: 50 };
|
|
945
|
+
const chartWidth = width - padding.left - padding.right;
|
|
946
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
947
|
+
const bars = allTps.map((tps, i) => {
|
|
948
|
+
const barWidth = chartWidth / allTps.length - 2;
|
|
949
|
+
const x = padding.left + i / allTps.length * chartWidth;
|
|
950
|
+
const barHeight = tps / maxTps * chartHeight;
|
|
951
|
+
const y = padding.top + chartHeight - barHeight;
|
|
952
|
+
const hue = 180 + tps / maxTps * 60;
|
|
953
|
+
const color = `hsl(${hue}, 100%, 60%)`;
|
|
954
|
+
return `
|
|
955
|
+
<rect
|
|
956
|
+
x="${x}"
|
|
957
|
+
y="${y}"
|
|
958
|
+
width="${barWidth}"
|
|
959
|
+
height="${barHeight}"
|
|
960
|
+
fill="${color}"
|
|
961
|
+
class="bar"
|
|
962
|
+
data-second="${i}"
|
|
963
|
+
data-tps="${tps.toFixed(2)}"
|
|
964
|
+
rx="2"
|
|
965
|
+
>
|
|
966
|
+
<title>${messages.htmlAverageTps || "Average TPS"} ${i}s: ${tps.toFixed(1)}</title>
|
|
967
|
+
<animate
|
|
968
|
+
attributeName="height"
|
|
969
|
+
from="0"
|
|
970
|
+
to="${barHeight}"
|
|
971
|
+
dur="0.8s"
|
|
972
|
+
fill="freeze"
|
|
973
|
+
calcMode="spline"
|
|
974
|
+
keySplines="0.4 0 0.2 1"
|
|
975
|
+
begin="${i * 0.05}s"
|
|
976
|
+
/>
|
|
977
|
+
<animate
|
|
978
|
+
attributeName="y"
|
|
979
|
+
from="${height - padding.bottom}"
|
|
980
|
+
to="${y}"
|
|
981
|
+
dur="0.8s"
|
|
982
|
+
fill="freeze"
|
|
983
|
+
calcMode="spline"
|
|
984
|
+
keySplines="0.4 0 0.2 1"
|
|
985
|
+
begin="${i * 0.05}s"
|
|
986
|
+
/>
|
|
987
|
+
</rect>
|
|
988
|
+
`;
|
|
989
|
+
}).join("");
|
|
990
|
+
const yLabels = [];
|
|
991
|
+
for (let i = 0; i <= 5; i++) {
|
|
992
|
+
const value = Math.round(maxTps * i / 5);
|
|
993
|
+
const y = padding.top + chartHeight - i / 5 * chartHeight;
|
|
994
|
+
yLabels.push(
|
|
995
|
+
`<text x="${padding.left - 10}" y="${y + 4}" text-anchor="end" font-size="11" fill="${PALETTE.textMuted}">${value}</text>`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const xLabels = [];
|
|
999
|
+
const xSteps = Math.min(allTps.length, 8);
|
|
1000
|
+
for (let i = 0; i < xSteps; i++) {
|
|
1001
|
+
const x = padding.left + i / Math.max(xSteps - 1, 1) * chartWidth;
|
|
1002
|
+
const label = i.toString();
|
|
1003
|
+
xLabels.push(
|
|
1004
|
+
`<text x="${x}" y="${height - padding.bottom + 18}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">${label}${messages.htmlTimeUnit}</text>`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return `
|
|
1008
|
+
<svg viewBox="0 0 ${width} ${height}" class="chart" id="tpsChart">
|
|
1009
|
+
<style>
|
|
1010
|
+
#tpsChart .bar { transition: all 0.2s ease; cursor: pointer; opacity: 0.9; }
|
|
1011
|
+
#tpsChart .bar:hover { opacity: 1; filter: brightness(1.2); }
|
|
1012
|
+
</style>
|
|
1013
|
+
<rect x="${padding.left}" y="${padding.top}" width="${chartWidth}" height="${chartHeight}" fill="${PALETTE.bgCard}" rx="4"/>
|
|
1014
|
+
${yLabels.join("\n ")}
|
|
1015
|
+
${xLabels.join("\n ")}
|
|
1016
|
+
<line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
|
|
1017
|
+
<line x1="${padding.left}" y1="${height - padding.bottom}" x2="${width - padding.right}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
|
|
1018
|
+
${bars}
|
|
1019
|
+
</svg>
|
|
1020
|
+
`;
|
|
1021
|
+
}
|
|
1022
|
+
function generateHTMLReport(options2) {
|
|
1023
|
+
const { config, singleResults, stats, lang, messages } = options2;
|
|
1024
|
+
const isZh = lang === "zh";
|
|
1025
|
+
const testTime = (/* @__PURE__ */ new Date()).toLocaleString(isZh ? "zh-CN" : "en-US");
|
|
1026
|
+
const summaryCards = [
|
|
1027
|
+
{
|
|
1028
|
+
label: messages.statsLabels.ttft,
|
|
1029
|
+
value: formatTime(stats.mean.ttft),
|
|
1030
|
+
detail: `${messages.statsHeaders.min}: ${formatTime(stats.min.ttft)} \xB7 ${messages.statsHeaders.max}: ${formatTime(stats.max.ttft)}`,
|
|
1031
|
+
accent: PALETTE.accent
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
label: messages.statsLabels.averageSpeed,
|
|
1035
|
+
value: formatNumber(stats.mean.averageSpeed),
|
|
1036
|
+
detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.averageSpeed)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.averageSpeed)}`,
|
|
1037
|
+
accent: PALETTE.accentSecondary,
|
|
1038
|
+
unit: messages.htmlSpeed
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
label: messages.statsLabels.peakSpeed,
|
|
1042
|
+
value: formatNumber(stats.mean.peakSpeed),
|
|
1043
|
+
detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.peakSpeed)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.peakSpeed)}`,
|
|
1044
|
+
accent: PALETTE.accentTertiary,
|
|
1045
|
+
unit: messages.htmlSpeed
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
label: messages.statsLabels.totalTokens,
|
|
1049
|
+
value: formatNumber(stats.mean.totalTokens, 0),
|
|
1050
|
+
detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.totalTokens, 0)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.totalTokens, 0)}`,
|
|
1051
|
+
accent: "#00ff88"
|
|
1052
|
+
}
|
|
1053
|
+
];
|
|
1054
|
+
const detailRows = singleResults.map(
|
|
1055
|
+
(result, idx) => `
|
|
1056
|
+
<tr>
|
|
1057
|
+
<td><span class="run-badge">${idx + 1}</span></td>
|
|
1058
|
+
<td>${formatTime(result.ttft)}</td>
|
|
1059
|
+
<td>${formatTime(result.totalTime)}</td>
|
|
1060
|
+
<td>${result.totalTokens}</td>
|
|
1061
|
+
<td>${formatNumber(result.averageSpeed)}</td>
|
|
1062
|
+
<td>${formatNumber(result.peakSpeed)}</td>
|
|
1063
|
+
<td>${result.peakTps}</td>
|
|
1064
|
+
</tr>
|
|
1065
|
+
`
|
|
1066
|
+
).join("");
|
|
1067
|
+
const statsRows = [
|
|
1068
|
+
{
|
|
1069
|
+
metric: messages.statsLabels.ttft,
|
|
1070
|
+
mean: formatTime(stats.mean.ttft),
|
|
1071
|
+
min: formatTime(stats.min.ttft),
|
|
1072
|
+
max: formatTime(stats.max.ttft),
|
|
1073
|
+
stdDev: formatTime(stats.stdDev.ttft)
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
metric: messages.statsLabels.totalTime,
|
|
1077
|
+
mean: formatTime(stats.mean.totalTime),
|
|
1078
|
+
min: formatTime(stats.min.totalTime),
|
|
1079
|
+
max: formatTime(stats.max.totalTime),
|
|
1080
|
+
stdDev: formatTime(stats.stdDev.totalTime)
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
metric: messages.statsLabels.totalTokens,
|
|
1084
|
+
mean: formatNumber(stats.mean.totalTokens, 1),
|
|
1085
|
+
min: formatNumber(stats.min.totalTokens, 0),
|
|
1086
|
+
max: formatNumber(stats.max.totalTokens, 0),
|
|
1087
|
+
stdDev: formatNumber(stats.stdDev.totalTokens, 1)
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
metric: messages.statsLabels.averageSpeed,
|
|
1091
|
+
mean: formatNumber(stats.mean.averageSpeed),
|
|
1092
|
+
min: formatNumber(stats.min.averageSpeed),
|
|
1093
|
+
max: formatNumber(stats.max.averageSpeed),
|
|
1094
|
+
stdDev: formatNumber(stats.stdDev.averageSpeed)
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
metric: messages.statsLabels.peakSpeed,
|
|
1098
|
+
mean: formatNumber(stats.mean.peakSpeed),
|
|
1099
|
+
min: formatNumber(stats.min.peakSpeed),
|
|
1100
|
+
max: formatNumber(stats.max.peakSpeed),
|
|
1101
|
+
stdDev: formatNumber(stats.stdDev.peakSpeed)
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
metric: messages.statsLabels.peakTps,
|
|
1105
|
+
mean: formatNumber(stats.mean.peakTps),
|
|
1106
|
+
min: formatNumber(stats.min.peakTps),
|
|
1107
|
+
max: formatNumber(stats.max.peakTps),
|
|
1108
|
+
stdDev: formatNumber(stats.stdDev.peakTps)
|
|
1109
|
+
}
|
|
1110
|
+
].map(
|
|
1111
|
+
(row) => `
|
|
1112
|
+
<tr>
|
|
1113
|
+
<td class="metric-name">${row.metric}</td>
|
|
1114
|
+
<td class="value-primary">${row.mean}</td>
|
|
1115
|
+
<td>${row.min}</td>
|
|
1116
|
+
<td>${row.max}</td>
|
|
1117
|
+
<td>${row.stdDev}</td>
|
|
1118
|
+
</tr>
|
|
1119
|
+
`
|
|
1120
|
+
).join("");
|
|
1121
|
+
const speedChart = generateSpeedChart(singleResults, messages);
|
|
1122
|
+
const tpsChart = generateTPSHistogram(stats, messages);
|
|
1123
|
+
const configGridHtml = `
|
|
1124
|
+
<div class="config-grid">
|
|
1125
|
+
<div class="config-item">
|
|
1126
|
+
<span class="config-label">${messages.configLabels.provider}</span>
|
|
1127
|
+
<span class="config-value">${config.provider.toUpperCase()}</span>
|
|
1128
|
+
</div>
|
|
1129
|
+
<div class="config-item">
|
|
1130
|
+
<span class="config-label">${messages.configLabels.model}</span>
|
|
1131
|
+
<span class="config-value">${escapeHtml(config.model)}</span>
|
|
1132
|
+
</div>
|
|
1133
|
+
<div class="config-item">
|
|
1134
|
+
<span class="config-label">${messages.configLabels.maxTokens}</span>
|
|
1135
|
+
<span class="config-value">${config.maxTokens}</span>
|
|
1136
|
+
</div>
|
|
1137
|
+
<div class="config-item">
|
|
1138
|
+
<span class="config-label">${messages.configLabels.runs}</span>
|
|
1139
|
+
<span class="config-value">${config.runCount}</span>
|
|
1140
|
+
</div>
|
|
1141
|
+
<div class="config-item wide">
|
|
1142
|
+
<span class="config-label">${messages.configLabels.prompt}</span>
|
|
1143
|
+
<span class="config-value">"${escapeHtml(config.prompt)}"</span>
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
`;
|
|
1147
|
+
const summaryCardsHtml = summaryCards.map(
|
|
1148
|
+
(card) => `
|
|
1149
|
+
<div class="card" style="--card-accent: ${card.accent}">
|
|
1150
|
+
<div class="card-label">${card.label}</div>
|
|
1151
|
+
<div class="card-value">${card.value}<span class="card-unit">${card.unit || ""}</span></div>
|
|
1152
|
+
<div class="card-detail">${card.detail}</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
`
|
|
1155
|
+
).join("");
|
|
1156
|
+
const chartsHtml = `
|
|
1157
|
+
<div class="charts-container">
|
|
1158
|
+
<div class="chart-wrapper">
|
|
1159
|
+
<div class="chart-title">${messages.speedChartTitle}</div>
|
|
1160
|
+
${speedChart}
|
|
1161
|
+
</div>
|
|
1162
|
+
<div class="chart-wrapper">
|
|
1163
|
+
<div class="chart-title">${messages.htmlTpsDistribution}</div>
|
|
1164
|
+
${tpsChart}
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
`;
|
|
1168
|
+
const statsTableHtml = `
|
|
1169
|
+
<div class="table-wrapper">
|
|
1170
|
+
<table>
|
|
1171
|
+
<thead>
|
|
1172
|
+
<tr>
|
|
1173
|
+
<th>${messages.statsHeaders.metric}</th>
|
|
1174
|
+
<th>${messages.statsHeaders.mean}</th>
|
|
1175
|
+
<th>${messages.statsHeaders.min}</th>
|
|
1176
|
+
<th>${messages.statsHeaders.max}</th>
|
|
1177
|
+
<th>${messages.statsHeaders.stdDev}</th>
|
|
1178
|
+
</tr>
|
|
1179
|
+
</thead>
|
|
1180
|
+
<tbody>
|
|
1181
|
+
${statsRows}
|
|
1182
|
+
</tbody>
|
|
1183
|
+
</table>
|
|
1184
|
+
</div>
|
|
1185
|
+
`;
|
|
1186
|
+
const detailsTableHtml = `
|
|
1187
|
+
<div class="table-wrapper">
|
|
1188
|
+
<table>
|
|
1189
|
+
<thead>
|
|
1190
|
+
<tr>
|
|
1191
|
+
<th>${messages.htmlRun}</th>
|
|
1192
|
+
<th>${messages.resultLabels.ttft}</th>
|
|
1193
|
+
<th>${messages.resultLabels.totalTime}</th>
|
|
1194
|
+
<th>${messages.resultLabels.totalTokens}</th>
|
|
1195
|
+
<th>${messages.resultLabels.averageSpeed}</th>
|
|
1196
|
+
<th>${messages.resultLabels.peakSpeed}</th>
|
|
1197
|
+
<th>${messages.resultLabels.peakTps}</th>
|
|
1198
|
+
</tr>
|
|
1199
|
+
</thead>
|
|
1200
|
+
<tbody>
|
|
1201
|
+
${detailRows}
|
|
1202
|
+
</tbody>
|
|
1203
|
+
</table>
|
|
1204
|
+
</div>
|
|
1205
|
+
`;
|
|
1206
|
+
const data = {
|
|
1207
|
+
lang,
|
|
1208
|
+
title: messages.htmlTitle,
|
|
1209
|
+
reportTitle: messages.htmlReportTitle,
|
|
1210
|
+
testTimeLabel: messages.htmlTestTime,
|
|
1211
|
+
testTime,
|
|
1212
|
+
configSection: messages.htmlConfigSection,
|
|
1213
|
+
summarySection: messages.htmlSummarySection,
|
|
1214
|
+
chartsSection: messages.htmlChartsSection,
|
|
1215
|
+
statsTitle: messages.statsSummaryTitle(stats.sampleSize),
|
|
1216
|
+
detailsSection: messages.htmlDetailsSection,
|
|
1217
|
+
configGrid: configGridHtml,
|
|
1218
|
+
summaryCards: summaryCardsHtml,
|
|
1219
|
+
charts: chartsHtml,
|
|
1220
|
+
statsTable: statsTableHtml,
|
|
1221
|
+
detailsTable: detailsTableHtml
|
|
1222
|
+
};
|
|
1223
|
+
const template = loadTemplate();
|
|
1224
|
+
return replaceTemplate(template, data);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
571
1227
|
// src/index.ts
|
|
572
1228
|
function getCliVersion() {
|
|
573
1229
|
try {
|
|
574
|
-
const currentDir =
|
|
1230
|
+
const currentDir = dirname2(fileURLToPath2(import.meta.url));
|
|
575
1231
|
const packagePath = join(currentDir, "..", "package.json");
|
|
576
|
-
const packageJson = JSON.parse(
|
|
1232
|
+
const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
|
|
577
1233
|
return packageJson.version ?? "unknown";
|
|
578
1234
|
} catch {
|
|
579
1235
|
return "unknown";
|
|
@@ -581,9 +1237,10 @@ function getCliVersion() {
|
|
|
581
1237
|
}
|
|
582
1238
|
var program = new Command();
|
|
583
1239
|
program.name("token-speed-test").description("A CLI tool to test LLM API token output speed").version(getCliVersion());
|
|
584
|
-
program.option("-k, --api-key <key>", "API Key (required)", "").option("-p, --provider <provider>", "API provider: anthropic or openai", "anthropic").option("-u, --url <url>", "Custom API endpoint URL").option("-m, --model <model>", "Model name").option("--max-tokens <number>", "Maximum output tokens", "1024").option("-r, --runs <number>", "Number of test runs", "3").option("--prompt <text>", "Test prompt", "
|
|
1240
|
+
program.option("-k, --api-key <key>", "API Key (required)", "").option("-p, --provider <provider>", "API provider: anthropic or openai", "anthropic").option("-u, --url <url>", "Custom API endpoint URL").option("-m, --model <model>", "Model name").option("--max-tokens <number>", "Maximum output tokens", "1024").option("-r, --runs <number>", "Number of test runs", "3").option("--prompt <text>", "Test prompt").option("--lang <lang>", "Output language: zh or en", "zh").option("--html", "Generate HTML report").option("-o, --output <path>", "HTML report output path", "report.html").parse(process.argv);
|
|
585
1241
|
var options = program.opts();
|
|
586
1242
|
async function main() {
|
|
1243
|
+
let messages = getMessages(DEFAULT_LANG);
|
|
587
1244
|
try {
|
|
588
1245
|
const config = parseConfig({
|
|
589
1246
|
apiKey: options.apiKey,
|
|
@@ -592,37 +1249,72 @@ async function main() {
|
|
|
592
1249
|
model: options.model,
|
|
593
1250
|
maxTokens: parseInt(options.maxTokens, 10),
|
|
594
1251
|
runs: parseInt(options.runs, 10),
|
|
595
|
-
prompt: options.prompt
|
|
1252
|
+
prompt: options.prompt,
|
|
1253
|
+
lang: options.lang
|
|
596
1254
|
});
|
|
597
|
-
|
|
1255
|
+
messages = getMessages(config.lang);
|
|
1256
|
+
console.log(chalk.cyan(`
|
|
1257
|
+
${messages.appTitle}`));
|
|
598
1258
|
console.log(chalk.gray("\u2500".repeat(50)));
|
|
599
|
-
console.log(chalk.gray(
|
|
600
|
-
console.log(chalk.gray(
|
|
601
|
-
console.log(chalk.gray(
|
|
602
|
-
console.log(chalk.gray(
|
|
1259
|
+
console.log(chalk.gray(`${messages.configLabels.provider}: ${chalk.white(config.provider)}`));
|
|
1260
|
+
console.log(chalk.gray(`${messages.configLabels.model}: ${chalk.white(config.model)}`));
|
|
1261
|
+
console.log(chalk.gray(`${messages.configLabels.maxTokens}: ${chalk.white(config.maxTokens)}`));
|
|
1262
|
+
console.log(chalk.gray(`${messages.configLabels.runs}: ${chalk.white(config.runCount)}`));
|
|
603
1263
|
console.log(
|
|
604
1264
|
chalk.gray(
|
|
605
|
-
|
|
1265
|
+
`${messages.configLabels.prompt}: ${chalk.white(config.prompt.substring(0, 50))}${config.prompt.length > 50 ? "..." : ""}`
|
|
606
1266
|
)
|
|
607
1267
|
);
|
|
608
1268
|
console.log(chalk.gray("\u2500".repeat(50)));
|
|
609
|
-
console.log(chalk.yellow(
|
|
610
|
-
|
|
1269
|
+
console.log(chalk.yellow(`
|
|
1270
|
+
${messages.runningTests}
|
|
1271
|
+
`));
|
|
1272
|
+
console.log(chalk.gray(`${messages.streamingOutput}
|
|
1273
|
+
`));
|
|
611
1274
|
const results = await runMultipleTests(config);
|
|
612
1275
|
const allMetrics = results.map((r) => calculateMetrics(r));
|
|
613
1276
|
for (let i = 0; i < allMetrics.length; i++) {
|
|
614
|
-
console.log(chalk.gray(renderSingleResult(allMetrics[i], i)));
|
|
1277
|
+
console.log(chalk.gray(renderSingleResult(allMetrics[i], i, config.lang)));
|
|
615
1278
|
}
|
|
616
1279
|
const stats = calculateStats(allMetrics);
|
|
617
|
-
console.log(chalk.cyan("\n" + renderReport(stats)));
|
|
618
|
-
console.log(chalk.green(
|
|
1280
|
+
console.log(chalk.cyan("\n" + renderReport(stats, config.lang)));
|
|
1281
|
+
console.log(chalk.green(`
|
|
1282
|
+
${messages.testComplete}
|
|
1283
|
+
`));
|
|
1284
|
+
if (options.html) {
|
|
1285
|
+
const htmlPath = options.output;
|
|
1286
|
+
const htmlContent = generateHTMLReport({
|
|
1287
|
+
config,
|
|
1288
|
+
singleResults: allMetrics,
|
|
1289
|
+
stats,
|
|
1290
|
+
lang: config.lang,
|
|
1291
|
+
messages
|
|
1292
|
+
});
|
|
1293
|
+
try {
|
|
1294
|
+
await fsWriteFile(htmlPath, htmlContent, "utf-8");
|
|
1295
|
+
console.log(chalk.cyan(messages.htmlGenerated(htmlPath)));
|
|
1296
|
+
await open(htmlPath).catch(() => {
|
|
1297
|
+
console.warn(chalk.yellow(messages.htmlOpenError(htmlPath)));
|
|
1298
|
+
});
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
console.error(
|
|
1301
|
+
chalk.red(
|
|
1302
|
+
`
|
|
1303
|
+
${messages.errorPrefix}: ${err instanceof Error ? err.message : String(err)}
|
|
1304
|
+
`
|
|
1305
|
+
)
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
619
1309
|
} catch (error) {
|
|
620
1310
|
if (error instanceof Error) {
|
|
621
1311
|
console.error(chalk.red(`
|
|
622
|
-
|
|
1312
|
+
${messages.errorPrefix}: ${error.message}
|
|
623
1313
|
`));
|
|
624
1314
|
} else {
|
|
625
|
-
console.error(chalk.red(
|
|
1315
|
+
console.error(chalk.red(`
|
|
1316
|
+
${messages.unknownError}
|
|
1317
|
+
`));
|
|
626
1318
|
}
|
|
627
1319
|
process.exit(1);
|
|
628
1320
|
}
|