token-speed-tester 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # Token Speed Tester
2
+
3
+ A CLI tool to test LLM API token output speed for Anthropic and OpenAI compatible APIs.
4
+
5
+ ## Features
6
+
7
+ - **Dual Protocol Support**: Works with both Anthropic Messages API and OpenAI Chat Completions API
8
+ - **Streaming Performance**: Measures each token's arrival time with precision
9
+ - **Comprehensive Metrics**: TTFT, average speed, peak speed, TPS curves
10
+ - **Statistical Analysis**: Mean, min, max, and standard deviation across multiple test runs
11
+ - **ASCII Visualization**: Beautiful terminal-based charts and tables
12
+ - **Custom Endpoints**: Test third-party APIs that are compatible with OpenAI/Anthropic protocols
13
+
14
+ ## Installation
15
+
16
+ ### Global Installation
17
+
18
+ ```bash
19
+ npm install -g token-speed-tester
20
+ ```
21
+
22
+ ### Local Installation
23
+
24
+ ```bash
25
+ npm install token-speed-tester
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Basic Usage
31
+
32
+ ```bash
33
+ # Test Anthropic API (default)
34
+ token-speed-test --api-key sk-ant-xxx
35
+
36
+ # Test OpenAI API
37
+ token-speed-test --api-key sk-xxx --provider openai
38
+ ```
39
+
40
+ ### Advanced Options
41
+
42
+ ```bash
43
+ # Custom model and multiple test runs
44
+ token-speed-test \
45
+ --api-key sk-ant-xxx \
46
+ --provider anthropic \
47
+ --model claude-3-5-sonnet-20241022 \
48
+ --runs 5
49
+
50
+ # Test with custom endpoint and prompt
51
+ token-speed-test \
52
+ --api-key sk-xxx \
53
+ --provider openai \
54
+ --url https://api.example.com/v1 \
55
+ --model custom-model \
56
+ --prompt "Explain quantum computing" \
57
+ --max-tokens 2048 \
58
+ --runs 10
59
+ ```
60
+
61
+ ### Local Development
62
+
63
+ ```bash
64
+ # Clone and install dependencies
65
+ git clone https://github.com/Cansiny0320/token-speed-tester.git
66
+ cd token-speed-tester
67
+ npm install
68
+
69
+ # Run directly with tsx
70
+ npm run dev -- --api-key sk-ant-xxx
71
+
72
+ # Or build and run
73
+ npm run build
74
+ node dist/index.js --api-key sk-ant-xxx
75
+ ```
76
+
77
+ ## Command Line Options
78
+
79
+ | Option | Short | Description | Default |
80
+ |--------|-------|-------------|---------|
81
+ | `--api-key` | `-k` | API Key (required) | - |
82
+ | `--provider` | `-p` | API type: `anthropic` or `openai` | `anthropic` |
83
+ | `--model` | `-m` | Model name | Auto-selected by provider |
84
+ | `--url` | `-u` | Custom API endpoint | Official endpoint |
85
+ | `--runs` | `-r` | Number of test runs | `3` |
86
+ | `--prompt` | | Test prompt | "写一篇关于 AI 的短文" |
87
+ | `--max-tokens` | | Maximum output tokens | `1024` |
88
+
89
+ ### Default Models
90
+
91
+ - **Anthropic**: `claude-3-5-sonnet-20241022`
92
+ - **OpenAI**: `gpt-4o-mini`
93
+
94
+ ## Output Example
95
+
96
+ ```
97
+ 🚀 Token 速度测试工具
98
+ ──────────────────────────────────────────
99
+ Provider: anthropic
100
+ Model: claude-3-5-sonnet-20241022
101
+ Max Tokens: 1024
102
+ Runs: 3
103
+ Prompt: 写一篇关于 AI 的短文
104
+ ──────────────────────────────────────────
105
+
106
+ ⏳ 正在运行测试...
107
+
108
+ [运行 1]
109
+ TTFT: 523ms
110
+ 总耗时: 3245ms
111
+ 总 Token 数: 412
112
+ 平均速度: 126.96 tokens/s
113
+ 峰值速度: 156.32 tokens/s
114
+
115
+ [运行 2]
116
+ TTFT: 487ms
117
+ 总耗时: 3189ms
118
+ 总 Token 数: 398
119
+ 平均速度: 124.84 tokens/s
120
+ 峰值速度: 158.41 tokens/s
121
+
122
+ [运行 3]
123
+ TTFT: 501ms
124
+ 总耗时: 3312ms
125
+ 总 Token 数: 405
126
+ 平均速度: 122.28 tokens/s
127
+ 峰值速度: 154.23 tokens/s
128
+
129
+ ======================================================================
130
+ Token 速度测试报告
131
+ ======================================================================
132
+
133
+ 统计汇总 (N=3)
134
+ ┌──────────────────────────────────────────────────────────────────────┐
135
+ │ 指标 │ 均值 │ 最小值 │ 最大值 │ 标准差 │
136
+ ├──────────────────────────────────────────────────────────────────────┤
137
+ │ TTFT (ms) │ 503.67 │ 487.00 │ 523.00 │ 14.57 │
138
+ ├──────────────────────────────────────────────────────────────────────┤
139
+ │ 总耗时 (ms) │ 3248.67 │ 3189.00 │ 3312.00 │ 51.92 │
140
+ ├──────────────────────────────────────────────────────────────────────┤
141
+ │ 总 Token 数 │ 405.00 │ 398.00 │ 412.00 │ 5.35 │
142
+ ├──────────────────────────────────────────────────────────────────────┤
143
+ │ 平均速度 │ 124.69 │ 122.28 │ 126.96 │ 1.88 │
144
+ ├──────────────────────────────────────────────────────────────────────┤
145
+ │ 峰值速度 │ 156.32 │ 154.23 │ 158.41 │ 1.82 │
146
+ └──────────────────────────────────────────────────────────────────────┘
147
+
148
+ Token 速度趋势图 (TPS)
149
+ ┌────────────────────────────────────────┐
150
+ │ 120 ┤ █ │
151
+ │ 100 ┤ █ █ █ █ │
152
+ │ 80 ┤ █ █ █ █ █ █ █ │
153
+ │ 60 ┤ █ █ █ █ █ █ █ █ █ █ │
154
+ │ 40 ┤ █ █ █ █ █ █ █ █ █ █ █ █ │
155
+ │ 20 ┤ █ █ █ █ █ █ █ █ █ █ █ █ █ █ │
156
+ │ 0 └────────────────────────────────── │
157
+ │ 0s 1s 2s 3s 4s 5s 6s │
158
+ └────────────────────────────────────────┘
159
+
160
+ TPS 分布
161
+ 0.0-12.0 │██████████████████████████████████████████████████ 45
162
+ 12.0-24.0 │██ 3
163
+ 24.0-36.0 │ 0
164
+ 36.0-48.0 │ 0
165
+ 48.0-60.0 │ 0
166
+ 60.0-72.0 │ 0
167
+ 72.0-84.0 │ 0
168
+ 84.0-96.0 │ 0
169
+ 96.0-108.0 │ 0
170
+ 108.0-120.0 │ 0
171
+
172
+ ✅ 测试完成!
173
+ ```
174
+
175
+ ## Metrics Explained
176
+
177
+ - **TTFT (Time to First Token)**: Time from request start to first token arrival
178
+ - **Total Time**: Total duration from request to completion
179
+ - **Total Tokens**: Number of output tokens received
180
+ - **Average Speed**: Mean tokens per second (totalTokens / totalTime)
181
+ - **Peak Speed**: Fastest speed measured over a 10-token window
182
+ - **TPS Curve**: Tokens received per second throughout the stream
183
+
184
+ ## Development
185
+
186
+ ### Running Tests
187
+
188
+ ```bash
189
+ # Run tests
190
+ npm test
191
+
192
+ # Run tests with UI
193
+ npm run test:ui
194
+
195
+ # Generate coverage report
196
+ npm run test:coverage
197
+ ```
198
+
199
+ ### Building
200
+
201
+ ```bash
202
+ npm run build
203
+ ```
204
+
205
+ ### Test Coverage
206
+
207
+ This project maintains 100% code coverage across all modules.
208
+
209
+ ## License
210
+
211
+ MIT License - see [LICENSE](LICENSE) for details.
212
+
213
+ ## Contributing
214
+
215
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,506 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+
7
+ // src/config.ts
8
+ var DEFAULT_MODELS = {
9
+ anthropic: "claude-3-5-sonnet-20241022",
10
+ openai: "gpt-4o-mini"
11
+ };
12
+ var DEFAULT_MAX_TOKENS = 1024;
13
+ var DEFAULT_RUNS = 3;
14
+ var DEFAULT_PROMPT = "\u5199\u4E00\u7BC7\u5173\u4E8E AI \u7684\u77ED\u6587";
15
+ function parseConfig(args) {
16
+ const {
17
+ apiKey,
18
+ provider = "anthropic",
19
+ url,
20
+ model,
21
+ maxTokens = DEFAULT_MAX_TOKENS,
22
+ runs = DEFAULT_RUNS,
23
+ prompt = DEFAULT_PROMPT
24
+ } = args;
25
+ if (!apiKey || apiKey.trim() === "") {
26
+ throw new Error("API Key is required. Use --api-key or -k to provide it.");
27
+ }
28
+ if (provider !== "anthropic" && provider !== "openai") {
29
+ throw new Error(`Invalid provider: ${provider}. Must be 'anthropic' or 'openai'.`);
30
+ }
31
+ if (maxTokens <= 0) {
32
+ throw new Error(`Invalid max-tokens: ${maxTokens}. Must be a positive number.`);
33
+ }
34
+ if (runs <= 0) {
35
+ throw new Error(`Invalid runs: ${runs}. Must be a positive number.`);
36
+ }
37
+ const finalModel = model || DEFAULT_MODELS[provider];
38
+ return {
39
+ provider,
40
+ apiKey: apiKey.trim(),
41
+ baseURL: url?.trim(),
42
+ model: finalModel,
43
+ maxTokens,
44
+ runCount: runs,
45
+ prompt: prompt.trim()
46
+ };
47
+ }
48
+
49
+ // src/client.ts
50
+ import Anthropic from "@anthropic-ai/sdk";
51
+ import OpenAI from "openai";
52
+ async function anthropicStreamTest(config) {
53
+ const startTime = Date.now();
54
+ const tokenTimes = [];
55
+ let ttft = 0;
56
+ let firstTokenRecorded = false;
57
+ const client = new Anthropic({
58
+ apiKey: config.apiKey,
59
+ baseURL: config.baseURL
60
+ });
61
+ try {
62
+ const stream = await client.messages.create({
63
+ model: config.model,
64
+ max_tokens: config.maxTokens,
65
+ messages: [{ role: "user", content: config.prompt }],
66
+ stream: true
67
+ });
68
+ for await (const event of stream) {
69
+ const currentTime = Date.now();
70
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
71
+ const text = event.delta.text;
72
+ if (text && text.length > 0) {
73
+ if (!firstTokenRecorded) {
74
+ ttft = currentTime - startTime;
75
+ firstTokenRecorded = true;
76
+ }
77
+ for (let i = 0; i < text.length; i++) {
78
+ tokenTimes.push(currentTime - startTime);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ } catch (error) {
84
+ if (error instanceof Error) {
85
+ throw new Error(`Anthropic API error: ${error.message}`);
86
+ }
87
+ throw error;
88
+ }
89
+ const endTime = Date.now();
90
+ const totalTime = endTime - startTime;
91
+ return {
92
+ ttft,
93
+ tokens: tokenTimes,
94
+ totalTokens: tokenTimes.length,
95
+ totalTime
96
+ };
97
+ }
98
+ async function openaiStreamTest(config) {
99
+ const startTime = Date.now();
100
+ const tokenTimes = [];
101
+ let ttft = 0;
102
+ let firstTokenRecorded = false;
103
+ const client = new OpenAI({
104
+ apiKey: config.apiKey,
105
+ baseURL: config.baseURL
106
+ });
107
+ try {
108
+ const stream = await client.chat.completions.create({
109
+ model: config.model,
110
+ max_tokens: config.maxTokens,
111
+ messages: [{ role: "user", content: config.prompt }],
112
+ stream: true
113
+ });
114
+ for await (const chunk of stream) {
115
+ const currentTime = Date.now();
116
+ const delta = chunk.choices[0]?.delta;
117
+ if (delta?.content) {
118
+ const content = delta.content;
119
+ if (!firstTokenRecorded && content.length > 0) {
120
+ ttft = currentTime - startTime;
121
+ firstTokenRecorded = true;
122
+ }
123
+ for (let i = 0; i < content.length; i++) {
124
+ tokenTimes.push(currentTime - startTime);
125
+ }
126
+ }
127
+ }
128
+ } catch (error) {
129
+ if (error instanceof Error) {
130
+ throw new Error(`OpenAI API error: ${error.message}`);
131
+ }
132
+ throw error;
133
+ }
134
+ const endTime = Date.now();
135
+ const totalTime = endTime - startTime;
136
+ return {
137
+ ttft,
138
+ tokens: tokenTimes,
139
+ totalTokens: tokenTimes.length,
140
+ totalTime
141
+ };
142
+ }
143
+ async function streamTest(config) {
144
+ if (config.provider === "anthropic") {
145
+ return anthropicStreamTest(config);
146
+ } else {
147
+ return openaiStreamTest(config);
148
+ }
149
+ }
150
+ async function runMultipleTests(config) {
151
+ const results = [];
152
+ for (let i = 0; i < config.runCount; i++) {
153
+ const result = await streamTest(config);
154
+ results.push(result);
155
+ }
156
+ return results;
157
+ }
158
+
159
+ // src/metrics.ts
160
+ function calculateTTFT(metrics) {
161
+ return metrics.ttft;
162
+ }
163
+ function calculateAverageSpeed(metrics) {
164
+ if (metrics.totalTime <= 0) {
165
+ return 0;
166
+ }
167
+ return metrics.totalTokens / metrics.totalTime * 1e3;
168
+ }
169
+ function calculatePeakSpeed(metrics, windowSize = 10) {
170
+ if (metrics.tokens.length < windowSize) {
171
+ if (metrics.tokens.length < 2) {
172
+ return 0;
173
+ }
174
+ const totalTime = metrics.tokens[metrics.tokens.length - 1] - metrics.tokens[0];
175
+ return totalTime > 0 ? (metrics.tokens.length - 1) / totalTime * 1e3 : 0;
176
+ }
177
+ let maxSpeed = 0;
178
+ for (let i = 0; i <= metrics.tokens.length - windowSize; i++) {
179
+ const startTime = metrics.tokens[i];
180
+ const endTime = metrics.tokens[i + windowSize - 1];
181
+ const duration = endTime - startTime;
182
+ if (duration > 0) {
183
+ const speed = (windowSize - 1) / duration * 1e3;
184
+ maxSpeed = Math.max(maxSpeed, speed);
185
+ }
186
+ }
187
+ return maxSpeed;
188
+ }
189
+ function calculateTPS(metrics) {
190
+ if (metrics.tokens.length === 0) {
191
+ return [];
192
+ }
193
+ const totalDuration = metrics.tokens[metrics.tokens.length - 1];
194
+ const totalSeconds = Math.ceil(totalDuration / 1e3);
195
+ if (totalSeconds <= 0) {
196
+ return metrics.tokens.length > 0 ? [metrics.tokens.length] : [];
197
+ }
198
+ const tps = new Array(totalSeconds).fill(0);
199
+ for (const tokenTime of metrics.tokens) {
200
+ const secondIndex = Math.floor(tokenTime / 1e3);
201
+ if (secondIndex < tps.length) {
202
+ tps[secondIndex]++;
203
+ }
204
+ }
205
+ return tps;
206
+ }
207
+ function calculateMetrics(metrics) {
208
+ return {
209
+ ttft: calculateTTFT(metrics),
210
+ totalTime: metrics.totalTime,
211
+ totalTokens: metrics.totalTokens,
212
+ averageSpeed: calculateAverageSpeed(metrics),
213
+ peakSpeed: calculatePeakSpeed(metrics),
214
+ tps: calculateTPS(metrics)
215
+ };
216
+ }
217
+ function mean(values) {
218
+ if (values.length === 0) return 0;
219
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
220
+ }
221
+ function standardDeviation(values) {
222
+ if (values.length < 2) return 0;
223
+ const avg = mean(values);
224
+ const squareDiffs = values.map((v) => Math.pow(v - avg, 2));
225
+ return Math.sqrt(mean(squareDiffs));
226
+ }
227
+ function calculateStats(allMetrics) {
228
+ if (allMetrics.length === 0) {
229
+ throw new Error("Cannot calculate stats from empty metrics array");
230
+ }
231
+ const sampleSize = allMetrics.length;
232
+ const ttfts = allMetrics.map((m) => m.ttft);
233
+ const totalTimes = allMetrics.map((m) => m.totalTime);
234
+ const totalTokens = allMetrics.map((m) => m.totalTokens);
235
+ const averageSpeeds = allMetrics.map((m) => m.averageSpeed);
236
+ const peakSpeeds = allMetrics.map((m) => m.peakSpeed);
237
+ const maxTpsLength = Math.max(...allMetrics.map((m) => m.tps.length));
238
+ const avgTps = [];
239
+ for (let i = 0; i < maxTpsLength; i++) {
240
+ const values = allMetrics.map((m) => m.tps[i] ?? 0);
241
+ avgTps.push(mean(values));
242
+ }
243
+ return {
244
+ mean: {
245
+ ttft: mean(ttfts),
246
+ totalTime: mean(totalTimes),
247
+ totalTokens: mean(totalTokens),
248
+ averageSpeed: mean(averageSpeeds),
249
+ peakSpeed: mean(peakSpeeds),
250
+ tps: avgTps
251
+ },
252
+ min: {
253
+ ttft: Math.min(...ttfts),
254
+ totalTime: Math.min(...totalTimes),
255
+ totalTokens: Math.min(...totalTokens),
256
+ averageSpeed: Math.min(...averageSpeeds),
257
+ peakSpeed: Math.min(...peakSpeeds),
258
+ tps: []
259
+ },
260
+ max: {
261
+ ttft: Math.max(...ttfts),
262
+ totalTime: Math.max(...totalTimes),
263
+ totalTokens: Math.max(...totalTokens),
264
+ averageSpeed: Math.max(...averageSpeeds),
265
+ peakSpeed: Math.max(...peakSpeeds),
266
+ tps: []
267
+ },
268
+ stdDev: {
269
+ ttft: standardDeviation(ttfts),
270
+ totalTime: standardDeviation(totalTimes),
271
+ totalTokens: standardDeviation(totalTokens),
272
+ averageSpeed: standardDeviation(averageSpeeds),
273
+ peakSpeed: standardDeviation(peakSpeeds),
274
+ tps: []
275
+ },
276
+ sampleSize
277
+ };
278
+ }
279
+
280
+ // src/chart.ts
281
+ var BLOCK_CHAR = "\u2588";
282
+ var CHART_WIDTH = 50;
283
+ var CHART_HEIGHT = 10;
284
+ function renderSpeedChart(tps, maxSpeed) {
285
+ if (tps.length === 0) {
286
+ return "No data available for chart";
287
+ }
288
+ const actualMax = maxSpeed ?? Math.max(...tps, 1);
289
+ const maxVal = Math.max(actualMax, 1);
290
+ const lines = [];
291
+ lines.push("Token \u901F\u5EA6\u8D8B\u52BF\u56FE (TPS)");
292
+ lines.push("\u250C" + "\u2500".repeat(CHART_WIDTH) + "\u2510");
293
+ for (let row = CHART_HEIGHT - 1; row >= 0; row--) {
294
+ const value = row / (CHART_HEIGHT - 1) * maxVal;
295
+ const label = value.toFixed(0).padStart(4);
296
+ let chartRow = "\u2502 " + label + " \u2524";
297
+ for (let col = 0; col < CHART_WIDTH; col++) {
298
+ const index = Math.floor(col / CHART_WIDTH * tps.length);
299
+ const tpsValue = tps[index] ?? 0;
300
+ const normalizedHeight = tpsValue / maxVal * (CHART_HEIGHT - 1);
301
+ if (normalizedHeight >= row) {
302
+ chartRow += BLOCK_CHAR;
303
+ } else {
304
+ chartRow += " ";
305
+ }
306
+ }
307
+ chartRow += " \u2502";
308
+ lines.push(chartRow);
309
+ }
310
+ lines.push("\u2502 " + "0".padStart(CHART_WIDTH) + "s \u2502");
311
+ lines.push("\u2514" + "\u2500".repeat(CHART_WIDTH) + "\u2518");
312
+ const xLabels = generateXLabels(tps.length, 6);
313
+ lines.push(" " + xLabels.join(" "));
314
+ return lines.join("\n");
315
+ }
316
+ function generateXLabels(dataPoints, maxLabels) {
317
+ if (dataPoints <= 1) {
318
+ return ["0s"];
319
+ }
320
+ const labels = [];
321
+ const step = Math.max(1, Math.floor(dataPoints / maxLabels));
322
+ for (let i = 0; i < dataPoints; i += step) {
323
+ labels.push(`${i}s`);
324
+ }
325
+ if (labels[labels.length - 1] !== `${dataPoints - 1}s`) {
326
+ labels.push(`${dataPoints - 1}s`);
327
+ }
328
+ return labels;
329
+ }
330
+ function renderTPSHistogram(tps) {
331
+ if (tps.length === 0) {
332
+ return "No TPS data available";
333
+ }
334
+ const lines = [];
335
+ lines.push("TPS \u5206\u5E03");
336
+ const maxTps = Math.max(...tps, 1);
337
+ const buckets = 10;
338
+ const bucketSize = maxTps / buckets;
339
+ const histogram = new Array(buckets).fill(0);
340
+ for (const t of tps) {
341
+ const bucketIndex = Math.min(Math.floor(t / bucketSize), buckets - 1);
342
+ histogram[bucketIndex]++;
343
+ }
344
+ const maxCount = Math.max(...histogram, 1);
345
+ for (let i = 0; i < buckets; i++) {
346
+ const bucketStart = (i * bucketSize).toFixed(1);
347
+ const bucketEnd = ((i + 1) * bucketSize).toFixed(1);
348
+ const count = histogram[i];
349
+ const barLength = Math.round(count / maxCount * CHART_WIDTH);
350
+ const bar = BLOCK_CHAR.repeat(barLength);
351
+ lines.push(`${bucketStart}-${bucketEnd} \u2502${bar} ${count}`);
352
+ }
353
+ return lines.join("\n");
354
+ }
355
+ function renderStatsTable(stats) {
356
+ const lines = [];
357
+ lines.push("");
358
+ lines.push("\u7EDF\u8BA1\u6C47\u603B (N=" + stats.sampleSize + ")");
359
+ lines.push("\u250C" + "\u2500".repeat(70) + "\u2510");
360
+ lines.push(
361
+ "\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"
362
+ );
363
+ lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
364
+ lines.push(
365
+ formatStatRow(
366
+ "TTFT (ms)",
367
+ stats.mean.ttft,
368
+ stats.min.ttft,
369
+ stats.max.ttft,
370
+ stats.stdDev.ttft,
371
+ "f"
372
+ )
373
+ );
374
+ lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
375
+ lines.push(
376
+ formatStatRow(
377
+ "\u603B\u8017\u65F6 (ms)",
378
+ stats.mean.totalTime,
379
+ stats.min.totalTime,
380
+ stats.max.totalTime,
381
+ stats.stdDev.totalTime,
382
+ "f"
383
+ )
384
+ );
385
+ lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
386
+ lines.push(
387
+ formatStatRow(
388
+ "\u603B Token \u6570",
389
+ stats.mean.totalTokens,
390
+ stats.min.totalTokens,
391
+ stats.max.totalTokens,
392
+ stats.stdDev.totalTokens,
393
+ "f"
394
+ )
395
+ );
396
+ lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
397
+ lines.push(
398
+ formatStatRow(
399
+ "\u5E73\u5747\u901F\u5EA6",
400
+ stats.mean.averageSpeed,
401
+ stats.min.averageSpeed,
402
+ stats.max.averageSpeed,
403
+ stats.stdDev.averageSpeed,
404
+ "f"
405
+ )
406
+ );
407
+ lines.push("\u251C" + "\u2500".repeat(70) + "\u2524");
408
+ lines.push(
409
+ formatStatRow(
410
+ "\u5CF0\u503C\u901F\u5EA6",
411
+ stats.mean.peakSpeed,
412
+ stats.min.peakSpeed,
413
+ stats.max.peakSpeed,
414
+ stats.stdDev.peakSpeed,
415
+ "f"
416
+ )
417
+ );
418
+ lines.push("\u2514" + "\u2500".repeat(70) + "\u2518");
419
+ return lines.join("\n");
420
+ }
421
+ function formatStatRow(label, mean2, min, max, stdDev, format) {
422
+ const fmt = (n) => format === "f" ? n.toFixed(2) : n.toFixed(0);
423
+ 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";
424
+ }
425
+ function formatTimeWithDecimals(ms) {
426
+ if (ms === Math.floor(ms)) {
427
+ return `${ms.toFixed(0)}ms`;
428
+ }
429
+ return `${ms.toFixed(2)}ms`;
430
+ }
431
+ function renderSingleResult(metrics, runIndex) {
432
+ const lines = [];
433
+ lines.push(`
434
+ [\u8FD0\u884C ${runIndex + 1}]`);
435
+ lines.push(` TTFT: ${formatTimeWithDecimals(metrics.ttft)}`);
436
+ lines.push(` \u603B\u8017\u65F6: ${formatTimeWithDecimals(metrics.totalTime)}`);
437
+ lines.push(` \u603B Token \u6570: ${metrics.totalTokens}`);
438
+ lines.push(` \u5E73\u5747\u901F\u5EA6: ${metrics.averageSpeed.toFixed(2)} tokens/s`);
439
+ lines.push(` \u5CF0\u503C\u901F\u5EA6: ${metrics.peakSpeed.toFixed(2)} tokens/s`);
440
+ return lines.join("\n");
441
+ }
442
+ function renderReport(stats) {
443
+ const lines = [];
444
+ lines.push("\n" + "\u2550".repeat(72));
445
+ lines.push("Token \u901F\u5EA6\u6D4B\u8BD5\u62A5\u544A");
446
+ lines.push("\u2550".repeat(72));
447
+ lines.push(renderStatsTable(stats));
448
+ if (stats.mean.tps.length > 0) {
449
+ lines.push("\n" + renderSpeedChart(stats.mean.tps));
450
+ }
451
+ if (stats.mean.tps.length > 0) {
452
+ lines.push("\n" + renderTPSHistogram(stats.mean.tps));
453
+ }
454
+ return lines.join("\n");
455
+ }
456
+
457
+ // src/index.ts
458
+ var program = new Command();
459
+ program.name("token-speed-test").description("A CLI tool to test LLM API token output speed").version("1.0.0");
460
+ 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);
461
+ var options = program.opts();
462
+ async function main() {
463
+ try {
464
+ const config = parseConfig({
465
+ apiKey: options.apiKey,
466
+ provider: options.provider,
467
+ url: options.url,
468
+ model: options.model,
469
+ maxTokens: parseInt(options.maxTokens, 10),
470
+ runs: parseInt(options.runs, 10),
471
+ prompt: options.prompt
472
+ });
473
+ console.log(chalk.cyan("\n\u{1F680} Token \u901F\u5EA6\u6D4B\u8BD5\u5DE5\u5177"));
474
+ console.log(chalk.gray("\u2500".repeat(50)));
475
+ console.log(chalk.gray(`Provider: ${chalk.white(config.provider)}`));
476
+ console.log(chalk.gray(`Model: ${chalk.white(config.model)}`));
477
+ console.log(chalk.gray(`Max Tokens: ${chalk.white(config.maxTokens)}`));
478
+ console.log(chalk.gray(`Runs: ${chalk.white(config.runCount)}`));
479
+ console.log(
480
+ chalk.gray(
481
+ `Prompt: ${chalk.white(config.prompt.substring(0, 50))}${config.prompt.length > 50 ? "..." : ""}`
482
+ )
483
+ );
484
+ console.log(chalk.gray("\u2500".repeat(50)));
485
+ console.log(chalk.yellow("\n\u23F3 \u6B63\u5728\u8FD0\u884C\u6D4B\u8BD5...\n"));
486
+ const results = await runMultipleTests(config);
487
+ const allMetrics = results.map((r) => calculateMetrics(r));
488
+ for (let i = 0; i < allMetrics.length; i++) {
489
+ console.log(chalk.gray(renderSingleResult(allMetrics[i], i)));
490
+ }
491
+ const stats = calculateStats(allMetrics);
492
+ console.log(chalk.cyan("\n" + renderReport(stats)));
493
+ console.log(chalk.green("\n\u2705 \u6D4B\u8BD5\u5B8C\u6210!\n"));
494
+ } catch (error) {
495
+ if (error instanceof Error) {
496
+ console.error(chalk.red(`
497
+ \u274C \u9519\u8BEF: ${error.message}
498
+ `));
499
+ } else {
500
+ console.error(chalk.red("\n\u274C \u53D1\u751F\u672A\u77E5\u9519\u8BEF\n"));
501
+ }
502
+ process.exit(1);
503
+ }
504
+ }
505
+ void main();
506
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 { 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\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(\"1.0.0\");\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-3-5-sonnet-20241022\",\n openai: \"gpt-4o-mini\",\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 (maxTokens <= 0) {\n throw new Error(`Invalid max-tokens: ${maxTokens}. Must be a positive number.`);\n }\n\n if (runs <= 0) {\n throw new Error(`Invalid runs: ${runs}. Must be a positive number.`);\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 (config.maxTokens <= 0) {\n return { valid: false, error: \"maxTokens must be positive\" };\n }\n\n if (config.runCount <= 0) {\n return { valid: false, error: \"runCount must be positive\" };\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","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"]}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "token-speed-tester",
3
+ "version": "1.0.0",
4
+ "description": "A CLI tool to test LLM API token output speed",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "token-speed-test": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup && tsc --noEmit",
17
+ "dev": "tsx src/index.ts",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix",
21
+ "format": "prettier --write .",
22
+ "format:check": "prettier --check .",
23
+ "test": "vitest --run",
24
+ "test:ui": "vitest --ui",
25
+ "test:coverage": "vitest --run --coverage",
26
+ "test:watch": "vitest",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "llm",
31
+ "token",
32
+ "speed",
33
+ "test",
34
+ "anthropic",
35
+ "openai",
36
+ "cli"
37
+ ],
38
+ "author": "Cansiny0320",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/Cansiny0320/token-speed-tester.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/Cansiny0320/token-speed-tester/issues"
46
+ },
47
+ "homepage": "https://github.com/Cansiny0320/token-speed-tester#readme",
48
+ "dependencies": {
49
+ "@anthropic-ai/sdk": "^0.32.1",
50
+ "chalk": "^5.4.1",
51
+ "cli-table3": "^0.6.5",
52
+ "commander": "^12.1.0",
53
+ "openai": "^4.77.3"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^9.39.2",
57
+ "@types/node": "^22.10.6",
58
+ "@vitest/coverage-v8": "^2.1.8",
59
+ "@vitest/ui": "^2.1.8",
60
+ "eslint": "^9.39.2",
61
+ "eslint-config-prettier": "^10.1.8",
62
+ "eslint-plugin-prettier": "^5.5.4",
63
+ "parser": "^0.1.4",
64
+ "prettier": "^3.7.4",
65
+ "tsup": "^8.5.1",
66
+ "tsx": "^4.19.2",
67
+ "typescript": "^5.7.3",
68
+ "typescript-eslint": "^8.52.0",
69
+ "vitest": "^2.1.8"
70
+ },
71
+ "engines": {
72
+ "node": ">=18.0.0"
73
+ }
74
+ }