token-speed-tester 1.6.0 → 1.8.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 +35 -1
- package/README.md +36 -2
- package/dist/index.js +640 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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";
|
|
9
11
|
|
|
10
12
|
// src/i18n.ts
|
|
11
13
|
var SUPPORTED_LANGS = ["zh", "en"];
|
|
@@ -38,7 +40,10 @@ var zhMessages = {
|
|
|
38
40
|
mean: "\u5747\u503C",
|
|
39
41
|
min: "\u6700\u5C0F\u503C",
|
|
40
42
|
max: "\u6700\u5927\u503C",
|
|
41
|
-
stdDev: "\u6807\u51C6\u5DEE"
|
|
43
|
+
stdDev: "\u6807\u51C6\u5DEE",
|
|
44
|
+
p50: "P50",
|
|
45
|
+
p95: "P95",
|
|
46
|
+
p99: "P99"
|
|
42
47
|
},
|
|
43
48
|
statsLabels: {
|
|
44
49
|
ttft: "TTFT (ms)",
|
|
@@ -55,7 +60,27 @@ var zhMessages = {
|
|
|
55
60
|
averageSpeed: "\u5E73\u5747\u901F\u5EA6",
|
|
56
61
|
peakSpeed: "\u5CF0\u503C\u901F\u5EA6",
|
|
57
62
|
peakTps: "\u5CF0\u503C TPS"
|
|
58
|
-
}
|
|
63
|
+
},
|
|
64
|
+
htmlTitle: "Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A",
|
|
65
|
+
htmlReportTitle: "LLM API Token \u6D41\u5F0F\u6027\u80FD\u6D4B\u8BD5\u62A5\u544A",
|
|
66
|
+
htmlGenerated: (file) => `\u2713 HTML \u62A5\u544A\u5DF2\u751F\u6210: ${file}`,
|
|
67
|
+
htmlOpenError: (file) => `\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u62A5\u544A\uFF0C\u8BF7\u624B\u52A8\u6253\u5F00: ${file}`,
|
|
68
|
+
htmlConfigSection: "\u6D4B\u8BD5\u914D\u7F6E",
|
|
69
|
+
htmlSummarySection: "\u7EDF\u8BA1\u6C47\u603B",
|
|
70
|
+
htmlChartsSection: "\u56FE\u8868\u5206\u6790",
|
|
71
|
+
htmlDetailsSection: "\u8BE6\u7EC6\u6570\u636E",
|
|
72
|
+
htmlTestTime: "\u6D4B\u8BD5\u65F6\u95F4",
|
|
73
|
+
htmlMetric: "\u6307\u6807",
|
|
74
|
+
htmlValue: "\u6570\u503C",
|
|
75
|
+
htmlRun: "\u8FD0\u884C",
|
|
76
|
+
htmlTokens: "Token \u6570",
|
|
77
|
+
htmlDuration: "\u8017\u65F6",
|
|
78
|
+
htmlSpeed: "\u901F\u5EA6",
|
|
79
|
+
htmlTps: "TPS",
|
|
80
|
+
htmlAverageTps: "\u5E73\u5747 TPS",
|
|
81
|
+
htmlTpsDistribution: "TPS \u5206\u5E03",
|
|
82
|
+
htmlTimeUnit: "\u79D2",
|
|
83
|
+
htmlTpsChartHover: "\u6B21"
|
|
59
84
|
};
|
|
60
85
|
var enMessages = {
|
|
61
86
|
defaultPrompt: "Write a short essay about AI",
|
|
@@ -85,7 +110,10 @@ var enMessages = {
|
|
|
85
110
|
mean: "Mean",
|
|
86
111
|
min: "Min",
|
|
87
112
|
max: "Max",
|
|
88
|
-
stdDev: "Std Dev"
|
|
113
|
+
stdDev: "Std Dev",
|
|
114
|
+
p50: "P50",
|
|
115
|
+
p95: "P95",
|
|
116
|
+
p99: "P99"
|
|
89
117
|
},
|
|
90
118
|
statsLabels: {
|
|
91
119
|
ttft: "TTFT (ms)",
|
|
@@ -102,7 +130,27 @@ var enMessages = {
|
|
|
102
130
|
averageSpeed: "Avg Speed",
|
|
103
131
|
peakSpeed: "Peak Speed",
|
|
104
132
|
peakTps: "Peak TPS"
|
|
105
|
-
}
|
|
133
|
+
},
|
|
134
|
+
htmlTitle: "Token Speed Test Report",
|
|
135
|
+
htmlReportTitle: "LLM API Token Streaming Performance Report",
|
|
136
|
+
htmlGenerated: (file) => `\u2713 HTML report generated: ${file}`,
|
|
137
|
+
htmlOpenError: (file) => `Could not auto-open report, please open manually: ${file}`,
|
|
138
|
+
htmlConfigSection: "Test Configuration",
|
|
139
|
+
htmlSummarySection: "Summary Statistics",
|
|
140
|
+
htmlChartsSection: "Chart Analysis",
|
|
141
|
+
htmlDetailsSection: "Detailed Data",
|
|
142
|
+
htmlTestTime: "Test Time",
|
|
143
|
+
htmlMetric: "Metric",
|
|
144
|
+
htmlValue: "Value",
|
|
145
|
+
htmlRun: "Run",
|
|
146
|
+
htmlTokens: "Tokens",
|
|
147
|
+
htmlDuration: "Duration",
|
|
148
|
+
htmlSpeed: "Speed",
|
|
149
|
+
htmlTps: "TPS",
|
|
150
|
+
htmlAverageTps: "Average TPS",
|
|
151
|
+
htmlTpsDistribution: "TPS Distribution",
|
|
152
|
+
htmlTimeUnit: "s",
|
|
153
|
+
htmlTpsChartHover: "count"
|
|
106
154
|
};
|
|
107
155
|
function isSupportedLang(value) {
|
|
108
156
|
return SUPPORTED_LANGS.includes(value);
|
|
@@ -405,6 +453,29 @@ function standardDeviation(values) {
|
|
|
405
453
|
const squareDiffs = values.map((v) => Math.pow(v - avg, 2));
|
|
406
454
|
return Math.sqrt(mean(squareDiffs));
|
|
407
455
|
}
|
|
456
|
+
function calculatePercentile(values, p) {
|
|
457
|
+
if (values.length === 0) return 0;
|
|
458
|
+
if (values.length === 1) return values[0];
|
|
459
|
+
const index = p / 100 * (values.length - 1);
|
|
460
|
+
const lowerIndex = Math.floor(index);
|
|
461
|
+
const upperIndex = Math.ceil(index);
|
|
462
|
+
const fraction = index - lowerIndex;
|
|
463
|
+
if (lowerIndex === upperIndex) {
|
|
464
|
+
return values[lowerIndex];
|
|
465
|
+
}
|
|
466
|
+
return values[lowerIndex] + fraction * (values[upperIndex] - values[lowerIndex]);
|
|
467
|
+
}
|
|
468
|
+
function calculatePercentiles(values) {
|
|
469
|
+
if (values.length === 0) {
|
|
470
|
+
return { p50: 0, p95: 0, p99: 0 };
|
|
471
|
+
}
|
|
472
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
473
|
+
return {
|
|
474
|
+
p50: calculatePercentile(sorted, 50),
|
|
475
|
+
p95: calculatePercentile(sorted, 95),
|
|
476
|
+
p99: calculatePercentile(sorted, 99)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
408
479
|
function calculateStats(allMetrics) {
|
|
409
480
|
if (allMetrics.length === 0) {
|
|
410
481
|
throw new Error("Cannot calculate stats from empty metrics array");
|
|
@@ -459,6 +530,14 @@ function calculateStats(allMetrics) {
|
|
|
459
530
|
peakTps: standardDeviation(peakTpsValues),
|
|
460
531
|
tps: []
|
|
461
532
|
},
|
|
533
|
+
percentiles: {
|
|
534
|
+
ttft: calculatePercentiles(ttfts),
|
|
535
|
+
totalTime: calculatePercentiles(totalTimes),
|
|
536
|
+
totalTokens: calculatePercentiles(totalTokens),
|
|
537
|
+
averageSpeed: calculatePercentiles(averageSpeeds),
|
|
538
|
+
peakSpeed: calculatePercentiles(peakSpeeds),
|
|
539
|
+
peakTps: calculatePercentiles(peakTpsValues)
|
|
540
|
+
},
|
|
462
541
|
sampleSize
|
|
463
542
|
};
|
|
464
543
|
}
|
|
@@ -469,7 +548,7 @@ var BLOCK_CHAR = "\u2588";
|
|
|
469
548
|
var CHART_WIDTH = 50;
|
|
470
549
|
var CHART_HEIGHT = 10;
|
|
471
550
|
var STAT_LABEL_WIDTH = 15;
|
|
472
|
-
var STAT_VALUE_WIDTH =
|
|
551
|
+
var STAT_VALUE_WIDTH = 8;
|
|
473
552
|
var Y_LABEL_WIDTH = 4;
|
|
474
553
|
function padEndWidth(text, width) {
|
|
475
554
|
const currentWidth = stringWidth(text);
|
|
@@ -580,7 +659,7 @@ function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
|
580
659
|
const lines = [];
|
|
581
660
|
lines.push("");
|
|
582
661
|
lines.push(messages.statsSummaryTitle(stats.sampleSize));
|
|
583
|
-
const headerRow = "\u2502 " + padEndWidth(messages.statsHeaders.metric, STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.mean, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.
|
|
662
|
+
const headerRow = "\u2502 " + padEndWidth(messages.statsHeaders.metric, STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.mean, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.p50, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.p95, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.p99, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.min, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.max, STAT_VALUE_WIDTH) + " \u2502";
|
|
584
663
|
const tableWidth = stringWidth(headerRow) - 2;
|
|
585
664
|
lines.push("\u250C" + "\u2500".repeat(tableWidth) + "\u2510");
|
|
586
665
|
lines.push(headerRow);
|
|
@@ -589,9 +668,11 @@ function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
|
589
668
|
formatStatRow(
|
|
590
669
|
messages.statsLabels.ttft,
|
|
591
670
|
stats.mean.ttft,
|
|
671
|
+
stats.percentiles.ttft.p50,
|
|
672
|
+
stats.percentiles.ttft.p95,
|
|
673
|
+
stats.percentiles.ttft.p99,
|
|
592
674
|
stats.min.ttft,
|
|
593
675
|
stats.max.ttft,
|
|
594
|
-
stats.stdDev.ttft,
|
|
595
676
|
"f"
|
|
596
677
|
)
|
|
597
678
|
);
|
|
@@ -600,9 +681,11 @@ function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
|
600
681
|
formatStatRow(
|
|
601
682
|
messages.statsLabels.totalTime,
|
|
602
683
|
stats.mean.totalTime,
|
|
684
|
+
stats.percentiles.totalTime.p50,
|
|
685
|
+
stats.percentiles.totalTime.p95,
|
|
686
|
+
stats.percentiles.totalTime.p99,
|
|
603
687
|
stats.min.totalTime,
|
|
604
688
|
stats.max.totalTime,
|
|
605
|
-
stats.stdDev.totalTime,
|
|
606
689
|
"f"
|
|
607
690
|
)
|
|
608
691
|
);
|
|
@@ -611,9 +694,11 @@ function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
|
611
694
|
formatStatRow(
|
|
612
695
|
messages.statsLabels.totalTokens,
|
|
613
696
|
stats.mean.totalTokens,
|
|
697
|
+
stats.percentiles.totalTokens.p50,
|
|
698
|
+
stats.percentiles.totalTokens.p95,
|
|
699
|
+
stats.percentiles.totalTokens.p99,
|
|
614
700
|
stats.min.totalTokens,
|
|
615
701
|
stats.max.totalTokens,
|
|
616
|
-
stats.stdDev.totalTokens,
|
|
617
702
|
"f"
|
|
618
703
|
)
|
|
619
704
|
);
|
|
@@ -622,9 +707,11 @@ function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
|
622
707
|
formatStatRow(
|
|
623
708
|
messages.statsLabels.averageSpeed,
|
|
624
709
|
stats.mean.averageSpeed,
|
|
710
|
+
stats.percentiles.averageSpeed.p50,
|
|
711
|
+
stats.percentiles.averageSpeed.p95,
|
|
712
|
+
stats.percentiles.averageSpeed.p99,
|
|
625
713
|
stats.min.averageSpeed,
|
|
626
714
|
stats.max.averageSpeed,
|
|
627
|
-
stats.stdDev.averageSpeed,
|
|
628
715
|
"f"
|
|
629
716
|
)
|
|
630
717
|
);
|
|
@@ -633,9 +720,11 @@ function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
|
633
720
|
formatStatRow(
|
|
634
721
|
messages.statsLabels.peakSpeed,
|
|
635
722
|
stats.mean.peakSpeed,
|
|
723
|
+
stats.percentiles.peakSpeed.p50,
|
|
724
|
+
stats.percentiles.peakSpeed.p95,
|
|
725
|
+
stats.percentiles.peakSpeed.p99,
|
|
636
726
|
stats.min.peakSpeed,
|
|
637
727
|
stats.max.peakSpeed,
|
|
638
|
-
stats.stdDev.peakSpeed,
|
|
639
728
|
"f"
|
|
640
729
|
)
|
|
641
730
|
);
|
|
@@ -644,18 +733,20 @@ function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
|
644
733
|
formatStatRow(
|
|
645
734
|
messages.statsLabels.peakTps,
|
|
646
735
|
stats.mean.peakTps,
|
|
736
|
+
stats.percentiles.peakTps.p50,
|
|
737
|
+
stats.percentiles.peakTps.p95,
|
|
738
|
+
stats.percentiles.peakTps.p99,
|
|
647
739
|
stats.min.peakTps,
|
|
648
740
|
stats.max.peakTps,
|
|
649
|
-
stats.stdDev.peakTps,
|
|
650
741
|
"f"
|
|
651
742
|
)
|
|
652
743
|
);
|
|
653
744
|
lines.push("\u2514" + "\u2500".repeat(tableWidth) + "\u2518");
|
|
654
745
|
return lines.join("\n");
|
|
655
746
|
}
|
|
656
|
-
function formatStatRow(label, mean2, min, max,
|
|
747
|
+
function formatStatRow(label, mean2, p50, p95, p99, min, max, format) {
|
|
657
748
|
const fmt = (n) => format === "f" ? n.toFixed(2) : n.toFixed(0);
|
|
658
|
-
return "\u2502 " + padEndWidth(label, STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth(fmt(mean2), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(
|
|
749
|
+
return "\u2502 " + padEndWidth(label, STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth(fmt(mean2), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(p50), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(p95), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(p99), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(min), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(max), STAT_VALUE_WIDTH) + " \u2502";
|
|
659
750
|
}
|
|
660
751
|
function formatTimeWithDecimals(ms) {
|
|
661
752
|
if (ms === Math.floor(ms)) {
|
|
@@ -694,12 +785,516 @@ function renderReport(stats, lang = DEFAULT_LANG) {
|
|
|
694
785
|
return lines.join("\n");
|
|
695
786
|
}
|
|
696
787
|
|
|
788
|
+
// src/html-report.ts
|
|
789
|
+
import { readFileSync } from "fs";
|
|
790
|
+
import { dirname, resolve } from "path";
|
|
791
|
+
import { fileURLToPath } from "url";
|
|
792
|
+
var CHART_COLORS = [
|
|
793
|
+
"#00f5ff",
|
|
794
|
+
// cyan
|
|
795
|
+
"#ff00aa",
|
|
796
|
+
// magenta
|
|
797
|
+
"#ffcc00",
|
|
798
|
+
// yellow
|
|
799
|
+
"#00ff88",
|
|
800
|
+
// green
|
|
801
|
+
"#ff6600",
|
|
802
|
+
// orange
|
|
803
|
+
"#aa00ff"
|
|
804
|
+
// purple
|
|
805
|
+
];
|
|
806
|
+
var PALETTE = {
|
|
807
|
+
bg: "#0a0a0f",
|
|
808
|
+
bgSecondary: "#12121a",
|
|
809
|
+
bgCard: "#1a1a24",
|
|
810
|
+
border: "#2a2a3a",
|
|
811
|
+
text: "#e4e4eb",
|
|
812
|
+
textMuted: "#6a6a7a",
|
|
813
|
+
accent: "#00f5ff",
|
|
814
|
+
accentSecondary: "#ff00aa",
|
|
815
|
+
accentTertiary: "#ffcc00"
|
|
816
|
+
};
|
|
817
|
+
function formatNumber(num, decimals = 2) {
|
|
818
|
+
return num.toFixed(decimals);
|
|
819
|
+
}
|
|
820
|
+
function formatTime(ms) {
|
|
821
|
+
if (ms < 1e3) {
|
|
822
|
+
return `${ms.toFixed(0)}ms`;
|
|
823
|
+
}
|
|
824
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
825
|
+
}
|
|
826
|
+
function escapeHtml(text) {
|
|
827
|
+
const map = {
|
|
828
|
+
"&": "&",
|
|
829
|
+
"<": "<",
|
|
830
|
+
">": ">",
|
|
831
|
+
'"': """,
|
|
832
|
+
"'": "'"
|
|
833
|
+
};
|
|
834
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
835
|
+
}
|
|
836
|
+
function loadTemplate() {
|
|
837
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
838
|
+
const __dirname = dirname(__filename);
|
|
839
|
+
return readFileSync(resolve(__dirname, "template.html"), "utf-8");
|
|
840
|
+
}
|
|
841
|
+
function replaceTemplate(template, data) {
|
|
842
|
+
let result = template;
|
|
843
|
+
for (const [key, value] of Object.entries(data)) {
|
|
844
|
+
result = result.replaceAll(new RegExp(`{{${key}}}`, "g"), value);
|
|
845
|
+
}
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
848
|
+
function generateSpeedChart(results, messages) {
|
|
849
|
+
const allTps = results.flatMap((r) => r.tps);
|
|
850
|
+
if (allTps.length === 0) {
|
|
851
|
+
return `<div class="no-data">${messages.noChartData || "No data available"}</div>`;
|
|
852
|
+
}
|
|
853
|
+
const maxTps = Math.max(...allTps, 1);
|
|
854
|
+
const maxDuration = Math.max(...results.map((r) => r.tps.length));
|
|
855
|
+
const width = 800;
|
|
856
|
+
const height = 320;
|
|
857
|
+
const padding = { top: 30, right: 30, bottom: 45, left: 55 };
|
|
858
|
+
const chartWidth = width - padding.left - padding.right;
|
|
859
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
860
|
+
const avgTps = [];
|
|
861
|
+
for (let i = 0; i < maxDuration; i++) {
|
|
862
|
+
const values = results.map((r) => r.tps[i] ?? 0);
|
|
863
|
+
avgTps.push(values.reduce((a, b) => a + b, 0) / values.length);
|
|
864
|
+
}
|
|
865
|
+
const polylines = results.map((result, idx) => {
|
|
866
|
+
const color = CHART_COLORS[idx % CHART_COLORS.length];
|
|
867
|
+
let points = "";
|
|
868
|
+
let areaPoints = `${padding.left},${height - padding.bottom} `;
|
|
869
|
+
result.tps.forEach((tps, i) => {
|
|
870
|
+
const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
|
|
871
|
+
const y = padding.top + chartHeight - tps / maxTps * chartHeight;
|
|
872
|
+
points += `${x},${y} `;
|
|
873
|
+
areaPoints += `${x},${y} `;
|
|
874
|
+
});
|
|
875
|
+
areaPoints += `${padding.left + chartWidth},${height - padding.bottom}`;
|
|
876
|
+
return `
|
|
877
|
+
<defs>
|
|
878
|
+
<linearGradient id="grad-${idx}" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
879
|
+
<stop offset="0%" style="stop-color:${color};stop-opacity:0.3"/>
|
|
880
|
+
<stop offset="100%" style="stop-color:${color};stop-opacity:0"/>
|
|
881
|
+
</linearGradient>
|
|
882
|
+
</defs>
|
|
883
|
+
<polygon
|
|
884
|
+
points="${areaPoints.trim()}"
|
|
885
|
+
fill="url(#grad-${idx})"
|
|
886
|
+
class="area-${idx}"
|
|
887
|
+
/>
|
|
888
|
+
<polyline
|
|
889
|
+
fill="none"
|
|
890
|
+
stroke="${color}"
|
|
891
|
+
stroke-width="2.5"
|
|
892
|
+
points="${points.trim()}"
|
|
893
|
+
class="line line-${idx}"
|
|
894
|
+
data-run="${idx + 1}"
|
|
895
|
+
>
|
|
896
|
+
<animate
|
|
897
|
+
attributeName="stroke-dasharray"
|
|
898
|
+
from="0,2000"
|
|
899
|
+
to="2000,0"
|
|
900
|
+
dur="1.5s"
|
|
901
|
+
fill="freeze"
|
|
902
|
+
calcMode="spline"
|
|
903
|
+
keySplines="0.4 0 0.2 1"
|
|
904
|
+
/>
|
|
905
|
+
</polyline>
|
|
906
|
+
${result.tps.map((tps, i) => {
|
|
907
|
+
const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
|
|
908
|
+
const y = padding.top + chartHeight - tps / maxTps * chartHeight;
|
|
909
|
+
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>
|
|
910
|
+
<animate attributeName="opacity" from="0" to="1" begin="${0.5 + i * 0.05}s" dur="0.3s" fill="freeze"/>
|
|
911
|
+
</circle>`;
|
|
912
|
+
}).join("")}
|
|
913
|
+
`;
|
|
914
|
+
}).join("\n ");
|
|
915
|
+
let avgPoints = "";
|
|
916
|
+
avgTps.forEach((tps, i) => {
|
|
917
|
+
const x = padding.left + i / Math.max(maxDuration - 1, 1) * chartWidth;
|
|
918
|
+
const y = padding.top + chartHeight - tps / maxTps * chartHeight;
|
|
919
|
+
avgPoints += `${x},${y} `;
|
|
920
|
+
});
|
|
921
|
+
const yLabels = [];
|
|
922
|
+
for (let i = 0; i <= 5; i++) {
|
|
923
|
+
const value = Math.round(maxTps * i / 5);
|
|
924
|
+
const y = padding.top + chartHeight - i / 5 * chartHeight;
|
|
925
|
+
yLabels.push(
|
|
926
|
+
`<text x="${padding.left - 12}" y="${y + 4}" text-anchor="end" font-size="11" fill="${PALETTE.textMuted}">${value}</text>`
|
|
927
|
+
);
|
|
928
|
+
if (i > 0) {
|
|
929
|
+
const yLine = padding.top + chartHeight - i / 5 * chartHeight;
|
|
930
|
+
yLabels.push(
|
|
931
|
+
`<line x1="${padding.left}" y1="${yLine}" x2="${width - padding.right}" y2="${yLine}" stroke="${PALETTE.border}" stroke-width="1" opacity="0.5"/>`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const xLabels = [];
|
|
936
|
+
const xSteps = Math.min(maxDuration, 10);
|
|
937
|
+
for (let i = 0; i < xSteps; i++) {
|
|
938
|
+
const x = padding.left + i / Math.max(xSteps - 1, 1) * chartWidth;
|
|
939
|
+
const label = i.toString();
|
|
940
|
+
xLabels.push(
|
|
941
|
+
`<text x="${x}" y="${height - padding.bottom + 20}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">${label}${messages.htmlTimeUnit}</text>`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
return `
|
|
945
|
+
<svg viewBox="0 0 ${width} ${height}" class="chart" id="speedChart">
|
|
946
|
+
<style>
|
|
947
|
+
#speedChart .line { stroke-dasharray: 2000; stroke-dashoffset: 0; }
|
|
948
|
+
#speedChart .line:hover { stroke-width: 4; filter: drop-shadow(0 0 8px currentColor); }
|
|
949
|
+
#speedChart circle { transition: all 0.2s ease; cursor: pointer; }
|
|
950
|
+
#speedChart circle:hover { r: 6; stroke-width: 3; }
|
|
951
|
+
</style>
|
|
952
|
+
<rect x="${padding.left}" y="${padding.top}" width="${chartWidth}" height="${chartHeight}" fill="${PALETTE.bgCard}" rx="4"/>
|
|
953
|
+
${yLabels.join("\n ")}
|
|
954
|
+
${xLabels.join("\n ")}
|
|
955
|
+
<line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
|
|
956
|
+
<line x1="${padding.left}" y1="${height - padding.bottom}" x2="${width - padding.right}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
|
|
957
|
+
${polylines}
|
|
958
|
+
<polyline
|
|
959
|
+
fill="none"
|
|
960
|
+
stroke="${PALETTE.text}"
|
|
961
|
+
stroke-width="2"
|
|
962
|
+
stroke-dasharray="6,4"
|
|
963
|
+
opacity="0.7"
|
|
964
|
+
points="${avgPoints.trim()}"
|
|
965
|
+
/>
|
|
966
|
+
<text x="${padding.left + chartWidth / 2}" y="${height - 8}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">TIME (${messages.htmlTimeUnit})</text>
|
|
967
|
+
<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>
|
|
968
|
+
</svg>
|
|
969
|
+
<div class="chart-legend">
|
|
970
|
+
<div class="legend-item">
|
|
971
|
+
<span class="legend-line avg"></span>
|
|
972
|
+
<span>${messages.htmlSummarySection}</span>
|
|
973
|
+
</div>
|
|
974
|
+
${results.map((_, idx) => {
|
|
975
|
+
const color = CHART_COLORS[idx % CHART_COLORS.length];
|
|
976
|
+
return `
|
|
977
|
+
<div class="legend-item">
|
|
978
|
+
<span class="legend-line" style="background: ${color};"></span>
|
|
979
|
+
<span>${messages.htmlRun} ${idx + 1}</span>
|
|
980
|
+
</div>`;
|
|
981
|
+
}).join("")}
|
|
982
|
+
</div>
|
|
983
|
+
`;
|
|
984
|
+
}
|
|
985
|
+
function generateTPSHistogram(stats, messages) {
|
|
986
|
+
const allTps = stats.mean.tps;
|
|
987
|
+
if (allTps.length === 0) {
|
|
988
|
+
return `<div class="no-data">${messages.noTpsData || "No TPS data available"}</div>`;
|
|
989
|
+
}
|
|
990
|
+
const maxTps = Math.max(...allTps);
|
|
991
|
+
const width = 400;
|
|
992
|
+
const height = 280;
|
|
993
|
+
const padding = { top: 25, right: 20, bottom: 40, left: 50 };
|
|
994
|
+
const chartWidth = width - padding.left - padding.right;
|
|
995
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
996
|
+
const bars = allTps.map((tps, i) => {
|
|
997
|
+
const barWidth = chartWidth / allTps.length - 2;
|
|
998
|
+
const x = padding.left + i / allTps.length * chartWidth;
|
|
999
|
+
const barHeight = tps / maxTps * chartHeight;
|
|
1000
|
+
const y = padding.top + chartHeight - barHeight;
|
|
1001
|
+
const hue = 180 + tps / maxTps * 60;
|
|
1002
|
+
const color = `hsl(${hue}, 100%, 60%)`;
|
|
1003
|
+
return `
|
|
1004
|
+
<rect
|
|
1005
|
+
x="${x}"
|
|
1006
|
+
y="${y}"
|
|
1007
|
+
width="${barWidth}"
|
|
1008
|
+
height="${barHeight}"
|
|
1009
|
+
fill="${color}"
|
|
1010
|
+
class="bar"
|
|
1011
|
+
data-second="${i}"
|
|
1012
|
+
data-tps="${tps.toFixed(2)}"
|
|
1013
|
+
rx="2"
|
|
1014
|
+
>
|
|
1015
|
+
<title>${messages.htmlAverageTps || "Average TPS"} ${i}s: ${tps.toFixed(1)}</title>
|
|
1016
|
+
<animate
|
|
1017
|
+
attributeName="height"
|
|
1018
|
+
from="0"
|
|
1019
|
+
to="${barHeight}"
|
|
1020
|
+
dur="0.8s"
|
|
1021
|
+
fill="freeze"
|
|
1022
|
+
calcMode="spline"
|
|
1023
|
+
keySplines="0.4 0 0.2 1"
|
|
1024
|
+
begin="${i * 0.05}s"
|
|
1025
|
+
/>
|
|
1026
|
+
<animate
|
|
1027
|
+
attributeName="y"
|
|
1028
|
+
from="${height - padding.bottom}"
|
|
1029
|
+
to="${y}"
|
|
1030
|
+
dur="0.8s"
|
|
1031
|
+
fill="freeze"
|
|
1032
|
+
calcMode="spline"
|
|
1033
|
+
keySplines="0.4 0 0.2 1"
|
|
1034
|
+
begin="${i * 0.05}s"
|
|
1035
|
+
/>
|
|
1036
|
+
</rect>
|
|
1037
|
+
`;
|
|
1038
|
+
}).join("");
|
|
1039
|
+
const yLabels = [];
|
|
1040
|
+
for (let i = 0; i <= 5; i++) {
|
|
1041
|
+
const value = Math.round(maxTps * i / 5);
|
|
1042
|
+
const y = padding.top + chartHeight - i / 5 * chartHeight;
|
|
1043
|
+
yLabels.push(
|
|
1044
|
+
`<text x="${padding.left - 10}" y="${y + 4}" text-anchor="end" font-size="11" fill="${PALETTE.textMuted}">${value}</text>`
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
const xLabels = [];
|
|
1048
|
+
const xSteps = Math.min(allTps.length, 8);
|
|
1049
|
+
for (let i = 0; i < xSteps; i++) {
|
|
1050
|
+
const x = padding.left + i / Math.max(xSteps - 1, 1) * chartWidth;
|
|
1051
|
+
const label = i.toString();
|
|
1052
|
+
xLabels.push(
|
|
1053
|
+
`<text x="${x}" y="${height - padding.bottom + 18}" text-anchor="middle" font-size="11" fill="${PALETTE.textMuted}">${label}${messages.htmlTimeUnit}</text>`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
return `
|
|
1057
|
+
<svg viewBox="0 0 ${width} ${height}" class="chart" id="tpsChart">
|
|
1058
|
+
<style>
|
|
1059
|
+
#tpsChart .bar { transition: all 0.2s ease; cursor: pointer; opacity: 0.9; }
|
|
1060
|
+
#tpsChart .bar:hover { opacity: 1; filter: brightness(1.2); }
|
|
1061
|
+
</style>
|
|
1062
|
+
<rect x="${padding.left}" y="${padding.top}" width="${chartWidth}" height="${chartHeight}" fill="${PALETTE.bgCard}" rx="4"/>
|
|
1063
|
+
${yLabels.join("\n ")}
|
|
1064
|
+
${xLabels.join("\n ")}
|
|
1065
|
+
<line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
|
|
1066
|
+
<line x1="${padding.left}" y1="${height - padding.bottom}" x2="${width - padding.right}" y2="${height - padding.bottom}" stroke="${PALETTE.border}" stroke-width="2"/>
|
|
1067
|
+
${bars}
|
|
1068
|
+
</svg>
|
|
1069
|
+
`;
|
|
1070
|
+
}
|
|
1071
|
+
function generateHTMLReport(options2) {
|
|
1072
|
+
const { config, singleResults, stats, lang, messages } = options2;
|
|
1073
|
+
const isZh = lang === "zh";
|
|
1074
|
+
const testTime = (/* @__PURE__ */ new Date()).toLocaleString(isZh ? "zh-CN" : "en-US");
|
|
1075
|
+
const summaryCards = [
|
|
1076
|
+
{
|
|
1077
|
+
label: messages.statsLabels.ttft,
|
|
1078
|
+
value: formatTime(stats.mean.ttft),
|
|
1079
|
+
detail: `${messages.statsHeaders.min}: ${formatTime(stats.min.ttft)} \xB7 ${messages.statsHeaders.max}: ${formatTime(stats.max.ttft)}`,
|
|
1080
|
+
accent: PALETTE.accent
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
label: messages.statsLabels.averageSpeed,
|
|
1084
|
+
value: formatNumber(stats.mean.averageSpeed),
|
|
1085
|
+
detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.averageSpeed)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.averageSpeed)}`,
|
|
1086
|
+
accent: PALETTE.accentSecondary,
|
|
1087
|
+
unit: messages.htmlSpeed
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
label: messages.statsLabels.peakSpeed,
|
|
1091
|
+
value: formatNumber(stats.mean.peakSpeed),
|
|
1092
|
+
detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.peakSpeed)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.peakSpeed)}`,
|
|
1093
|
+
accent: PALETTE.accentTertiary,
|
|
1094
|
+
unit: messages.htmlSpeed
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
label: messages.statsLabels.totalTokens,
|
|
1098
|
+
value: formatNumber(stats.mean.totalTokens, 0),
|
|
1099
|
+
detail: `${messages.statsHeaders.min}: ${formatNumber(stats.min.totalTokens, 0)} \xB7 ${messages.statsHeaders.max}: ${formatNumber(stats.max.totalTokens, 0)}`,
|
|
1100
|
+
accent: "#00ff88"
|
|
1101
|
+
}
|
|
1102
|
+
];
|
|
1103
|
+
const detailRows = singleResults.map(
|
|
1104
|
+
(result, idx) => `
|
|
1105
|
+
<tr>
|
|
1106
|
+
<td><span class="run-badge">${idx + 1}</span></td>
|
|
1107
|
+
<td>${formatTime(result.ttft)}</td>
|
|
1108
|
+
<td>${formatTime(result.totalTime)}</td>
|
|
1109
|
+
<td>${result.totalTokens}</td>
|
|
1110
|
+
<td>${formatNumber(result.averageSpeed)}</td>
|
|
1111
|
+
<td>${formatNumber(result.peakSpeed)}</td>
|
|
1112
|
+
<td>${result.peakTps}</td>
|
|
1113
|
+
</tr>
|
|
1114
|
+
`
|
|
1115
|
+
).join("");
|
|
1116
|
+
const statsRows = [
|
|
1117
|
+
{
|
|
1118
|
+
metric: messages.statsLabels.ttft,
|
|
1119
|
+
mean: formatTime(stats.mean.ttft),
|
|
1120
|
+
p50: formatTime(stats.percentiles.ttft.p50),
|
|
1121
|
+
p95: formatTime(stats.percentiles.ttft.p95),
|
|
1122
|
+
p99: formatTime(stats.percentiles.ttft.p99),
|
|
1123
|
+
min: formatTime(stats.min.ttft),
|
|
1124
|
+
max: formatTime(stats.max.ttft)
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
metric: messages.statsLabels.totalTime,
|
|
1128
|
+
mean: formatTime(stats.mean.totalTime),
|
|
1129
|
+
p50: formatTime(stats.percentiles.totalTime.p50),
|
|
1130
|
+
p95: formatTime(stats.percentiles.totalTime.p95),
|
|
1131
|
+
p99: formatTime(stats.percentiles.totalTime.p99),
|
|
1132
|
+
min: formatTime(stats.min.totalTime),
|
|
1133
|
+
max: formatTime(stats.max.totalTime)
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
metric: messages.statsLabels.totalTokens,
|
|
1137
|
+
mean: formatNumber(stats.mean.totalTokens, 1),
|
|
1138
|
+
p50: formatNumber(stats.percentiles.totalTokens.p50, 0),
|
|
1139
|
+
p95: formatNumber(stats.percentiles.totalTokens.p95, 0),
|
|
1140
|
+
p99: formatNumber(stats.percentiles.totalTokens.p99, 0),
|
|
1141
|
+
min: formatNumber(stats.min.totalTokens, 0),
|
|
1142
|
+
max: formatNumber(stats.max.totalTokens, 0)
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
metric: messages.statsLabels.averageSpeed,
|
|
1146
|
+
mean: formatNumber(stats.mean.averageSpeed),
|
|
1147
|
+
p50: formatNumber(stats.percentiles.averageSpeed.p50),
|
|
1148
|
+
p95: formatNumber(stats.percentiles.averageSpeed.p95),
|
|
1149
|
+
p99: formatNumber(stats.percentiles.averageSpeed.p99),
|
|
1150
|
+
min: formatNumber(stats.min.averageSpeed),
|
|
1151
|
+
max: formatNumber(stats.max.averageSpeed)
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
metric: messages.statsLabels.peakSpeed,
|
|
1155
|
+
mean: formatNumber(stats.mean.peakSpeed),
|
|
1156
|
+
p50: formatNumber(stats.percentiles.peakSpeed.p50),
|
|
1157
|
+
p95: formatNumber(stats.percentiles.peakSpeed.p95),
|
|
1158
|
+
p99: formatNumber(stats.percentiles.peakSpeed.p99),
|
|
1159
|
+
min: formatNumber(stats.min.peakSpeed),
|
|
1160
|
+
max: formatNumber(stats.max.peakSpeed)
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
metric: messages.statsLabels.peakTps,
|
|
1164
|
+
mean: formatNumber(stats.mean.peakTps),
|
|
1165
|
+
p50: formatNumber(stats.percentiles.peakTps.p50),
|
|
1166
|
+
p95: formatNumber(stats.percentiles.peakTps.p95),
|
|
1167
|
+
p99: formatNumber(stats.percentiles.peakTps.p99),
|
|
1168
|
+
min: formatNumber(stats.min.peakTps),
|
|
1169
|
+
max: formatNumber(stats.max.peakTps)
|
|
1170
|
+
}
|
|
1171
|
+
].map(
|
|
1172
|
+
(row) => `
|
|
1173
|
+
<tr>
|
|
1174
|
+
<td class="metric-name">${row.metric}</td>
|
|
1175
|
+
<td class="value-primary">${row.mean}</td>
|
|
1176
|
+
<td>${row.p50}</td>
|
|
1177
|
+
<td>${row.p95}</td>
|
|
1178
|
+
<td>${row.p99}</td>
|
|
1179
|
+
<td>${row.min}</td>
|
|
1180
|
+
<td>${row.max}</td>
|
|
1181
|
+
</tr>
|
|
1182
|
+
`
|
|
1183
|
+
).join("");
|
|
1184
|
+
const speedChart = generateSpeedChart(singleResults, messages);
|
|
1185
|
+
const tpsChart = generateTPSHistogram(stats, messages);
|
|
1186
|
+
const configGridHtml = `
|
|
1187
|
+
<div class="config-grid">
|
|
1188
|
+
<div class="config-item">
|
|
1189
|
+
<span class="config-label">${messages.configLabels.provider}</span>
|
|
1190
|
+
<span class="config-value">${config.provider.toUpperCase()}</span>
|
|
1191
|
+
</div>
|
|
1192
|
+
<div class="config-item">
|
|
1193
|
+
<span class="config-label">${messages.configLabels.model}</span>
|
|
1194
|
+
<span class="config-value">${escapeHtml(config.model)}</span>
|
|
1195
|
+
</div>
|
|
1196
|
+
<div class="config-item">
|
|
1197
|
+
<span class="config-label">${messages.configLabels.maxTokens}</span>
|
|
1198
|
+
<span class="config-value">${config.maxTokens}</span>
|
|
1199
|
+
</div>
|
|
1200
|
+
<div class="config-item">
|
|
1201
|
+
<span class="config-label">${messages.configLabels.runs}</span>
|
|
1202
|
+
<span class="config-value">${config.runCount}</span>
|
|
1203
|
+
</div>
|
|
1204
|
+
<div class="config-item wide">
|
|
1205
|
+
<span class="config-label">${messages.configLabels.prompt}</span>
|
|
1206
|
+
<span class="config-value">"${escapeHtml(config.prompt)}"</span>
|
|
1207
|
+
</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
`;
|
|
1210
|
+
const summaryCardsHtml = summaryCards.map(
|
|
1211
|
+
(card) => `
|
|
1212
|
+
<div class="card" style="--card-accent: ${card.accent}">
|
|
1213
|
+
<div class="card-label">${card.label}</div>
|
|
1214
|
+
<div class="card-value">${card.value}<span class="card-unit">${card.unit || ""}</span></div>
|
|
1215
|
+
<div class="card-detail">${card.detail}</div>
|
|
1216
|
+
</div>
|
|
1217
|
+
`
|
|
1218
|
+
).join("");
|
|
1219
|
+
const chartsHtml = `
|
|
1220
|
+
<div class="charts-container">
|
|
1221
|
+
<div class="chart-wrapper">
|
|
1222
|
+
<div class="chart-title">${messages.speedChartTitle}</div>
|
|
1223
|
+
${speedChart}
|
|
1224
|
+
</div>
|
|
1225
|
+
<div class="chart-wrapper">
|
|
1226
|
+
<div class="chart-title">${messages.htmlTpsDistribution}</div>
|
|
1227
|
+
${tpsChart}
|
|
1228
|
+
</div>
|
|
1229
|
+
</div>
|
|
1230
|
+
`;
|
|
1231
|
+
const statsTableHtml = `
|
|
1232
|
+
<div class="table-wrapper">
|
|
1233
|
+
<table>
|
|
1234
|
+
<thead>
|
|
1235
|
+
<tr>
|
|
1236
|
+
<th>${messages.statsHeaders.metric}</th>
|
|
1237
|
+
<th>${messages.statsHeaders.mean}</th>
|
|
1238
|
+
<th>${messages.statsHeaders.p50}</th>
|
|
1239
|
+
<th>${messages.statsHeaders.p95}</th>
|
|
1240
|
+
<th>${messages.statsHeaders.p99}</th>
|
|
1241
|
+
<th>${messages.statsHeaders.min}</th>
|
|
1242
|
+
<th>${messages.statsHeaders.max}</th>
|
|
1243
|
+
</tr>
|
|
1244
|
+
</thead>
|
|
1245
|
+
<tbody>
|
|
1246
|
+
${statsRows}
|
|
1247
|
+
</tbody>
|
|
1248
|
+
</table>
|
|
1249
|
+
</div>
|
|
1250
|
+
`;
|
|
1251
|
+
const detailsTableHtml = `
|
|
1252
|
+
<div class="table-wrapper">
|
|
1253
|
+
<table>
|
|
1254
|
+
<thead>
|
|
1255
|
+
<tr>
|
|
1256
|
+
<th>${messages.htmlRun}</th>
|
|
1257
|
+
<th>${messages.resultLabels.ttft}</th>
|
|
1258
|
+
<th>${messages.resultLabels.totalTime}</th>
|
|
1259
|
+
<th>${messages.resultLabels.totalTokens}</th>
|
|
1260
|
+
<th>${messages.resultLabels.averageSpeed}</th>
|
|
1261
|
+
<th>${messages.resultLabels.peakSpeed}</th>
|
|
1262
|
+
<th>${messages.resultLabels.peakTps}</th>
|
|
1263
|
+
</tr>
|
|
1264
|
+
</thead>
|
|
1265
|
+
<tbody>
|
|
1266
|
+
${detailRows}
|
|
1267
|
+
</tbody>
|
|
1268
|
+
</table>
|
|
1269
|
+
</div>
|
|
1270
|
+
`;
|
|
1271
|
+
const data = {
|
|
1272
|
+
lang,
|
|
1273
|
+
title: messages.htmlTitle,
|
|
1274
|
+
reportTitle: messages.htmlReportTitle,
|
|
1275
|
+
testTimeLabel: messages.htmlTestTime,
|
|
1276
|
+
testTime,
|
|
1277
|
+
configSection: messages.htmlConfigSection,
|
|
1278
|
+
summarySection: messages.htmlSummarySection,
|
|
1279
|
+
chartsSection: messages.htmlChartsSection,
|
|
1280
|
+
statsTitle: messages.statsSummaryTitle(stats.sampleSize),
|
|
1281
|
+
detailsSection: messages.htmlDetailsSection,
|
|
1282
|
+
configGrid: configGridHtml,
|
|
1283
|
+
summaryCards: summaryCardsHtml,
|
|
1284
|
+
charts: chartsHtml,
|
|
1285
|
+
statsTable: statsTableHtml,
|
|
1286
|
+
detailsTable: detailsTableHtml
|
|
1287
|
+
};
|
|
1288
|
+
const template = loadTemplate();
|
|
1289
|
+
return replaceTemplate(template, data);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
697
1292
|
// src/index.ts
|
|
698
1293
|
function getCliVersion() {
|
|
699
1294
|
try {
|
|
700
|
-
const currentDir =
|
|
1295
|
+
const currentDir = dirname2(fileURLToPath2(import.meta.url));
|
|
701
1296
|
const packagePath = join(currentDir, "..", "package.json");
|
|
702
|
-
const packageJson = JSON.parse(
|
|
1297
|
+
const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
|
|
703
1298
|
return packageJson.version ?? "unknown";
|
|
704
1299
|
} catch {
|
|
705
1300
|
return "unknown";
|
|
@@ -707,7 +1302,7 @@ function getCliVersion() {
|
|
|
707
1302
|
}
|
|
708
1303
|
var program = new Command();
|
|
709
1304
|
program.name("token-speed-test").description("A CLI tool to test LLM API token output speed").version(getCliVersion());
|
|
710
|
-
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").parse(process.argv);
|
|
1305
|
+
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);
|
|
711
1306
|
var options = program.opts();
|
|
712
1307
|
async function main() {
|
|
713
1308
|
let messages = getMessages(DEFAULT_LANG);
|
|
@@ -751,6 +1346,31 @@ ${messages.runningTests}
|
|
|
751
1346
|
console.log(chalk.green(`
|
|
752
1347
|
${messages.testComplete}
|
|
753
1348
|
`));
|
|
1349
|
+
if (options.html) {
|
|
1350
|
+
const htmlPath = options.output;
|
|
1351
|
+
const htmlContent = generateHTMLReport({
|
|
1352
|
+
config,
|
|
1353
|
+
singleResults: allMetrics,
|
|
1354
|
+
stats,
|
|
1355
|
+
lang: config.lang,
|
|
1356
|
+
messages
|
|
1357
|
+
});
|
|
1358
|
+
try {
|
|
1359
|
+
await fsWriteFile(htmlPath, htmlContent, "utf-8");
|
|
1360
|
+
console.log(chalk.cyan(messages.htmlGenerated(htmlPath)));
|
|
1361
|
+
await open(htmlPath).catch(() => {
|
|
1362
|
+
console.warn(chalk.yellow(messages.htmlOpenError(htmlPath)));
|
|
1363
|
+
});
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
console.error(
|
|
1366
|
+
chalk.red(
|
|
1367
|
+
`
|
|
1368
|
+
${messages.errorPrefix}: ${err instanceof Error ? err.message : String(err)}
|
|
1369
|
+
`
|
|
1370
|
+
)
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
754
1374
|
} catch (error) {
|
|
755
1375
|
if (error instanceof Error) {
|
|
756
1376
|
console.error(chalk.red(`
|