token-speed-tester 1.4.2 → 1.6.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 +54 -71
- package/README.md +14 -0
- package/dist/index.js +343 -94
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/README.en.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> A CLI tool to measure and analyze LLM API token streaming performance
|
|
4
4
|
|
|
5
|
-
[
|
|
5
|
+
[Chinese README](README.md)
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/token-speed-tester)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -18,6 +18,7 @@ A powerful command-line tool for testing token output speed of LLM APIs. Support
|
|
|
18
18
|
- **TTFT** (Time to First Token): Latency before first token arrives
|
|
19
19
|
- **Average Speed**: Mean tokens per second
|
|
20
20
|
- **Peak Speed**: Fastest speed over a 10-token window
|
|
21
|
+
- **Peak TPS**: Highest tokens received within a single second
|
|
21
22
|
- **TPS Curve**: Tokens received per second throughout the stream
|
|
22
23
|
- **Statistical Analysis**: Mean, min, max, and standard deviation across multiple test runs
|
|
23
24
|
- **ASCII Visualization**: Beautiful terminal-based charts and tables
|
|
@@ -50,7 +51,8 @@ npm install token-speed-tester
|
|
|
50
51
|
```bash
|
|
51
52
|
# Test Anthropic API (default)
|
|
52
53
|
token-speed-test --api-key sk-ant-xxx
|
|
53
|
-
|
|
54
|
+
# English output
|
|
55
|
+
token-speed-test --api-key sk-ant-xxx --lang en
|
|
54
56
|
# Test OpenAI API
|
|
55
57
|
token-speed-test --api-key sk-xxx --provider openai
|
|
56
58
|
```
|
|
@@ -101,8 +103,11 @@ node dist/index.js --api-key sk-ant-xxx
|
|
|
101
103
|
| `--model` | `-m` | Model name | Auto-selected by provider |
|
|
102
104
|
| `--url` | `-u` | Custom API endpoint | Official endpoint |
|
|
103
105
|
| `--runs` | `-r` | Number of test runs | `3` |
|
|
104
|
-
| `--prompt` | | Test prompt |
|
|
106
|
+
| `--prompt` | | Test prompt | Language-specific |
|
|
105
107
|
| `--max-tokens` | | Maximum output tokens | `1024` |
|
|
108
|
+
| `--lang` | | Output language: `zh` or `en` | `zh` |
|
|
109
|
+
|
|
110
|
+
Note: The default prompt follows the selected language. Use `--lang en` for the English default prompt.
|
|
106
111
|
|
|
107
112
|
### Default Models
|
|
108
113
|
|
|
@@ -112,82 +117,57 @@ node dist/index.js --api-key sk-ant-xxx
|
|
|
112
117
|
## Output Example
|
|
113
118
|
|
|
114
119
|
```
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
Token Speed Test
|
|
121
|
+
--------------------------------------------------
|
|
117
122
|
Provider: anthropic
|
|
118
123
|
Model: claude-opus-4-5-20251101
|
|
119
124
|
Max Tokens: 1024
|
|
120
125
|
Runs: 3
|
|
121
|
-
Prompt:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
[运行 1]
|
|
126
|
+
Prompt: Write a short essay about AI
|
|
127
|
+
--------------------------------------------------
|
|
128
|
+
Running tests...
|
|
129
|
+
Model output (streaming):
|
|
130
|
+
[Run 1]
|
|
127
131
|
TTFT: 523ms
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
[
|
|
132
|
+
Total Time: 3245ms
|
|
133
|
+
Total Tokens: 412
|
|
134
|
+
Avg Speed: 126.96 tokens/s
|
|
135
|
+
Peak Speed: 156.32 tokens/s
|
|
136
|
+
Peak TPS: 168.00 tokens/s
|
|
137
|
+
[Run 2]
|
|
134
138
|
TTFT: 487ms
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
[
|
|
139
|
+
Total Time: 3189ms
|
|
140
|
+
Total Tokens: 398
|
|
141
|
+
Avg Speed: 124.84 tokens/s
|
|
142
|
+
Peak Speed: 158.41 tokens/s
|
|
143
|
+
Peak TPS: 171.00 tokens/s
|
|
144
|
+
[Run 3]
|
|
141
145
|
TTFT: 501ms
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
Total Time: 3312ms
|
|
147
|
+
Total Tokens: 405
|
|
148
|
+
Avg Speed: 122.28 tokens/s
|
|
149
|
+
Peak Speed: 154.23 tokens/s
|
|
150
|
+
Peak TPS: 166.00 tokens/s
|
|
147
151
|
======================================================================
|
|
148
|
-
Token
|
|
152
|
+
Token Speed Test Report
|
|
149
153
|
======================================================================
|
|
154
|
+
Summary (N=3)
|
|
155
|
+
+-----------------+--------+-------+-------+---------+
|
|
156
|
+
| Metric | Mean | Min | Max | Std Dev |
|
|
157
|
+
+-----------------+--------+-------+-------+---------+
|
|
158
|
+
| TTFT (ms) | 503.67 | 487.00| 523.00| 14.57 |
|
|
159
|
+
| Total Time (ms) | 3248.67| 3189.00|3312.00|51.92 |
|
|
160
|
+
| Total Tokens | 405.00 | 398.00| 412.00| 5.35 |
|
|
161
|
+
| Avg Speed | 124.69 | 122.28| 126.96| 1.88 |
|
|
162
|
+
| Peak Speed | 156.32 | 154.23| 158.41| 1.82 |
|
|
163
|
+
| Peak TPS | 168.33 | 166.00| 171.00| 2.05 |
|
|
164
|
+
+-----------------+--------+-------+-------+---------+
|
|
165
|
+
Token Speed Trend (TPS)
|
|
166
|
+
[chart omitted]
|
|
167
|
+
TPS Distribution
|
|
168
|
+
[histogram omitted]
|
|
169
|
+
Tests complete!
|
|
150
170
|
|
|
151
|
-
统计汇总 (N=3)
|
|
152
|
-
┌──────────────────────────────────────────────────────────────────────┐
|
|
153
|
-
│ 指标 │ 均值 │ 最小值 │ 最大值 │ 标准差 │
|
|
154
|
-
├──────────────────────────────────────────────────────────────────────┤
|
|
155
|
-
│ TTFT (ms) │ 503.67 │ 487.00 │ 523.00 │ 14.57 │
|
|
156
|
-
├──────────────────────────────────────────────────────────────────────┤
|
|
157
|
-
│ 总耗时 (ms) │ 3248.67 │ 3189.00 │ 3312.00 │ 51.92 │
|
|
158
|
-
├──────────────────────────────────────────────────────────────────────┤
|
|
159
|
-
│ 总 Token 数 │ 405.00 │ 398.00 │ 412.00 │ 5.35 │
|
|
160
|
-
├──────────────────────────────────────────────────────────────────────┤
|
|
161
|
-
│ 平均速度 │ 124.69 │ 122.28 │ 126.96 │ 1.88 │
|
|
162
|
-
├──────────────────────────────────────────────────────────────────────┤
|
|
163
|
-
│ 峰值速度 │ 156.32 │ 154.23 │ 158.41 │ 1.82 │
|
|
164
|
-
└──────────────────────────────────────────────────────────────────────┘
|
|
165
|
-
|
|
166
|
-
Token 速度趋势图 (TPS)
|
|
167
|
-
┌────────────────────────────────────────┐
|
|
168
|
-
│ 120 ┤ █ │
|
|
169
|
-
│ 100 ┤ █ █ █ █ │
|
|
170
|
-
│ 80 ┤ █ █ █ █ █ █ █ │
|
|
171
|
-
│ 60 ┤ █ █ █ █ █ █ █ █ █ █ │
|
|
172
|
-
│ 40 ┤ █ █ █ █ █ █ █ █ █ █ █ █ │
|
|
173
|
-
│ 20 ┤ █ █ █ █ █ █ █ █ █ █ █ █ █ █ │
|
|
174
|
-
│ 0 └────────────────────────────────── │
|
|
175
|
-
│ 0s 1s 2s 3s 4s 5s 6s │
|
|
176
|
-
└────────────────────────────────────────┘
|
|
177
|
-
|
|
178
|
-
TPS 分布
|
|
179
|
-
0.0-12.0 │██████████████████████████████████████████████████ 45
|
|
180
|
-
12.0-24.0 │██ 3
|
|
181
|
-
24.0-36.0 │ 0
|
|
182
|
-
36.0-48.0 │ 0
|
|
183
|
-
48.0-60.0 │ 0
|
|
184
|
-
60.0-72.0 │ 0
|
|
185
|
-
72.0-84.0 │ 0
|
|
186
|
-
84.0-96.0 │ 0
|
|
187
|
-
96.0-108.0 │ 0
|
|
188
|
-
108.0-120.0 │ 0
|
|
189
|
-
|
|
190
|
-
✅ 测试完成!
|
|
191
171
|
```
|
|
192
172
|
|
|
193
173
|
## Metrics Explained
|
|
@@ -197,10 +177,13 @@ TPS 分布
|
|
|
197
177
|
| **TTFT** | Time to First Token - latency from request to first token arrival |
|
|
198
178
|
| **Total Time** | Complete duration from request to stream completion |
|
|
199
179
|
| **Total Tokens** | Number of output tokens received |
|
|
200
|
-
| **Average Speed** | Mean tokens per second (totalTokens / totalTime
|
|
180
|
+
| **Average Speed** | Mean tokens per second (totalTokens / totalTime x 1000) |
|
|
201
181
|
| **Peak Speed** | Fastest speed measured over a sliding 10-token window |
|
|
182
|
+
| **Peak TPS** | Highest tokens received within a single second |
|
|
202
183
|
| **TPS Curve** | Tokens received per second throughout the streaming response |
|
|
203
184
|
|
|
185
|
+
Note: Token counting uses the model tokenizer per stream chunk; boundary splits may cause slight differences.
|
|
186
|
+
|
|
204
187
|
## Development
|
|
205
188
|
|
|
206
189
|
### Running Tests
|
|
@@ -259,7 +242,7 @@ This project maintains high code coverage:
|
|
|
259
242
|
|
|
260
243
|
## License
|
|
261
244
|
|
|
262
|
-
MIT
|
|
245
|
+
MIT (c) [Cansiny0320](https://github.com/Cansiny0320)
|
|
263
246
|
|
|
264
247
|
## Contributing
|
|
265
248
|
|
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- **TTFT**(首字延迟):首个 Token 到达前的延迟
|
|
19
19
|
- **平均速度**:每秒平均 Token 数
|
|
20
20
|
- **峰值速度**:10 个 Token 滑动窗口内的最快速度
|
|
21
|
+
- **峰值 TPS**:单秒内的最高 Token 数
|
|
21
22
|
- **TPS 曲线**:整个流式响应中每秒接收的 Token 数
|
|
22
23
|
- **统计分析**:多次测试运行的均值、最小值、最大值和标准差
|
|
23
24
|
- **ASCII 可视化**:精美的终端图表和数据表格
|
|
@@ -50,6 +51,8 @@ npm install token-speed-tester
|
|
|
50
51
|
```bash
|
|
51
52
|
# 测试 Anthropic API(默认)
|
|
52
53
|
token-speed-test --api-key sk-ant-xxx
|
|
54
|
+
# 输出英文结果
|
|
55
|
+
token-speed-test --api-key sk-ant-xxx --lang en
|
|
53
56
|
|
|
54
57
|
# 测试 OpenAI API
|
|
55
58
|
token-speed-test --api-key sk-xxx --provider openai
|
|
@@ -103,6 +106,7 @@ node dist/index.js --api-key sk-ant-xxx
|
|
|
103
106
|
| `--runs` | `-r` | 测试次数 | `3` |
|
|
104
107
|
| `--prompt` | | 测试提示词 | "写一篇关于 AI 的短文" |
|
|
105
108
|
| `--max-tokens` | | 最大输出 Token 数 | `1024` |
|
|
109
|
+
| `--lang` | | 输出语言: `zh` 或 `en` | `zh` |
|
|
106
110
|
|
|
107
111
|
### 默认模型
|
|
108
112
|
|
|
@@ -123,12 +127,15 @@ Prompt: 写一篇关于 AI 的短文
|
|
|
123
127
|
|
|
124
128
|
⏳ 正在运行测试...
|
|
125
129
|
|
|
130
|
+
模型输出 (流式):
|
|
131
|
+
|
|
126
132
|
[运行 1]
|
|
127
133
|
TTFT: 523ms
|
|
128
134
|
总耗时: 3245ms
|
|
129
135
|
总 Token 数: 412
|
|
130
136
|
平均速度: 126.96 tokens/s
|
|
131
137
|
峰值速度: 156.32 tokens/s
|
|
138
|
+
峰值 TPS: 168.00 tokens/s
|
|
132
139
|
|
|
133
140
|
[运行 2]
|
|
134
141
|
TTFT: 487ms
|
|
@@ -136,6 +143,7 @@ Prompt: 写一篇关于 AI 的短文
|
|
|
136
143
|
总 Token 数: 398
|
|
137
144
|
平均速度: 124.84 tokens/s
|
|
138
145
|
峰值速度: 158.41 tokens/s
|
|
146
|
+
峰值 TPS: 171.00 tokens/s
|
|
139
147
|
|
|
140
148
|
[运行 3]
|
|
141
149
|
TTFT: 501ms
|
|
@@ -143,6 +151,7 @@ Prompt: 写一篇关于 AI 的短文
|
|
|
143
151
|
总 Token 数: 405
|
|
144
152
|
平均速度: 122.28 tokens/s
|
|
145
153
|
峰值速度: 154.23 tokens/s
|
|
154
|
+
峰值 TPS: 166.00 tokens/s
|
|
146
155
|
|
|
147
156
|
======================================================================
|
|
148
157
|
Token 速度测试报告
|
|
@@ -161,6 +170,8 @@ Token 速度测试报告
|
|
|
161
170
|
│ 平均速度 │ 124.69 │ 122.28 │ 126.96 │ 1.88 │
|
|
162
171
|
├──────────────────────────────────────────────────────────────────────┤
|
|
163
172
|
│ 峰值速度 │ 156.32 │ 154.23 │ 158.41 │ 1.82 │
|
|
173
|
+
├──────────────────────────────────────────────────────────────────────┤
|
|
174
|
+
│ 峰值 TPS │ 168.33 │ 166.00 │ 171.00 │ 2.05 │
|
|
164
175
|
└──────────────────────────────────────────────────────────────────────┘
|
|
165
176
|
|
|
166
177
|
Token 速度趋势图 (TPS)
|
|
@@ -199,8 +210,11 @@ TPS 分布
|
|
|
199
210
|
| **总 Token 数** | 接收到的输出 Token 数量 |
|
|
200
211
|
| **平均速度** | 每秒平均 Token 数(totalTokens / totalTime × 1000) |
|
|
201
212
|
| **峰值速度** | 10 个 Token 滑动窗口内测量的最快速度 |
|
|
213
|
+
| **峰值 TPS** | 单秒内最高 Token 数 |
|
|
202
214
|
| **TPS 曲线** | 整个流式响应中每秒接收的 Token 数 |
|
|
203
215
|
|
|
216
|
+
注:Token 统计基于模型 tokenizer,并按流式分片计数,分片边界可能带来轻微差异。
|
|
217
|
+
|
|
204
218
|
## 开发
|
|
205
219
|
|
|
206
220
|
### 运行测试
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,120 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
|
|
10
|
+
// src/i18n.ts
|
|
11
|
+
var SUPPORTED_LANGS = ["zh", "en"];
|
|
12
|
+
var DEFAULT_LANG = "zh";
|
|
13
|
+
var zhMessages = {
|
|
14
|
+
defaultPrompt: "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587",
|
|
15
|
+
appTitle: "\u{1F680} Token \u901F\u5EA6\u6D4B\u8BD5\u5DE5\u5177",
|
|
16
|
+
runningTests: "\u23F3 \u6B63\u5728\u8FD0\u884C\u6D4B\u8BD5...",
|
|
17
|
+
streamingOutput: "\u6A21\u578B\u8F93\u51FA (\u6D41\u5F0F):",
|
|
18
|
+
testComplete: "\u2705 \u6D4B\u8BD5\u5B8C\u6210!",
|
|
19
|
+
errorPrefix: "\u274C \u9519\u8BEF",
|
|
20
|
+
unknownError: "\u274C \u53D1\u751F\u672A\u77E5\u9519\u8BEF",
|
|
21
|
+
configLabels: {
|
|
22
|
+
provider: "Provider",
|
|
23
|
+
model: "Model",
|
|
24
|
+
maxTokens: "Max Tokens",
|
|
25
|
+
runs: "Runs",
|
|
26
|
+
prompt: "Prompt"
|
|
27
|
+
},
|
|
28
|
+
runLabel: (index) => `[\u8FD0\u884C ${index}]`,
|
|
29
|
+
runProgressLabel: (current, total) => `[\u8FD0\u884C ${current}/${total}]`,
|
|
30
|
+
reportTitle: "Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A",
|
|
31
|
+
speedChartTitle: "Token \u901F\u5EA6\u8D8B\u52BF\u56FE (TPS)",
|
|
32
|
+
tpsHistogramTitle: "TPS \u5206\u5E03",
|
|
33
|
+
noChartData: "\u6CA1\u6709\u53EF\u7528\u4E8E\u56FE\u8868\u7684\u6570\u636E",
|
|
34
|
+
noTpsData: "\u6CA1\u6709 TPS \u6570\u636E\u53EF\u7528",
|
|
35
|
+
statsSummaryTitle: (sampleSize) => `\u7EDF\u8BA1\u6C47\u603B (N=${sampleSize})`,
|
|
36
|
+
statsHeaders: {
|
|
37
|
+
metric: "\u6307\u6807",
|
|
38
|
+
mean: "\u5747\u503C",
|
|
39
|
+
min: "\u6700\u5C0F\u503C",
|
|
40
|
+
max: "\u6700\u5927\u503C",
|
|
41
|
+
stdDev: "\u6807\u51C6\u5DEE"
|
|
42
|
+
},
|
|
43
|
+
statsLabels: {
|
|
44
|
+
ttft: "TTFT (ms)",
|
|
45
|
+
totalTime: "\u603B\u8017\u65F6 (ms)",
|
|
46
|
+
totalTokens: "\u603B Token \u6570",
|
|
47
|
+
averageSpeed: "\u5E73\u5747\u901F\u5EA6",
|
|
48
|
+
peakSpeed: "\u5CF0\u503C\u901F\u5EA6",
|
|
49
|
+
peakTps: "\u5CF0\u503C TPS"
|
|
50
|
+
},
|
|
51
|
+
resultLabels: {
|
|
52
|
+
ttft: "TTFT",
|
|
53
|
+
totalTime: "\u603B\u8017\u65F6",
|
|
54
|
+
totalTokens: "\u603B Token \u6570",
|
|
55
|
+
averageSpeed: "\u5E73\u5747\u901F\u5EA6",
|
|
56
|
+
peakSpeed: "\u5CF0\u503C\u901F\u5EA6",
|
|
57
|
+
peakTps: "\u5CF0\u503C TPS"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var enMessages = {
|
|
61
|
+
defaultPrompt: "Write a short essay about AI",
|
|
62
|
+
appTitle: "\u{1F680} Token Speed Test",
|
|
63
|
+
runningTests: "\u23F3 Running tests...",
|
|
64
|
+
streamingOutput: "Model output (streaming):",
|
|
65
|
+
testComplete: "\u2705 Tests complete!",
|
|
66
|
+
errorPrefix: "\u274C Error",
|
|
67
|
+
unknownError: "\u274C An unknown error occurred",
|
|
68
|
+
configLabels: {
|
|
69
|
+
provider: "Provider",
|
|
70
|
+
model: "Model",
|
|
71
|
+
maxTokens: "Max Tokens",
|
|
72
|
+
runs: "Runs",
|
|
73
|
+
prompt: "Prompt"
|
|
74
|
+
},
|
|
75
|
+
runLabel: (index) => `[Run ${index}]`,
|
|
76
|
+
runProgressLabel: (current, total) => `[Run ${current}/${total}]`,
|
|
77
|
+
reportTitle: "Token Speed Test Report",
|
|
78
|
+
speedChartTitle: "Token Speed Trend (TPS)",
|
|
79
|
+
tpsHistogramTitle: "TPS Distribution",
|
|
80
|
+
noChartData: "No data available for chart",
|
|
81
|
+
noTpsData: "No TPS data available",
|
|
82
|
+
statsSummaryTitle: (sampleSize) => `Summary (N=${sampleSize})`,
|
|
83
|
+
statsHeaders: {
|
|
84
|
+
metric: "Metric",
|
|
85
|
+
mean: "Mean",
|
|
86
|
+
min: "Min",
|
|
87
|
+
max: "Max",
|
|
88
|
+
stdDev: "Std Dev"
|
|
89
|
+
},
|
|
90
|
+
statsLabels: {
|
|
91
|
+
ttft: "TTFT (ms)",
|
|
92
|
+
totalTime: "Total Time (ms)",
|
|
93
|
+
totalTokens: "Total Tokens",
|
|
94
|
+
averageSpeed: "Avg Speed",
|
|
95
|
+
peakSpeed: "Peak Speed",
|
|
96
|
+
peakTps: "Peak TPS"
|
|
97
|
+
},
|
|
98
|
+
resultLabels: {
|
|
99
|
+
ttft: "TTFT",
|
|
100
|
+
totalTime: "Total Time",
|
|
101
|
+
totalTokens: "Total Tokens",
|
|
102
|
+
averageSpeed: "Avg Speed",
|
|
103
|
+
peakSpeed: "Peak Speed",
|
|
104
|
+
peakTps: "Peak TPS"
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function isSupportedLang(value) {
|
|
108
|
+
return SUPPORTED_LANGS.includes(value);
|
|
109
|
+
}
|
|
110
|
+
function resolveLang(value) {
|
|
111
|
+
if (!value) {
|
|
112
|
+
return DEFAULT_LANG;
|
|
113
|
+
}
|
|
114
|
+
const normalized = value.toLowerCase();
|
|
115
|
+
if (!isSupportedLang(normalized)) {
|
|
116
|
+
throw new Error(`Invalid lang: ${value}. Must be 'zh' or 'en'.`);
|
|
117
|
+
}
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
function getMessages(lang) {
|
|
121
|
+
return lang === "en" ? enMessages : zhMessages;
|
|
122
|
+
}
|
|
123
|
+
|
|
10
124
|
// src/config.ts
|
|
11
125
|
var DEFAULT_MODELS = {
|
|
12
126
|
anthropic: "claude-opus-4-5-20251101",
|
|
@@ -14,7 +128,6 @@ var DEFAULT_MODELS = {
|
|
|
14
128
|
};
|
|
15
129
|
var DEFAULT_MAX_TOKENS = 1024;
|
|
16
130
|
var DEFAULT_RUNS = 3;
|
|
17
|
-
var DEFAULT_PROMPT = "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587";
|
|
18
131
|
function parseConfig(args) {
|
|
19
132
|
const {
|
|
20
133
|
apiKey,
|
|
@@ -23,8 +136,12 @@ function parseConfig(args) {
|
|
|
23
136
|
model,
|
|
24
137
|
maxTokens = DEFAULT_MAX_TOKENS,
|
|
25
138
|
runs = DEFAULT_RUNS,
|
|
26
|
-
prompt
|
|
139
|
+
prompt,
|
|
140
|
+
lang: langInput
|
|
27
141
|
} = args;
|
|
142
|
+
const lang = resolveLang(langInput);
|
|
143
|
+
const messages = getMessages(lang);
|
|
144
|
+
const finalPrompt = prompt ?? messages.defaultPrompt;
|
|
28
145
|
if (!apiKey || apiKey.trim() === "") {
|
|
29
146
|
throw new Error("API Key is required. Use --api-key or -k to provide it.");
|
|
30
147
|
}
|
|
@@ -45,18 +162,40 @@ function parseConfig(args) {
|
|
|
45
162
|
model: finalModel,
|
|
46
163
|
maxTokens,
|
|
47
164
|
runCount: runs,
|
|
48
|
-
prompt:
|
|
165
|
+
prompt: finalPrompt.trim(),
|
|
166
|
+
lang
|
|
49
167
|
};
|
|
50
168
|
}
|
|
51
169
|
|
|
52
170
|
// src/client.ts
|
|
171
|
+
import { performance } from "perf_hooks";
|
|
53
172
|
import Anthropic from "@anthropic-ai/sdk";
|
|
54
173
|
import OpenAI from "openai";
|
|
174
|
+
|
|
175
|
+
// src/tokenizer.ts
|
|
176
|
+
import { encoding_for_model, get_encoding } from "tiktoken";
|
|
177
|
+
var FALLBACK_ENCODING = "cl100k_base";
|
|
178
|
+
function createTokenizer(model) {
|
|
179
|
+
try {
|
|
180
|
+
const normalized = model.trim();
|
|
181
|
+
if (!normalized) {
|
|
182
|
+
return get_encoding(FALLBACK_ENCODING);
|
|
183
|
+
}
|
|
184
|
+
return encoding_for_model(normalized);
|
|
185
|
+
} catch {
|
|
186
|
+
return get_encoding(FALLBACK_ENCODING);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/client.ts
|
|
55
191
|
async function anthropicStreamTest(config) {
|
|
56
|
-
const startTime =
|
|
192
|
+
const startTime = performance.now();
|
|
57
193
|
const tokenTimes = [];
|
|
58
194
|
let ttft = 0;
|
|
59
195
|
let firstTokenRecorded = false;
|
|
196
|
+
let tokenCount = 0;
|
|
197
|
+
let wroteOutput = false;
|
|
198
|
+
const encoding = createTokenizer(config.model);
|
|
60
199
|
const client = new Anthropic({
|
|
61
200
|
apiKey: config.apiKey,
|
|
62
201
|
baseURL: config.baseURL
|
|
@@ -69,16 +208,23 @@ async function anthropicStreamTest(config) {
|
|
|
69
208
|
stream: true
|
|
70
209
|
});
|
|
71
210
|
for await (const event of stream) {
|
|
72
|
-
const currentTime =
|
|
211
|
+
const currentTime = performance.now();
|
|
73
212
|
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
74
213
|
const text = event.delta.text;
|
|
75
214
|
if (text && text.length > 0) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
215
|
+
process.stdout.write(text);
|
|
216
|
+
wroteOutput = true;
|
|
217
|
+
const encoded = encoding.encode(text);
|
|
218
|
+
const newTokens = encoded.length;
|
|
219
|
+
if (newTokens > 0) {
|
|
220
|
+
if (!firstTokenRecorded) {
|
|
221
|
+
ttft = currentTime - startTime;
|
|
222
|
+
firstTokenRecorded = true;
|
|
223
|
+
}
|
|
224
|
+
for (let i = 0; i < newTokens; i++) {
|
|
225
|
+
tokenTimes.push(currentTime - startTime);
|
|
226
|
+
}
|
|
227
|
+
tokenCount += newTokens;
|
|
82
228
|
}
|
|
83
229
|
}
|
|
84
230
|
}
|
|
@@ -88,21 +234,29 @@ async function anthropicStreamTest(config) {
|
|
|
88
234
|
throw new Error(`Anthropic API error: ${error.message}`);
|
|
89
235
|
}
|
|
90
236
|
throw error;
|
|
237
|
+
} finally {
|
|
238
|
+
if (wroteOutput) {
|
|
239
|
+
process.stdout.write("\n");
|
|
240
|
+
}
|
|
241
|
+
encoding.free();
|
|
91
242
|
}
|
|
92
|
-
const endTime =
|
|
243
|
+
const endTime = performance.now();
|
|
93
244
|
const totalTime = endTime - startTime;
|
|
94
245
|
return {
|
|
95
246
|
ttft,
|
|
96
247
|
tokens: tokenTimes,
|
|
97
|
-
totalTokens:
|
|
248
|
+
totalTokens: tokenCount,
|
|
98
249
|
totalTime
|
|
99
250
|
};
|
|
100
251
|
}
|
|
101
252
|
async function openaiStreamTest(config) {
|
|
102
|
-
const startTime =
|
|
253
|
+
const startTime = performance.now();
|
|
103
254
|
const tokenTimes = [];
|
|
104
255
|
let ttft = 0;
|
|
105
256
|
let firstTokenRecorded = false;
|
|
257
|
+
let tokenCount = 0;
|
|
258
|
+
let wroteOutput = false;
|
|
259
|
+
const encoding = createTokenizer(config.model);
|
|
106
260
|
const client = new OpenAI({
|
|
107
261
|
apiKey: config.apiKey,
|
|
108
262
|
baseURL: config.baseURL
|
|
@@ -115,16 +269,25 @@ async function openaiStreamTest(config) {
|
|
|
115
269
|
stream: true
|
|
116
270
|
});
|
|
117
271
|
for await (const chunk of stream) {
|
|
118
|
-
const currentTime =
|
|
272
|
+
const currentTime = performance.now();
|
|
119
273
|
const delta = chunk.choices[0]?.delta;
|
|
120
274
|
if (delta?.content) {
|
|
121
275
|
const content = delta.content;
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
276
|
+
if (content.length > 0) {
|
|
277
|
+
process.stdout.write(content);
|
|
278
|
+
wroteOutput = true;
|
|
279
|
+
const encoded = encoding.encode(content);
|
|
280
|
+
const newTokens = encoded.length;
|
|
281
|
+
if (newTokens > 0) {
|
|
282
|
+
if (!firstTokenRecorded) {
|
|
283
|
+
ttft = currentTime - startTime;
|
|
284
|
+
firstTokenRecorded = true;
|
|
285
|
+
}
|
|
286
|
+
for (let i = 0; i < newTokens; i++) {
|
|
287
|
+
tokenTimes.push(currentTime - startTime);
|
|
288
|
+
}
|
|
289
|
+
tokenCount += newTokens;
|
|
290
|
+
}
|
|
128
291
|
}
|
|
129
292
|
}
|
|
130
293
|
}
|
|
@@ -133,13 +296,18 @@ async function openaiStreamTest(config) {
|
|
|
133
296
|
throw new Error(`OpenAI API error: ${error.message}`);
|
|
134
297
|
}
|
|
135
298
|
throw error;
|
|
299
|
+
} finally {
|
|
300
|
+
if (wroteOutput) {
|
|
301
|
+
process.stdout.write("\n");
|
|
302
|
+
}
|
|
303
|
+
encoding.free();
|
|
136
304
|
}
|
|
137
|
-
const endTime =
|
|
305
|
+
const endTime = performance.now();
|
|
138
306
|
const totalTime = endTime - startTime;
|
|
139
307
|
return {
|
|
140
308
|
ttft,
|
|
141
309
|
tokens: tokenTimes,
|
|
142
|
-
totalTokens:
|
|
310
|
+
totalTokens: tokenCount,
|
|
143
311
|
totalTime
|
|
144
312
|
};
|
|
145
313
|
}
|
|
@@ -152,7 +320,14 @@ async function streamTest(config) {
|
|
|
152
320
|
}
|
|
153
321
|
async function runMultipleTests(config) {
|
|
154
322
|
const results = [];
|
|
323
|
+
const messages = getMessages(config.lang);
|
|
155
324
|
for (let i = 0; i < config.runCount; i++) {
|
|
325
|
+
if (config.runCount > 1) {
|
|
326
|
+
const label = `
|
|
327
|
+
${messages.runProgressLabel(i + 1, config.runCount)}`;
|
|
328
|
+
console.log(label);
|
|
329
|
+
console.log("-".repeat(label.length - 1));
|
|
330
|
+
}
|
|
156
331
|
const result = await streamTest(config);
|
|
157
332
|
results.push(result);
|
|
158
333
|
}
|
|
@@ -169,23 +344,24 @@ function calculateAverageSpeed(metrics) {
|
|
|
169
344
|
}
|
|
170
345
|
return metrics.totalTokens / metrics.totalTime * 1e3;
|
|
171
346
|
}
|
|
347
|
+
var MIN_PEAK_WINDOW_MS = 50;
|
|
172
348
|
function calculatePeakSpeed(metrics, windowSize = 10) {
|
|
173
349
|
if (metrics.tokens.length < windowSize) {
|
|
174
350
|
if (metrics.tokens.length < 2) {
|
|
175
351
|
return 0;
|
|
176
352
|
}
|
|
177
353
|
const totalTime = metrics.tokens[metrics.tokens.length - 1] - metrics.tokens[0];
|
|
178
|
-
|
|
354
|
+
const durationMs = Math.max(totalTime, MIN_PEAK_WINDOW_MS);
|
|
355
|
+
return (metrics.tokens.length - 1) / durationMs * 1e3;
|
|
179
356
|
}
|
|
180
357
|
let maxSpeed = 0;
|
|
181
358
|
for (let i = 0; i <= metrics.tokens.length - windowSize; i++) {
|
|
182
359
|
const startTime = metrics.tokens[i];
|
|
183
360
|
const endTime = metrics.tokens[i + windowSize - 1];
|
|
184
361
|
const duration = endTime - startTime;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
362
|
+
const durationMs = Math.max(duration, MIN_PEAK_WINDOW_MS);
|
|
363
|
+
const speed = (windowSize - 1) / durationMs * 1e3;
|
|
364
|
+
maxSpeed = Math.max(maxSpeed, speed);
|
|
189
365
|
}
|
|
190
366
|
return maxSpeed;
|
|
191
367
|
}
|
|
@@ -208,13 +384,15 @@ function calculateTPS(metrics) {
|
|
|
208
384
|
return tps;
|
|
209
385
|
}
|
|
210
386
|
function calculateMetrics(metrics) {
|
|
387
|
+
const tps = calculateTPS(metrics);
|
|
211
388
|
return {
|
|
212
389
|
ttft: calculateTTFT(metrics),
|
|
213
390
|
totalTime: metrics.totalTime,
|
|
214
391
|
totalTokens: metrics.totalTokens,
|
|
215
392
|
averageSpeed: calculateAverageSpeed(metrics),
|
|
216
393
|
peakSpeed: calculatePeakSpeed(metrics),
|
|
217
|
-
|
|
394
|
+
peakTps: tps.length > 0 ? Math.max(...tps) : 0,
|
|
395
|
+
tps
|
|
218
396
|
};
|
|
219
397
|
}
|
|
220
398
|
function mean(values) {
|
|
@@ -237,6 +415,7 @@ function calculateStats(allMetrics) {
|
|
|
237
415
|
const totalTokens = allMetrics.map((m) => m.totalTokens);
|
|
238
416
|
const averageSpeeds = allMetrics.map((m) => m.averageSpeed);
|
|
239
417
|
const peakSpeeds = allMetrics.map((m) => m.peakSpeed);
|
|
418
|
+
const peakTpsValues = allMetrics.map((m) => m.peakTps);
|
|
240
419
|
const maxTpsLength = Math.max(...allMetrics.map((m) => m.tps.length));
|
|
241
420
|
const avgTps = [];
|
|
242
421
|
for (let i = 0; i < maxTpsLength; i++) {
|
|
@@ -250,6 +429,7 @@ function calculateStats(allMetrics) {
|
|
|
250
429
|
totalTokens: mean(totalTokens),
|
|
251
430
|
averageSpeed: mean(averageSpeeds),
|
|
252
431
|
peakSpeed: mean(peakSpeeds),
|
|
432
|
+
peakTps: mean(peakTpsValues),
|
|
253
433
|
tps: avgTps
|
|
254
434
|
},
|
|
255
435
|
min: {
|
|
@@ -258,6 +438,7 @@ function calculateStats(allMetrics) {
|
|
|
258
438
|
totalTokens: Math.min(...totalTokens),
|
|
259
439
|
averageSpeed: Math.min(...averageSpeeds),
|
|
260
440
|
peakSpeed: Math.min(...peakSpeeds),
|
|
441
|
+
peakTps: Math.min(...peakTpsValues),
|
|
261
442
|
tps: []
|
|
262
443
|
},
|
|
263
444
|
max: {
|
|
@@ -266,6 +447,7 @@ function calculateStats(allMetrics) {
|
|
|
266
447
|
totalTokens: Math.max(...totalTokens),
|
|
267
448
|
averageSpeed: Math.max(...averageSpeeds),
|
|
268
449
|
peakSpeed: Math.max(...peakSpeeds),
|
|
450
|
+
peakTps: Math.max(...peakTpsValues),
|
|
269
451
|
tps: []
|
|
270
452
|
},
|
|
271
453
|
stdDev: {
|
|
@@ -274,6 +456,7 @@ function calculateStats(allMetrics) {
|
|
|
274
456
|
totalTokens: standardDeviation(totalTokens),
|
|
275
457
|
averageSpeed: standardDeviation(averageSpeeds),
|
|
276
458
|
peakSpeed: standardDeviation(peakSpeeds),
|
|
459
|
+
peakTps: standardDeviation(peakTpsValues),
|
|
277
460
|
tps: []
|
|
278
461
|
},
|
|
279
462
|
sampleSize
|
|
@@ -281,39 +464,69 @@ function calculateStats(allMetrics) {
|
|
|
281
464
|
}
|
|
282
465
|
|
|
283
466
|
// src/chart.ts
|
|
467
|
+
import stringWidth from "string-width";
|
|
284
468
|
var BLOCK_CHAR = "\u2588";
|
|
285
469
|
var CHART_WIDTH = 50;
|
|
286
470
|
var CHART_HEIGHT = 10;
|
|
287
|
-
|
|
471
|
+
var STAT_LABEL_WIDTH = 15;
|
|
472
|
+
var STAT_VALUE_WIDTH = 10;
|
|
473
|
+
var Y_LABEL_WIDTH = 4;
|
|
474
|
+
function padEndWidth(text, width) {
|
|
475
|
+
const currentWidth = stringWidth(text);
|
|
476
|
+
if (currentWidth >= width) {
|
|
477
|
+
return text;
|
|
478
|
+
}
|
|
479
|
+
return text + " ".repeat(width - currentWidth);
|
|
480
|
+
}
|
|
481
|
+
function padStartWidth(text, width) {
|
|
482
|
+
const currentWidth = stringWidth(text);
|
|
483
|
+
if (currentWidth >= width) {
|
|
484
|
+
return text;
|
|
485
|
+
}
|
|
486
|
+
return " ".repeat(width - currentWidth) + text;
|
|
487
|
+
}
|
|
488
|
+
function renderSpeedChart(tps, maxSpeed, lang = DEFAULT_LANG) {
|
|
489
|
+
const messages = getMessages(lang);
|
|
288
490
|
if (tps.length === 0) {
|
|
289
|
-
return
|
|
491
|
+
return messages.noChartData;
|
|
290
492
|
}
|
|
291
493
|
const actualMax = maxSpeed ?? Math.max(...tps, 1);
|
|
292
494
|
const maxVal = Math.max(actualMax, 1);
|
|
495
|
+
const buildRow = (label, bars) => `\u2502 ${padStartWidth(label, Y_LABEL_WIDTH)} \u2524${bars} \u2502`;
|
|
496
|
+
const emptyRow = buildRow("0", " ".repeat(CHART_WIDTH));
|
|
497
|
+
const chartWidth = stringWidth(emptyRow) - 2;
|
|
498
|
+
const axisPrefix = `\u2502 ${padStartWidth("", Y_LABEL_WIDTH)} \u253C`;
|
|
293
499
|
const lines = [];
|
|
294
|
-
lines.push(
|
|
295
|
-
lines.push("\u250C" + "\u2500".repeat(
|
|
500
|
+
lines.push(messages.speedChartTitle);
|
|
501
|
+
lines.push("\u250C" + "\u2500".repeat(chartWidth) + "\u2510");
|
|
296
502
|
for (let row = CHART_HEIGHT - 1; row >= 0; row--) {
|
|
297
503
|
const value = row / (CHART_HEIGHT - 1) * maxVal;
|
|
298
|
-
const label = value.toFixed(0)
|
|
299
|
-
let
|
|
504
|
+
const label = value.toFixed(0);
|
|
505
|
+
let bars = "";
|
|
300
506
|
for (let col = 0; col < CHART_WIDTH; col++) {
|
|
301
507
|
const index = Math.floor(col / CHART_WIDTH * tps.length);
|
|
302
508
|
const tpsValue = tps[index] ?? 0;
|
|
303
509
|
const normalizedHeight = tpsValue / maxVal * (CHART_HEIGHT - 1);
|
|
304
|
-
|
|
305
|
-
chartRow += BLOCK_CHAR;
|
|
306
|
-
} else {
|
|
307
|
-
chartRow += " ";
|
|
308
|
-
}
|
|
510
|
+
bars += normalizedHeight >= row ? BLOCK_CHAR : " ";
|
|
309
511
|
}
|
|
310
|
-
|
|
311
|
-
lines.push(chartRow);
|
|
512
|
+
lines.push(buildRow(label, bars));
|
|
312
513
|
}
|
|
313
|
-
lines.push("\
|
|
314
|
-
lines.push("\u2514" + "\u2500".repeat(
|
|
514
|
+
lines.push(`${axisPrefix}${"\u2500".repeat(CHART_WIDTH)} \u2502`);
|
|
515
|
+
lines.push("\u2514" + "\u2500".repeat(chartWidth) + "\u2518");
|
|
315
516
|
const xLabels = generateXLabels(tps.length, 6);
|
|
316
|
-
|
|
517
|
+
const labelLine = new Array(CHART_WIDTH).fill(" ");
|
|
518
|
+
const maxIndex = Math.max(tps.length - 1, 1);
|
|
519
|
+
for (const label of xLabels) {
|
|
520
|
+
const seconds = parseInt(label.replace("s", ""), 10);
|
|
521
|
+
const position = Math.min(
|
|
522
|
+
CHART_WIDTH - 1,
|
|
523
|
+
Math.round(seconds / maxIndex * (CHART_WIDTH - 1))
|
|
524
|
+
);
|
|
525
|
+
for (let i = 0; i < label.length && position + i < CHART_WIDTH; i++) {
|
|
526
|
+
labelLine[position + i] = label[i];
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
lines.push(" ".repeat(stringWidth(axisPrefix)) + labelLine.join(""));
|
|
317
530
|
return lines.join("\n");
|
|
318
531
|
}
|
|
319
532
|
function generateXLabels(dataPoints, maxLabels) {
|
|
@@ -330,12 +543,13 @@ function generateXLabels(dataPoints, maxLabels) {
|
|
|
330
543
|
}
|
|
331
544
|
return labels;
|
|
332
545
|
}
|
|
333
|
-
function renderTPSHistogram(tps) {
|
|
546
|
+
function renderTPSHistogram(tps, lang = DEFAULT_LANG) {
|
|
547
|
+
const messages = getMessages(lang);
|
|
334
548
|
if (tps.length === 0) {
|
|
335
|
-
return
|
|
549
|
+
return messages.noTpsData;
|
|
336
550
|
}
|
|
337
551
|
const lines = [];
|
|
338
|
-
lines.push(
|
|
552
|
+
lines.push(messages.tpsHistogramTitle);
|
|
339
553
|
const maxTps = Math.max(...tps, 1);
|
|
340
554
|
const buckets = 10;
|
|
341
555
|
const bucketSize = maxTps / buckets;
|
|
@@ -345,28 +559,35 @@ function renderTPSHistogram(tps) {
|
|
|
345
559
|
histogram[bucketIndex]++;
|
|
346
560
|
}
|
|
347
561
|
const maxCount = Math.max(...histogram, 1);
|
|
348
|
-
|
|
562
|
+
const labels = histogram.map((_, i) => {
|
|
349
563
|
const bucketStart = (i * bucketSize).toFixed(1);
|
|
350
564
|
const bucketEnd = ((i + 1) * bucketSize).toFixed(1);
|
|
565
|
+
return `${bucketStart}-${bucketEnd}`;
|
|
566
|
+
});
|
|
567
|
+
const labelWidth = Math.max(...labels.map((l) => stringWidth(l)));
|
|
568
|
+
for (let i = 0; i < buckets; i++) {
|
|
569
|
+
const label = padEndWidth(labels[i], labelWidth);
|
|
351
570
|
const count = histogram[i];
|
|
352
571
|
const barLength = Math.round(count / maxCount * CHART_WIDTH);
|
|
353
572
|
const bar = BLOCK_CHAR.repeat(barLength);
|
|
354
|
-
|
|
573
|
+
const countSuffix = count > 0 ? ` ${count}` : "";
|
|
574
|
+
lines.push(`${label} \u2502${bar}${countSuffix}`);
|
|
355
575
|
}
|
|
356
576
|
return lines.join("\n");
|
|
357
577
|
}
|
|
358
|
-
function renderStatsTable(stats) {
|
|
578
|
+
function renderStatsTable(stats, lang = DEFAULT_LANG) {
|
|
579
|
+
const messages = getMessages(lang);
|
|
359
580
|
const lines = [];
|
|
360
581
|
lines.push("");
|
|
361
|
-
lines.push(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
);
|
|
366
|
-
lines.push("\u251C" + "\u2500".repeat(
|
|
582
|
+
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.min, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.max, STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(messages.statsHeaders.stdDev, STAT_VALUE_WIDTH) + " \u2502";
|
|
584
|
+
const tableWidth = stringWidth(headerRow) - 2;
|
|
585
|
+
lines.push("\u250C" + "\u2500".repeat(tableWidth) + "\u2510");
|
|
586
|
+
lines.push(headerRow);
|
|
587
|
+
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
367
588
|
lines.push(
|
|
368
589
|
formatStatRow(
|
|
369
|
-
|
|
590
|
+
messages.statsLabels.ttft,
|
|
370
591
|
stats.mean.ttft,
|
|
371
592
|
stats.min.ttft,
|
|
372
593
|
stats.max.ttft,
|
|
@@ -374,10 +595,10 @@ function renderStatsTable(stats) {
|
|
|
374
595
|
"f"
|
|
375
596
|
)
|
|
376
597
|
);
|
|
377
|
-
lines.push("\u251C" + "\u2500".repeat(
|
|
598
|
+
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
378
599
|
lines.push(
|
|
379
600
|
formatStatRow(
|
|
380
|
-
|
|
601
|
+
messages.statsLabels.totalTime,
|
|
381
602
|
stats.mean.totalTime,
|
|
382
603
|
stats.min.totalTime,
|
|
383
604
|
stats.max.totalTime,
|
|
@@ -385,10 +606,10 @@ function renderStatsTable(stats) {
|
|
|
385
606
|
"f"
|
|
386
607
|
)
|
|
387
608
|
);
|
|
388
|
-
lines.push("\u251C" + "\u2500".repeat(
|
|
609
|
+
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
389
610
|
lines.push(
|
|
390
611
|
formatStatRow(
|
|
391
|
-
|
|
612
|
+
messages.statsLabels.totalTokens,
|
|
392
613
|
stats.mean.totalTokens,
|
|
393
614
|
stats.min.totalTokens,
|
|
394
615
|
stats.max.totalTokens,
|
|
@@ -396,10 +617,10 @@ function renderStatsTable(stats) {
|
|
|
396
617
|
"f"
|
|
397
618
|
)
|
|
398
619
|
);
|
|
399
|
-
lines.push("\u251C" + "\u2500".repeat(
|
|
620
|
+
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
400
621
|
lines.push(
|
|
401
622
|
formatStatRow(
|
|
402
|
-
|
|
623
|
+
messages.statsLabels.averageSpeed,
|
|
403
624
|
stats.mean.averageSpeed,
|
|
404
625
|
stats.min.averageSpeed,
|
|
405
626
|
stats.max.averageSpeed,
|
|
@@ -407,10 +628,10 @@ function renderStatsTable(stats) {
|
|
|
407
628
|
"f"
|
|
408
629
|
)
|
|
409
630
|
);
|
|
410
|
-
lines.push("\u251C" + "\u2500".repeat(
|
|
631
|
+
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
411
632
|
lines.push(
|
|
412
633
|
formatStatRow(
|
|
413
|
-
|
|
634
|
+
messages.statsLabels.peakSpeed,
|
|
414
635
|
stats.mean.peakSpeed,
|
|
415
636
|
stats.min.peakSpeed,
|
|
416
637
|
stats.max.peakSpeed,
|
|
@@ -418,12 +639,23 @@ function renderStatsTable(stats) {
|
|
|
418
639
|
"f"
|
|
419
640
|
)
|
|
420
641
|
);
|
|
421
|
-
lines.push("\
|
|
642
|
+
lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
|
|
643
|
+
lines.push(
|
|
644
|
+
formatStatRow(
|
|
645
|
+
messages.statsLabels.peakTps,
|
|
646
|
+
stats.mean.peakTps,
|
|
647
|
+
stats.min.peakTps,
|
|
648
|
+
stats.max.peakTps,
|
|
649
|
+
stats.stdDev.peakTps,
|
|
650
|
+
"f"
|
|
651
|
+
)
|
|
652
|
+
);
|
|
653
|
+
lines.push("\u2514" + "\u2500".repeat(tableWidth) + "\u2518");
|
|
422
654
|
return lines.join("\n");
|
|
423
655
|
}
|
|
424
656
|
function formatStatRow(label, mean2, min, max, stdDev, format) {
|
|
425
657
|
const fmt = (n) => format === "f" ? n.toFixed(2) : n.toFixed(0);
|
|
426
|
-
return "\u2502 " + label
|
|
658
|
+
return "\u2502 " + padEndWidth(label, STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth(fmt(mean2), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(min), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(max), STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth(fmt(stdDev), STAT_VALUE_WIDTH) + " \u2502";
|
|
427
659
|
}
|
|
428
660
|
function formatTimeWithDecimals(ms) {
|
|
429
661
|
if (ms === Math.floor(ms)) {
|
|
@@ -431,28 +663,33 @@ function formatTimeWithDecimals(ms) {
|
|
|
431
663
|
}
|
|
432
664
|
return `${ms.toFixed(2)}ms`;
|
|
433
665
|
}
|
|
434
|
-
function renderSingleResult(metrics, runIndex) {
|
|
666
|
+
function renderSingleResult(metrics, runIndex, lang = DEFAULT_LANG) {
|
|
667
|
+
const messages = getMessages(lang);
|
|
435
668
|
const lines = [];
|
|
436
669
|
lines.push(`
|
|
437
|
-
|
|
438
|
-
lines.push(`
|
|
439
|
-
lines.push(`
|
|
440
|
-
lines.push(`
|
|
441
|
-
lines.push(
|
|
442
|
-
|
|
670
|
+
${messages.runLabel(runIndex + 1)}`);
|
|
671
|
+
lines.push(` ${messages.resultLabels.ttft}: ${formatTimeWithDecimals(metrics.ttft)}`);
|
|
672
|
+
lines.push(` ${messages.resultLabels.totalTime}: ${formatTimeWithDecimals(metrics.totalTime)}`);
|
|
673
|
+
lines.push(` ${messages.resultLabels.totalTokens}: ${metrics.totalTokens}`);
|
|
674
|
+
lines.push(
|
|
675
|
+
` ${messages.resultLabels.averageSpeed}: ${metrics.averageSpeed.toFixed(2)} tokens/s`
|
|
676
|
+
);
|
|
677
|
+
lines.push(` ${messages.resultLabels.peakSpeed}: ${metrics.peakSpeed.toFixed(2)} tokens/s`);
|
|
678
|
+
lines.push(` ${messages.resultLabels.peakTps}: ${metrics.peakTps.toFixed(2)} tokens/s`);
|
|
443
679
|
return lines.join("\n");
|
|
444
680
|
}
|
|
445
|
-
function renderReport(stats) {
|
|
681
|
+
function renderReport(stats, lang = DEFAULT_LANG) {
|
|
682
|
+
const messages = getMessages(lang);
|
|
446
683
|
const lines = [];
|
|
447
684
|
lines.push("\n" + "\u2550".repeat(72));
|
|
448
|
-
lines.push(
|
|
685
|
+
lines.push(messages.reportTitle);
|
|
449
686
|
lines.push("\u2550".repeat(72));
|
|
450
|
-
lines.push(renderStatsTable(stats));
|
|
687
|
+
lines.push(renderStatsTable(stats, lang));
|
|
451
688
|
if (stats.mean.tps.length > 0) {
|
|
452
|
-
lines.push("\n" + renderSpeedChart(stats.mean.tps));
|
|
689
|
+
lines.push("\n" + renderSpeedChart(stats.mean.tps, void 0, lang));
|
|
453
690
|
}
|
|
454
691
|
if (stats.mean.tps.length > 0) {
|
|
455
|
-
lines.push("\n" + renderTPSHistogram(stats.mean.tps));
|
|
692
|
+
lines.push("\n" + renderTPSHistogram(stats.mean.tps, lang));
|
|
456
693
|
}
|
|
457
694
|
return lines.join("\n");
|
|
458
695
|
}
|
|
@@ -470,9 +707,10 @@ function getCliVersion() {
|
|
|
470
707
|
}
|
|
471
708
|
var program = new Command();
|
|
472
709
|
program.name("token-speed-test").description("A CLI tool to test LLM API token output speed").version(getCliVersion());
|
|
473
|
-
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", "
|
|
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);
|
|
474
711
|
var options = program.opts();
|
|
475
712
|
async function main() {
|
|
713
|
+
let messages = getMessages(DEFAULT_LANG);
|
|
476
714
|
try {
|
|
477
715
|
const config = parseConfig({
|
|
478
716
|
apiKey: options.apiKey,
|
|
@@ -481,36 +719,47 @@ async function main() {
|
|
|
481
719
|
model: options.model,
|
|
482
720
|
maxTokens: parseInt(options.maxTokens, 10),
|
|
483
721
|
runs: parseInt(options.runs, 10),
|
|
484
|
-
prompt: options.prompt
|
|
722
|
+
prompt: options.prompt,
|
|
723
|
+
lang: options.lang
|
|
485
724
|
});
|
|
486
|
-
|
|
725
|
+
messages = getMessages(config.lang);
|
|
726
|
+
console.log(chalk.cyan(`
|
|
727
|
+
${messages.appTitle}`));
|
|
487
728
|
console.log(chalk.gray("\u2500".repeat(50)));
|
|
488
|
-
console.log(chalk.gray(
|
|
489
|
-
console.log(chalk.gray(
|
|
490
|
-
console.log(chalk.gray(
|
|
491
|
-
console.log(chalk.gray(
|
|
729
|
+
console.log(chalk.gray(`${messages.configLabels.provider}: ${chalk.white(config.provider)}`));
|
|
730
|
+
console.log(chalk.gray(`${messages.configLabels.model}: ${chalk.white(config.model)}`));
|
|
731
|
+
console.log(chalk.gray(`${messages.configLabels.maxTokens}: ${chalk.white(config.maxTokens)}`));
|
|
732
|
+
console.log(chalk.gray(`${messages.configLabels.runs}: ${chalk.white(config.runCount)}`));
|
|
492
733
|
console.log(
|
|
493
734
|
chalk.gray(
|
|
494
|
-
|
|
735
|
+
`${messages.configLabels.prompt}: ${chalk.white(config.prompt.substring(0, 50))}${config.prompt.length > 50 ? "..." : ""}`
|
|
495
736
|
)
|
|
496
737
|
);
|
|
497
738
|
console.log(chalk.gray("\u2500".repeat(50)));
|
|
498
|
-
console.log(chalk.yellow(
|
|
739
|
+
console.log(chalk.yellow(`
|
|
740
|
+
${messages.runningTests}
|
|
741
|
+
`));
|
|
742
|
+
console.log(chalk.gray(`${messages.streamingOutput}
|
|
743
|
+
`));
|
|
499
744
|
const results = await runMultipleTests(config);
|
|
500
745
|
const allMetrics = results.map((r) => calculateMetrics(r));
|
|
501
746
|
for (let i = 0; i < allMetrics.length; i++) {
|
|
502
|
-
console.log(chalk.gray(renderSingleResult(allMetrics[i], i)));
|
|
747
|
+
console.log(chalk.gray(renderSingleResult(allMetrics[i], i, config.lang)));
|
|
503
748
|
}
|
|
504
749
|
const stats = calculateStats(allMetrics);
|
|
505
|
-
console.log(chalk.cyan("\n" + renderReport(stats)));
|
|
506
|
-
console.log(chalk.green(
|
|
750
|
+
console.log(chalk.cyan("\n" + renderReport(stats, config.lang)));
|
|
751
|
+
console.log(chalk.green(`
|
|
752
|
+
${messages.testComplete}
|
|
753
|
+
`));
|
|
507
754
|
} catch (error) {
|
|
508
755
|
if (error instanceof Error) {
|
|
509
756
|
console.error(chalk.red(`
|
|
510
|
-
|
|
757
|
+
${messages.errorPrefix}: ${error.message}
|
|
511
758
|
`));
|
|
512
759
|
} else {
|
|
513
|
-
console.error(chalk.red(
|
|
760
|
+
console.error(chalk.red(`
|
|
761
|
+
${messages.unknownError}
|
|
762
|
+
`));
|
|
514
763
|
}
|
|
515
764
|
process.exit(1);
|
|
516
765
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/client.ts","../src/metrics.ts","../src/chart.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport type { Provider } from \"./config.js\";\nimport { parseConfig } from \"./config.js\";\nimport { runMultipleTests } from \"./client.js\";\nimport { calculateMetrics, calculateStats } from \"./metrics.js\";\nimport { renderReport, renderSingleResult } from \"./chart.js\";\n\nfunction getCliVersion(): string {\n try {\n const currentDir = dirname(fileURLToPath(import.meta.url));\n const packagePath = join(currentDir, \"..\", \"package.json\");\n const packageJson = JSON.parse(readFileSync(packagePath, \"utf-8\")) as { version?: string };\n return packageJson.version ?? \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\nconst program = new Command();\n\nprogram\n .name(\"token-speed-test\")\n .description(\"A CLI tool to test LLM API token output speed\")\n .version(getCliVersion());\n\nprogram\n .option(\"-k, --api-key <key>\", \"API Key (required)\", \"\")\n .option(\"-p, --provider <provider>\", \"API provider: anthropic or openai\", \"anthropic\")\n .option(\"-u, --url <url>\", \"Custom API endpoint URL\")\n .option(\"-m, --model <model>\", \"Model name\")\n .option(\"--max-tokens <number>\", \"Maximum output tokens\", \"1024\")\n .option(\"-r, --runs <number>\", \"Number of test runs\", \"3\")\n .option(\"--prompt <text>\", \"Test prompt\", \"写一篇关于 AI 的短文\")\n .parse(process.argv);\n\nconst options = program.opts();\n\nasync function main() {\n try {\n // 解析配置\n const config = parseConfig({\n apiKey: options.apiKey,\n provider: options.provider as Provider,\n url: options.url,\n model: options.model,\n maxTokens: parseInt(options.maxTokens, 10),\n runs: parseInt(options.runs, 10),\n prompt: options.prompt,\n });\n\n // 显示配置信息\n console.log(chalk.cyan(\"\\n🚀 Token 速度测试工具\"));\n console.log(chalk.gray(\"─\".repeat(50)));\n console.log(chalk.gray(`Provider: ${chalk.white(config.provider)}`));\n console.log(chalk.gray(`Model: ${chalk.white(config.model)}`));\n console.log(chalk.gray(`Max Tokens: ${chalk.white(config.maxTokens)}`));\n console.log(chalk.gray(`Runs: ${chalk.white(config.runCount)}`));\n console.log(\n chalk.gray(\n `Prompt: ${chalk.white(config.prompt.substring(0, 50))}${config.prompt.length > 50 ? \"...\" : \"\"}`\n )\n );\n console.log(chalk.gray(\"─\".repeat(50)));\n\n // 执行测试\n console.log(chalk.yellow(\"\\n⏳ 正在运行测试...\\n\"));\n\n const results = await runMultipleTests(config);\n\n // 计算指标\n const allMetrics = results.map((r) => calculateMetrics(r));\n\n // 显示每次运行的结果\n for (let i = 0; i < allMetrics.length; i++) {\n console.log(chalk.gray(renderSingleResult(allMetrics[i], i)));\n }\n\n // 计算统计\n const stats = calculateStats(allMetrics);\n\n // 显示报告\n console.log(chalk.cyan(\"\\n\" + renderReport(stats)));\n\n console.log(chalk.green(\"\\n✅ 测试完成!\\n\"));\n } catch (error) {\n if (error instanceof Error) {\n console.error(chalk.red(`\\n❌ 错误: ${error.message}\\n`));\n } else {\n console.error(chalk.red(\"\\n❌ 发生未知错误\\n\"));\n }\n process.exit(1);\n }\n}\n\nvoid main();\n","export type Provider = \"anthropic\" | \"openai\";\n\nexport interface Config {\n provider: Provider;\n apiKey: string;\n baseURL?: string;\n model: string;\n maxTokens: number;\n runCount: number;\n prompt: string;\n}\n\nexport interface ParsedArgs {\n apiKey: string;\n provider?: Provider;\n url?: string;\n model?: string;\n maxTokens?: number;\n runs?: number;\n prompt?: string;\n}\n\nconst DEFAULT_MODELS: Record<Provider, string> = {\n anthropic: \"claude-opus-4-5-20251101\",\n openai: \"gpt-5.2\",\n};\n\nconst DEFAULT_MAX_TOKENS = 1024;\nconst DEFAULT_RUNS = 3;\nconst DEFAULT_PROMPT = \"写一篇关于 AI 的短文\";\n\n/**\n * 解析命令行参数并生成配置\n */\nexport function parseConfig(args: ParsedArgs): Config {\n const {\n apiKey,\n provider = \"anthropic\",\n url,\n model,\n maxTokens = DEFAULT_MAX_TOKENS,\n runs = DEFAULT_RUNS,\n prompt = DEFAULT_PROMPT,\n } = args;\n\n // 验证必填参数\n if (!apiKey || apiKey.trim() === \"\") {\n throw new Error(\"API Key is required. Use --api-key or -k to provide it.\");\n }\n\n // 验证 provider\n if (provider !== \"anthropic\" && provider !== \"openai\") {\n throw new Error(`Invalid provider: ${provider}. Must be 'anthropic' or 'openai'.`);\n }\n\n // 验证数值参数\n if (!Number.isFinite(maxTokens) || !Number.isInteger(maxTokens) || maxTokens <= 0) {\n throw new Error(`Invalid max-tokens: ${maxTokens}. Must be a positive integer.`);\n }\n\n if (!Number.isFinite(runs) || !Number.isInteger(runs) || runs <= 0) {\n throw new Error(`Invalid runs: ${runs}. Must be a positive integer.`);\n }\n\n // 使用默认模型或用户指定的模型\n const finalModel = model || DEFAULT_MODELS[provider];\n\n return {\n provider,\n apiKey: apiKey.trim(),\n baseURL: url?.trim(),\n model: finalModel,\n maxTokens,\n runCount: runs,\n prompt: prompt.trim(),\n };\n}\n\n/**\n * 获取默认模型名称\n */\nexport function getDefaultModel(provider: Provider): string {\n return DEFAULT_MODELS[provider];\n}\n\n/**\n * 验证配置有效性\n */\nexport function validateConfig(config: Config): { valid: boolean; error?: string } {\n if (!config.apiKey) {\n return { valid: false, error: \"API Key is required\" };\n }\n\n if (config.provider !== \"anthropic\" && config.provider !== \"openai\") {\n return { valid: false, error: `Invalid provider: ${config.provider}` };\n }\n\n if (\n !Number.isFinite(config.maxTokens) ||\n !Number.isInteger(config.maxTokens) ||\n config.maxTokens <= 0\n ) {\n return { valid: false, error: \"maxTokens must be a positive integer\" };\n }\n\n if (\n !Number.isFinite(config.runCount) ||\n !Number.isInteger(config.runCount) ||\n config.runCount <= 0\n ) {\n return { valid: false, error: \"runCount must be a positive integer\" };\n }\n\n if (!config.prompt || config.prompt.trim() === \"\") {\n return { valid: false, error: \"prompt cannot be empty\" };\n }\n\n return { valid: true };\n}\n// test\n","import Anthropic from \"@anthropic-ai/sdk\";\nimport OpenAI from \"openai\";\nimport type { Config } from \"./config.js\";\nimport type { StreamMetrics } from \"./metrics.js\";\n\n/**\n * 执行 Anthropic API 流式测试\n */\nexport async function anthropicStreamTest(config: Config): Promise<StreamMetrics> {\n const startTime = Date.now();\n const tokenTimes: number[] = [];\n let ttft = 0;\n let firstTokenRecorded = false;\n\n const client = new Anthropic({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n });\n\n try {\n const stream = await client.messages.create({\n model: config.model,\n max_tokens: config.maxTokens,\n messages: [{ role: \"user\", content: config.prompt }],\n stream: true,\n });\n\n for await (const event of stream) {\n const currentTime = Date.now();\n\n if (event.type === \"content_block_delta\" && event.delta.type === \"text_delta\") {\n const text = event.delta.text;\n\n if (text && text.length > 0) {\n if (!firstTokenRecorded) {\n ttft = currentTime - startTime;\n firstTokenRecorded = true;\n }\n\n // 记录每个字符的到达时间(作为近似的 token 时间)\n for (let i = 0; i < text.length; i++) {\n tokenTimes.push(currentTime - startTime);\n }\n }\n }\n }\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Anthropic API error: ${error.message}`);\n }\n throw error;\n }\n\n const endTime = Date.now();\n const totalTime = endTime - startTime;\n\n return {\n ttft,\n tokens: tokenTimes,\n totalTokens: tokenTimes.length,\n totalTime,\n };\n}\n\n/**\n * 执行 OpenAI API 流式测试\n */\nexport async function openaiStreamTest(config: Config): Promise<StreamMetrics> {\n const startTime = Date.now();\n const tokenTimes: number[] = [];\n let ttft = 0;\n let firstTokenRecorded = false;\n\n const client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n });\n\n try {\n const stream = await client.chat.completions.create({\n model: config.model,\n max_tokens: config.maxTokens,\n messages: [{ role: \"user\", content: config.prompt }],\n stream: true,\n });\n\n for await (const chunk of stream) {\n const currentTime = Date.now();\n\n const delta = chunk.choices[0]?.delta;\n\n if (delta?.content) {\n const content = delta.content;\n\n if (!firstTokenRecorded && content.length > 0) {\n ttft = currentTime - startTime;\n firstTokenRecorded = true;\n }\n\n // 记录每个字符的到达时间\n for (let i = 0; i < content.length; i++) {\n tokenTimes.push(currentTime - startTime);\n }\n }\n }\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`OpenAI API error: ${error.message}`);\n }\n throw error;\n }\n\n const endTime = Date.now();\n const totalTime = endTime - startTime;\n\n return {\n ttft,\n tokens: tokenTimes,\n totalTokens: tokenTimes.length,\n totalTime,\n };\n}\n\n/**\n * 根据配置执行流式测试\n */\nexport async function streamTest(config: Config): Promise<StreamMetrics> {\n if (config.provider === \"anthropic\") {\n return anthropicStreamTest(config);\n } else {\n return openaiStreamTest(config);\n }\n}\n\n/**\n * 执行多次测试\n */\nexport async function runMultipleTests(config: Config): Promise<StreamMetrics[]> {\n const results: StreamMetrics[] = [];\n\n for (let i = 0; i < config.runCount; i++) {\n const result = await streamTest(config);\n results.push(result);\n }\n\n return results;\n}\n","/**\n * 单次流式测试的原始计时数据\n */\nexport interface StreamMetrics {\n ttft: number; // Time to First Token (ms)\n tokens: number[]; // 每个 token 的到达时间(相对开始时间,单位:ms)\n totalTokens: number;\n totalTime: number;\n}\n\n/**\n * 计算后的统计指标\n */\nexport interface CalculatedMetrics {\n ttft: number; // Time to First Token (ms)\n totalTime: number; // 总耗时 (ms)\n totalTokens: number; // 总 token 数\n averageSpeed: number; // 平均速度 (tokens/s)\n peakSpeed: number; // 峰值速度 (tokens/s)\n tps: number[]; // 每秒 token 数 (TPS curve)\n}\n\n/**\n * 多次测试的统计结果\n */\nexport interface StatsResult {\n mean: CalculatedMetrics;\n min: CalculatedMetrics;\n max: CalculatedMetrics;\n stdDev: CalculatedMetrics;\n sampleSize: number;\n}\n\n/**\n * 计算 TTFT (Time to First Token)\n */\nexport function calculateTTFT(metrics: StreamMetrics): number {\n return metrics.ttft;\n}\n\n/**\n * 计算平均速度 (tokens/s)\n */\nexport function calculateAverageSpeed(metrics: StreamMetrics): number {\n if (metrics.totalTime <= 0) {\n return 0;\n }\n return (metrics.totalTokens / metrics.totalTime) * 1000;\n}\n\n/**\n * 计算峰值速度 - 最快连续 N 个 token 的平均速度\n */\nexport function calculatePeakSpeed(metrics: StreamMetrics, windowSize: number = 10): number {\n if (metrics.tokens.length < windowSize) {\n // 如果 token 数少于窗口大小,使用全部 token 计算\n if (metrics.tokens.length < 2) {\n return 0;\n }\n const totalTime = metrics.tokens[metrics.tokens.length - 1] - metrics.tokens[0];\n return totalTime > 0 ? ((metrics.tokens.length - 1) / totalTime) * 1000 : 0;\n }\n\n let maxSpeed = 0;\n for (let i = 0; i <= metrics.tokens.length - windowSize; i++) {\n const startTime = metrics.tokens[i];\n const endTime = metrics.tokens[i + windowSize - 1];\n const duration = endTime - startTime;\n if (duration > 0) {\n const speed = ((windowSize - 1) / duration) * 1000;\n maxSpeed = Math.max(maxSpeed, speed);\n }\n }\n\n return maxSpeed;\n}\n\n/**\n * 计算 TPS (Tokens Per Second) 曲线\n */\nexport function calculateTPS(metrics: StreamMetrics): number[] {\n if (metrics.tokens.length === 0) {\n return [];\n }\n\n const totalDuration = metrics.tokens[metrics.tokens.length - 1];\n const totalSeconds = Math.ceil(totalDuration / 1000);\n\n if (totalSeconds <= 0) {\n return metrics.tokens.length > 0 ? [metrics.tokens.length] : [];\n }\n\n const tps: number[] = new Array(totalSeconds).fill(0);\n\n // 计算每秒内的 token 数\n for (const tokenTime of metrics.tokens) {\n const secondIndex = Math.floor(tokenTime / 1000);\n if (secondIndex < tps.length) {\n tps[secondIndex]++;\n }\n }\n\n return tps;\n}\n\n/**\n * 从 StreamMetrics 计算完整的指标\n */\nexport function calculateMetrics(metrics: StreamMetrics): CalculatedMetrics {\n return {\n ttft: calculateTTFT(metrics),\n totalTime: metrics.totalTime,\n totalTokens: metrics.totalTokens,\n averageSpeed: calculateAverageSpeed(metrics),\n peakSpeed: calculatePeakSpeed(metrics),\n tps: calculateTPS(metrics),\n };\n}\n\n/**\n * 计算一组数值的平均值\n */\nfunction mean(values: number[]): number {\n if (values.length === 0) return 0;\n return values.reduce((sum, v) => sum + v, 0) / values.length;\n}\n\n/**\n * 计算一组数值的标准差\n */\nfunction standardDeviation(values: number[]): number {\n if (values.length < 2) return 0;\n const avg = mean(values);\n const squareDiffs = values.map((v) => Math.pow(v - avg, 2));\n return Math.sqrt(mean(squareDiffs));\n}\n\n/**\n * 从多个 CalculatedMetrics 计算统计结果\n */\nexport function calculateStats(allMetrics: CalculatedMetrics[]): StatsResult {\n if (allMetrics.length === 0) {\n throw new Error(\"Cannot calculate stats from empty metrics array\");\n }\n\n const sampleSize = allMetrics.length;\n\n // 提取各项指标的数组\n const ttfts = allMetrics.map((m) => m.ttft);\n const totalTimes = allMetrics.map((m) => m.totalTime);\n const totalTokens = allMetrics.map((m) => m.totalTokens);\n const averageSpeeds = allMetrics.map((m) => m.averageSpeed);\n const peakSpeeds = allMetrics.map((m) => m.peakSpeed);\n\n // 找到最长的 TPS 数组\n const maxTpsLength = Math.max(...allMetrics.map((m) => m.tps.length));\n const avgTps: number[] = [];\n for (let i = 0; i < maxTpsLength; i++) {\n const values = allMetrics.map((m) => m.tps[i] ?? 0);\n avgTps.push(mean(values));\n }\n\n return {\n mean: {\n ttft: mean(ttfts),\n totalTime: mean(totalTimes),\n totalTokens: mean(totalTokens),\n averageSpeed: mean(averageSpeeds),\n peakSpeed: mean(peakSpeeds),\n tps: avgTps,\n },\n min: {\n ttft: Math.min(...ttfts),\n totalTime: Math.min(...totalTimes),\n totalTokens: Math.min(...totalTokens),\n averageSpeed: Math.min(...averageSpeeds),\n peakSpeed: Math.min(...peakSpeeds),\n tps: [],\n },\n max: {\n ttft: Math.max(...ttfts),\n totalTime: Math.max(...totalTimes),\n totalTokens: Math.max(...totalTokens),\n averageSpeed: Math.max(...averageSpeeds),\n peakSpeed: Math.max(...peakSpeeds),\n tps: [],\n },\n stdDev: {\n ttft: standardDeviation(ttfts),\n totalTime: standardDeviation(totalTimes),\n totalTokens: standardDeviation(totalTokens),\n averageSpeed: standardDeviation(averageSpeeds),\n peakSpeed: standardDeviation(peakSpeeds),\n tps: [],\n },\n sampleSize,\n };\n}\n\n/**\n * 格式化速度显示\n */\nexport function formatSpeed(tokensPerSecond: number): string {\n return tokensPerSecond.toFixed(2);\n}\n\n/**\n * 格式化时间显示\n */\nexport function formatTime(ms: number): string {\n if (ms < 1000) {\n return `${ms.toFixed(0)}ms`;\n }\n return `${(ms / 1000).toFixed(2)}s`;\n}\n","import type { CalculatedMetrics, StatsResult } from \"./metrics.js\";\n\nconst BLOCK_CHAR = \"█\";\nconst CHART_WIDTH = 50;\nconst CHART_HEIGHT = 10;\n\n/**\n * 渲染速度趋势图\n */\nexport function renderSpeedChart(tps: number[], maxSpeed?: number): string {\n if (tps.length === 0) {\n return \"No data available for chart\";\n }\n\n const actualMax = maxSpeed ?? Math.max(...tps, 1);\n const maxVal = Math.max(actualMax, 1);\n\n const lines: string[] = [];\n lines.push(\"Token 速度趋势图 (TPS)\");\n\n // Y 轴标签和边框\n lines.push(\"┌\" + \"─\".repeat(CHART_WIDTH) + \"┐\");\n\n for (let row = CHART_HEIGHT - 1; row >= 0; row--) {\n const value = (row / (CHART_HEIGHT - 1)) * maxVal;\n const label = value.toFixed(0).padStart(4);\n\n let chartRow = \"│ \" + label + \" ┤\";\n\n for (let col = 0; col < CHART_WIDTH; col++) {\n const index = Math.floor((col / CHART_WIDTH) * tps.length);\n const tpsValue = tps[index] ?? 0;\n const normalizedHeight = (tpsValue / maxVal) * (CHART_HEIGHT - 1);\n\n if (normalizedHeight >= row) {\n chartRow += BLOCK_CHAR;\n } else {\n chartRow += \" \";\n }\n }\n\n chartRow += \" │\";\n lines.push(chartRow);\n }\n\n // X 轴\n lines.push(\"│ \" + \"0\".padStart(CHART_WIDTH) + \"s │\");\n lines.push(\"└\" + \"─\".repeat(CHART_WIDTH) + \"┘\");\n\n // X 轴标签 (时间标记)\n const xLabels = generateXLabels(tps.length, 6);\n lines.push(\" \" + xLabels.join(\" \"));\n\n return lines.join(\"\\n\");\n}\n\n/**\n * 生成 X 轴时间标签\n */\nfunction generateXLabels(dataPoints: number, maxLabels: number): string[] {\n if (dataPoints <= 1) {\n return [\"0s\"];\n }\n\n const labels: string[] = [];\n const step = Math.max(1, Math.floor(dataPoints / maxLabels));\n\n for (let i = 0; i < dataPoints; i += step) {\n labels.push(`${i}s`);\n }\n\n // 确保最后一个标签是结束时间\n if (labels[labels.length - 1] !== `${dataPoints - 1}s`) {\n labels.push(`${dataPoints - 1}s`);\n }\n\n return labels;\n}\n\n/**\n * 渲染 TPS 直方图\n */\nexport function renderTPSHistogram(tps: number[]): string {\n if (tps.length === 0) {\n return \"No TPS data available\";\n }\n\n const lines: string[] = [];\n lines.push(\"TPS 分布\");\n\n // 计算分布区间\n const maxTps = Math.max(...tps, 1);\n const buckets = 10;\n const bucketSize = maxTps / buckets;\n const histogram = new Array(buckets).fill(0);\n\n for (const t of tps) {\n const bucketIndex = Math.min(Math.floor(t / bucketSize), buckets - 1);\n histogram[bucketIndex]++;\n }\n\n const maxCount = Math.max(...histogram, 1);\n\n for (let i = 0; i < buckets; i++) {\n const bucketStart = (i * bucketSize).toFixed(1);\n const bucketEnd = ((i + 1) * bucketSize).toFixed(1);\n const count = histogram[i];\n const barLength = Math.round((count / maxCount) * CHART_WIDTH);\n const bar = BLOCK_CHAR.repeat(barLength);\n\n lines.push(`${bucketStart}-${bucketEnd} │${bar} ${count}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * 渲染统计汇总表\n */\nexport function renderStatsTable(stats: StatsResult): string {\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(\"统计汇总 (N=\" + stats.sampleSize + \")\");\n lines.push(\"┌\" + \"─\".repeat(70) + \"┐\");\n\n // 表头\n lines.push(\n \"│ \" +\n \"指标\".padEnd(15) +\n \" │ \" +\n \"均值\".padStart(10) +\n \" │ \" +\n \"最小值\".padStart(10) +\n \" │ \" +\n \"最大值\".padStart(10) +\n \" │ \" +\n \"标准差\".padStart(10) +\n \" │\"\n );\n lines.push(\"├\" + \"─\".repeat(70) + \"┤\");\n\n // TTFT\n lines.push(\n formatStatRow(\n \"TTFT (ms)\",\n stats.mean.ttft,\n stats.min.ttft,\n stats.max.ttft,\n stats.stdDev.ttft,\n \"f\"\n )\n );\n lines.push(\"├\" + \"─\".repeat(70) + \"┤\");\n\n // 总耗时\n lines.push(\n formatStatRow(\n \"总耗时 (ms)\",\n stats.mean.totalTime,\n stats.min.totalTime,\n stats.max.totalTime,\n stats.stdDev.totalTime,\n \"f\"\n )\n );\n lines.push(\"├\" + \"─\".repeat(70) + \"┤\");\n\n // 总 token 数\n lines.push(\n formatStatRow(\n \"总 Token 数\",\n stats.mean.totalTokens,\n stats.min.totalTokens,\n stats.max.totalTokens,\n stats.stdDev.totalTokens,\n \"f\"\n )\n );\n lines.push(\"├\" + \"─\".repeat(70) + \"┤\");\n\n // 平均速度\n lines.push(\n formatStatRow(\n \"平均速度\",\n stats.mean.averageSpeed,\n stats.min.averageSpeed,\n stats.max.averageSpeed,\n stats.stdDev.averageSpeed,\n \"f\"\n )\n );\n lines.push(\"├\" + \"─\".repeat(70) + \"┤\");\n\n // 峰值速度\n lines.push(\n formatStatRow(\n \"峰值速度\",\n stats.mean.peakSpeed,\n stats.min.peakSpeed,\n stats.max.peakSpeed,\n stats.stdDev.peakSpeed,\n \"f\"\n )\n );\n\n lines.push(\"└\" + \"─\".repeat(70) + \"┘\");\n\n return lines.join(\"\\n\");\n}\n\n/**\n * 格式化统计表格的一行\n */\nfunction formatStatRow(\n label: string,\n mean: number,\n min: number,\n max: number,\n stdDev: number,\n format: \"f\" | \"d\"\n): string {\n const fmt = (n: number) => (format === \"f\" ? n.toFixed(2) : n.toFixed(0));\n\n return (\n \"│ \" +\n label.padEnd(15) +\n \" │ \" +\n fmt(mean).padStart(10) +\n \" │ \" +\n fmt(min).padStart(10) +\n \" │ \" +\n fmt(max).padStart(10) +\n \" │ \" +\n fmt(stdDev).padStart(10) +\n \" │\"\n );\n}\n\n/**\n * 格式化时间显示(带小数)\n */\nfunction formatTimeWithDecimals(ms: number): string {\n if (ms === Math.floor(ms)) {\n return `${ms.toFixed(0)}ms`;\n }\n return `${ms.toFixed(2)}ms`;\n}\n\n/**\n * 渲染单次测试结果\n */\nexport function renderSingleResult(metrics: CalculatedMetrics, runIndex: number): string {\n const lines: string[] = [];\n lines.push(`\\n[运行 ${runIndex + 1}]`);\n lines.push(` TTFT: ${formatTimeWithDecimals(metrics.ttft)}`);\n lines.push(` 总耗时: ${formatTimeWithDecimals(metrics.totalTime)}`);\n lines.push(` 总 Token 数: ${metrics.totalTokens}`);\n lines.push(` 平均速度: ${metrics.averageSpeed.toFixed(2)} tokens/s`);\n lines.push(` 峰值速度: ${metrics.peakSpeed.toFixed(2)} tokens/s`);\n return lines.join(\"\\n\");\n}\n\n/**\n * 渲染完整的测试报告\n */\nexport function renderReport(stats: StatsResult): string {\n const lines: string[] = [];\n\n lines.push(\"\\n\" + \"═\".repeat(72));\n lines.push(\"Token 速度测试报告\");\n lines.push(\"═\".repeat(72));\n\n // 汇总统计\n lines.push(renderStatsTable(stats));\n\n // 速度趋势图\n if (stats.mean.tps.length > 0) {\n lines.push(\"\\n\" + renderSpeedChart(stats.mean.tps));\n }\n\n // TPS 直方图\n if (stats.mean.tps.length > 0) {\n lines.push(\"\\n\" + renderTPSHistogram(stats.mean.tps));\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["mean"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/i18n.ts","../src/config.ts","../src/client.ts","../src/tokenizer.ts","../src/metrics.ts","../src/chart.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport type { Provider } from \"./config.js\";\nimport { parseConfig } from \"./config.js\";\nimport { runMultipleTests } from \"./client.js\";\nimport { calculateMetrics, calculateStats } from \"./metrics.js\";\nimport { renderReport, renderSingleResult } from \"./chart.js\";\nimport { DEFAULT_LANG, getMessages } from \"./i18n.js\";\n\nfunction getCliVersion(): string {\n try {\n const currentDir = dirname(fileURLToPath(import.meta.url));\n const packagePath = join(currentDir, \"..\", \"package.json\");\n const packageJson = JSON.parse(readFileSync(packagePath, \"utf-8\")) as { version?: string };\n return packageJson.version ?? \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\nconst program = new Command();\n\nprogram\n .name(\"token-speed-test\")\n .description(\"A CLI tool to test LLM API token output speed\")\n .version(getCliVersion());\n\nprogram\n .option(\"-k, --api-key <key>\", \"API Key (required)\", \"\")\n .option(\"-p, --provider <provider>\", \"API provider: anthropic or openai\", \"anthropic\")\n .option(\"-u, --url <url>\", \"Custom API endpoint URL\")\n .option(\"-m, --model <model>\", \"Model name\")\n .option(\"--max-tokens <number>\", \"Maximum output tokens\", \"1024\")\n .option(\"-r, --runs <number>\", \"Number of test runs\", \"3\")\n .option(\"--prompt <text>\", \"Test prompt\")\n .option(\"--lang <lang>\", \"Output language: zh or en\", \"zh\")\n .parse(process.argv);\n\nconst options = program.opts();\n\nasync function main() {\n let messages = getMessages(DEFAULT_LANG);\n try {\n // 解析配置\n const config = parseConfig({\n apiKey: options.apiKey,\n provider: options.provider as Provider,\n url: options.url,\n model: options.model,\n maxTokens: parseInt(options.maxTokens, 10),\n runs: parseInt(options.runs, 10),\n prompt: options.prompt,\n lang: options.lang,\n });\n messages = getMessages(config.lang);\n\n // 显示配置信息\n console.log(chalk.cyan(`\\n${messages.appTitle}`));\n console.log(chalk.gray(\"─\".repeat(50)));\n console.log(chalk.gray(`${messages.configLabels.provider}: ${chalk.white(config.provider)}`));\n console.log(chalk.gray(`${messages.configLabels.model}: ${chalk.white(config.model)}`));\n console.log(chalk.gray(`${messages.configLabels.maxTokens}: ${chalk.white(config.maxTokens)}`));\n console.log(chalk.gray(`${messages.configLabels.runs}: ${chalk.white(config.runCount)}`));\n console.log(\n chalk.gray(\n `${messages.configLabels.prompt}: ${chalk.white(config.prompt.substring(0, 50))}${\n config.prompt.length > 50 ? \"...\" : \"\"\n }`\n )\n );\n console.log(chalk.gray(\"─\".repeat(50)));\n\n // 执行测试\n console.log(chalk.yellow(`\\n${messages.runningTests}\\n`));\n console.log(chalk.gray(`${messages.streamingOutput}\\n`));\n\n const results = await runMultipleTests(config);\n\n // 计算指标\n const allMetrics = results.map((r) => calculateMetrics(r));\n\n // 显示每次运行的结果\n for (let i = 0; i < allMetrics.length; i++) {\n console.log(chalk.gray(renderSingleResult(allMetrics[i], i, config.lang)));\n }\n\n // 计算统计\n const stats = calculateStats(allMetrics);\n\n // 显示报告\n console.log(chalk.cyan(\"\\n\" + renderReport(stats, config.lang)));\n\n console.log(chalk.green(`\\n${messages.testComplete}\\n`));\n } catch (error) {\n if (error instanceof Error) {\n console.error(chalk.red(`\\n${messages.errorPrefix}: ${error.message}\\n`));\n } else {\n console.error(chalk.red(`\\n${messages.unknownError}\\n`));\n }\n process.exit(1);\n }\n}\n\nvoid main();\n","export const SUPPORTED_LANGS = [\"zh\", \"en\"] as const;\nexport type Lang = (typeof SUPPORTED_LANGS)[number];\n\nexport const DEFAULT_LANG: Lang = \"zh\";\n\nexport interface Messages {\n defaultPrompt: string;\n appTitle: string;\n runningTests: string;\n streamingOutput: string;\n testComplete: string;\n errorPrefix: string;\n unknownError: string;\n configLabels: {\n provider: string;\n model: string;\n maxTokens: string;\n runs: string;\n prompt: string;\n };\n runLabel: (index: number) => string;\n runProgressLabel: (current: number, total: number) => string;\n reportTitle: string;\n speedChartTitle: string;\n tpsHistogramTitle: string;\n noChartData: string;\n noTpsData: string;\n statsSummaryTitle: (sampleSize: number) => string;\n statsHeaders: {\n metric: string;\n mean: string;\n min: string;\n max: string;\n stdDev: string;\n };\n statsLabels: {\n ttft: string;\n totalTime: string;\n totalTokens: string;\n averageSpeed: string;\n peakSpeed: string;\n peakTps: string;\n };\n resultLabels: {\n ttft: string;\n totalTime: string;\n totalTokens: string;\n averageSpeed: string;\n peakSpeed: string;\n peakTps: string;\n };\n}\n\nconst zhMessages: Messages = {\n defaultPrompt: \"写一篇关于 AI 的短文\",\n appTitle: \"🚀 Token 速度测试工具\",\n runningTests: \"⏳ 正在运行测试...\",\n streamingOutput: \"模型输出 (流式):\",\n testComplete: \"✅ 测试完成!\",\n errorPrefix: \"❌ 错误\",\n unknownError: \"❌ 发生未知错误\",\n configLabels: {\n provider: \"Provider\",\n model: \"Model\",\n maxTokens: \"Max Tokens\",\n runs: \"Runs\",\n prompt: \"Prompt\",\n },\n runLabel: (index: number) => `[运行 ${index}]`,\n runProgressLabel: (current: number, total: number) => `[运行 ${current}/${total}]`,\n reportTitle: \"Token 速度测试报告\",\n speedChartTitle: \"Token 速度趋势图 (TPS)\",\n tpsHistogramTitle: \"TPS 分布\",\n noChartData: \"没有可用于图表的数据\",\n noTpsData: \"没有 TPS 数据可用\",\n statsSummaryTitle: (sampleSize: number) => `统计汇总 (N=${sampleSize})`,\n statsHeaders: {\n metric: \"指标\",\n mean: \"均值\",\n min: \"最小值\",\n max: \"最大值\",\n stdDev: \"标准差\",\n },\n statsLabels: {\n ttft: \"TTFT (ms)\",\n totalTime: \"总耗时 (ms)\",\n totalTokens: \"总 Token 数\",\n averageSpeed: \"平均速度\",\n peakSpeed: \"峰值速度\",\n peakTps: \"峰值 TPS\",\n },\n resultLabels: {\n ttft: \"TTFT\",\n totalTime: \"总耗时\",\n totalTokens: \"总 Token 数\",\n averageSpeed: \"平均速度\",\n peakSpeed: \"峰值速度\",\n peakTps: \"峰值 TPS\",\n },\n};\n\nconst enMessages: Messages = {\n defaultPrompt: \"Write a short essay about AI\",\n appTitle: \"🚀 Token Speed Test\",\n runningTests: \"⏳ Running tests...\",\n streamingOutput: \"Model output (streaming):\",\n testComplete: \"✅ Tests complete!\",\n errorPrefix: \"❌ Error\",\n unknownError: \"❌ An unknown error occurred\",\n configLabels: {\n provider: \"Provider\",\n model: \"Model\",\n maxTokens: \"Max Tokens\",\n runs: \"Runs\",\n prompt: \"Prompt\",\n },\n runLabel: (index: number) => `[Run ${index}]`,\n runProgressLabel: (current: number, total: number) => `[Run ${current}/${total}]`,\n reportTitle: \"Token Speed Test Report\",\n speedChartTitle: \"Token Speed Trend (TPS)\",\n tpsHistogramTitle: \"TPS Distribution\",\n noChartData: \"No data available for chart\",\n noTpsData: \"No TPS data available\",\n statsSummaryTitle: (sampleSize: number) => `Summary (N=${sampleSize})`,\n statsHeaders: {\n metric: \"Metric\",\n mean: \"Mean\",\n min: \"Min\",\n max: \"Max\",\n stdDev: \"Std Dev\",\n },\n statsLabels: {\n ttft: \"TTFT (ms)\",\n totalTime: \"Total Time (ms)\",\n totalTokens: \"Total Tokens\",\n averageSpeed: \"Avg Speed\",\n peakSpeed: \"Peak Speed\",\n peakTps: \"Peak TPS\",\n },\n resultLabels: {\n ttft: \"TTFT\",\n totalTime: \"Total Time\",\n totalTokens: \"Total Tokens\",\n averageSpeed: \"Avg Speed\",\n peakSpeed: \"Peak Speed\",\n peakTps: \"Peak TPS\",\n },\n};\n\nexport function isSupportedLang(value: string): value is Lang {\n return SUPPORTED_LANGS.includes(value as Lang);\n}\n\nexport function resolveLang(value?: string): Lang {\n if (!value) {\n return DEFAULT_LANG;\n }\n const normalized = value.toLowerCase();\n if (!isSupportedLang(normalized)) {\n throw new Error(`Invalid lang: ${value}. Must be 'zh' or 'en'.`);\n }\n return normalized;\n}\n\nexport function getMessages(lang: Lang): Messages {\n return lang === \"en\" ? enMessages : zhMessages;\n}\n","import { getMessages, resolveLang, type Lang } from \"./i18n.js\";\n\nexport type Provider = \"anthropic\" | \"openai\";\n\nexport interface Config {\n provider: Provider;\n apiKey: string;\n baseURL?: string;\n model: string;\n maxTokens: number;\n runCount: number;\n prompt: string;\n lang: Lang;\n}\n\nexport interface ParsedArgs {\n apiKey: string;\n provider?: Provider;\n url?: string;\n model?: string;\n maxTokens?: number;\n runs?: number;\n prompt?: string;\n lang?: string;\n}\n\nconst DEFAULT_MODELS: Record<Provider, string> = {\n anthropic: \"claude-opus-4-5-20251101\",\n openai: \"gpt-5.2\",\n};\n\nconst DEFAULT_MAX_TOKENS = 1024;\nconst DEFAULT_RUNS = 3;\n/**\n * 解析命令行参数并生成配置\n */\nexport function parseConfig(args: ParsedArgs): Config {\n const {\n apiKey,\n provider = \"anthropic\",\n url,\n model,\n maxTokens = DEFAULT_MAX_TOKENS,\n runs = DEFAULT_RUNS,\n prompt,\n lang: langInput,\n } = args;\n const lang = resolveLang(langInput);\n const messages = getMessages(lang);\n const finalPrompt = prompt ?? messages.defaultPrompt;\n\n // 验证必填参数\n if (!apiKey || apiKey.trim() === \"\") {\n throw new Error(\"API Key is required. Use --api-key or -k to provide it.\");\n }\n\n // 验证 provider\n if (provider !== \"anthropic\" && provider !== \"openai\") {\n throw new Error(`Invalid provider: ${provider}. Must be 'anthropic' or 'openai'.`);\n }\n\n // 验证数值参数\n if (!Number.isFinite(maxTokens) || !Number.isInteger(maxTokens) || maxTokens <= 0) {\n throw new Error(`Invalid max-tokens: ${maxTokens}. Must be a positive integer.`);\n }\n\n if (!Number.isFinite(runs) || !Number.isInteger(runs) || runs <= 0) {\n throw new Error(`Invalid runs: ${runs}. Must be a positive integer.`);\n }\n\n // 使用默认模型或用户指定的模型\n const finalModel = model || DEFAULT_MODELS[provider];\n\n return {\n provider,\n apiKey: apiKey.trim(),\n baseURL: url?.trim(),\n model: finalModel,\n maxTokens,\n runCount: runs,\n prompt: finalPrompt.trim(),\n lang,\n };\n}\n\n/**\n * 获取默认模型名称\n */\nexport function getDefaultModel(provider: Provider): string {\n return DEFAULT_MODELS[provider];\n}\n\n/**\n * 验证配置有效性\n */\nexport function validateConfig(config: Config): { valid: boolean; error?: string } {\n if (!config.apiKey) {\n return { valid: false, error: \"API Key is required\" };\n }\n\n if (config.provider !== \"anthropic\" && config.provider !== \"openai\") {\n return { valid: false, error: `Invalid provider: ${config.provider}` };\n }\n\n if (\n !Number.isFinite(config.maxTokens) ||\n !Number.isInteger(config.maxTokens) ||\n config.maxTokens <= 0\n ) {\n return { valid: false, error: \"maxTokens must be a positive integer\" };\n }\n\n if (\n !Number.isFinite(config.runCount) ||\n !Number.isInteger(config.runCount) ||\n config.runCount <= 0\n ) {\n return { valid: false, error: \"runCount must be a positive integer\" };\n }\n\n if (!config.prompt || config.prompt.trim() === \"\") {\n return { valid: false, error: \"prompt cannot be empty\" };\n }\n\n if (config.lang !== \"zh\" && config.lang !== \"en\") {\n return { valid: false, error: `Invalid lang: ${config.lang}` };\n }\n\n return { valid: true };\n}\n// test\n","import { performance } from \"node:perf_hooks\";\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport OpenAI from \"openai\";\nimport type { Config } from \"./config.js\";\nimport { getMessages } from \"./i18n.js\";\nimport type { StreamMetrics } from \"./metrics.js\";\nimport { createTokenizer } from \"./tokenizer.js\";\n\n/**\n * 执行 Anthropic API 流式测试\n */\nexport async function anthropicStreamTest(config: Config): Promise<StreamMetrics> {\n const startTime = performance.now();\n const tokenTimes: number[] = [];\n let ttft = 0;\n let firstTokenRecorded = false;\n let tokenCount = 0;\n let wroteOutput = false;\n\n const encoding = createTokenizer(config.model);\n const client = new Anthropic({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n });\n\n try {\n const stream = await client.messages.create({\n model: config.model,\n max_tokens: config.maxTokens,\n messages: [{ role: \"user\", content: config.prompt }],\n stream: true,\n });\n\n for await (const event of stream) {\n const currentTime = performance.now();\n\n if (event.type === \"content_block_delta\" && event.delta.type === \"text_delta\") {\n const text = event.delta.text;\n\n if (text && text.length > 0) {\n process.stdout.write(text);\n wroteOutput = true;\n const encoded = encoding.encode(text);\n const newTokens = encoded.length;\n\n if (newTokens > 0) {\n if (!firstTokenRecorded) {\n ttft = currentTime - startTime;\n firstTokenRecorded = true;\n }\n\n // 为当前批次的新增 token 记录到达时间\n for (let i = 0; i < newTokens; i++) {\n tokenTimes.push(currentTime - startTime);\n }\n\n tokenCount += newTokens;\n }\n }\n }\n }\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Anthropic API error: ${error.message}`);\n }\n throw error;\n } finally {\n if (wroteOutput) {\n process.stdout.write(\"\\n\");\n }\n encoding.free();\n }\n\n const endTime = performance.now();\n const totalTime = endTime - startTime;\n\n return {\n ttft,\n tokens: tokenTimes,\n totalTokens: tokenCount,\n totalTime,\n };\n}\n\n/**\n * 执行 OpenAI API 流式测试\n */\nexport async function openaiStreamTest(config: Config): Promise<StreamMetrics> {\n const startTime = performance.now();\n const tokenTimes: number[] = [];\n let ttft = 0;\n let firstTokenRecorded = false;\n let tokenCount = 0;\n let wroteOutput = false;\n\n const encoding = createTokenizer(config.model);\n const client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n });\n\n try {\n const stream = await client.chat.completions.create({\n model: config.model,\n max_tokens: config.maxTokens,\n messages: [{ role: \"user\", content: config.prompt }],\n stream: true,\n });\n\n for await (const chunk of stream) {\n const currentTime = performance.now();\n\n const delta = chunk.choices[0]?.delta;\n\n if (delta?.content) {\n const content = delta.content;\n\n if (content.length > 0) {\n process.stdout.write(content);\n wroteOutput = true;\n const encoded = encoding.encode(content);\n const newTokens = encoded.length;\n\n if (newTokens > 0) {\n if (!firstTokenRecorded) {\n ttft = currentTime - startTime;\n firstTokenRecorded = true;\n }\n\n // 为当前批次的新增 token 记录到达时间\n for (let i = 0; i < newTokens; i++) {\n tokenTimes.push(currentTime - startTime);\n }\n\n tokenCount += newTokens;\n }\n }\n }\n }\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`OpenAI API error: ${error.message}`);\n }\n throw error;\n } finally {\n if (wroteOutput) {\n process.stdout.write(\"\\n\");\n }\n encoding.free();\n }\n\n const endTime = performance.now();\n const totalTime = endTime - startTime;\n\n return {\n ttft,\n tokens: tokenTimes,\n totalTokens: tokenCount,\n totalTime,\n };\n}\n\n/**\n * 根据配置执行流式测试\n */\nexport async function streamTest(config: Config): Promise<StreamMetrics> {\n if (config.provider === \"anthropic\") {\n return anthropicStreamTest(config);\n } else {\n return openaiStreamTest(config);\n }\n}\n\n/**\n * 执行多次测试\n */\nexport async function runMultipleTests(config: Config): Promise<StreamMetrics[]> {\n const results: StreamMetrics[] = [];\n const messages = getMessages(config.lang);\n\n for (let i = 0; i < config.runCount; i++) {\n if (config.runCount > 1) {\n const label = `\\n${messages.runProgressLabel(i + 1, config.runCount)}`;\n console.log(label);\n console.log(\"-\".repeat(label.length - 1));\n }\n const result = await streamTest(config);\n results.push(result);\n }\n\n return results;\n}\n","import { encoding_for_model, get_encoding } from \"tiktoken\";\nimport type { TiktokenModel } from \"tiktoken\";\n\nconst FALLBACK_ENCODING = \"cl100k_base\";\n\nexport function createTokenizer(model: string) {\n try {\n const normalized = model.trim();\n if (!normalized) {\n return get_encoding(FALLBACK_ENCODING);\n }\n return encoding_for_model(normalized as TiktokenModel);\n } catch {\n return get_encoding(FALLBACK_ENCODING);\n }\n}\n","/**\n * 单次流式测试的原始计时数据\n */\nexport interface StreamMetrics {\n ttft: number; // Time to First Token (ms)\n tokens: number[]; // 每个 token 的到达时间(相对开始时间,单位:ms)\n totalTokens: number;\n totalTime: number;\n}\n\n/**\n * 计算后的统计指标\n */\nexport interface CalculatedMetrics {\n ttft: number; // Time to First Token (ms)\n totalTime: number; // 总耗时 (ms)\n totalTokens: number; // 总 token 数\n averageSpeed: number; // 平均速度 (tokens/s)\n peakSpeed: number; // 峰值速度 (tokens/s)\n peakTps: number; // 峰值 TPS (tokens/s)\n tps: number[]; // 每秒 token 数 (TPS curve)\n}\n\n/**\n * 多次测试的统计结果\n */\nexport interface StatsResult {\n mean: CalculatedMetrics;\n min: CalculatedMetrics;\n max: CalculatedMetrics;\n stdDev: CalculatedMetrics;\n sampleSize: number;\n}\n\n/**\n * 计算 TTFT (Time to First Token)\n */\nexport function calculateTTFT(metrics: StreamMetrics): number {\n return metrics.ttft;\n}\n\n/**\n * 计算平均速度 (tokens/s)\n */\nexport function calculateAverageSpeed(metrics: StreamMetrics): number {\n if (metrics.totalTime <= 0) {\n return 0;\n }\n return (metrics.totalTokens / metrics.totalTime) * 1000;\n}\n\n/**\n * 计算峰值速度 - 最快连续 N 个 token 的平均速度\n */\nconst MIN_PEAK_WINDOW_MS = 50;\n\nexport function calculatePeakSpeed(metrics: StreamMetrics, windowSize: number = 10): number {\n if (metrics.tokens.length < windowSize) {\n // 如果 token 数少于窗口大小,使用全部 token 计算\n if (metrics.tokens.length < 2) {\n return 0;\n }\n const totalTime = metrics.tokens[metrics.tokens.length - 1] - metrics.tokens[0];\n const durationMs = Math.max(totalTime, MIN_PEAK_WINDOW_MS);\n return ((metrics.tokens.length - 1) / durationMs) * 1000;\n }\n\n let maxSpeed = 0;\n for (let i = 0; i <= metrics.tokens.length - windowSize; i++) {\n const startTime = metrics.tokens[i];\n const endTime = metrics.tokens[i + windowSize - 1];\n const duration = endTime - startTime;\n const durationMs = Math.max(duration, MIN_PEAK_WINDOW_MS);\n const speed = ((windowSize - 1) / durationMs) * 1000;\n maxSpeed = Math.max(maxSpeed, speed);\n }\n\n return maxSpeed;\n}\n\n/**\n * 计算 TPS (Tokens Per Second) 曲线\n */\nexport function calculateTPS(metrics: StreamMetrics): number[] {\n if (metrics.tokens.length === 0) {\n return [];\n }\n\n const totalDuration = metrics.tokens[metrics.tokens.length - 1];\n const totalSeconds = Math.ceil(totalDuration / 1000);\n\n if (totalSeconds <= 0) {\n return metrics.tokens.length > 0 ? [metrics.tokens.length] : [];\n }\n\n const tps: number[] = new Array(totalSeconds).fill(0);\n\n // 计算每秒内的 token 数\n for (const tokenTime of metrics.tokens) {\n const secondIndex = Math.floor(tokenTime / 1000);\n if (secondIndex < tps.length) {\n tps[secondIndex]++;\n }\n }\n\n return tps;\n}\n\n/**\n * 从 StreamMetrics 计算完整的指标\n */\nexport function calculateMetrics(metrics: StreamMetrics): CalculatedMetrics {\n const tps = calculateTPS(metrics);\n return {\n ttft: calculateTTFT(metrics),\n totalTime: metrics.totalTime,\n totalTokens: metrics.totalTokens,\n averageSpeed: calculateAverageSpeed(metrics),\n peakSpeed: calculatePeakSpeed(metrics),\n peakTps: tps.length > 0 ? Math.max(...tps) : 0,\n tps,\n };\n}\n\n/**\n * 计算一组数值的平均值\n */\nfunction mean(values: number[]): number {\n if (values.length === 0) return 0;\n return values.reduce((sum, v) => sum + v, 0) / values.length;\n}\n\n/**\n * 计算一组数值的标准差\n */\nfunction standardDeviation(values: number[]): number {\n if (values.length < 2) return 0;\n const avg = mean(values);\n const squareDiffs = values.map((v) => Math.pow(v - avg, 2));\n return Math.sqrt(mean(squareDiffs));\n}\n\n/**\n * 从多个 CalculatedMetrics 计算统计结果\n */\nexport function calculateStats(allMetrics: CalculatedMetrics[]): StatsResult {\n if (allMetrics.length === 0) {\n throw new Error(\"Cannot calculate stats from empty metrics array\");\n }\n\n const sampleSize = allMetrics.length;\n\n // 提取各项指标的数组\n const ttfts = allMetrics.map((m) => m.ttft);\n const totalTimes = allMetrics.map((m) => m.totalTime);\n const totalTokens = allMetrics.map((m) => m.totalTokens);\n const averageSpeeds = allMetrics.map((m) => m.averageSpeed);\n const peakSpeeds = allMetrics.map((m) => m.peakSpeed);\n const peakTpsValues = allMetrics.map((m) => m.peakTps);\n\n // 找到最长的 TPS 数组\n const maxTpsLength = Math.max(...allMetrics.map((m) => m.tps.length));\n const avgTps: number[] = [];\n for (let i = 0; i < maxTpsLength; i++) {\n const values = allMetrics.map((m) => m.tps[i] ?? 0);\n avgTps.push(mean(values));\n }\n\n return {\n mean: {\n ttft: mean(ttfts),\n totalTime: mean(totalTimes),\n totalTokens: mean(totalTokens),\n averageSpeed: mean(averageSpeeds),\n peakSpeed: mean(peakSpeeds),\n peakTps: mean(peakTpsValues),\n tps: avgTps,\n },\n min: {\n ttft: Math.min(...ttfts),\n totalTime: Math.min(...totalTimes),\n totalTokens: Math.min(...totalTokens),\n averageSpeed: Math.min(...averageSpeeds),\n peakSpeed: Math.min(...peakSpeeds),\n peakTps: Math.min(...peakTpsValues),\n tps: [],\n },\n max: {\n ttft: Math.max(...ttfts),\n totalTime: Math.max(...totalTimes),\n totalTokens: Math.max(...totalTokens),\n averageSpeed: Math.max(...averageSpeeds),\n peakSpeed: Math.max(...peakSpeeds),\n peakTps: Math.max(...peakTpsValues),\n tps: [],\n },\n stdDev: {\n ttft: standardDeviation(ttfts),\n totalTime: standardDeviation(totalTimes),\n totalTokens: standardDeviation(totalTokens),\n averageSpeed: standardDeviation(averageSpeeds),\n peakSpeed: standardDeviation(peakSpeeds),\n peakTps: standardDeviation(peakTpsValues),\n tps: [],\n },\n sampleSize,\n };\n}\n\n/**\n * 格式化速度显示\n */\nexport function formatSpeed(tokensPerSecond: number): string {\n return tokensPerSecond.toFixed(2);\n}\n\n/**\n * 格式化时间显示\n */\nexport function formatTime(ms: number): string {\n if (ms < 1000) {\n return `${ms.toFixed(0)}ms`;\n }\n return `${(ms / 1000).toFixed(2)}s`;\n}\n","import stringWidth from \"string-width\";\nimport type { CalculatedMetrics, StatsResult } from \"./metrics.js\";\nimport { DEFAULT_LANG, getMessages, type Lang } from \"./i18n.js\";\n\nconst BLOCK_CHAR = \"█\";\nconst CHART_WIDTH = 50;\nconst CHART_HEIGHT = 10;\nconst STAT_LABEL_WIDTH = 15;\nconst STAT_VALUE_WIDTH = 10;\nconst Y_LABEL_WIDTH = 4;\n\nfunction padEndWidth(text: string, width: number): string {\n const currentWidth = stringWidth(text);\n if (currentWidth >= width) {\n return text;\n }\n return text + \" \".repeat(width - currentWidth);\n}\n\nfunction padStartWidth(text: string, width: number): string {\n const currentWidth = stringWidth(text);\n if (currentWidth >= width) {\n return text;\n }\n return \" \".repeat(width - currentWidth) + text;\n}\n\n/**\n * 渲染速度趋势图\n */\nexport function renderSpeedChart(\n tps: number[],\n maxSpeed?: number,\n lang: Lang = DEFAULT_LANG\n): string {\n const messages = getMessages(lang);\n if (tps.length === 0) {\n return messages.noChartData;\n }\n\n const actualMax = maxSpeed ?? Math.max(...tps, 1);\n const maxVal = Math.max(actualMax, 1);\n\n const buildRow = (label: string, bars: string) =>\n `│ ${padStartWidth(label, Y_LABEL_WIDTH)} ┤${bars} │`;\n const emptyRow = buildRow(\"0\", \" \".repeat(CHART_WIDTH));\n const chartWidth = stringWidth(emptyRow) - 2;\n const axisPrefix = `│ ${padStartWidth(\"\", Y_LABEL_WIDTH)} ┼`;\n\n const lines: string[] = [];\n lines.push(messages.speedChartTitle);\n\n // Y 轴标签和边框\n lines.push(\"┌\" + \"─\".repeat(chartWidth) + \"┐\");\n\n for (let row = CHART_HEIGHT - 1; row >= 0; row--) {\n const value = (row / (CHART_HEIGHT - 1)) * maxVal;\n const label = value.toFixed(0);\n\n let bars = \"\";\n for (let col = 0; col < CHART_WIDTH; col++) {\n const index = Math.floor((col / CHART_WIDTH) * tps.length);\n const tpsValue = tps[index] ?? 0;\n const normalizedHeight = (tpsValue / maxVal) * (CHART_HEIGHT - 1);\n bars += normalizedHeight >= row ? BLOCK_CHAR : \" \";\n }\n\n lines.push(buildRow(label, bars));\n }\n\n // X 轴\n lines.push(`${axisPrefix}${\"─\".repeat(CHART_WIDTH)} │`);\n lines.push(\"└\" + \"─\".repeat(chartWidth) + \"┘\");\n\n // X 轴标签 (时间标记)\n const xLabels = generateXLabels(tps.length, 6);\n const labelLine = new Array(CHART_WIDTH).fill(\" \");\n const maxIndex = Math.max(tps.length - 1, 1);\n for (const label of xLabels) {\n const seconds = parseInt(label.replace(\"s\", \"\"), 10);\n const position = Math.min(\n CHART_WIDTH - 1,\n Math.round((seconds / maxIndex) * (CHART_WIDTH - 1))\n );\n for (let i = 0; i < label.length && position + i < CHART_WIDTH; i++) {\n labelLine[position + i] = label[i];\n }\n }\n lines.push(\" \".repeat(stringWidth(axisPrefix)) + labelLine.join(\"\"));\n\n return lines.join(\"\\n\");\n}\n\n/**\n * 生成 X 轴时间标签\n */\nfunction generateXLabels(dataPoints: number, maxLabels: number): string[] {\n if (dataPoints <= 1) {\n return [\"0s\"];\n }\n\n const labels: string[] = [];\n const step = Math.max(1, Math.floor(dataPoints / maxLabels));\n\n for (let i = 0; i < dataPoints; i += step) {\n labels.push(`${i}s`);\n }\n\n // 确保最后一个标签是结束时间\n if (labels[labels.length - 1] !== `${dataPoints - 1}s`) {\n labels.push(`${dataPoints - 1}s`);\n }\n\n return labels;\n}\n\n/**\n * 渲染 TPS 直方图\n */\nexport function renderTPSHistogram(tps: number[], lang: Lang = DEFAULT_LANG): string {\n const messages = getMessages(lang);\n if (tps.length === 0) {\n return messages.noTpsData;\n }\n\n const lines: string[] = [];\n lines.push(messages.tpsHistogramTitle);\n\n // 计算分布区间\n const maxTps = Math.max(...tps, 1);\n const buckets = 10;\n const bucketSize = maxTps / buckets;\n const histogram = new Array(buckets).fill(0);\n\n for (const t of tps) {\n const bucketIndex = Math.min(Math.floor(t / bucketSize), buckets - 1);\n histogram[bucketIndex]++;\n }\n\n const maxCount = Math.max(...histogram, 1);\n\n const labels = histogram.map((_, i) => {\n const bucketStart = (i * bucketSize).toFixed(1);\n const bucketEnd = ((i + 1) * bucketSize).toFixed(1);\n return `${bucketStart}-${bucketEnd}`;\n });\n const labelWidth = Math.max(...labels.map((l) => stringWidth(l)));\n\n for (let i = 0; i < buckets; i++) {\n const label = padEndWidth(labels[i], labelWidth);\n const count = histogram[i];\n const barLength = Math.round((count / maxCount) * CHART_WIDTH);\n const bar = BLOCK_CHAR.repeat(barLength);\n\n const countSuffix = count > 0 ? ` ${count}` : \"\";\n lines.push(`${label} │${bar}${countSuffix}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * 渲染统计汇总表\n */\nexport function renderStatsTable(stats: StatsResult, lang: Lang = DEFAULT_LANG): string {\n const messages = getMessages(lang);\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(messages.statsSummaryTitle(stats.sampleSize));\n\n // 表头\n const headerRow =\n \"│ \" +\n padEndWidth(messages.statsHeaders.metric, STAT_LABEL_WIDTH) +\n \" │ \" +\n padStartWidth(messages.statsHeaders.mean, STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(messages.statsHeaders.min, STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(messages.statsHeaders.max, STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(messages.statsHeaders.stdDev, STAT_VALUE_WIDTH) +\n \" │\";\n\n const tableWidth = stringWidth(headerRow) - 2;\n lines.push(\"┌\" + \"─\".repeat(tableWidth) + \"┐\");\n lines.push(headerRow);\n lines.push(\"├\" + \"─\".repeat(tableWidth) + \"┤\");\n\n // TTFT\n lines.push(\n formatStatRow(\n messages.statsLabels.ttft,\n stats.mean.ttft,\n stats.min.ttft,\n stats.max.ttft,\n stats.stdDev.ttft,\n \"f\"\n )\n );\n lines.push(\"├\" + \"─\".repeat(tableWidth) + \"┤\");\n\n // 总耗时\n lines.push(\n formatStatRow(\n messages.statsLabels.totalTime,\n stats.mean.totalTime,\n stats.min.totalTime,\n stats.max.totalTime,\n stats.stdDev.totalTime,\n \"f\"\n )\n );\n lines.push(\"├\" + \"─\".repeat(tableWidth) + \"┤\");\n\n // 总 token 数\n lines.push(\n formatStatRow(\n messages.statsLabels.totalTokens,\n stats.mean.totalTokens,\n stats.min.totalTokens,\n stats.max.totalTokens,\n stats.stdDev.totalTokens,\n \"f\"\n )\n );\n lines.push(\"├\" + \"─\".repeat(tableWidth) + \"┤\");\n\n // 平均速度\n lines.push(\n formatStatRow(\n messages.statsLabels.averageSpeed,\n stats.mean.averageSpeed,\n stats.min.averageSpeed,\n stats.max.averageSpeed,\n stats.stdDev.averageSpeed,\n \"f\"\n )\n );\n lines.push(\"├\" + \"─\".repeat(tableWidth) + \"┤\");\n\n // 峰值速度\n lines.push(\n formatStatRow(\n messages.statsLabels.peakSpeed,\n stats.mean.peakSpeed,\n stats.min.peakSpeed,\n stats.max.peakSpeed,\n stats.stdDev.peakSpeed,\n \"f\"\n )\n );\n\n lines.push(\"├\" + \"─\".repeat(tableWidth) + \"┤\");\n\n // 峰值 TPS\n lines.push(\n formatStatRow(\n messages.statsLabels.peakTps,\n stats.mean.peakTps,\n stats.min.peakTps,\n stats.max.peakTps,\n stats.stdDev.peakTps,\n \"f\"\n )\n );\n\n lines.push(\"└\" + \"─\".repeat(tableWidth) + \"┘\");\n\n return lines.join(\"\\n\");\n}\n\n/**\n * 格式化统计表格的一行\n */\nfunction formatStatRow(\n label: string,\n mean: number,\n min: number,\n max: number,\n stdDev: number,\n format: \"f\" | \"d\"\n): string {\n const fmt = (n: number) => (format === \"f\" ? n.toFixed(2) : n.toFixed(0));\n\n return (\n \"│ \" +\n padEndWidth(label, STAT_LABEL_WIDTH) +\n \" │ \" +\n padStartWidth(fmt(mean), STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(fmt(min), STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(fmt(max), STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(fmt(stdDev), STAT_VALUE_WIDTH) +\n \" │\"\n );\n}\n\n/**\n * 格式化时间显示(带小数)\n */\nfunction formatTimeWithDecimals(ms: number): string {\n if (ms === Math.floor(ms)) {\n return `${ms.toFixed(0)}ms`;\n }\n return `${ms.toFixed(2)}ms`;\n}\n\n/**\n * 渲染单次测试结果\n */\nexport function renderSingleResult(\n metrics: CalculatedMetrics,\n runIndex: number,\n lang: Lang = DEFAULT_LANG\n): string {\n const messages = getMessages(lang);\n const lines: string[] = [];\n lines.push(`\\n${messages.runLabel(runIndex + 1)}`);\n lines.push(` ${messages.resultLabels.ttft}: ${formatTimeWithDecimals(metrics.ttft)}`);\n lines.push(` ${messages.resultLabels.totalTime}: ${formatTimeWithDecimals(metrics.totalTime)}`);\n lines.push(` ${messages.resultLabels.totalTokens}: ${metrics.totalTokens}`);\n lines.push(\n ` ${messages.resultLabels.averageSpeed}: ${metrics.averageSpeed.toFixed(2)} tokens/s`\n );\n lines.push(` ${messages.resultLabels.peakSpeed}: ${metrics.peakSpeed.toFixed(2)} tokens/s`);\n lines.push(` ${messages.resultLabels.peakTps}: ${metrics.peakTps.toFixed(2)} tokens/s`);\n return lines.join(\"\\n\");\n}\n\n/**\n * 渲染完整的测试报告\n */\nexport function renderReport(stats: StatsResult, lang: Lang = DEFAULT_LANG): string {\n const messages = getMessages(lang);\n const lines: string[] = [];\n\n lines.push(\"\\n\" + \"═\".repeat(72));\n lines.push(messages.reportTitle);\n lines.push(\"═\".repeat(72));\n\n // 汇总统计\n lines.push(renderStatsTable(stats, lang));\n\n // 速度趋势图\n if (stats.mean.tps.length > 0) {\n lines.push(\"\\n\" + renderSpeedChart(stats.mean.tps, undefined, lang));\n }\n\n // TPS 直方图\n if (stats.mean.tps.length > 0) {\n lines.push(\"\\n\" + renderTPSHistogram(stats.mean.tps, lang));\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":";;;AACA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,OAAO,WAAW;;;ACLX,IAAM,kBAAkB,CAAC,MAAM,IAAI;AAGnC,IAAM,eAAqB;AAkDlC,IAAM,aAAuB;AAAA,EAC3B,eAAe;AAAA,EACf,UAAU;AAAA,EACV,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAAA,EACd,cAAc;AAAA,IACZ,UAAU;AAAA,IACV,OAAO;AAAA,IACP,WAAW;AAAA,IACX,MAAM;AAAA,IACN,QAAQ;AAAA,EACV;AAAA,EACA,UAAU,CAAC,UAAkB,iBAAO,KAAK;AAAA,EACzC,kBAAkB,CAAC,SAAiB,UAAkB,iBAAO,OAAO,IAAI,KAAK;AAAA,EAC7E,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,WAAW;AAAA,EACX,mBAAmB,CAAC,eAAuB,+BAAW,UAAU;AAAA,EAChE,cAAc;AAAA,IACZ,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AAAA,EACA,aAAa;AAAA,IACX,MAAM;AAAA,IACN,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AAAA,EACA,cAAc;AAAA,IACZ,MAAM;AAAA,IACN,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AACF;AAEA,IAAM,aAAuB;AAAA,EAC3B,eAAe;AAAA,EACf,UAAU;AAAA,EACV,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAAA,EACd,cAAc;AAAA,IACZ,UAAU;AAAA,IACV,OAAO;AAAA,IACP,WAAW;AAAA,IACX,MAAM;AAAA,IACN,QAAQ;AAAA,EACV;AAAA,EACA,UAAU,CAAC,UAAkB,QAAQ,KAAK;AAAA,EAC1C,kBAAkB,CAAC,SAAiB,UAAkB,QAAQ,OAAO,IAAI,KAAK;AAAA,EAC9E,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,WAAW;AAAA,EACX,mBAAmB,CAAC,eAAuB,cAAc,UAAU;AAAA,EACnE,cAAc;AAAA,IACZ,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AAAA,IACL,QAAQ;AAAA,EACV;AAAA,EACA,aAAa;AAAA,IACX,MAAM;AAAA,IACN,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AAAA,EACA,cAAc;AAAA,IACZ,MAAM;AAAA,IACN,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AACF;AAEO,SAAS,gBAAgB,OAA8B;AAC5D,SAAO,gBAAgB,SAAS,KAAa;AAC/C;AAEO,SAAS,YAAY,OAAsB;AAChD,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,aAAa,MAAM,YAAY;AACrC,MAAI,CAAC,gBAAgB,UAAU,GAAG;AAChC,UAAM,IAAI,MAAM,iBAAiB,KAAK,yBAAyB;AAAA,EACjE;AACA,SAAO;AACT;AAEO,SAAS,YAAY,MAAsB;AAChD,SAAO,SAAS,OAAO,aAAa;AACtC;;;AC5IA,IAAM,iBAA2C;AAAA,EAC/C,WAAW;AAAA,EACX,QAAQ;AACV;AAEA,IAAM,qBAAqB;AAC3B,IAAM,eAAe;AAId,SAAS,YAAY,MAA0B;AACpD,QAAM;AAAA,IACJ;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,OAAO;AAAA,IACP;AAAA,IACA,MAAM;AAAA,EACR,IAAI;AACJ,QAAM,OAAO,YAAY,SAAS;AAClC,QAAM,WAAW,YAAY,IAAI;AACjC,QAAM,cAAc,UAAU,SAAS;AAGvC,MAAI,CAAC,UAAU,OAAO,KAAK,MAAM,IAAI;AACnC,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAGA,MAAI,aAAa,eAAe,aAAa,UAAU;AACrD,UAAM,IAAI,MAAM,qBAAqB,QAAQ,oCAAoC;AAAA,EACnF;AAGA,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,CAAC,OAAO,UAAU,SAAS,KAAK,aAAa,GAAG;AACjF,UAAM,IAAI,MAAM,uBAAuB,SAAS,+BAA+B;AAAA,EACjF;AAEA,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,CAAC,OAAO,UAAU,IAAI,KAAK,QAAQ,GAAG;AAClE,UAAM,IAAI,MAAM,iBAAiB,IAAI,+BAA+B;AAAA,EACtE;AAGA,QAAM,aAAa,SAAS,eAAe,QAAQ;AAEnD,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,OAAO,KAAK;AAAA,IACpB,SAAS,KAAK,KAAK;AAAA,IACnB,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,QAAQ,YAAY,KAAK;AAAA,IACzB;AAAA,EACF;AACF;;;ACnFA,SAAS,mBAAmB;AAC5B,OAAO,eAAe;AACtB,OAAO,YAAY;;;ACFnB,SAAS,oBAAoB,oBAAoB;AAGjD,IAAM,oBAAoB;AAEnB,SAAS,gBAAgB,OAAe;AAC7C,MAAI;AACF,UAAM,aAAa,MAAM,KAAK;AAC9B,QAAI,CAAC,YAAY;AACf,aAAO,aAAa,iBAAiB;AAAA,IACvC;AACA,WAAO,mBAAmB,UAA2B;AAAA,EACvD,QAAQ;AACN,WAAO,aAAa,iBAAiB;AAAA,EACvC;AACF;;;ADJA,eAAsB,oBAAoB,QAAwC;AAChF,QAAM,YAAY,YAAY,IAAI;AAClC,QAAM,aAAuB,CAAC;AAC9B,MAAI,OAAO;AACX,MAAI,qBAAqB;AACzB,MAAI,aAAa;AACjB,MAAI,cAAc;AAElB,QAAM,WAAW,gBAAgB,OAAO,KAAK;AAC7C,QAAM,SAAS,IAAI,UAAU;AAAA,IAC3B,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,EAClB,CAAC;AAED,MAAI;AACF,UAAM,SAAS,MAAM,OAAO,SAAS,OAAO;AAAA,MAC1C,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,OAAO,CAAC;AAAA,MACnD,QAAQ;AAAA,IACV,CAAC;AAED,qBAAiB,SAAS,QAAQ;AAChC,YAAM,cAAc,YAAY,IAAI;AAEpC,UAAI,MAAM,SAAS,yBAAyB,MAAM,MAAM,SAAS,cAAc;AAC7E,cAAM,OAAO,MAAM,MAAM;AAEzB,YAAI,QAAQ,KAAK,SAAS,GAAG;AAC3B,kBAAQ,OAAO,MAAM,IAAI;AACzB,wBAAc;AACd,gBAAM,UAAU,SAAS,OAAO,IAAI;AACpC,gBAAM,YAAY,QAAQ;AAE1B,cAAI,YAAY,GAAG;AACjB,gBAAI,CAAC,oBAAoB;AACvB,qBAAO,cAAc;AACrB,mCAAqB;AAAA,YACvB;AAGA,qBAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,yBAAW,KAAK,cAAc,SAAS;AAAA,YACzC;AAEA,0BAAc;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI,MAAM,wBAAwB,MAAM,OAAO,EAAE;AAAA,IACzD;AACA,UAAM;AAAA,EACR,UAAE;AACA,QAAI,aAAa;AACf,cAAQ,OAAO,MAAM,IAAI;AAAA,IAC3B;AACA,aAAS,KAAK;AAAA,EAChB;AAEA,QAAM,UAAU,YAAY,IAAI;AAChC,QAAM,YAAY,UAAU;AAE5B,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR,aAAa;AAAA,IACb;AAAA,EACF;AACF;AAKA,eAAsB,iBAAiB,QAAwC;AAC7E,QAAM,YAAY,YAAY,IAAI;AAClC,QAAM,aAAuB,CAAC;AAC9B,MAAI,OAAO;AACX,MAAI,qBAAqB;AACzB,MAAI,aAAa;AACjB,MAAI,cAAc;AAElB,QAAM,WAAW,gBAAgB,OAAO,KAAK;AAC7C,QAAM,SAAS,IAAI,OAAO;AAAA,IACxB,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,EAClB,CAAC;AAED,MAAI;AACF,UAAM,SAAS,MAAM,OAAO,KAAK,YAAY,OAAO;AAAA,MAClD,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,OAAO,CAAC;AAAA,MACnD,QAAQ;AAAA,IACV,CAAC;AAED,qBAAiB,SAAS,QAAQ;AAChC,YAAM,cAAc,YAAY,IAAI;AAEpC,YAAM,QAAQ,MAAM,QAAQ,CAAC,GAAG;AAEhC,UAAI,OAAO,SAAS;AAClB,cAAM,UAAU,MAAM;AAEtB,YAAI,QAAQ,SAAS,GAAG;AACtB,kBAAQ,OAAO,MAAM,OAAO;AAC5B,wBAAc;AACd,gBAAM,UAAU,SAAS,OAAO,OAAO;AACvC,gBAAM,YAAY,QAAQ;AAE1B,cAAI,YAAY,GAAG;AACjB,gBAAI,CAAC,oBAAoB;AACvB,qBAAO,cAAc;AACrB,mCAAqB;AAAA,YACvB;AAGA,qBAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,yBAAW,KAAK,cAAc,SAAS;AAAA,YACzC;AAEA,0BAAc;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI,MAAM,qBAAqB,MAAM,OAAO,EAAE;AAAA,IACtD;AACA,UAAM;AAAA,EACR,UAAE;AACA,QAAI,aAAa;AACf,cAAQ,OAAO,MAAM,IAAI;AAAA,IAC3B;AACA,aAAS,KAAK;AAAA,EAChB;AAEA,QAAM,UAAU,YAAY,IAAI;AAChC,QAAM,YAAY,UAAU;AAE5B,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR,aAAa;AAAA,IACb;AAAA,EACF;AACF;AAKA,eAAsB,WAAW,QAAwC;AACvE,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO,oBAAoB,MAAM;AAAA,EACnC,OAAO;AACL,WAAO,iBAAiB,MAAM;AAAA,EAChC;AACF;AAKA,eAAsB,iBAAiB,QAA0C;AAC/E,QAAM,UAA2B,CAAC;AAClC,QAAM,WAAW,YAAY,OAAO,IAAI;AAExC,WAAS,IAAI,GAAG,IAAI,OAAO,UAAU,KAAK;AACxC,QAAI,OAAO,WAAW,GAAG;AACvB,YAAM,QAAQ;AAAA,EAAK,SAAS,iBAAiB,IAAI,GAAG,OAAO,QAAQ,CAAC;AACpE,cAAQ,IAAI,KAAK;AACjB,cAAQ,IAAI,IAAI,OAAO,MAAM,SAAS,CAAC,CAAC;AAAA,IAC1C;AACA,UAAM,SAAS,MAAM,WAAW,MAAM;AACtC,YAAQ,KAAK,MAAM;AAAA,EACrB;AAEA,SAAO;AACT;;;AE1JO,SAAS,cAAc,SAAgC;AAC5D,SAAO,QAAQ;AACjB;AAKO,SAAS,sBAAsB,SAAgC;AACpE,MAAI,QAAQ,aAAa,GAAG;AAC1B,WAAO;AAAA,EACT;AACA,SAAQ,QAAQ,cAAc,QAAQ,YAAa;AACrD;AAKA,IAAM,qBAAqB;AAEpB,SAAS,mBAAmB,SAAwB,aAAqB,IAAY;AAC1F,MAAI,QAAQ,OAAO,SAAS,YAAY;AAEtC,QAAI,QAAQ,OAAO,SAAS,GAAG;AAC7B,aAAO;AAAA,IACT;AACA,UAAM,YAAY,QAAQ,OAAO,QAAQ,OAAO,SAAS,CAAC,IAAI,QAAQ,OAAO,CAAC;AAC9E,UAAM,aAAa,KAAK,IAAI,WAAW,kBAAkB;AACzD,YAAS,QAAQ,OAAO,SAAS,KAAK,aAAc;AAAA,EACtD;AAEA,MAAI,WAAW;AACf,WAAS,IAAI,GAAG,KAAK,QAAQ,OAAO,SAAS,YAAY,KAAK;AAC5D,UAAM,YAAY,QAAQ,OAAO,CAAC;AAClC,UAAM,UAAU,QAAQ,OAAO,IAAI,aAAa,CAAC;AACjD,UAAM,WAAW,UAAU;AAC3B,UAAM,aAAa,KAAK,IAAI,UAAU,kBAAkB;AACxD,UAAM,SAAU,aAAa,KAAK,aAAc;AAChD,eAAW,KAAK,IAAI,UAAU,KAAK;AAAA,EACrC;AAEA,SAAO;AACT;AAKO,SAAS,aAAa,SAAkC;AAC7D,MAAI,QAAQ,OAAO,WAAW,GAAG;AAC/B,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,gBAAgB,QAAQ,OAAO,QAAQ,OAAO,SAAS,CAAC;AAC9D,QAAM,eAAe,KAAK,KAAK,gBAAgB,GAAI;AAEnD,MAAI,gBAAgB,GAAG;AACrB,WAAO,QAAQ,OAAO,SAAS,IAAI,CAAC,QAAQ,OAAO,MAAM,IAAI,CAAC;AAAA,EAChE;AAEA,QAAM,MAAgB,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC;AAGpD,aAAW,aAAa,QAAQ,QAAQ;AACtC,UAAM,cAAc,KAAK,MAAM,YAAY,GAAI;AAC/C,QAAI,cAAc,IAAI,QAAQ;AAC5B,UAAI,WAAW;AAAA,IACjB;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,iBAAiB,SAA2C;AAC1E,QAAM,MAAM,aAAa,OAAO;AAChC,SAAO;AAAA,IACL,MAAM,cAAc,OAAO;AAAA,IAC3B,WAAW,QAAQ;AAAA,IACnB,aAAa,QAAQ;AAAA,IACrB,cAAc,sBAAsB,OAAO;AAAA,IAC3C,WAAW,mBAAmB,OAAO;AAAA,IACrC,SAAS,IAAI,SAAS,IAAI,KAAK,IAAI,GAAG,GAAG,IAAI;AAAA,IAC7C;AAAA,EACF;AACF;AAKA,SAAS,KAAK,QAA0B;AACtC,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC,IAAI,OAAO;AACxD;AAKA,SAAS,kBAAkB,QAA0B;AACnD,MAAI,OAAO,SAAS,EAAG,QAAO;AAC9B,QAAM,MAAM,KAAK,MAAM;AACvB,QAAM,cAAc,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC;AAC1D,SAAO,KAAK,KAAK,KAAK,WAAW,CAAC;AACpC;AAKO,SAAS,eAAe,YAA8C;AAC3E,MAAI,WAAW,WAAW,GAAG;AAC3B,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AAEA,QAAM,aAAa,WAAW;AAG9B,QAAM,QAAQ,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI;AAC1C,QAAM,aAAa,WAAW,IAAI,CAAC,MAAM,EAAE,SAAS;AACpD,QAAM,cAAc,WAAW,IAAI,CAAC,MAAM,EAAE,WAAW;AACvD,QAAM,gBAAgB,WAAW,IAAI,CAAC,MAAM,EAAE,YAAY;AAC1D,QAAM,aAAa,WAAW,IAAI,CAAC,MAAM,EAAE,SAAS;AACpD,QAAM,gBAAgB,WAAW,IAAI,CAAC,MAAM,EAAE,OAAO;AAGrD,QAAM,eAAe,KAAK,IAAI,GAAG,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI,MAAM,CAAC;AACpE,QAAM,SAAmB,CAAC;AAC1B,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACrC,UAAM,SAAS,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC;AAClD,WAAO,KAAK,KAAK,MAAM,CAAC;AAAA,EAC1B;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,MAAM,KAAK,KAAK;AAAA,MAChB,WAAW,KAAK,UAAU;AAAA,MAC1B,aAAa,KAAK,WAAW;AAAA,MAC7B,cAAc,KAAK,aAAa;AAAA,MAChC,WAAW,KAAK,UAAU;AAAA,MAC1B,SAAS,KAAK,aAAa;AAAA,MAC3B,KAAK;AAAA,IACP;AAAA,IACA,KAAK;AAAA,MACH,MAAM,KAAK,IAAI,GAAG,KAAK;AAAA,MACvB,WAAW,KAAK,IAAI,GAAG,UAAU;AAAA,MACjC,aAAa,KAAK,IAAI,GAAG,WAAW;AAAA,MACpC,cAAc,KAAK,IAAI,GAAG,aAAa;AAAA,MACvC,WAAW,KAAK,IAAI,GAAG,UAAU;AAAA,MACjC,SAAS,KAAK,IAAI,GAAG,aAAa;AAAA,MAClC,KAAK,CAAC;AAAA,IACR;AAAA,IACA,KAAK;AAAA,MACH,MAAM,KAAK,IAAI,GAAG,KAAK;AAAA,MACvB,WAAW,KAAK,IAAI,GAAG,UAAU;AAAA,MACjC,aAAa,KAAK,IAAI,GAAG,WAAW;AAAA,MACpC,cAAc,KAAK,IAAI,GAAG,aAAa;AAAA,MACvC,WAAW,KAAK,IAAI,GAAG,UAAU;AAAA,MACjC,SAAS,KAAK,IAAI,GAAG,aAAa;AAAA,MAClC,KAAK,CAAC;AAAA,IACR;AAAA,IACA,QAAQ;AAAA,MACN,MAAM,kBAAkB,KAAK;AAAA,MAC7B,WAAW,kBAAkB,UAAU;AAAA,MACvC,aAAa,kBAAkB,WAAW;AAAA,MAC1C,cAAc,kBAAkB,aAAa;AAAA,MAC7C,WAAW,kBAAkB,UAAU;AAAA,MACvC,SAAS,kBAAkB,aAAa;AAAA,MACxC,KAAK,CAAC;AAAA,IACR;AAAA,IACA;AAAA,EACF;AACF;;;AC/MA,OAAO,iBAAiB;AAIxB,IAAM,aAAa;AACnB,IAAM,cAAc;AACpB,IAAM,eAAe;AACrB,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,gBAAgB;AAEtB,SAAS,YAAY,MAAc,OAAuB;AACxD,QAAM,eAAe,YAAY,IAAI;AACrC,MAAI,gBAAgB,OAAO;AACzB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,IAAI,OAAO,QAAQ,YAAY;AAC/C;AAEA,SAAS,cAAc,MAAc,OAAuB;AAC1D,QAAM,eAAe,YAAY,IAAI;AACrC,MAAI,gBAAgB,OAAO;AACzB,WAAO;AAAA,EACT;AACA,SAAO,IAAI,OAAO,QAAQ,YAAY,IAAI;AAC5C;AAKO,SAAS,iBACd,KACA,UACA,OAAa,cACL;AACR,QAAM,WAAW,YAAY,IAAI;AACjC,MAAI,IAAI,WAAW,GAAG;AACpB,WAAO,SAAS;AAAA,EAClB;AAEA,QAAM,YAAY,YAAY,KAAK,IAAI,GAAG,KAAK,CAAC;AAChD,QAAM,SAAS,KAAK,IAAI,WAAW,CAAC;AAEpC,QAAM,WAAW,CAAC,OAAe,SAC/B,UAAK,cAAc,OAAO,aAAa,CAAC,UAAK,IAAI;AACnD,QAAM,WAAW,SAAS,KAAK,IAAI,OAAO,WAAW,CAAC;AACtD,QAAM,aAAa,YAAY,QAAQ,IAAI;AAC3C,QAAM,aAAa,UAAK,cAAc,IAAI,aAAa,CAAC;AAExD,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,SAAS,eAAe;AAGnC,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAE7C,WAAS,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO;AAChD,UAAM,QAAS,OAAO,eAAe,KAAM;AAC3C,UAAM,QAAQ,MAAM,QAAQ,CAAC;AAE7B,QAAI,OAAO;AACX,aAAS,MAAM,GAAG,MAAM,aAAa,OAAO;AAC1C,YAAM,QAAQ,KAAK,MAAO,MAAM,cAAe,IAAI,MAAM;AACzD,YAAM,WAAW,IAAI,KAAK,KAAK;AAC/B,YAAM,mBAAoB,WAAW,UAAW,eAAe;AAC/D,cAAQ,oBAAoB,MAAM,aAAa;AAAA,IACjD;AAEA,UAAM,KAAK,SAAS,OAAO,IAAI,CAAC;AAAA,EAClC;AAGA,QAAM,KAAK,GAAG,UAAU,GAAG,SAAI,OAAO,WAAW,CAAC,SAAI;AACtD,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAG7C,QAAM,UAAU,gBAAgB,IAAI,QAAQ,CAAC;AAC7C,QAAM,YAAY,IAAI,MAAM,WAAW,EAAE,KAAK,GAAG;AACjD,QAAM,WAAW,KAAK,IAAI,IAAI,SAAS,GAAG,CAAC;AAC3C,aAAW,SAAS,SAAS;AAC3B,UAAM,UAAU,SAAS,MAAM,QAAQ,KAAK,EAAE,GAAG,EAAE;AACnD,UAAM,WAAW,KAAK;AAAA,MACpB,cAAc;AAAA,MACd,KAAK,MAAO,UAAU,YAAa,cAAc,EAAE;AAAA,IACrD;AACA,aAAS,IAAI,GAAG,IAAI,MAAM,UAAU,WAAW,IAAI,aAAa,KAAK;AACnE,gBAAU,WAAW,CAAC,IAAI,MAAM,CAAC;AAAA,IACnC;AAAA,EACF;AACA,QAAM,KAAK,IAAI,OAAO,YAAY,UAAU,CAAC,IAAI,UAAU,KAAK,EAAE,CAAC;AAEnE,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,SAAS,gBAAgB,YAAoB,WAA6B;AACxE,MAAI,cAAc,GAAG;AACnB,WAAO,CAAC,IAAI;AAAA,EACd;AAEA,QAAM,SAAmB,CAAC;AAC1B,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,SAAS,CAAC;AAE3D,WAAS,IAAI,GAAG,IAAI,YAAY,KAAK,MAAM;AACzC,WAAO,KAAK,GAAG,CAAC,GAAG;AAAA,EACrB;AAGA,MAAI,OAAO,OAAO,SAAS,CAAC,MAAM,GAAG,aAAa,CAAC,KAAK;AACtD,WAAO,KAAK,GAAG,aAAa,CAAC,GAAG;AAAA,EAClC;AAEA,SAAO;AACT;AAKO,SAAS,mBAAmB,KAAe,OAAa,cAAsB;AACnF,QAAM,WAAW,YAAY,IAAI;AACjC,MAAI,IAAI,WAAW,GAAG;AACpB,WAAO,SAAS;AAAA,EAClB;AAEA,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,SAAS,iBAAiB;AAGrC,QAAM,SAAS,KAAK,IAAI,GAAG,KAAK,CAAC;AACjC,QAAM,UAAU;AAChB,QAAM,aAAa,SAAS;AAC5B,QAAM,YAAY,IAAI,MAAM,OAAO,EAAE,KAAK,CAAC;AAE3C,aAAW,KAAK,KAAK;AACnB,UAAM,cAAc,KAAK,IAAI,KAAK,MAAM,IAAI,UAAU,GAAG,UAAU,CAAC;AACpE,cAAU,WAAW;AAAA,EACvB;AAEA,QAAM,WAAW,KAAK,IAAI,GAAG,WAAW,CAAC;AAEzC,QAAM,SAAS,UAAU,IAAI,CAAC,GAAG,MAAM;AACrC,UAAM,eAAe,IAAI,YAAY,QAAQ,CAAC;AAC9C,UAAM,cAAc,IAAI,KAAK,YAAY,QAAQ,CAAC;AAClD,WAAO,GAAG,WAAW,IAAI,SAAS;AAAA,EACpC,CAAC;AACD,QAAM,aAAa,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,MAAM,YAAY,CAAC,CAAC,CAAC;AAEhE,WAAS,IAAI,GAAG,IAAI,SAAS,KAAK;AAChC,UAAM,QAAQ,YAAY,OAAO,CAAC,GAAG,UAAU;AAC/C,UAAM,QAAQ,UAAU,CAAC;AACzB,UAAM,YAAY,KAAK,MAAO,QAAQ,WAAY,WAAW;AAC7D,UAAM,MAAM,WAAW,OAAO,SAAS;AAEvC,UAAM,cAAc,QAAQ,IAAI,IAAI,KAAK,KAAK;AAC9C,UAAM,KAAK,GAAG,KAAK,UAAK,GAAG,GAAG,WAAW,EAAE;AAAA,EAC7C;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKO,SAAS,iBAAiB,OAAoB,OAAa,cAAsB;AACtF,QAAM,WAAW,YAAY,IAAI;AACjC,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,SAAS,kBAAkB,MAAM,UAAU,CAAC;AAGvD,QAAM,YACJ,YACA,YAAY,SAAS,aAAa,QAAQ,gBAAgB,IAC1D,aACA,cAAc,SAAS,aAAa,MAAM,gBAAgB,IAC1D,aACA,cAAc,SAAS,aAAa,KAAK,gBAAgB,IACzD,aACA,cAAc,SAAS,aAAa,KAAK,gBAAgB,IACzD,aACA,cAAc,SAAS,aAAa,QAAQ,gBAAgB,IAC5D;AAEF,QAAM,aAAa,YAAY,SAAS,IAAI;AAC5C,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAC7C,QAAM,KAAK,SAAS;AACpB,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAG7C,QAAM;AAAA,IACJ;AAAA,MACE,SAAS,YAAY;AAAA,MACrB,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAG7C,QAAM;AAAA,IACJ;AAAA,MACE,SAAS,YAAY;AAAA,MACrB,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAG7C,QAAM;AAAA,IACJ;AAAA,MACE,SAAS,YAAY;AAAA,MACrB,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAG7C,QAAM;AAAA,IACJ;AAAA,MACE,SAAS,YAAY;AAAA,MACrB,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAG7C,QAAM;AAAA,IACJ;AAAA,MACE,SAAS,YAAY;AAAA,MACrB,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAG7C,QAAM;AAAA,IACJ;AAAA,MACE,SAAS,YAAY;AAAA,MACrB,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,WAAM,SAAI,OAAO,UAAU,IAAI,QAAG;AAE7C,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,SAAS,cACP,OACAA,OACA,KACA,KACA,QACA,QACQ;AACR,QAAM,MAAM,CAAC,MAAe,WAAW,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;AAEvE,SACE,YACA,YAAY,OAAO,gBAAgB,IACnC,aACA,cAAc,IAAIA,KAAI,GAAG,gBAAgB,IACzC,aACA,cAAc,IAAI,GAAG,GAAG,gBAAgB,IACxC,aACA,cAAc,IAAI,GAAG,GAAG,gBAAgB,IACxC,aACA,cAAc,IAAI,MAAM,GAAG,gBAAgB,IAC3C;AAEJ;AAKA,SAAS,uBAAuB,IAAoB;AAClD,MAAI,OAAO,KAAK,MAAM,EAAE,GAAG;AACzB,WAAO,GAAG,GAAG,QAAQ,CAAC,CAAC;AAAA,EACzB;AACA,SAAO,GAAG,GAAG,QAAQ,CAAC,CAAC;AACzB;AAKO,SAAS,mBACd,SACA,UACA,OAAa,cACL;AACR,QAAM,WAAW,YAAY,IAAI;AACjC,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK;AAAA,EAAK,SAAS,SAAS,WAAW,CAAC,CAAC,EAAE;AACjD,QAAM,KAAK,KAAK,SAAS,aAAa,IAAI,KAAK,uBAAuB,QAAQ,IAAI,CAAC,EAAE;AACrF,QAAM,KAAK,KAAK,SAAS,aAAa,SAAS,KAAK,uBAAuB,QAAQ,SAAS,CAAC,EAAE;AAC/F,QAAM,KAAK,KAAK,SAAS,aAAa,WAAW,KAAK,QAAQ,WAAW,EAAE;AAC3E,QAAM;AAAA,IACJ,KAAK,SAAS,aAAa,YAAY,KAAK,QAAQ,aAAa,QAAQ,CAAC,CAAC;AAAA,EAC7E;AACA,QAAM,KAAK,KAAK,SAAS,aAAa,SAAS,KAAK,QAAQ,UAAU,QAAQ,CAAC,CAAC,WAAW;AAC3F,QAAM,KAAK,KAAK,SAAS,aAAa,OAAO,KAAK,QAAQ,QAAQ,QAAQ,CAAC,CAAC,WAAW;AACvF,SAAO,MAAM,KAAK,IAAI;AACxB;AAKO,SAAS,aAAa,OAAoB,OAAa,cAAsB;AAClF,QAAM,WAAW,YAAY,IAAI;AACjC,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,OAAO,SAAI,OAAO,EAAE,CAAC;AAChC,QAAM,KAAK,SAAS,WAAW;AAC/B,QAAM,KAAK,SAAI,OAAO,EAAE,CAAC;AAGzB,QAAM,KAAK,iBAAiB,OAAO,IAAI,CAAC;AAGxC,MAAI,MAAM,KAAK,IAAI,SAAS,GAAG;AAC7B,UAAM,KAAK,OAAO,iBAAiB,MAAM,KAAK,KAAK,QAAW,IAAI,CAAC;AAAA,EACrE;AAGA,MAAI,MAAM,KAAK,IAAI,SAAS,GAAG;AAC7B,UAAM,KAAK,OAAO,mBAAmB,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,EAC5D;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;;;ANxVA,SAAS,gBAAwB;AAC/B,MAAI;AACF,UAAM,aAAa,QAAQ,cAAc,YAAY,GAAG,CAAC;AACzD,UAAM,cAAc,KAAK,YAAY,MAAM,cAAc;AACzD,UAAM,cAAc,KAAK,MAAM,aAAa,aAAa,OAAO,CAAC;AACjE,WAAO,YAAY,WAAW;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,kBAAkB,EACvB,YAAY,+CAA+C,EAC3D,QAAQ,cAAc,CAAC;AAE1B,QACG,OAAO,uBAAuB,sBAAsB,EAAE,EACtD,OAAO,6BAA6B,qCAAqC,WAAW,EACpF,OAAO,mBAAmB,yBAAyB,EACnD,OAAO,uBAAuB,YAAY,EAC1C,OAAO,yBAAyB,yBAAyB,MAAM,EAC/D,OAAO,uBAAuB,uBAAuB,GAAG,EACxD,OAAO,mBAAmB,aAAa,EACvC,OAAO,iBAAiB,6BAA6B,IAAI,EACzD,MAAM,QAAQ,IAAI;AAErB,IAAM,UAAU,QAAQ,KAAK;AAE7B,eAAe,OAAO;AACpB,MAAI,WAAW,YAAY,YAAY;AACvC,MAAI;AAEF,UAAM,SAAS,YAAY;AAAA,MACzB,QAAQ,QAAQ;AAAA,MAChB,UAAU,QAAQ;AAAA,MAClB,KAAK,QAAQ;AAAA,MACb,OAAO,QAAQ;AAAA,MACf,WAAW,SAAS,QAAQ,WAAW,EAAE;AAAA,MACzC,MAAM,SAAS,QAAQ,MAAM,EAAE;AAAA,MAC/B,QAAQ,QAAQ;AAAA,MAChB,MAAM,QAAQ;AAAA,IAChB,CAAC;AACD,eAAW,YAAY,OAAO,IAAI;AAGlC,YAAQ,IAAI,MAAM,KAAK;AAAA,EAAK,SAAS,QAAQ,EAAE,CAAC;AAChD,YAAQ,IAAI,MAAM,KAAK,SAAI,OAAO,EAAE,CAAC,CAAC;AACtC,YAAQ,IAAI,MAAM,KAAK,GAAG,SAAS,aAAa,QAAQ,KAAK,MAAM,MAAM,OAAO,QAAQ,CAAC,EAAE,CAAC;AAC5F,YAAQ,IAAI,MAAM,KAAK,GAAG,SAAS,aAAa,KAAK,KAAK,MAAM,MAAM,OAAO,KAAK,CAAC,EAAE,CAAC;AACtF,YAAQ,IAAI,MAAM,KAAK,GAAG,SAAS,aAAa,SAAS,KAAK,MAAM,MAAM,OAAO,SAAS,CAAC,EAAE,CAAC;AAC9F,YAAQ,IAAI,MAAM,KAAK,GAAG,SAAS,aAAa,IAAI,KAAK,MAAM,MAAM,OAAO,QAAQ,CAAC,EAAE,CAAC;AACxF,YAAQ;AAAA,MACN,MAAM;AAAA,QACJ,GAAG,SAAS,aAAa,MAAM,KAAK,MAAM,MAAM,OAAO,OAAO,UAAU,GAAG,EAAE,CAAC,CAAC,GAC7E,OAAO,OAAO,SAAS,KAAK,QAAQ,EACtC;AAAA,MACF;AAAA,IACF;AACA,YAAQ,IAAI,MAAM,KAAK,SAAI,OAAO,EAAE,CAAC,CAAC;AAGtC,YAAQ,IAAI,MAAM,OAAO;AAAA,EAAK,SAAS,YAAY;AAAA,CAAI,CAAC;AACxD,YAAQ,IAAI,MAAM,KAAK,GAAG,SAAS,eAAe;AAAA,CAAI,CAAC;AAEvD,UAAM,UAAU,MAAM,iBAAiB,MAAM;AAG7C,UAAM,aAAa,QAAQ,IAAI,CAAC,MAAM,iBAAiB,CAAC,CAAC;AAGzD,aAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,cAAQ,IAAI,MAAM,KAAK,mBAAmB,WAAW,CAAC,GAAG,GAAG,OAAO,IAAI,CAAC,CAAC;AAAA,IAC3E;AAGA,UAAM,QAAQ,eAAe,UAAU;AAGvC,YAAQ,IAAI,MAAM,KAAK,OAAO,aAAa,OAAO,OAAO,IAAI,CAAC,CAAC;AAE/D,YAAQ,IAAI,MAAM,MAAM;AAAA,EAAK,SAAS,YAAY;AAAA,CAAI,CAAC;AAAA,EACzD,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,cAAQ,MAAM,MAAM,IAAI;AAAA,EAAK,SAAS,WAAW,KAAK,MAAM,OAAO;AAAA,CAAI,CAAC;AAAA,IAC1E,OAAO;AACL,cAAQ,MAAM,MAAM,IAAI;AAAA,EAAK,SAAS,YAAY;AAAA,CAAI,CAAC;AAAA,IACzD;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK,KAAK;","names":["mean"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-speed-tester",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A CLI tool to test LLM API token output speed",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
"chalk": "^5.4.1",
|
|
53
53
|
"cli-table3": "^0.6.5",
|
|
54
54
|
"commander": "^12.1.0",
|
|
55
|
-
"openai": "^4.77.3"
|
|
55
|
+
"openai": "^4.77.3",
|
|
56
|
+
"string-width": "^7.2.0",
|
|
57
|
+
"tiktoken": "^1.0.22"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@eslint/js": "^9.39.2",
|