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 +21 -0
- package/README.md +215 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +506 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|