ai-speedometer 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -282
- package/cli.js +176 -14
- package/dist/ai-speedometer +86 -86
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,309 +1,58 @@
|
|
|
1
1
|
# Ai-speedometer
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A CLI tool for benchmarking AI models across multiple providers with parallel execution and performance metrics.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
9
|
-
# Install dependencies
|
|
10
|
-
npm install
|
|
11
|
-
# Set up your API keys and providers (see Setup Guide below)
|
|
12
|
-
# Start the CLI
|
|
13
|
-
npm run cli
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
npm install -g ai-speedometer
|
|
16
9
|
```
|
|
17
|
-
Debug
|
|
18
|
-
```bash
|
|
19
|
-
# Start with debug logging (for troubleshooting)
|
|
20
|
-
npm run cli:debug
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Setup Guide
|
|
24
|
-
|
|
25
|
-
### Before You Begin
|
|
26
|
-
|
|
27
|
-
Before running the benchmark, you need to configure your AI providers with API keys and base URLs. The tool supports two types of providers:
|
|
28
|
-
|
|
29
|
-
1. **OpenAI-Compatible providers** (OpenAI, local models, custom endpoints)
|
|
30
|
-
2. **Anthropic providers** (Claude models)
|
|
31
|
-
|
|
32
|
-
### Step 1: Get Your API Keys
|
|
33
|
-
|
|
34
|
-
#### OpenAI-Compatible Providers
|
|
35
|
-
- **OpenAI**: Get your API key from [OpenAI API Keys](https://platform.openai.com/api-keys)
|
|
36
|
-
- **Other providers**: Check your provider's documentation for API key access
|
|
37
10
|
|
|
38
|
-
|
|
39
|
-
- **Anthropic**: Get your API key from [Anthropic Console](https://console.anthropic.com/settings/keys)
|
|
11
|
+
## What It Measures
|
|
40
12
|
|
|
41
|
-
|
|
13
|
+
- **TTFT** (Time to First Token) - How fast the first response token arrives
|
|
14
|
+
- **Total Time** - Complete request duration
|
|
15
|
+
- **Tokens/Second** - Real-time throughput
|
|
16
|
+
- **Token Counts** - Input, output, and total tokens used
|
|
42
17
|
|
|
43
|
-
|
|
18
|
+
## Quick Setup
|
|
44
19
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
1. Run the CLI:
|
|
20
|
+
1. **Set Model**
|
|
48
21
|
```bash
|
|
49
|
-
|
|
22
|
+
ai-speedometer
|
|
23
|
+
# Select "Set Model" → "Add Verified Provider" → Choose provider (OpenAI, Anthropic, etc.)
|
|
24
|
+
# Enter your API key when prompted
|
|
50
25
|
```
|
|
51
26
|
|
|
52
|
-
2.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
4. Select the provider type:
|
|
57
|
-
- **OpenAI Compatible**: For OpenAI, local models, or custom endpoints
|
|
58
|
-
- **Anthropic**: For Claude models
|
|
27
|
+
2. **Choose Model Provider**
|
|
28
|
+
- Verified providers (OpenAI, Anthropic, Google) - auto-configured
|
|
29
|
+
- Custom providers (Ollama, local models) - add your base URL
|
|
59
30
|
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
- **API Key**: Your secret API key
|
|
64
|
-
- **Model name**: The specific model you want to test
|
|
31
|
+
3. **Add API Key**
|
|
32
|
+
- Get API keys from your provider's dashboard
|
|
33
|
+
- Enter when prompted - stored securely
|
|
65
34
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
1. Copy the template:
|
|
35
|
+
4. **Run Benchmark**
|
|
69
36
|
```bash
|
|
70
|
-
|
|
37
|
+
ai-speedometer
|
|
38
|
+
# Select "Run Benchmark (AI SDK)" → Choose models → Press ENTER
|
|
71
39
|
```
|
|
72
40
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
```json
|
|
76
|
-
{
|
|
77
|
-
"providers": [
|
|
78
|
-
{
|
|
79
|
-
"id": "my_openai",
|
|
80
|
-
"name": "OpenAI",
|
|
81
|
-
"type": "openai-compatible",
|
|
82
|
-
"baseUrl": "https://api.openai.com/v1",
|
|
83
|
-
"apiKey": "sk-your-openai-key-here",
|
|
84
|
-
"models": [
|
|
85
|
-
{
|
|
86
|
-
"name": "gpt-4",
|
|
87
|
-
"id": "gpt4_model"
|
|
88
|
-
}
|
|
89
|
-
]
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"id": "my_anthropic",
|
|
93
|
-
"name": "Anthropic",
|
|
94
|
-
"type": "anthropic",
|
|
95
|
-
"baseUrl": "https://api.anthropic.com",
|
|
96
|
-
"apiKey": "sk-ant-your-anthropic-key-here",
|
|
97
|
-
"models": [
|
|
98
|
-
{
|
|
99
|
-
"name": "claude-3-sonnet-20240229",
|
|
100
|
-
"id": "claude3_sonnet"
|
|
101
|
-
}
|
|
102
|
-
]
|
|
103
|
-
}
|
|
104
|
-
]
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Step 3: Common Base URL Examples
|
|
109
|
-
|
|
110
|
-
#### OpenAI-Compatible Providers
|
|
111
|
-
- **OpenAI**: `https://api.openai.com/v1`
|
|
112
|
-
- **Local Ollama**: `http://localhost:11434/v1`
|
|
113
|
-
- **Groq**: `https://api.groq.com/openai/v1`
|
|
114
|
-
- **Together AI**: `https://api.together.xyz/v1`
|
|
115
|
-
- **Anyscale**: `https://api.endpoints.anyscale.com/v1`
|
|
116
|
-
- **Fireworks AI**: `https://api.fireworks.ai/inference/v1`
|
|
117
|
-
|
|
118
|
-
#### Anthropic Providers
|
|
119
|
-
- **Anthropic Official**: `https://api.anthropic.com`
|
|
120
|
-
- **Custom Anthropic endpoints**: Check with your provider
|
|
121
|
-
|
|
122
|
-
### Step 4: Security
|
|
123
|
-
|
|
124
|
-
Your configuration file contains sensitive API keys. The `.gitignore` file already excludes `ai-benchmark-config.json` to prevent accidental commits.
|
|
125
|
-
|
|
126
|
-
**Never commit your API keys to version control!**
|
|
127
|
-
|
|
128
|
-
### Step 5: Verify Configuration
|
|
129
|
-
|
|
130
|
-
After setting up, run the CLI and check that your providers appear in the model selection menu. If you see your providers and models listed, you're ready to benchmark!
|
|
131
|
-
|
|
132
|
-
### Troubleshooting
|
|
133
|
-
|
|
134
|
-
- **"Provider not found"**: Check your base URL and API key
|
|
135
|
-
- **"Model not available"**: Verify the model name is correct for your provider
|
|
136
|
-
- **"Connection failed"**: Ensure your base URL is accessible and you have internet access
|
|
137
|
-
- **"Invalid API key"**: Double-check your API key is correct and has proper permissions
|
|
138
|
-
- **Debug Mode**: Use `npm run cli:debug` to enable detailed logging. This creates a `debug.log` file with API request/response details for troubleshooting connection issues.
|
|
139
|
-
|
|
140
|
-
## Usage Examples
|
|
141
|
-
|
|
142
|
-
### Main Menu (Modern Arrow Navigation)
|
|
143
|
-
```
|
|
144
|
-
Ai-speedometer
|
|
145
|
-
=============================
|
|
146
|
-
Note: opencode uses ai-sdk
|
|
147
|
-
|
|
148
|
-
Use ↑↓ arrows to navigate, ENTER to select
|
|
149
|
-
Navigation is circular
|
|
150
|
-
|
|
151
|
-
● Set Model
|
|
152
|
-
○ Run Benchmark (AI SDK)
|
|
153
|
-
○ Run Benchmark (REST API)
|
|
154
|
-
○ Exit
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Model Selection (Circle-Based UI)
|
|
158
|
-
```
|
|
159
|
-
Select Models for Benchmark
|
|
160
|
-
|
|
161
|
-
Use ↑↓ arrows to navigate, SPACE to select/deselect, ENTER to confirm
|
|
162
|
-
Navigation is circular - moving past bottom/top wraps around
|
|
163
|
-
Press "A" to select all models, "N" to deselect all
|
|
164
|
-
Circle states: ●=Current+Selected ○=Current+Unselected ●=Selected ○=Unselected
|
|
165
|
-
|
|
166
|
-
Available Models:
|
|
167
|
-
● gpt-4 (OpenAI)
|
|
168
|
-
○ claude-3-sonnet (Anthropic)
|
|
169
|
-
|
|
170
|
-
Selected: 1 models
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Provider Management (Vertical Stacking)
|
|
174
|
-
```
|
|
175
|
-
Available Providers
|
|
176
|
-
|
|
177
|
-
1. chutes (openai-compatible)
|
|
178
|
-
Models:
|
|
179
|
-
1. zai-org/GLM-4.5-turbo
|
|
180
|
-
2. deepseek-ai/DeepSeek-V3.1-turbo
|
|
181
|
-
|
|
182
|
-
2. zai (openai-compatible)
|
|
183
|
-
Models:
|
|
184
|
-
1. glm-4.5
|
|
185
|
-
|
|
186
|
-
3. zai-anthropic (anthropic)
|
|
187
|
-
Models:
|
|
188
|
-
1. claude-3-sonnet-20240229
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Benchmark Results (Professional Tables + Enhanced Charts)
|
|
192
|
-
```
|
|
193
|
-
BENCHMARK RESULTS
|
|
194
|
-
=========================
|
|
195
|
-
Method: AI SDK
|
|
196
|
-
|
|
197
|
-
COMPREHENSIVE PERFORMANCE SUMMARY
|
|
198
|
-
Note: AI SDK method does not count thinking tokens as first token. REST API method does not use streaming.
|
|
199
|
-
┌─────────────────────────┬─────────────────────┬─────────────────┬────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┐
|
|
200
|
-
│ Model │ Provider │ Total Time(s) │ TTFT(s) │ Tokens/Sec │ Output Tokens │ Prompt Tokens │ Total Tokens │
|
|
201
|
-
├─────────────────────────┼─────────────────────┼─────────────────┼────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┤
|
|
202
|
-
│ zai-org/GLM-4.5-turbo │ chutes │ 11.47 │ 1.00 │ 81.5 │ 935 │ 14 │ 1205 │
|
|
203
|
-
│ deepseek-ai/DeepSeek-V3 │ chutes │ 5.21 │ 0.83 │ 178.6 │ 930 │ 14 │ 742 │
|
|
204
|
-
│ glm-4.5 │ zai │ 11.30 │ 5.30 │ 72.9 │ 824 │ 14 │ 1087 │
|
|
205
|
-
└─────────────────────────┴─────────────────────┴─────────────────┴────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┘
|
|
206
|
-
|
|
207
|
-
PERFORMANCE COMPARISON CHARTS
|
|
208
|
-
──────────────────────────────────────────────────────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
TOTAL TIME COMPARISON (lower is better)
|
|
211
|
-
5.21s | 178.6 tok/s | deepseek-ai/DeepSeek-V3.1-turbo | ████████████████████████████████████████████████
|
|
212
|
-
11.30s | 72.9 tok/s | glm-4.5 | ████████████████████████████████████░░░░░░░░░
|
|
213
|
-
11.47s | 81.5 tok/s | zai-org/GLM-4.5-turbo | ████████████████████████████████████░░░░░░░░░
|
|
214
|
-
|
|
215
|
-
TOKENS PER SECOND COMPARISON (higher is better)
|
|
216
|
-
178.6 tok/s | 5.21s | deepseek-ai/DeepSeek-V3.1-turbo | █████████████████████████████████████████████████████
|
|
217
|
-
81.5 tok/s | 11.47s | zai-org/GLM-4.5-turbo | ████████████████████████████████░░░░░░░░░░░░░░░░
|
|
218
|
-
72.9 tok/s | 11.30s | glm-4.5 | ████████████████████████████████░░░░░░░░░░░░░░░░
|
|
219
|
-
|
|
220
|
-
Benchmark completed!
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Configuration
|
|
224
|
-
|
|
225
|
-
### Adding Providers (Arrow Key Navigation)
|
|
226
|
-
|
|
227
|
-
#### OpenAI-Compatible Providers
|
|
228
|
-
```
|
|
229
|
-
Add New Provider
|
|
41
|
+
## Usage
|
|
230
42
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Select provider type:
|
|
43
|
+
```bash
|
|
44
|
+
# Start CLI
|
|
45
|
+
ai-speedometer
|
|
235
46
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
○ Back to main menu
|
|
239
|
-
```
|
|
47
|
+
# Or use short alias
|
|
48
|
+
aispeed
|
|
240
49
|
|
|
241
|
-
|
|
50
|
+
# Debug mode
|
|
51
|
+
ai-speedometer --debug
|
|
242
52
|
```
|
|
243
|
-
Enter provider name (e.g., MyAnthropic):
|
|
244
|
-
Enter base URL (e.g., https://api.anthropic.com):
|
|
245
|
-
Enter Anthropic API key: [your-key]
|
|
246
|
-
Enter model name (e.g., claude-3-sonnet-20240229):
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
**Note**: The system automatically handles `/v1` path requirements for custom Anthropic endpoints. If you encounter issues with custom base URLs, run `npm run cli:debug` to see detailed API request logs.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
## Performance Metrics Explained
|
|
253
|
-
|
|
254
|
-
### Core Metrics
|
|
255
|
-
- **Total Time**: Complete request duration (seconds)
|
|
256
|
-
- **Time to First Token (TTFT)**: Latency until first streaming token arrives (0 for REST API since it doesn't use streaming)
|
|
257
|
-
- **Tokens per Second**: Real-time throughput calculation
|
|
258
|
-
- **Output Tokens**: Number of tokens in the AI response
|
|
259
|
-
- **Prompt Tokens**: Number of tokens in the input prompt
|
|
260
|
-
- **Total Tokens**: Combined prompt + output tokens
|
|
261
|
-
|
|
262
|
-
### Benchmark Methods
|
|
263
|
-
- **AI SDK Method**: Uses streaming with Vercel AI SDK, doesn't count thinking tokens as first token
|
|
264
|
-
- **REST API Method**: Uses direct HTTP calls, no streaming, TTFT is always 0
|
|
265
|
-
|
|
266
|
-
### Chart Features
|
|
267
|
-
- **Dual Comparison Charts**: Both time and performance perspectives
|
|
268
|
-
- **Left-Side Metrics**: Shows actual values alongside bar charts
|
|
269
|
-
- **Color Coding**: Red bars for time (lower is better), green for performance (higher is better)
|
|
270
|
-
- **Dynamic Scaling**: Bars scale proportionally to the best/worst performers
|
|
271
|
-
|
|
272
|
-
## Tech Stack
|
|
273
|
-
|
|
274
|
-
- **AI SDK**: Vercel AI SDK with streaming support (opencode uses it)
|
|
275
|
-
- **Table Rendering**: `cli-table3` for professional tables
|
|
276
|
-
- **Providers**: OpenAI-compatible and Anthropic APIs with custom baseUrl support
|
|
277
|
-
- **Navigation**: Circular arrow key navigation throughout
|
|
278
|
-
- **Colors**: ANSI escape codes for terminal styling
|
|
279
|
-
- **Configuration**: JSON-based persistent storage
|
|
280
|
-
- **Security**: .gitignore protection for sensitive files
|
|
281
|
-
- **Debug Logging**: Built-in debugging system for troubleshooting API connections
|
|
282
53
|
|
|
283
54
|
## Requirements
|
|
284
55
|
|
|
285
56
|
- Node.js 18+
|
|
286
57
|
- API keys for AI providers
|
|
287
|
-
- Terminal
|
|
288
|
-
- Git (for security configuration)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
## Advanced Features
|
|
292
|
-
|
|
293
|
-
### Parallel Execution
|
|
294
|
-
- **Speed**: Runs all selected models simultaneously
|
|
295
|
-
- **Efficiency**: No sequential waiting between models
|
|
296
|
-
- **Results**: Comprehensive comparison across all models
|
|
297
|
-
|
|
298
|
-
### Advanced Navigation
|
|
299
|
-
- **Universal Pattern**: All menus use the same arrow key navigation
|
|
300
|
-
- **Circular Movement**: Navigation wraps at top/bottom for seamless UX
|
|
301
|
-
- **Visual Feedback**: Clear indicators for current selections
|
|
302
|
-
- **Keyboard Shortcuts**: Quick actions like select all ('A') and deselect all ('N')
|
|
303
|
-
|
|
304
|
-
### Professional Output
|
|
305
|
-
- **Table Format**: Clean, aligned columns with proper spacing
|
|
306
|
-
- **Color Coding**: Different colors for different metric types
|
|
307
|
-
- **Comprehensive Data**: All relevant metrics in one view
|
|
308
|
-
- **Visual Charts**: Bar charts for quick visual comparison
|
|
309
|
-
|
|
58
|
+
- Terminal with arrow keys and ANSI colors
|
package/cli.js
CHANGED
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
getVerifiedProvidersFromConfig,
|
|
22
22
|
addCustomProvider,
|
|
23
23
|
addModelToCustomProvider,
|
|
24
|
-
getAIConfigDebugPaths
|
|
24
|
+
getAIConfigDebugPaths,
|
|
25
|
+
addToRecentModels,
|
|
26
|
+
getRecentModels,
|
|
27
|
+
cleanupRecentModelsFromConfig
|
|
25
28
|
} from './ai-config.js';
|
|
26
29
|
import 'dotenv/config';
|
|
27
30
|
import Table from 'cli-table3';
|
|
@@ -203,9 +206,12 @@ async function selectModelsCircular() {
|
|
|
203
206
|
showHeader();
|
|
204
207
|
console.log(colorText('Select Models for Benchmark', 'magenta'));
|
|
205
208
|
console.log('');
|
|
206
|
-
|
|
209
|
+
|
|
207
210
|
const config = await loadConfig();
|
|
208
|
-
|
|
211
|
+
|
|
212
|
+
// Clean up recent models from main config and migrate to cache
|
|
213
|
+
await cleanupRecentModelsFromConfig();
|
|
214
|
+
|
|
209
215
|
if (config.providers.length === 0) {
|
|
210
216
|
console.log(colorText('No providers available. Please add a provider first.', 'red'));
|
|
211
217
|
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
@@ -230,16 +236,38 @@ async function selectModelsCircular() {
|
|
|
230
236
|
});
|
|
231
237
|
});
|
|
232
238
|
|
|
239
|
+
// Load recent models
|
|
240
|
+
const recentModelsData = await getRecentModels();
|
|
241
|
+
|
|
242
|
+
// Create a mapping of recent models to actual model objects
|
|
243
|
+
const recentModelObjects = [];
|
|
244
|
+
recentModelsData.forEach(recentModel => {
|
|
245
|
+
const modelObj = allModels.find(model =>
|
|
246
|
+
model.id === recentModel.modelId &&
|
|
247
|
+
model.providerName === recentModel.providerName
|
|
248
|
+
);
|
|
249
|
+
if (modelObj) {
|
|
250
|
+
recentModelObjects.push({
|
|
251
|
+
...modelObj,
|
|
252
|
+
isRecent: true
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
233
257
|
let currentIndex = 0;
|
|
234
258
|
let currentPage = 0;
|
|
235
259
|
let searchQuery = '';
|
|
236
|
-
let filteredModels = [...allModels];
|
|
237
260
|
|
|
238
261
|
// Create a reusable filter function to avoid code duplication
|
|
239
262
|
const filterModels = (query) => {
|
|
240
263
|
if (!query.trim()) {
|
|
241
|
-
return
|
|
264
|
+
// When search is empty, return the combined list with recent models at top
|
|
265
|
+
const recentModelIds = new Set(recentModelObjects.map(m => m.id));
|
|
266
|
+
const nonRecentModels = allModels.filter(model => !recentModelIds.has(model.id));
|
|
267
|
+
return [...recentModelObjects, ...nonRecentModels];
|
|
242
268
|
}
|
|
269
|
+
|
|
270
|
+
// When searching, search through all models (no recent section)
|
|
243
271
|
const lowercaseQuery = query.toLowerCase();
|
|
244
272
|
return allModels.filter(model => {
|
|
245
273
|
const modelNameMatch = model.name.toLowerCase().includes(lowercaseQuery);
|
|
@@ -251,6 +279,9 @@ async function selectModelsCircular() {
|
|
|
251
279
|
});
|
|
252
280
|
};
|
|
253
281
|
|
|
282
|
+
// Initialize filtered models using the filter function
|
|
283
|
+
let filteredModels = filterModels('');
|
|
284
|
+
|
|
254
285
|
// Debounce function to reduce filtering frequency
|
|
255
286
|
let searchTimeout;
|
|
256
287
|
const debouncedFilter = (query, callback) => {
|
|
@@ -275,7 +306,7 @@ async function selectModelsCircular() {
|
|
|
275
306
|
screenContent += colorText('Type to search (real-time filtering)', 'cyan') + '\n';
|
|
276
307
|
screenContent += colorText('Press "A" to select all models, "N" to deselect all', 'cyan') + '\n';
|
|
277
308
|
screenContent += colorText('Circle states: ●=Current+Selected ○=Current+Unselected ●=Selected ○=Unselected', 'dim') + '\n';
|
|
278
|
-
screenContent += colorText('Quick run: ENTER on any model | Multi-select: TAB then ENTER', 'dim') + '\n';
|
|
309
|
+
screenContent += colorText('Quick run: ENTER on any model | Multi-select: TAB then ENTER | Recent: R', 'dim') + '\n';
|
|
279
310
|
screenContent += '\n';
|
|
280
311
|
|
|
281
312
|
// Search interface - always visible
|
|
@@ -294,13 +325,51 @@ async function selectModelsCircular() {
|
|
|
294
325
|
const endIndex = Math.min(startIndex + visibleItemsCount, filteredModels.length);
|
|
295
326
|
|
|
296
327
|
// Display models in a vertical layout with pagination
|
|
297
|
-
|
|
298
|
-
|
|
328
|
+
let hasRecentModelsInCurrentPage = false;
|
|
329
|
+
let recentSectionDisplayed = false;
|
|
330
|
+
let nonRecentSectionDisplayed = false;
|
|
331
|
+
|
|
332
|
+
// Only show recent section when search is empty and we have recent models
|
|
333
|
+
const showRecentSection = searchQuery.length === 0 && recentModelObjects.length > 0;
|
|
334
|
+
|
|
335
|
+
// Check if current page contains any recent models (only when search is empty)
|
|
336
|
+
if (showRecentSection) {
|
|
337
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
338
|
+
if (filteredModels[i].isRecent) {
|
|
339
|
+
hasRecentModelsInCurrentPage = true;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
299
344
|
|
|
345
|
+
// Display models with proper section headers
|
|
300
346
|
for (let i = startIndex; i < endIndex; i++) {
|
|
301
347
|
const model = filteredModels[i];
|
|
302
348
|
const isCurrent = i === currentIndex;
|
|
303
|
-
|
|
349
|
+
// For recent models, check selection state from the original model
|
|
350
|
+
let isSelected;
|
|
351
|
+
if (model.isRecent) {
|
|
352
|
+
const originalModelIndex = allModels.findIndex(originalModel =>
|
|
353
|
+
originalModel.id === model.id &&
|
|
354
|
+
originalModel.providerName === model.providerName &&
|
|
355
|
+
!originalModel.isRecent
|
|
356
|
+
);
|
|
357
|
+
isSelected = originalModelIndex !== -1 ? allModels[originalModelIndex].selected : false;
|
|
358
|
+
} else {
|
|
359
|
+
isSelected = model.selected;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Show recent section header if we encounter a recent model and haven't shown the header yet
|
|
363
|
+
if (model.isRecent && !recentSectionDisplayed && hasRecentModelsInCurrentPage && showRecentSection) {
|
|
364
|
+
screenContent += colorText('-------recent--------', 'dim') + '\n';
|
|
365
|
+
recentSectionDisplayed = true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Show separator between recent and non-recent models
|
|
369
|
+
if (!model.isRecent && recentSectionDisplayed && !nonRecentSectionDisplayed && showRecentSection) {
|
|
370
|
+
screenContent += colorText('-------recent--------', 'dim') + '\n';
|
|
371
|
+
nonRecentSectionDisplayed = true;
|
|
372
|
+
}
|
|
304
373
|
|
|
305
374
|
// Single circle that shows both current state and selection
|
|
306
375
|
let circle;
|
|
@@ -378,10 +447,28 @@ async function selectModelsCircular() {
|
|
|
378
447
|
}
|
|
379
448
|
} else if (key === '\t') {
|
|
380
449
|
// Tab - select/deselect current model
|
|
381
|
-
const
|
|
450
|
+
const currentModel = filteredModels[currentIndex];
|
|
451
|
+
let actualModelIndex;
|
|
452
|
+
|
|
453
|
+
if (currentModel.isRecent) {
|
|
454
|
+
// For recent models, find by matching the original model ID and provider name
|
|
455
|
+
actualModelIndex = allModels.findIndex(model =>
|
|
456
|
+
model.id === currentModel.id &&
|
|
457
|
+
model.providerName === currentModel.providerName &&
|
|
458
|
+
!model.isRecent // Don't match the recent copy, match the original
|
|
459
|
+
);
|
|
460
|
+
} else {
|
|
461
|
+
// For regular models, use the standard matching
|
|
462
|
+
actualModelIndex = allModels.findIndex(model =>
|
|
463
|
+
model.id === currentModel.id && model.providerName === currentModel.providerName
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
382
467
|
if (actualModelIndex !== -1) {
|
|
383
468
|
allModels[actualModelIndex].selected = !allModels[actualModelIndex].selected;
|
|
384
469
|
}
|
|
470
|
+
// Force immediate screen redraw by continuing to next iteration
|
|
471
|
+
continue;
|
|
385
472
|
} else if (key === '\r') {
|
|
386
473
|
// Enter - run benchmark on selected models
|
|
387
474
|
const currentModel = filteredModels[currentIndex];
|
|
@@ -448,6 +535,35 @@ async function selectModelsCircular() {
|
|
|
448
535
|
currentPage = 0;
|
|
449
536
|
});
|
|
450
537
|
}
|
|
538
|
+
} else if (key === 'R' || key === 'r') {
|
|
539
|
+
// Run recent models - only when search is empty and we have recent models
|
|
540
|
+
if (searchQuery.length === 0 && recentModelObjects.length > 0) {
|
|
541
|
+
// Deselect all models first
|
|
542
|
+
allModels.forEach(model => model.selected = false);
|
|
543
|
+
|
|
544
|
+
// Select all recent models by finding the original models
|
|
545
|
+
recentModelObjects.forEach(recentModel => {
|
|
546
|
+
const actualModelIndex = allModels.findIndex(model =>
|
|
547
|
+
model.id === recentModel.id &&
|
|
548
|
+
model.providerName === recentModel.providerName &&
|
|
549
|
+
!model.isRecent // Match the original, not the recent copy
|
|
550
|
+
);
|
|
551
|
+
if (actualModelIndex !== -1) {
|
|
552
|
+
allModels[actualModelIndex].selected = true;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Break out of loop to run benchmark
|
|
557
|
+
break;
|
|
558
|
+
} else {
|
|
559
|
+
// If search is active or no recent models, add 'R' to search query
|
|
560
|
+
searchQuery += key;
|
|
561
|
+
debouncedFilter(searchQuery, (newFilteredModels) => {
|
|
562
|
+
filteredModels = newFilteredModels;
|
|
563
|
+
currentIndex = 0;
|
|
564
|
+
currentPage = 0;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
451
567
|
} else if (key === 'a' || key === 'n') {
|
|
452
568
|
// Lowercase 'a' and 'n' go to search field (not select all/none)
|
|
453
569
|
searchQuery += key;
|
|
@@ -653,11 +769,11 @@ async function runStreamingBenchmark(models) {
|
|
|
653
769
|
console.log('');
|
|
654
770
|
console.log(colorText('All benchmarks completed!', 'green'));
|
|
655
771
|
|
|
656
|
-
await displayColorfulResults(results, 'AI SDK');
|
|
772
|
+
await displayColorfulResults(results, 'AI SDK', models);
|
|
657
773
|
}
|
|
658
774
|
|
|
659
775
|
// Colorful results display with comprehensive table and enhanced bars
|
|
660
|
-
async function displayColorfulResults(results, method = 'AI SDK') {
|
|
776
|
+
async function displayColorfulResults(results, method = 'AI SDK', models = []) {
|
|
661
777
|
clearScreen();
|
|
662
778
|
showHeader();
|
|
663
779
|
console.log(colorText('BENCHMARK RESULTS', 'magenta'));
|
|
@@ -820,6 +936,26 @@ async function displayColorfulResults(results, method = 'AI SDK') {
|
|
|
820
936
|
console.log('');
|
|
821
937
|
}
|
|
822
938
|
|
|
939
|
+
// Add successful models to recent models list
|
|
940
|
+
const successfulModels = results
|
|
941
|
+
.filter(r => r.success)
|
|
942
|
+
.map(r => {
|
|
943
|
+
// Find the actual model object that matches this benchmark result
|
|
944
|
+
const modelObj = models.find(model =>
|
|
945
|
+
model.name === r.model && model.providerName === r.provider
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
return {
|
|
949
|
+
modelId: modelObj ? modelObj.id : r.model, // Use actual ID if found, fallback to name
|
|
950
|
+
modelName: r.model,
|
|
951
|
+
providerName: r.provider
|
|
952
|
+
};
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
if (successfulModels.length > 0) {
|
|
956
|
+
await addToRecentModels(successfulModels);
|
|
957
|
+
}
|
|
958
|
+
|
|
823
959
|
console.log(colorText('Benchmark completed!', 'green'));
|
|
824
960
|
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
825
961
|
}
|
|
@@ -1695,7 +1831,27 @@ async function runRestApiBenchmark(models) {
|
|
|
1695
1831
|
console.log('');
|
|
1696
1832
|
console.log(colorText('All REST API benchmarks completed!', 'green'));
|
|
1697
1833
|
|
|
1698
|
-
await displayColorfulResults(results, 'REST API');
|
|
1834
|
+
await displayColorfulResults(results, 'REST API', models);
|
|
1835
|
+
|
|
1836
|
+
// Add successful models to recent models list
|
|
1837
|
+
const successfulModels = results
|
|
1838
|
+
.filter(r => r.success)
|
|
1839
|
+
.map(r => {
|
|
1840
|
+
// Find the actual model object that matches this benchmark result
|
|
1841
|
+
const modelObj = models.find(model =>
|
|
1842
|
+
model.name === r.model && model.providerName === r.provider
|
|
1843
|
+
);
|
|
1844
|
+
|
|
1845
|
+
return {
|
|
1846
|
+
modelId: modelObj ? modelObj.id : r.model, // Use actual ID if found, fallback to name
|
|
1847
|
+
modelName: r.model,
|
|
1848
|
+
providerName: r.provider
|
|
1849
|
+
};
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
if (successfulModels.length > 0) {
|
|
1853
|
+
await addToRecentModels(successfulModels);
|
|
1854
|
+
}
|
|
1699
1855
|
}
|
|
1700
1856
|
|
|
1701
1857
|
// Main menu with arrow key navigation
|
|
@@ -1840,7 +1996,13 @@ process.on('SIGINT', () => {
|
|
|
1840
1996
|
if (import.meta.url === `file://${process.argv[1]}` ||
|
|
1841
1997
|
process.argv.length === 2 ||
|
|
1842
1998
|
(process.argv.length === 3 && process.argv[2] === '--debug')) {
|
|
1843
|
-
|
|
1999
|
+
|
|
2000
|
+
// Clean up recent models from main config and migrate to cache on startup
|
|
2001
|
+
cleanupRecentModelsFromConfig().then(() => {
|
|
2002
|
+
showMainMenu();
|
|
2003
|
+
}).catch(() => {
|
|
2004
|
+
showMainMenu();
|
|
2005
|
+
});
|
|
1844
2006
|
}
|
|
1845
2007
|
|
|
1846
2008
|
export { showMainMenu, listProviders, selectModelsCircular, runStreamingBenchmark, loadConfig, saveConfig };
|