token-speed-tester 1.4.2 → 1.5.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
@@ -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
@@ -123,12 +124,15 @@ Prompt: 写一篇关于 AI 的短文
123
124
 
124
125
  ⏳ 正在运行测试...
125
126
 
127
+ 模型输出 (流式):
128
+
126
129
  [运行 1]
127
130
  TTFT: 523ms
128
131
  总耗时: 3245ms
129
132
  总 Token 数: 412
130
133
  平均速度: 126.96 tokens/s
131
134
  峰值速度: 156.32 tokens/s
135
+ 峰值 TPS: 168.00 tokens/s
132
136
 
133
137
  [运行 2]
134
138
  TTFT: 487ms
@@ -136,6 +140,7 @@ Prompt: 写一篇关于 AI 的短文
136
140
  总 Token 数: 398
137
141
  平均速度: 124.84 tokens/s
138
142
  峰值速度: 158.41 tokens/s
143
+ 峰值 TPS: 171.00 tokens/s
139
144
 
140
145
  [运行 3]
141
146
  TTFT: 501ms
@@ -143,6 +148,7 @@ Prompt: 写一篇关于 AI 的短文
143
148
  总 Token 数: 405
144
149
  平均速度: 122.28 tokens/s
145
150
  峰值速度: 154.23 tokens/s
151
+ 峰值 TPS: 166.00 tokens/s
146
152
 
147
153
  ======================================================================
148
154
  Token 速度测试报告
@@ -161,6 +167,8 @@ Token 速度测试报告
161
167
  │ 平均速度 │ 124.69 │ 122.28 │ 126.96 │ 1.88 │
162
168
  ├──────────────────────────────────────────────────────────────────────┤
163
169
  │ 峰值速度 │ 156.32 │ 154.23 │ 158.41 │ 1.82 │
170
+ ├──────────────────────────────────────────────────────────────────────┤
171
+ │ 峰值 TPS │ 168.33 │ 166.00 │ 171.00 │ 2.05 │
164
172
  └──────────────────────────────────────────────────────────────────────┘
165
173
 
166
174
  Token 速度趋势图 (TPS)
@@ -199,8 +207,11 @@ TPS 分布
199
207
  | **Total Tokens** | Number of output tokens received |
200
208
  | **Average Speed** | Mean tokens per second (totalTokens / totalTime × 1000) |
201
209
  | **Peak Speed** | Fastest speed measured over a sliding 10-token window |
210
+ | **Peak TPS** | Highest tokens received within a single second |
202
211
  | **TPS Curve** | Tokens received per second throughout the streaming response |
203
212
 
213
+ Note: Token counting uses the model tokenizer per stream chunk; boundary splits may cause slight differences.
214
+
204
215
  ## Development
205
216
 
206
217
  ### Running Tests
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 可视化**:精美的终端图表和数据表格
@@ -123,12 +124,15 @@ Prompt: 写一篇关于 AI 的短文
123
124
 
124
125
  ⏳ 正在运行测试...
125
126
 
127
+ 模型输出 (流式):
128
+
126
129
  [运行 1]
127
130
  TTFT: 523ms
128
131
  总耗时: 3245ms
129
132
  总 Token 数: 412
130
133
  平均速度: 126.96 tokens/s
131
134
  峰值速度: 156.32 tokens/s
135
+ 峰值 TPS: 168.00 tokens/s
132
136
 
133
137
  [运行 2]
134
138
  TTFT: 487ms
@@ -136,6 +140,7 @@ Prompt: 写一篇关于 AI 的短文
136
140
  总 Token 数: 398
137
141
  平均速度: 124.84 tokens/s
138
142
  峰值速度: 158.41 tokens/s
143
+ 峰值 TPS: 171.00 tokens/s
139
144
 
140
145
  [运行 3]
141
146
  TTFT: 501ms
@@ -143,6 +148,7 @@ Prompt: 写一篇关于 AI 的短文
143
148
  总 Token 数: 405
144
149
  平均速度: 122.28 tokens/s
145
150
  峰值速度: 154.23 tokens/s
151
+ 峰值 TPS: 166.00 tokens/s
146
152
 
147
153
  ======================================================================
148
154
  Token 速度测试报告
@@ -161,6 +167,8 @@ Token 速度测试报告
161
167
  │ 平均速度 │ 124.69 │ 122.28 │ 126.96 │ 1.88 │
162
168
  ├──────────────────────────────────────────────────────────────────────┤
163
169
  │ 峰值速度 │ 156.32 │ 154.23 │ 158.41 │ 1.82 │
170
+ ├──────────────────────────────────────────────────────────────────────┤
171
+ │ 峰值 TPS │ 168.33 │ 166.00 │ 171.00 │ 2.05 │
164
172
  └──────────────────────────────────────────────────────────────────────┘
165
173
 
166
174
  Token 速度趋势图 (TPS)
@@ -199,8 +207,11 @@ TPS 分布
199
207
  | **总 Token 数** | 接收到的输出 Token 数量 |
200
208
  | **平均速度** | 每秒平均 Token 数(totalTokens / totalTime × 1000) |
201
209
  | **峰值速度** | 10 个 Token 滑动窗口内测量的最快速度 |
210
+ | **峰值 TPS** | 单秒内最高 Token 数 |
202
211
  | **TPS 曲线** | 整个流式响应中每秒接收的 Token 数 |
203
212
 
213
+ 注:Token 统计基于模型 tokenizer,并按流式分片计数,分片边界可能带来轻微差异。
214
+
204
215
  ## 开发
205
216
 
206
217
  ### 运行测试
package/dist/index.js CHANGED
@@ -50,13 +50,34 @@ function parseConfig(args) {
50
50
  }
51
51
 
52
52
  // src/client.ts
53
+ import { performance } from "perf_hooks";
53
54
  import Anthropic from "@anthropic-ai/sdk";
54
55
  import OpenAI from "openai";
