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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > A CLI tool to measure and analyze LLM API token streaming performance
4
4
 
5
- [中文文档](README.md) |
5
+ [Chinese README](README.md)
6
6
 
7
7
  [![npm version](https://badge.fury.io/js/token-speed-tester.svg)](https://www.npmjs.com/package/token-speed-tester)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | "写一篇关于 AI 的短文" |
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
- 🚀 Token 速度测试工具
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: 写一篇关于 AI 的短文
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
- 总耗时: 3245ms
129
- Token 数: 412
130
- 平均速度: 126.96 tokens/s
131
- 峰值速度: 156.32 tokens/s
132
-
133
- [运行 2]
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
- 总耗时: 3189ms
136
- Token 数: 398
137
- 平均速度: 124.84 tokens/s
138
- 峰值速度: 158.41 tokens/s
139
-
140
- [运行 3]
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
- 总耗时: 3312ms
143
- Token 数: 405
144
- 平均速度: 122.28 tokens/s
145
- 峰值速度: 154.23 tokens/s
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 × 1000) |
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 © [Cansiny0320](https://github.com/Cansiny0320)
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 = DEFAULT_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: prompt.trim()
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 = Date.now();
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 = Date.now();
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
- if (!firstTokenRecorded) {
77
- ttft = currentTime - startTime;
78
- firstTokenRecorded = true;
79
- }
80
- for (let i = 0; i < text.length; i++) {
81
- tokenTimes.push(currentTime - startTime);
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 = Date.now();
243
+ const endTime = performance.now();
93
244
  const totalTime = endTime - startTime;
94
245
  return {
95
246
  ttft,
96
247
  tokens: tokenTimes,
97
- totalTokens: tokenTimes.length,
248
+ totalTokens: tokenCount,
98
249
  totalTime
99
250
  };
100
251
  }
101
252
  async function openaiStreamTest(config) {
102
- const startTime = Date.now();
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 = Date.now();
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 (!firstTokenRecorded && content.length > 0) {
123
- ttft = currentTime - startTime;
124
- firstTokenRecorded = true;
125
- }
126
- for (let i = 0; i < content.length; i++) {
127
- tokenTimes.push(currentTime - startTime);
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 = Date.now();
305
+ const endTime = performance.now();
138
306
  const totalTime = endTime - startTime;
139
307
  return {
140
308
  ttft,
141
309
  tokens: tokenTimes,
142
- totalTokens: tokenTimes.length,
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
- return totalTime > 0 ? (metrics.tokens.length - 1) / totalTime * 1e3 : 0;
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
- if (duration > 0) {
186
- const speed = (windowSize - 1) / duration * 1e3;
187
- maxSpeed = Math.max(maxSpeed, speed);
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
- tps: calculateTPS(metrics)
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
- function renderSpeedChart(tps, maxSpeed) {
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 "No data available for chart";
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("Token \u901F\u5EA6\u8D8B\u52BF\u56FE (TPS)");
295
- lines.push("\u250C" + "\u2500".repeat(CHART_WIDTH) + "\u2510");
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).padStart(4);
299
- let chartRow = "\u2502 " + label + " \u2524";
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
- if (normalizedHeight >= row) {
305
- chartRow += BLOCK_CHAR;
306
- } else {
307
- chartRow += " ";
308
- }
510
+ bars += normalizedHeight >= row ? BLOCK_CHAR : " ";
309
511
  }
310
- chartRow += " \u2502";
311
- lines.push(chartRow);
512
+ lines.push(buildRow(label, bars));
312
513
  }
313
- lines.push("\u2502 " + "0".padStart(CHART_WIDTH) + "s \u2502");
314
- lines.push("\u2514" + "\u2500".repeat(CHART_WIDTH) + "\u2518");
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
- lines.push(" " + xLabels.join(" "));
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 "No TPS data available";
549
+ return messages.noTpsData;
336
550
  }
337
551
  const lines = [];
338
- lines.push("TPS \u5206\u5E03");
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
- for (let i = 0; i < buckets; i++) {
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
- lines.push(`${bucketStart}-${bucketEnd} \u2502${bar} ${count}`);
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("\u7EDF\u8BA1\u6C47\u603B (N=" + stats.sampleSize + ")");
362
- lines.push("\u250C" + "\u2500".repeat(70) + "\u2510");
363
- lines.push(
364
- "\u2502 " + "\u6307\u6807".padEnd(15) + " \u2502 " + "\u5747\u503C".padStart(10) + " \u2502 " + "\u6700\u5C0F\u503C".padStart(10) + " \u2502 " + "\u6700\u5927\u503C".padStart(10) + " \u2502 " + "\u6807\u51C6\u5DEE".padStart(10) + " \u2502"
365
- );
366
- lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
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
- "TTFT (ms)",
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(70) + "\u2524");
598
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
378
599
  lines.push(
379
600
  formatStatRow(
380
- "\u603B\u8017\u65F6 (ms)",
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(70) + "\u2524");
609
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
389
610
  lines.push(
390
611
  formatStatRow(
391
- "\u603B Token \u6570",
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(70) + "\u2524");
620
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
400
621
  lines.push(
401
622
  formatStatRow(
402
- "\u5E73\u5747\u901F\u5EA6",
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(70) + "\u2524");
631
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
411
632
  lines.push(
412
633
  formatStatRow(
413
- "\u5CF0\u503C\u901F\u5EA6",
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("\u2514" + "\u2500".repeat(70) + "\u2518");
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.padEnd(15) + " \u2502 " + fmt(mean2).padStart(10) + " \u2502 " + fmt(min).padStart(10) + " \u2502 " + fmt(max).padStart(10) + " \u2502 " + fmt(stdDev).padStart(10) + " \u2502";
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
- [\u8FD0\u884C ${runIndex + 1}]`);
438
- lines.push(` TTFT: ${formatTimeWithDecimals(metrics.ttft)}`);
439
- lines.push(` \u603B\u8017\u65F6: ${formatTimeWithDecimals(metrics.totalTime)}`);
440
- lines.push(` \u603B Token \u6570: ${metrics.totalTokens}`);
441
- lines.push(` \u5E73\u5747\u901F\u5EA6: ${metrics.averageSpeed.toFixed(2)} tokens/s`);
442
- lines.push(` \u5CF0\u503C\u901F\u5EA6: ${metrics.peakSpeed.toFixed(2)} tokens/s`);
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("Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A");
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", "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587").parse(process.argv);
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
- console.log(chalk.cyan("\n\u{1F680} Token \u901F\u5EA6\u6D4B\u8BD5\u5DE5\u5177"));
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(`Provider: ${chalk.white(config.provider)}`));
489
- console.log(chalk.gray(`Model: ${chalk.white(config.model)}`));
490
- console.log(chalk.gray(`Max Tokens: ${chalk.white(config.maxTokens)}`));
491
- console.log(chalk.gray(`Runs: ${chalk.white(config.runCount)}`));
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
- `Prompt: ${chalk.white(config.prompt.substring(0, 50))}${config.prompt.length > 50 ? "..." : ""}`
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("\n\u23F3 \u6B63\u5728\u8FD0\u884C\u6D4B\u8BD5...\n"));
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("\n\u2705 \u6D4B\u8BD5\u5B8C\u6210!\n"));
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
- \u274C \u9519\u8BEF: ${error.message}
757
+ ${messages.errorPrefix}: ${error.message}
511
758
  `));
512
759
  } else {
513
- console.error(chalk.red("\n\u274C \u53D1\u751F\u672A\u77E5\u9519\u8BEF\n"));
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":"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","names":["mean"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-speed-tester",
3
- "version": "1.4.2",
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",