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 CHANGED
@@ -1,309 +1,58 @@
1
1
  # Ai-speedometer
2
2
 
3
- A comprehensive, modern CLI tool for benchmarking AI models across multiple providers with **parallel execution**, **professional tables**, **arrow key navigation**, and **advanced metrics**.
3
+ A CLI tool for benchmarking AI models across multiple providers with parallel execution and performance metrics.
4
4
 
5
- ## Quick Start
5
+ ## Install
6
6
 
7
7
  ```bash
8
- git clone https://github.com/aptdnfapt/Ai-speedometer
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
- #### Anthropic Providers
39
- - **Anthropic**: Get your API key from [Anthropic Console](https://console.anthropic.com/settings/keys)
11
+ ## What It Measures
40
12
 
41
- ### Step 2: Configure Providers
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
- You have two ways to configure providers:
18
+ ## Quick Setup
44
19
 
45
- #### Method A: Use the Interactive CLI (Recommended)
46
-
47
- 1. Run the CLI:
20
+ 1. **Set Model**
48
21
  ```bash
49
- npm run cli
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. Select "Set Model" from the main menu
53
-
54
- 3. Choose "Add New Provider"
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
- 5. Enter the required information:
61
- - **Provider name**: A friendly name (e.g., "My OpenAI", "Local Ollama")
62
- - **Base URL**: The API endpoint (see examples below)
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
- #### Method B: Manual Configuration
67
-
68
- 1. Copy the template:
35
+ 4. **Run Benchmark**
69
36
  ```bash
70
- cp ai-benchmark-config.json.template ai-benchmark-config.json
37
+ ai-speedometer
38
+ # Select "Run Benchmark (AI SDK)" → Choose models → Press ENTER
71
39
  ```
72
40
 
73
- 2. Edit `ai-benchmark-config.json` with your provider details:
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
- Use ↑↓ arrows to navigate, ENTER to select
232
- Navigation is circular
233
-
234
- Select provider type:
43
+ ```bash
44
+ # Start CLI
45
+ ai-speedometer
235
46
 
236
- OpenAI Compatible
237
- ○ Anthropic
238
- ○ Back to main menu
239
- ```
47
+ # Or use short alias
48
+ aispeed
240
49
 
241
- #### Anthropic Providers (Now Supports Custom Base URLs)
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 that supports ANSI colors and arrow keys
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 [...allModels];
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
- screenContent += colorText('Available Models:', 'yellow') + '\n';
298
- screenContent += '\n';
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
- const isSelected = model.selected;
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 actualModelIndex = allModels.indexOf(filteredModels[currentIndex]);
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
- showMainMenu();
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 };