56
+
57
+ // src/tokenizer.ts
58
+ import { encoding_for_model, get_encoding } from "tiktoken";
59
+ var FALLBACK_ENCODING = "cl100k_base";
60
+ function createTokenizer(model) {
61
+ try {
62
+ const normalized = model.trim();
63
+ if (!normalized) {
64
+ return get_encoding(FALLBACK_ENCODING);
65
+ }
66
+ return encoding_for_model(normalized);
67
+ } catch {
68
+ return get_encoding(FALLBACK_ENCODING);
69
+ }
70
+ }
71
+
72
+ // src/client.ts
55
73
  async function anthropicStreamTest(config) {
56
- const startTime = Date.now();
74
+ const startTime = performance.now();
57
75
  const tokenTimes = [];
58
76
  let ttft = 0;
59
77
  let firstTokenRecorded = false;
78
+ let tokenCount = 0;
79
+ let wroteOutput = false;
80
+ const encoding = createTokenizer(config.model);
60
81
  const client = new Anthropic({
61
82
  apiKey: config.apiKey,
62
83
  baseURL: config.baseURL
@@ -69,16 +90,23 @@ async function anthropicStreamTest(config) {
69
90
  stream: true
70
91
  });
71
92
  for await (const event of stream) {
72
- const currentTime = Date.now();
93
+ const currentTime = performance.now();
73
94
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
74
95
  const text = event.delta.text;
75
96
  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);
97
+ process.stdout.write(text);
98
+ wroteOutput = true;
99
+ const encoded = encoding.encode(text);
100
+ const newTokens = encoded.length;
101
+ if (newTokens > 0) {
102
+ if (!firstTokenRecorded) {
103
+ ttft = currentTime - startTime;
104
+ firstTokenRecorded = true;
105
+ }
106
+ for (let i = 0; i < newTokens; i++) {
107
+ tokenTimes.push(currentTime - startTime);
108
+ }
109
+ tokenCount += newTokens;
82
110
  }
83
111
  }
84
112
  }
@@ -88,21 +116,29 @@ async function anthropicStreamTest(config) {
88
116
  throw new Error(`Anthropic API error: ${error.message}`);
89
117
  }
90
118
  throw error;
119
+ } finally {
120
+ if (wroteOutput) {
121
+ process.stdout.write("\n");
122
+ }
123
+ encoding.free();
91
124
  }
92
- const endTime = Date.now();
125
+ const endTime = performance.now();
93
126
  const totalTime = endTime - startTime;
94
127
  return {
95
128
  ttft,
96
129
  tokens: tokenTimes,
97
- totalTokens: tokenTimes.length,
130
+ totalTokens: tokenCount,
98
131
  totalTime
99
132
  };
100
133
  }
101
134
  async function openaiStreamTest(config) {
102
- const startTime = Date.now();
135
+ const startTime = performance.now();
103
136
  const tokenTimes = [];
104
137
  let ttft = 0;
105
138
  let firstTokenRecorded = false;
139
+ let tokenCount = 0;
140
+ let wroteOutput = false;
141
+ const encoding = createTokenizer(config.model);
106
142
  const client = new OpenAI({
107
143
  apiKey: config.apiKey,
108
144
  baseURL: config.baseURL
@@ -115,16 +151,25 @@ async function openaiStreamTest(config) {
115
151
  stream: true
116
152
  });
117
153
  for await (const chunk of stream) {
118
- const currentTime = Date.now();
154
+ const currentTime = performance.now();
119
155
  const delta = chunk.choices[0]?.delta;
120
156
  if (delta?.content) {
121
157
  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);
158
+ if (content.length > 0) {
159
+ process.stdout.write(content);
160
+ wroteOutput = true;
161
+ const encoded = encoding.encode(content);
162
+ const newTokens = encoded.length;
163
+ if (newTokens > 0) {
164
+ if (!firstTokenRecorded) {
165
+ ttft = currentTime - startTime;
166
+ firstTokenRecorded = true;
167
+ }
168
+ for (let i = 0; i < newTokens; i++) {
169
+ tokenTimes.push(currentTime - startTime);
170
+ }
171
+ tokenCount += newTokens;
172
+ }
128
173
  }
129
174
  }
130
175
  }
@@ -133,13 +178,18 @@ async function openaiStreamTest(config) {
133
178
  throw new Error(`OpenAI API error: ${error.message}`);
134
179
  }
135
180
  throw error;
181
+ } finally {
182
+ if (wroteOutput) {
183
+ process.stdout.write("\n");
184
+ }
185
+ encoding.free();
136
186
  }
137
- const endTime = Date.now();
187
+ const endTime = performance.now();
138
188
  const totalTime = endTime - startTime;
139
189
  return {
140
190
  ttft,
141
191
  tokens: tokenTimes,
142
- totalTokens: tokenTimes.length,
192
+ totalTokens: tokenCount,
143
193
  totalTime
144
194
  };
145
195
  }
@@ -153,6 +203,12 @@ async function streamTest(config) {
153
203
  async function runMultipleTests(config) {
154
204
  const results = [];
155
205
  for (let i = 0; i < config.runCount; i++) {
206
+ if (config.runCount > 1) {
207
+ const label = `
208
+ [\u8FD0\u884C ${i + 1}/${config.runCount}]`;
209
+ console.log(label);
210
+ console.log("-".repeat(label.length - 1));
211
+ }
156
212
  const result = await streamTest(config);
157
213
  results.push(result);
158
214
  }
@@ -169,23 +225,24 @@ function calculateAverageSpeed(metrics) {
169
225
  }
170
226
  return metrics.totalTokens / metrics.totalTime * 1e3;
171
227
  }
228
+ var MIN_PEAK_WINDOW_MS = 50;
172
229
  function calculatePeakSpeed(metrics, windowSize = 10) {
173
230
  if (metrics.tokens.length < windowSize) {
174
231
  if (metrics.tokens.length < 2) {
175
232
  return 0;
176
233
  }
177
234
  const totalTime = metrics.tokens[metrics.tokens.length - 1] - metrics.tokens[0];
178
- return totalTime > 0 ? (metrics.tokens.length - 1) / totalTime * 1e3 : 0;
235
+ const durationMs = Math.max(totalTime, MIN_PEAK_WINDOW_MS);
236
+ return (metrics.tokens.length - 1) / durationMs * 1e3;
179
237
  }
180
238
  let maxSpeed = 0;
181
239
  for (let i = 0; i <= metrics.tokens.length - windowSize; i++) {
182
240
  const startTime = metrics.tokens[i];
183
241
  const endTime = metrics.tokens[i + windowSize - 1];
184
242
  const duration = endTime - startTime;
185
- if (duration > 0) {
186
- const speed = (windowSize - 1) / duration * 1e3;
187
- maxSpeed = Math.max(maxSpeed, speed);
188
- }
243
+ const durationMs = Math.max(duration, MIN_PEAK_WINDOW_MS);
244
+ const speed = (windowSize - 1) / durationMs * 1e3;
245
+ maxSpeed = Math.max(maxSpeed, speed);
189
246
  }
190
247
  return maxSpeed;
191
248
  }
@@ -208,13 +265,15 @@ function calculateTPS(metrics) {
208
265
  return tps;
209
266
  }
210
267
  function calculateMetrics(metrics) {
268
+ const tps = calculateTPS(metrics);
211
269
  return {
212
270
  ttft: calculateTTFT(metrics),
213
271
  totalTime: metrics.totalTime,
214
272
  totalTokens: metrics.totalTokens,
215
273
  averageSpeed: calculateAverageSpeed(metrics),
216
274
  peakSpeed: calculatePeakSpeed(metrics),
217
- tps: calculateTPS(metrics)
275
+ peakTps: tps.length > 0 ? Math.max(...tps) : 0,
276
+ tps
218
277
  };
219
278
  }
220
279
  function mean(values) {
@@ -237,6 +296,7 @@ function calculateStats(allMetrics) {
237
296
  const totalTokens = allMetrics.map((m) => m.totalTokens);
238
297
  const averageSpeeds = allMetrics.map((m) => m.averageSpeed);
239
298
  const peakSpeeds = allMetrics.map((m) => m.peakSpeed);
299
+ const peakTpsValues = allMetrics.map((m) => m.peakTps);
240
300
  const maxTpsLength = Math.max(...allMetrics.map((m) => m.tps.length));
241
301
  const avgTps = [];
242
302
  for (let i = 0; i < maxTpsLength; i++) {
@@ -250,6 +310,7 @@ function calculateStats(allMetrics) {
250
310
  totalTokens: mean(totalTokens),
251
311
  averageSpeed: mean(averageSpeeds),
252
312
  peakSpeed: mean(peakSpeeds),
313
+ peakTps: mean(peakTpsValues),
253
314
  tps: avgTps
254
315
  },
255
316
  min: {
@@ -258,6 +319,7 @@ function calculateStats(allMetrics) {
258
319
  totalTokens: Math.min(...totalTokens),
259
320
  averageSpeed: Math.min(...averageSpeeds),
260
321
  peakSpeed: Math.min(...peakSpeeds),
322
+ peakTps: Math.min(...peakTpsValues),
261
323
  tps: []
262
324
  },
263
325
  max: {
@@ -266,6 +328,7 @@ function calculateStats(allMetrics) {
266
328
  totalTokens: Math.max(...totalTokens),
267
329
  averageSpeed: Math.max(...averageSpeeds),
268
330
  peakSpeed: Math.max(...peakSpeeds),
331
+ peakTps: Math.max(...peakTpsValues),
269
332
  tps: []
270
333
  },
271
334
  stdDev: {
@@ -274,6 +337,7 @@ function calculateStats(allMetrics) {
274
337
  totalTokens: standardDeviation(totalTokens),
275
338
  averageSpeed: standardDeviation(averageSpeeds),
276
339
  peakSpeed: standardDeviation(peakSpeeds),
340
+ peakTps: standardDeviation(peakTpsValues),
277
341
  tps: []
278
342
  },
279
343
  sampleSize
@@ -281,39 +345,68 @@ function calculateStats(allMetrics) {
281
345
  }
282
346
 
283
347
  // src/chart.ts
348
+ import stringWidth from "string-width";
284
349
  var BLOCK_CHAR = "\u2588";
285
350
  var CHART_WIDTH = 50;
286
351
  var CHART_HEIGHT = 10;
352
+ var STAT_LABEL_WIDTH = 15;
353
+ var STAT_VALUE_WIDTH = 10;
354
+ var Y_LABEL_WIDTH = 4;
355
+ function padEndWidth(text, width) {
356
+ const currentWidth = stringWidth(text);
357
+ if (currentWidth >= width) {
358
+ return text;
359
+ }
360
+ return text + " ".repeat(width - currentWidth);
361
+ }
362
+ function padStartWidth(text, width) {
363
+ const currentWidth = stringWidth(text);
364
+ if (currentWidth >= width) {
365
+ return text;
366
+ }
367
+ return " ".repeat(width - currentWidth) + text;
368
+ }
287
369
  function renderSpeedChart(tps, maxSpeed) {
288
370
  if (tps.length === 0) {
289
371
  return "No data available for chart";
290
372
  }
291
373
  const actualMax = maxSpeed ?? Math.max(...tps, 1);
292
374
  const maxVal = Math.max(actualMax, 1);
375
+ const buildRow = (label, bars) => `\u2502 ${padStartWidth(label, Y_LABEL_WIDTH)} \u2524${bars} \u2502`;
376
+ const emptyRow = buildRow("0", " ".repeat(CHART_WIDTH));
377
+ const chartWidth = stringWidth(emptyRow) - 2;
378
+ const axisPrefix = `\u2502 ${padStartWidth("", Y_LABEL_WIDTH)} \u253C`;
293
379
  const lines = [];
294
380
  lines.push("Token \u901F\u5EA6\u8D8B\u52BF\u56FE (TPS)");
295
- lines.push("\u250C" + "\u2500".repeat(CHART_WIDTH) + "\u2510");
381
+ lines.push("\u250C" + "\u2500".repeat(chartWidth) + "\u2510");
296
382
  for (let row = CHART_HEIGHT - 1; row >= 0; row--) {
297
383
  const value = row / (CHART_HEIGHT - 1) * maxVal;
298
- const label = value.toFixed(0).padStart(4);
299
- let chartRow = "\u2502 " + label + " \u2524";
384
+ const label = value.toFixed(0);
385
+ let bars = "";
300
386
  for (let col = 0; col < CHART_WIDTH; col++) {
301
387
  const index = Math.floor(col / CHART_WIDTH * tps.length);
302
388
  const tpsValue = tps[index] ?? 0;
303
389
  const normalizedHeight = tpsValue / maxVal * (CHART_HEIGHT - 1);
304
- if (normalizedHeight >= row) {
305
- chartRow += BLOCK_CHAR;
306
- } else {
307
- chartRow += " ";
308
- }
390
+ bars += normalizedHeight >= row ? BLOCK_CHAR : " ";
309
391
  }
310
- chartRow += " \u2502";
311
- lines.push(chartRow);
392
+ lines.push(buildRow(label, bars));
312
393
  }
313
- lines.push("\u2502 " + "0".padStart(CHART_WIDTH) + "s \u2502");
314
- lines.push("\u2514" + "\u2500".repeat(CHART_WIDTH) + "\u2518");
394
+ lines.push(`${axisPrefix}${"\u2500".repeat(CHART_WIDTH)} \u2502`);
395
+ lines.push("\u2514" + "\u2500".repeat(chartWidth) + "\u2518");
315
396
  const xLabels = generateXLabels(tps.length, 6);
316
- lines.push(" " + xLabels.join(" "));
397
+ const labelLine = new Array(CHART_WIDTH).fill(" ");
398
+ const maxIndex = Math.max(tps.length - 1, 1);
399
+ for (const label of xLabels) {
400
+ const seconds = parseInt(label.replace("s", ""), 10);
401
+ const position = Math.min(
402
+ CHART_WIDTH - 1,
403
+ Math.round(seconds / maxIndex * (CHART_WIDTH - 1))
404
+ );
405
+ for (let i = 0; i < label.length && position + i < CHART_WIDTH; i++) {
406
+ labelLine[position + i] = label[i];
407
+ }
408
+ }
409
+ lines.push(" ".repeat(stringWidth(axisPrefix)) + labelLine.join(""));
317
410
  return lines.join("\n");
318
411
  }
319
412
  function generateXLabels(dataPoints, maxLabels) {
@@ -345,13 +438,19 @@ function renderTPSHistogram(tps) {
345
438
  histogram[bucketIndex]++;
346
439
  }
347
440
  const maxCount = Math.max(...histogram, 1);
348
- for (let i = 0; i < buckets; i++) {
441
+ const labels = histogram.map((_, i) => {
349
442
  const bucketStart = (i * bucketSize).toFixed(1);
350
443
  const bucketEnd = ((i + 1) * bucketSize).toFixed(1);
444
+ return `${bucketStart}-${bucketEnd}`;
445
+ });
446
+ const labelWidth = Math.max(...labels.map((l) => stringWidth(l)));
447
+ for (let i = 0; i < buckets; i++) {
448
+ const label = padEndWidth(labels[i], labelWidth);
351
449
  const count = histogram[i];
352
450
  const barLength = Math.round(count / maxCount * CHART_WIDTH);
353
451
  const bar = BLOCK_CHAR.repeat(barLength);
354
- lines.push(`${bucketStart}-${bucketEnd} \u2502${bar} ${count}`);
452
+ const countSuffix = count > 0 ? ` ${count}` : "";
453
+ lines.push(`${label} \u2502${bar}${countSuffix}`);
355
454
  }
356
455
  return lines.join("\n");
357
456
  }
@@ -359,11 +458,11 @@ function renderStatsTable(stats) {
359
458
  const lines = [];
360
459
  lines.push("");
361
460
  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");
461
+ const headerRow = "\u2502 " + padEndWidth("\u6307\u6807", STAT_LABEL_WIDTH) + " \u2502 " + padStartWidth("\u5747\u503C", STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth("\u6700\u5C0F\u503C", STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth("\u6700\u5927\u503C", STAT_VALUE_WIDTH) + " \u2502 " + padStartWidth("\u6807\u51C6\u5DEE", STAT_VALUE_WIDTH) + " \u2502";
462
+ const tableWidth = stringWidth(headerRow) - 2;
463
+ lines.push("\u250C" + "\u2500".repeat(tableWidth) + "\u2510");
464
+ lines.push(headerRow);
465
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
367
466
  lines.push(
368
467
  formatStatRow(
369
468
  "TTFT (ms)",
@@ -374,7 +473,7 @@ function renderStatsTable(stats) {
374
473
  "f"
375
474
  )
376
475
  );
377
- lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
476
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
378
477
  lines.push(
379
478
  formatStatRow(
380
479
  "\u603B\u8017\u65F6 (ms)",
@@ -385,7 +484,7 @@ function renderStatsTable(stats) {
385
484
  "f"
386
485
  )
387
486
  );
388
- lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
487
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
389
488
  lines.push(
390
489
  formatStatRow(
391
490
  "\u603B Token \u6570",
@@ -396,7 +495,7 @@ function renderStatsTable(stats) {
396
495
  "f"
397
496
  )
398
497
  );
399
- lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
498
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
400
499
  lines.push(
401
500
  formatStatRow(
402
501
  "\u5E73\u5747\u901F\u5EA6",
@@ -407,7 +506,7 @@ function renderStatsTable(stats) {
407
506
  "f"
408
507
  )
409
508
  );
410
- lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
509
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
411
510
  lines.push(
412
511
  formatStatRow(
413
512
  "\u5CF0\u503C\u901F\u5EA6",
@@ -418,12 +517,23 @@ function renderStatsTable(stats) {
418
517
  "f"
419
518
  )
420
519
  );
421
- lines.push("\u2514" + "\u2500".repeat(70) + "\u2518");
520
+ lines.push("\u251C" + "\u2500".repeat(tableWidth) + "\u2524");
521
+ lines.push(
522
+ formatStatRow(
523
+ "\u5CF0\u503C TPS",
524
+ stats.mean.peakTps,
525
+ stats.min.peakTps,
526
+ stats.max.peakTps,
527
+ stats.stdDev.peakTps,
528
+ "f"
529
+ )
530
+ );
531
+ lines.push("\u2514" + "\u2500".repeat(tableWidth) + "\u2518");
422
532
  return lines.join("\n");
423
533
  }
424
534
  function formatStatRow(label, mean2, min, max, stdDev, format) {
425
535
  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";
536
+ 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
537
  }
428
538
  function formatTimeWithDecimals(ms) {
429
539
  if (ms === Math.floor(ms)) {
@@ -440,6 +550,7 @@ function renderSingleResult(metrics, runIndex) {
440
550
  lines.push(` \u603B Token \u6570: ${metrics.totalTokens}`);
441
551
  lines.push(` \u5E73\u5747\u901F\u5EA6: ${metrics.averageSpeed.toFixed(2)} tokens/s`);
442
552
  lines.push(` \u5CF0\u503C\u901F\u5EA6: ${metrics.peakSpeed.toFixed(2)} tokens/s`);
553
+ lines.push(` \u5CF0\u503C TPS: ${metrics.peakTps.toFixed(2)} tokens/s`);
443
554
  return lines.join("\n");
444
555
  }
445
556
  function renderReport(stats) {
@@ -496,6 +607,7 @@ async function main() {
496
607
  );
497
608
  console.log(chalk.gray("\u2500".repeat(50)));
498
609
  console.log(chalk.yellow("\n\u23F3 \u6B63\u5728\u8FD0\u884C\u6D4B\u8BD5...\n"));
610
+ console.log(chalk.gray("\u6A21\u578B\u8F93\u51FA (\u6D41\u5F0F):\n"));
499
611
  const results = await runMultipleTests(config);
500
612
  const allMetrics = results.map((r) => calculateMetrics(r));
501
613
  for (let i = 0; i < allMetrics.length; i++) {
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":";;;AACA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,OAAO,WAAW;;;ACiBlB,IAAM,iBAA2C;AAAA,EAC/C,WAAW;AAAA,EACX,QAAQ;AACV;AAEA,IAAM,qBAAqB;AAC3B,IAAM,eAAe;AACrB,IAAM,iBAAiB;AAKhB,SAAS,YAAY,MAA0B;AACpD,QAAM;AAAA,IACJ;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,OAAO;AAAA,IACP,SAAS;AAAA,EACX,IAAI;AAGJ,MAAI,CAAC,UAAU,OAAO,KAAK,MAAM,IAAI;AACnC,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAGA,MAAI,aAAa,eAAe,aAAa,UAAU;AACrD,UAAM,IAAI,MAAM,qBAAqB,QAAQ,oCAAoC;AAAA,EACnF;AAGA,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,CAAC,OAAO,UAAU,SAAS,KAAK,aAAa,GAAG;AACjF,UAAM,IAAI,MAAM,uBAAuB,SAAS,+BAA+B;AAAA,EACjF;AAEA,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,CAAC,OAAO,UAAU,IAAI,KAAK,QAAQ,GAAG;AAClE,UAAM,IAAI,MAAM,iBAAiB,IAAI,+BAA+B;AAAA,EACtE;AAGA,QAAM,aAAa,SAAS,eAAe,QAAQ;AAEnD,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,OAAO,KAAK;AAAA,IACpB,SAAS,KAAK,KAAK;AAAA,IACnB,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,QAAQ,OAAO,KAAK;AAAA,EACtB;AACF;;;AC5EA,OAAO,eAAe;AACtB,OAAO,YAAY;AAOnB,eAAsB,oBAAoB,QAAwC;AAChF,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,aAAuB,CAAC;AAC9B,MAAI,OAAO;AACX,MAAI,qBAAqB;AAEzB,QAAM,SAAS,IAAI,UAAU;AAAA,IAC3B,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,EAClB,CAAC;AAED,MAAI;AACF,UAAM,SAAS,MAAM,OAAO,SAAS,OAAO;AAAA,MAC1C,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,OAAO,CAAC;AAAA,MACnD,QAAQ;AAAA,IACV,CAAC;AAED,qBAAiB,SAAS,QAAQ;AAChC,YAAM,cAAc,KAAK,IAAI;AAE7B,UAAI,MAAM,SAAS,yBAAyB,MAAM,MAAM,SAAS,cAAc;AAC7E,cAAM,OAAO,MAAM,MAAM;AAEzB,YAAI,QAAQ,KAAK,SAAS,GAAG;AAC3B,cAAI,CAAC,oBAAoB;AACvB,mBAAO,cAAc;AACrB,iCAAqB;AAAA,UACvB;AAGA,mBAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,uBAAW,KAAK,cAAc,SAAS;AAAA,UACzC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI,MAAM,wBAAwB,MAAM,OAAO,EAAE;AAAA,IACzD;AACA,UAAM;AAAA,EACR;AAEA,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,YAAY,UAAU;AAE5B,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR,aAAa,WAAW;AAAA,IACxB;AAAA,EACF;AACF;AAKA,eAAsB,iBAAiB,QAAwC;AAC7E,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,aAAuB,CAAC;AAC9B,MAAI,OAAO;AACX,MAAI,qBAAqB;AAEzB,QAAM,SAAS,IAAI,OAAO;AAAA,IACxB,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,EAClB,CAAC;AAED,MAAI;AACF,UAAM,SAAS,MAAM,OAAO,KAAK,YAAY,OAAO;AAAA,MAClD,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,OAAO,CAAC;AAAA,MACnD,QAAQ;AAAA,IACV,CAAC;AAED,qBAAiB,SAAS,QAAQ;AAChC,YAAM,cAAc,KAAK,IAAI;AAE7B,YAAM,QAAQ,MAAM,QAAQ,CAAC,GAAG;AAEhC,UAAI,OAAO,SAAS;AAClB,cAAM,UAAU,MAAM;AAEtB,YAAI,CAAC,sBAAsB,QAAQ,SAAS,GAAG;AAC7C,iBAAO,cAAc;AACrB,+BAAqB;AAAA,QACvB;AAGA,iBAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,qBAAW,KAAK,cAAc,SAAS;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI,MAAM,qBAAqB,MAAM,OAAO,EAAE;AAAA,IACtD;AACA,UAAM;AAAA,EACR;AAEA,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,YAAY,UAAU;AAE5B,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR,aAAa,WAAW;AAAA,IACxB;AAAA,EACF;AACF;AAKA,eAAsB,WAAW,QAAwC;AACvE,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO,oBAAoB,MAAM;AAAA,EACnC,OAAO;AACL,WAAO,iBAAiB,MAAM;AAAA,EAChC;AACF;AAKA,eAAsB,iBAAiB,QAA0C;AAC/E,QAAM,UAA2B,CAAC;AAElC,WAAS,IAAI,GAAG,IAAI,OAAO,UAAU,KAAK;AACxC,UAAM,SAAS,MAAM,WAAW,MAAM;AACtC,YAAQ,KAAK,MAAM;AAAA,EACrB;AAEA,SAAO;AACT;;;AC9GO,SAAS,cAAc,SAAgC;AAC5D,SAAO,QAAQ;AACjB;AAKO,SAAS,sBAAsB,SAAgC;AACpE,MAAI,QAAQ,aAAa,GAAG;AAC1B,WAAO;AAAA,EACT;AACA,SAAQ,QAAQ,cAAc,QAAQ,YAAa;AACrD;AAKO,SAAS,mBAAmB,SAAwB,aAAqB,IAAY;AAC1F,MAAI,QAAQ,OAAO,SAAS,YAAY;AAEtC,QAAI,QAAQ,OAAO,SAAS,GAAG;AAC7B,aAAO;AAAA,IACT;AACA,UAAM,YAAY,QAAQ,OAAO,QAAQ,OAAO,SAAS,CAAC,IAAI,QAAQ,OAAO,CAAC;AAC9E,WAAO,YAAY,KAAM,QAAQ,OAAO,SAAS,KAAK,YAAa,MAAO;AAAA,EAC5E;AAEA,MAAI,WAAW;AACf,WAAS,IAAI,GAAG,KAAK,QAAQ,OAAO,SAAS,YAAY,KAAK;AAC5D,UAAM,YAAY,QAAQ,OAAO,CAAC;AAClC,UAAM,UAAU,QAAQ,OAAO,IAAI,aAAa,CAAC;AACjD,UAAM,WAAW,UAAU;AAC3B,QAAI,WAAW,GAAG;AAChB,YAAM,SAAU,aAAa,KAAK,WAAY;AAC9C,iBAAW,KAAK,IAAI,UAAU,KAAK;AAAA,IACrC;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,aAAa,SAAkC;AAC7D,MAAI,QAAQ,OAAO,WAAW,GAAG;AAC/B,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,gBAAgB,QAAQ,OAAO,QAAQ,OAAO,SAAS,CAAC;AAC9D,QAAM,eAAe,KAAK,KAAK,gBAAgB,GAAI;AAEnD,MAAI,gBAAgB,GAAG;AACrB,WAAO,QAAQ,OAAO,SAAS,IAAI,CAAC,QAAQ,OAAO,MAAM,IAAI,CAAC;AAAA,EAChE;AAEA,QAAM,MAAgB,IAAI,MAAM,YAAY,EAAE,KAAK,CAAC;AAGpD,aAAW,aAAa,QAAQ,QAAQ;AACtC,UAAM,cAAc,KAAK,MAAM,YAAY,GAAI;AAC/C,QAAI,cAAc,IAAI,QAAQ;AAC5B,UAAI,WAAW;AAAA,IACjB;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,iBAAiB,SAA2C;AAC1E,SAAO;AAAA,IACL,MAAM,cAAc,OAAO;AAAA,IAC3B,WAAW,QAAQ;AAAA,IACnB,aAAa,QAAQ;AAAA,IACrB,cAAc,sBAAsB,OAAO;AAAA,IAC3C,WAAW,mBAAmB,OAAO;AAAA,IACrC,KAAK,aAAa,OAAO;AAAA,EAC3B;AACF;AAKA,SAAS,KAAK,QAA0B;AACtC,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC,IAAI,OAAO;AACxD;AAKA,SAAS,kBAAkB,QAA0B;AACnD,MAAI,OAAO,SAAS,EAAG,QAAO;AAC9B,QAAM,MAAM,KAAK,MAAM;AACvB,QAAM,cAAc,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC;AAC1D,SAAO,KAAK,KAAK,KAAK,WAAW,CAAC;AACpC;AAKO,SAAS,eAAe,YAA8C;AAC3E,MAAI,WAAW,WAAW,GAAG;AAC3B,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AAEA,QAAM,aAAa,WAAW;AAG9B,QAAM,QAAQ,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI;AAC1C,QAAM,aAAa,WAAW,IAAI,CAAC,MAAM,EAAE,SAAS;AACpD,QAAM,cAAc,WAAW,IAAI,CAAC,MAAM,EAAE,WAAW;AACvD,QAAM,gBAAgB,WAAW,IAAI,CAAC,MAAM,EAAE,YAAY;AAC1D,QAAM,aAAa,WAAW,IAAI,CAAC,MAAM,EAAE,SAAS;AAGpD,QAAM,eAAe,KAAK,IAAI,GAAG,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI,MAAM,CAAC;AACpE,QAAM,SAAmB,CAAC;AAC1B,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACrC,UAAM,SAAS,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC;AAClD,WAAO,KAAK,KAAK,MAAM,CAAC;AAAA,EAC1B;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,MAAM,KAAK,KAAK;AAAA,MAChB,WAAW,KAAK,UAAU;AAAA,MAC1B,aAAa,KAAK,WAAW;AAAA,MAC7B,cAAc,KAAK,aAAa;AAAA,MAChC,WAAW,KAAK,UAAU;AAAA,MAC1B,KAAK;AAAA,IACP;AAAA,IACA,KAAK;AAAA,MACH,MAAM,KAAK,IAAI,GAAG,KAAK;AAAA,MACvB,WAAW,KAAK,IAAI,GAAG,UAAU;AAAA,MACjC,aAAa,KAAK,IAAI,GAAG,WAAW;AAAA,MACpC,cAAc,KAAK,IAAI,GAAG,aAAa;AAAA,MACvC,WAAW,KAAK,IAAI,GAAG,UAAU;AAAA,MACjC,KAAK,CAAC;AAAA,IACR;AAAA,IACA,KAAK;AAAA,MACH,MAAM,KAAK,IAAI,GAAG,KAAK;AAAA,MACvB,WAAW,KAAK,IAAI,GAAG,UAAU;AAAA,MACjC,aAAa,KAAK,IAAI,GAAG,WAAW;AAAA,MACpC,cAAc,KAAK,IAAI,GAAG,aAAa;AAAA,MACvC,WAAW,KAAK,IAAI,GAAG,UAAU;AAAA,MACjC,KAAK,CAAC;AAAA,IACR;AAAA,IACA,QAAQ;AAAA,MACN,MAAM,kBAAkB,KAAK;AAAA,MAC7B,WAAW,kBAAkB,UAAU;AAAA,MACvC,aAAa,kBAAkB,WAAW;AAAA,MAC1C,cAAc,kBAAkB,aAAa;AAAA,MAC7C,WAAW,kBAAkB,UAAU;AAAA,MACvC,KAAK,CAAC;AAAA,IACR;AAAA,IACA;AAAA,EACF;AACF;;;ACnMA,IAAM,aAAa;AACnB,IAAM,cAAc;AACpB,IAAM,eAAe;AAKd,SAAS,iBAAiB,KAAe,UAA2B;AACzE,MAAI,IAAI,WAAW,GAAG;AACpB,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,YAAY,KAAK,IAAI,GAAG,KAAK,CAAC;AAChD,QAAM,SAAS,KAAK,IAAI,WAAW,CAAC;AAEpC,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,4CAAmB;AAG9B,QAAM,KAAK,WAAM,SAAI,OAAO,WAAW,IAAI,QAAG;AAE9C,WAAS,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO;AAChD,UAAM,QAAS,OAAO,eAAe,KAAM;AAC3C,UAAM,QAAQ,MAAM,QAAQ,CAAC,EAAE,SAAS,CAAC;AAEzC,QAAI,WAAW,YAAO,QAAQ;AAE9B,aAAS,MAAM,GAAG,MAAM,aAAa,OAAO;AAC1C,YAAM,QAAQ,KAAK,MAAO,MAAM,cAAe,IAAI,MAAM;AACzD,YAAM,WAAW,IAAI,KAAK,KAAK;AAC/B,YAAM,mBAAoB,WAAW,UAAW,eAAe;AAE/D,UAAI,oBAAoB,KAAK;AAC3B,oBAAY;AAAA,MACd,OAAO;AACL,oBAAY;AAAA,MACd;AAAA,IACF;AAEA,gBAAY;AACZ,UAAM,KAAK,QAAQ;AAAA,EACrB;AAGA,QAAM,KAAK,cAAS,IAAI,SAAS,WAAW,IAAI,UAAK;AACrD,QAAM,KAAK,WAAM,SAAI,OAAO,WAAW,IAAI,QAAG;AAG9C,QAAM,UAAU,gBAAgB,IAAI,QAAQ,CAAC;AAC7C,QAAM,KAAK,SAAS,QAAQ,KAAK,GAAG,CAAC;AAErC,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,SAAS,gBAAgB,YAAoB,WAA6B;AACxE,MAAI,cAAc,GAAG;AACnB,WAAO,CAAC,IAAI;AAAA,EACd;AAEA,QAAM,SAAmB,CAAC;AAC1B,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,SAAS,CAAC;AAE3D,WAAS,IAAI,GAAG,IAAI,YAAY,KAAK,MAAM;AACzC,WAAO,KAAK,GAAG,CAAC,GAAG;AAAA,EACrB;AAGA,MAAI,OAAO,OAAO,SAAS,CAAC,MAAM,GAAG,aAAa,CAAC,KAAK;AACtD,WAAO,KAAK,GAAG,aAAa,CAAC,GAAG;AAAA,EAClC;AAEA,SAAO;AACT;AAKO,SAAS,mBAAmB,KAAuB;AACxD,MAAI,IAAI,WAAW,GAAG;AACpB,WAAO;AAAA,EACT;AAEA,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,kBAAQ;AAGnB,QAAM,SAAS,KAAK,IAAI,GAAG,KAAK,CAAC;AACjC,QAAM,UAAU;AAChB,QAAM,aAAa,SAAS;AAC5B,QAAM,YAAY,IAAI,MAAM,OAAO,EAAE,KAAK,CAAC;AAE3C,aAAW,KAAK,KAAK;AACnB,UAAM,cAAc,KAAK,IAAI,KAAK,MAAM,IAAI,UAAU,GAAG,UAAU,CAAC;AACpE,cAAU,WAAW;AAAA,EACvB;AAEA,QAAM,WAAW,KAAK,IAAI,GAAG,WAAW,CAAC;AAEzC,WAAS,IAAI,GAAG,IAAI,SAAS,KAAK;AAChC,UAAM,eAAe,IAAI,YAAY,QAAQ,CAAC;AAC9C,UAAM,cAAc,IAAI,KAAK,YAAY,QAAQ,CAAC;AAClD,UAAM,QAAQ,UAAU,CAAC;AACzB,UAAM,YAAY,KAAK,MAAO,QAAQ,WAAY,WAAW;AAC7D,UAAM,MAAM,WAAW,OAAO,SAAS;AAEvC,UAAM,KAAK,GAAG,WAAW,IAAI,SAAS,UAAK,GAAG,IAAI,KAAK,EAAE;AAAA,EAC3D;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKO,SAAS,iBAAiB,OAA4B;AAC3D,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,iCAAa,MAAM,aAAa,GAAG;AAC9C,QAAM,KAAK,WAAM,SAAI,OAAO,EAAE,IAAI,QAAG;AAGrC,QAAM;AAAA,IACJ,YACE,eAAK,OAAO,EAAE,IACd,aACA,eAAK,SAAS,EAAE,IAChB,aACA,qBAAM,SAAS,EAAE,IACjB,aACA,qBAAM,SAAS,EAAE,IACjB,aACA,qBAAM,SAAS,EAAE,IACjB;AAAA,EACJ;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,EAAE,IAAI,QAAG;AAGrC,QAAM;AAAA,IACJ;AAAA,MACE;AAAA,MACA,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,EAAE,IAAI,QAAG;AAGrC,QAAM;AAAA,IACJ;AAAA,MACE;AAAA,MACA,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,EAAE,IAAI,QAAG;AAGrC,QAAM;AAAA,IACJ;AAAA,MACE;AAAA,MACA,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,EAAE,IAAI,QAAG;AAGrC,QAAM;AAAA,IACJ;AAAA,MACE;AAAA,MACA,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,WAAM,SAAI,OAAO,EAAE,IAAI,QAAG;AAGrC,QAAM;AAAA,IACJ;AAAA,MACE;AAAA,MACA,MAAM,KAAK;AAAA,MACX,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,OAAO;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,WAAM,SAAI,OAAO,EAAE,IAAI,QAAG;AAErC,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,SAAS,cACP,OACAA,OACA,KACA,KACA,QACA,QACQ;AACR,QAAM,MAAM,CAAC,MAAe,WAAW,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;AAEvE,SACE,YACA,MAAM,OAAO,EAAE,IACf,aACA,IAAIA,KAAI,EAAE,SAAS,EAAE,IACrB,aACA,IAAI,GAAG,EAAE,SAAS,EAAE,IACpB,aACA,IAAI,GAAG,EAAE,SAAS,EAAE,IACpB,aACA,IAAI,MAAM,EAAE,SAAS,EAAE,IACvB;AAEJ;AAKA,SAAS,uBAAuB,IAAoB;AAClD,MAAI,OAAO,KAAK,MAAM,EAAE,GAAG;AACzB,WAAO,GAAG,GAAG,QAAQ,CAAC,CAAC;AAAA,EACzB;AACA,SAAO,GAAG,GAAG,QAAQ,CAAC,CAAC;AACzB;AAKO,SAAS,mBAAmB,SAA4B,UAA0B;AACvF,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK;AAAA,gBAAS,WAAW,CAAC,GAAG;AACnC,QAAM,KAAK,WAAW,uBAAuB,QAAQ,IAAI,CAAC,EAAE;AAC5D,QAAM,KAAK,yBAAU,uBAAuB,QAAQ,SAAS,CAAC,EAAE;AAChE,QAAM,KAAK,0BAAgB,QAAQ,WAAW,EAAE;AAChD,QAAM,KAAK,+BAAW,QAAQ,aAAa,QAAQ,CAAC,CAAC,WAAW;AAChE,QAAM,KAAK,+BAAW,QAAQ,UAAU,QAAQ,CAAC,CAAC,WAAW;AAC7D,SAAO,MAAM,KAAK,IAAI;AACxB;AAKO,SAAS,aAAa,OAA4B;AACvD,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,OAAO,SAAI,OAAO,EAAE,CAAC;AAChC,QAAM,KAAK,4CAAc;AACzB,QAAM,KAAK,SAAI,OAAO,EAAE,CAAC;AAGzB,QAAM,KAAK,iBAAiB,KAAK,CAAC;AAGlC,MAAI,MAAM,KAAK,IAAI,SAAS,GAAG;AAC7B,UAAM,KAAK,OAAO,iBAAiB,MAAM,KAAK,GAAG,CAAC;AAAA,EACpD;AAGA,MAAI,MAAM,KAAK,IAAI,SAAS,GAAG;AAC7B,UAAM,KAAK,OAAO,mBAAmB,MAAM,KAAK,GAAG,CAAC;AAAA,EACtD;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;;;AJlRA,SAAS,gBAAwB;AAC/B,MAAI;AACF,UAAM,aAAa,QAAQ,cAAc,YAAY,GAAG,CAAC;AACzD,UAAM,cAAc,KAAK,YAAY,MAAM,cAAc;AACzD,UAAM,cAAc,KAAK,MAAM,aAAa,aAAa,OAAO,CAAC;AACjE,WAAO,YAAY,WAAW;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,kBAAkB,EACvB,YAAY,+CAA+C,EAC3D,QAAQ,cAAc,CAAC;AAE1B,QACG,OAAO,uBAAuB,sBAAsB,EAAE,EACtD,OAAO,6BAA6B,qCAAqC,WAAW,EACpF,OAAO,mBAAmB,yBAAyB,EACnD,OAAO,uBAAuB,YAAY,EAC1C,OAAO,yBAAyB,yBAAyB,MAAM,EAC/D,OAAO,uBAAuB,uBAAuB,GAAG,EACxD,OAAO,mBAAmB,eAAe,sDAAc,EACvD,MAAM,QAAQ,IAAI;AAErB,IAAM,UAAU,QAAQ,KAAK;AAE7B,eAAe,OAAO;AACpB,MAAI;AAEF,UAAM,SAAS,YAAY;AAAA,MACzB,QAAQ,QAAQ;AAAA,MAChB,UAAU,QAAQ;AAAA,MAClB,KAAK,QAAQ;AAAA,MACb,OAAO,QAAQ;AAAA,MACf,WAAW,SAAS,QAAQ,WAAW,EAAE;AAAA,MACzC,MAAM,SAAS,QAAQ,MAAM,EAAE;AAAA,MAC/B,QAAQ,QAAQ;AAAA,IAClB,CAAC;AAGD,YAAQ,IAAI,MAAM,KAAK,wDAAmB,CAAC;AAC3C,YAAQ,IAAI,MAAM,KAAK,SAAI,OAAO,EAAE,CAAC,CAAC;AACtC,YAAQ,IAAI,MAAM,KAAK,aAAa,MAAM,MAAM,OAAO,QAAQ,CAAC,EAAE,CAAC;AACnE,YAAQ,IAAI,MAAM,KAAK,UAAU,MAAM,MAAM,OAAO,KAAK,CAAC,EAAE,CAAC;AAC7D,YAAQ,IAAI,MAAM,KAAK,eAAe,MAAM,MAAM,OAAO,SAAS,CAAC,EAAE,CAAC;AACtE,YAAQ,IAAI,MAAM,KAAK,SAAS,MAAM,MAAM,OAAO,QAAQ,CAAC,EAAE,CAAC;AAC/D,YAAQ;AAAA,MACN,MAAM;AAAA,QACJ,WAAW,MAAM,MAAM,OAAO,OAAO,UAAU,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,SAAS,KAAK,QAAQ,EAAE;AAAA,MACjG;AAAA,IACF;AACA,YAAQ,IAAI,MAAM,KAAK,SAAI,OAAO,EAAE,CAAC,CAAC;AAGtC,YAAQ,IAAI,MAAM,OAAO,oDAAiB,CAAC;AAE3C,UAAM,UAAU,MAAM,iBAAiB,MAAM;AAG7C,UAAM,aAAa,QAAQ,IAAI,CAAC,MAAM,iBAAiB,CAAC,CAAC;AAGzD,aAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,cAAQ,IAAI,MAAM,KAAK,mBAAmB,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;AAAA,IAC9D;AAGA,UAAM,QAAQ,eAAe,UAAU;AAGvC,YAAQ,IAAI,MAAM,KAAK,OAAO,aAAa,KAAK,CAAC,CAAC;AAElD,YAAQ,IAAI,MAAM,MAAM,sCAAa,CAAC;AAAA,EACxC,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,cAAQ,MAAM,MAAM,IAAI;AAAA,uBAAW,MAAM,OAAO;AAAA,CAAI,CAAC;AAAA,IACvD,OAAO;AACL,cAAQ,MAAM,MAAM,IAAI,iDAAc,CAAC;AAAA,IACzC;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK,KAAK;","names":["mean"]}
1
+ {"version":3,"sources":["../src/index.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\";\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 console.log(chalk.gray(\"模型输出 (流式):\\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 { performance } from \"node:perf_hooks\";\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport OpenAI from \"openai\";\nimport type { Config } from \"./config.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\n for (let i = 0; i < config.runCount; i++) {\n if (config.runCount > 1) {\n const label = `\\n[运行 ${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\";\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(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 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(\"Token 速度趋势图 (TPS)\");\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[]): 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 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): string {\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(\"统计汇总 (N=\" + stats.sampleSize + \")\");\n\n // 表头\n const headerRow =\n \"│ \" +\n padEndWidth(\"指标\", STAT_LABEL_WIDTH) +\n \" │ \" +\n padStartWidth(\"均值\", STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(\"最小值\", STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(\"最大值\", STAT_VALUE_WIDTH) +\n \" │ \" +\n padStartWidth(\"标准差\", 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 \"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(tableWidth) + \"┤\");\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(tableWidth) + \"┤\");\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(tableWidth) + \"┤\");\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(tableWidth) + \"┤\");\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(tableWidth) + \"┤\");\n\n // 峰值 TPS\n lines.push(\n formatStatRow(\n \"峰值 TPS\",\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(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 lines.push(` 峰值 TPS: ${metrics.peakTps.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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-speed-tester",
3
- "version": "1.4.2",
3
+ "version": "1.5.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",