ai-speedometer 1.2.0 → 1.2.1
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 +17 -3
- package/cli.js +143 -133
- package/dist/ai-speedometer +98 -98
- package/docs/README.md +20 -2
- package/docs/custom-verified-providers.md +463 -0
- package/docs/models-dev-integration.md +23 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,12 +25,16 @@ npm install -g ai-speedometer
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
2. **Choose Model Provider**
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
- Verified providers (OpenAI, Anthropic, Google) - auto-configured
|
|
29
|
+
- Custom verified providers (pre-configured trusted providers) - add API key
|
|
30
|
+
- Custom providers (Ollama, local models) - add your base URL
|
|
30
31
|
|
|
31
32
|
3. **Add API Key**
|
|
32
33
|
- Get API keys from your provider's dashboard
|
|
33
|
-
- Enter when prompted - stored securely
|
|
34
|
+
- Enter when prompted - stored securely in:
|
|
35
|
+
- `~/.local/share/opencode/auth.json` (primary storage)
|
|
36
|
+
- `~/.config/ai-speedometer/ai-benchmark-config.json` (backup storage)
|
|
37
|
+
- Both files store verified and custom verified provider keys
|
|
34
38
|
|
|
35
39
|
4. **Run Benchmark**
|
|
36
40
|
```bash
|
|
@@ -51,6 +55,16 @@ aispeed
|
|
|
51
55
|
ai-speedometer --debug
|
|
52
56
|
```
|
|
53
57
|
|
|
58
|
+
## Configuration Files
|
|
59
|
+
|
|
60
|
+
API keys and configuration are stored in:
|
|
61
|
+
|
|
62
|
+
- **Verified + Custom Verified Providers**:
|
|
63
|
+
- Primary: `~/.local/share/opencode/auth.json`
|
|
64
|
+
- Backup: `~/.config/ai-speedometer/ai-benchmark-config.json` (verifiedProviders section)
|
|
65
|
+
- **Custom Providers**: `~/.config/ai-speedometer/ai-benchmark-config.json` (customProviders section)
|
|
66
|
+
- **Provider Definitions**: `./custom-verified-providers.json`
|
|
67
|
+
|
|
54
68
|
## Requirements
|
|
55
69
|
|
|
56
70
|
- Node.js 18+
|
package/cli.js
CHANGED
|
@@ -266,150 +266,152 @@ async function selectModelsCircular() {
|
|
|
266
266
|
const nonRecentModels = allModels.filter(model => !recentModelIds.has(model.id));
|
|
267
267
|
return [...recentModelObjects, ...nonRecentModels];
|
|
268
268
|
}
|
|
269
|
-
|
|
270
|
-
// When searching, search through all models (no recent section)
|
|
271
|
-
const
|
|
269
|
+
|
|
270
|
+
// When searching, search through all models (no recent section) with fuzzy matching
|
|
271
|
+
const queryWords = query.toLowerCase().split(/\s+/).filter(word => word.length > 0);
|
|
272
272
|
return allModels.filter(model => {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
const providerIdMatch = model.providerId.toLowerCase().includes(lowercaseQuery);
|
|
276
|
-
const providerTypeMatch = model.providerType.toLowerCase().includes(lowercaseQuery);
|
|
277
|
-
|
|
278
|
-
return modelNameMatch || providerNameMatch || providerIdMatch || providerTypeMatch;
|
|
273
|
+
const searchableText = `${model.name} ${model.providerName} ${model.providerId} ${model.providerType}`.toLowerCase();
|
|
274
|
+
return queryWords.every(word => searchableText.includes(word));
|
|
279
275
|
});
|
|
280
276
|
};
|
|
281
277
|
|
|
282
278
|
// Initialize filtered models using the filter function
|
|
283
279
|
let filteredModels = filterModels('');
|
|
284
|
-
|
|
280
|
+
let needsRedraw = true;
|
|
281
|
+
|
|
285
282
|
// Debounce function to reduce filtering frequency
|
|
286
283
|
let searchTimeout;
|
|
287
284
|
const debouncedFilter = (query, callback) => {
|
|
288
285
|
clearTimeout(searchTimeout);
|
|
289
286
|
searchTimeout = setTimeout(() => {
|
|
290
|
-
|
|
287
|
+
filteredModels = filterModels(query);
|
|
288
|
+
needsRedraw = true;
|
|
289
|
+
callback(filteredModels);
|
|
291
290
|
}, 50); // 50ms debounce delay
|
|
292
291
|
};
|
|
293
|
-
|
|
292
|
+
|
|
294
293
|
while (true) {
|
|
295
|
-
//
|
|
296
|
-
let screenContent = '';
|
|
297
|
-
|
|
298
|
-
// Add header
|
|
299
|
-
screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
|
|
300
|
-
screenContent += colorText('=============================', 'cyan') + '\n';
|
|
301
|
-
screenContent += colorText('Note: opencode uses ai-sdk', 'dim') + '\n';
|
|
302
|
-
screenContent += '\n';
|
|
303
|
-
|
|
304
|
-
screenContent += colorText('Select Models for Benchmark', 'magenta') + '\n';
|
|
305
|
-
screenContent += colorText('Use ↑↓ arrows to navigate, TAB to select/deselect, ENTER to run benchmark', 'cyan') + '\n';
|
|
306
|
-
screenContent += colorText('Type to search (real-time filtering)', 'cyan') + '\n';
|
|
307
|
-
screenContent += colorText('Press "A" to select all models, "N" to deselect all', 'cyan') + '\n';
|
|
308
|
-
screenContent += colorText('Circle states: ●=Current+Selected ○=Current+Unselected ●=Selected ○=Unselected', 'dim') + '\n';
|
|
309
|
-
screenContent += colorText('Quick run: ENTER on any model | Multi-select: TAB then ENTER | Recent: R', 'dim') + '\n';
|
|
310
|
-
screenContent += '\n';
|
|
311
|
-
|
|
312
|
-
// Search interface - always visible
|
|
313
|
-
screenContent += colorText('Search: ', 'yellow') + colorText(searchQuery + '_', 'bright') + '\n';
|
|
314
|
-
screenContent += '\n';
|
|
315
|
-
|
|
316
|
-
// Calculate pagination
|
|
294
|
+
// Calculate pagination (needed for key handlers)
|
|
317
295
|
const visibleItemsCount = getVisibleItemsCount(12); // Extra space for search bar
|
|
318
296
|
const totalPages = Math.ceil(filteredModels.length / visibleItemsCount);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
297
|
+
|
|
298
|
+
if (needsRedraw) {
|
|
299
|
+
// Build screen content in memory (double buffering)
|
|
300
|
+
let screenContent = '';
|
|
301
|
+
|
|
302
|
+
// Add header
|
|
303
|
+
screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
|
|
304
|
+
screenContent += colorText('=============================', 'cyan') + '\n';
|
|
305
|
+
screenContent += colorText('Note: opencode uses ai-sdk', 'dim') + '\n';
|
|
306
|
+
screenContent += '\n';
|
|
307
|
+
|
|
308
|
+
screenContent += colorText('Select Models for Benchmark', 'magenta') + '\n';
|
|
309
|
+
screenContent += colorText('Use ↑↓ arrows to navigate, TAB to select/deselect, ENTER to run benchmark', 'cyan') + '\n';
|
|
310
|
+
screenContent += colorText('Type to search (real-time filtering)', 'cyan') + '\n';
|
|
311
|
+
screenContent += colorText('Press "A" to select all models, "N" to deselect all', 'cyan') + '\n';
|
|
312
|
+
screenContent += colorText('Circle states: ●=Current+Selected ○=Current+Unselected ●=Selected ○=Unselected', 'dim') + '\n';
|
|
313
|
+
screenContent += colorText('Quick run: ENTER on any model | Multi-select: TAB then ENTER | Recent: R', 'dim') + '\n';
|
|
314
|
+
screenContent += '\n';
|
|
315
|
+
|
|
316
|
+
// Search interface - always visible
|
|
317
|
+
screenContent += colorText('Search: ', 'yellow') + colorText(searchQuery + '_', 'bright') + '\n';
|
|
318
|
+
screenContent += '\n';
|
|
319
|
+
|
|
320
|
+
// Ensure current page is valid
|
|
321
|
+
if (currentPage >= totalPages) currentPage = totalPages - 1;
|
|
322
|
+
if (currentPage < 0) currentPage = 0;
|
|
323
|
+
|
|
324
|
+
const startIndex = currentPage * visibleItemsCount;
|
|
325
|
+
const endIndex = Math.min(startIndex + visibleItemsCount, filteredModels.length);
|
|
326
|
+
|
|
327
|
+
// Display models in a vertical layout with pagination
|
|
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
|
+
}
|
|
341
342
|
}
|
|
342
343
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
344
|
+
|
|
345
|
+
// Display models with proper section headers
|
|
346
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
347
|
+
const model = filteredModels[i];
|
|
348
|
+
const isCurrent = i === currentIndex;
|
|
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
|
+
}
|
|
373
|
+
|
|
374
|
+
// Single circle that shows both current state and selection
|
|
375
|
+
let circle;
|
|
376
|
+
if (isCurrent && isSelected) {
|
|
377
|
+
circle = colorText('●', 'green'); // Current and selected - filled green
|
|
378
|
+
} else if (isCurrent && !isSelected) {
|
|
379
|
+
circle = colorText('○', 'green'); // Current but not selected - empty green
|
|
380
|
+
} else if (!isCurrent && isSelected) {
|
|
381
|
+
circle = colorText('●', 'cyan'); // Selected but not current - filled cyan
|
|
382
|
+
} else {
|
|
383
|
+
circle = colorText('○', 'dim'); // Not current and not selected - empty dim
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Model name highlighting
|
|
387
|
+
let modelName = isCurrent ? colorText(model.name, 'bright') : colorText(model.name, 'white');
|
|
388
|
+
|
|
389
|
+
// Provider name
|
|
390
|
+
let providerName = isCurrent ? colorText(`(${model.providerName})`, 'cyan') : colorText(`(${model.providerName})`, 'dim');
|
|
391
|
+
|
|
392
|
+
screenContent += `${circle} ${modelName} ${providerName}\n`;
|
|
384
393
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
// Show pagination info
|
|
399
|
-
if (totalPages > 1) {
|
|
400
|
-
const pageInfo = colorText(`Page ${currentPage + 1}/${totalPages}`, 'cyan');
|
|
401
|
-
const navHint = colorText('Use Page Up/Down to navigate pages', 'dim');
|
|
402
|
-
screenContent += `${pageInfo} ${navHint}\n`;
|
|
403
|
-
|
|
404
|
-
if (currentPage < totalPages - 1) {
|
|
405
|
-
screenContent += colorText('↓ More models below', 'dim') + '\n';
|
|
394
|
+
|
|
395
|
+
screenContent += '\n';
|
|
396
|
+
screenContent += colorText(`Selected: ${allModels.filter(m => m.selected).length} models`, 'yellow') + '\n';
|
|
397
|
+
|
|
398
|
+
// Show pagination info
|
|
399
|
+
if (totalPages > 1) {
|
|
400
|
+
const pageInfo = colorText(`Page ${currentPage + 1}/${totalPages}`, 'cyan');
|
|
401
|
+
const navHint = colorText('Use Page Up/Down to navigate pages', 'dim');
|
|
402
|
+
screenContent += `${pageInfo} ${navHint}\n`;
|
|
403
|
+
|
|
404
|
+
if (currentPage < totalPages - 1) {
|
|
405
|
+
screenContent += colorText('↓ More models below', 'dim') + '\n';
|
|
406
|
+
}
|
|
406
407
|
}
|
|
408
|
+
|
|
409
|
+
// Clear screen and output entire buffer at once
|
|
410
|
+
clearScreen();
|
|
411
|
+
console.log(screenContent);
|
|
412
|
+
needsRedraw = false;
|
|
407
413
|
}
|
|
408
414
|
|
|
409
|
-
// Clear screen and output entire buffer at once
|
|
410
|
-
clearScreen();
|
|
411
|
-
console.log(screenContent);
|
|
412
|
-
|
|
413
415
|
const key = await getKeyPress();
|
|
414
416
|
|
|
415
417
|
// Navigation keys - only handle special keys
|
|
@@ -417,56 +419,61 @@ async function selectModelsCircular() {
|
|
|
417
419
|
// Up arrow - circular navigation within current page
|
|
418
420
|
const pageStartIndex = currentPage * visibleItemsCount;
|
|
419
421
|
const pageEndIndex = Math.min(pageStartIndex + visibleItemsCount, filteredModels.length);
|
|
420
|
-
|
|
422
|
+
|
|
421
423
|
if (currentIndex <= pageStartIndex) {
|
|
422
424
|
currentIndex = pageEndIndex - 1;
|
|
423
425
|
} else {
|
|
424
426
|
currentIndex--;
|
|
425
427
|
}
|
|
428
|
+
needsRedraw = true;
|
|
426
429
|
} else if (key === '\u001b[B') {
|
|
427
430
|
// Down arrow - circular navigation within current page
|
|
428
431
|
const pageStartIndex = currentPage * visibleItemsCount;
|
|
429
432
|
const pageEndIndex = Math.min(pageStartIndex + visibleItemsCount, filteredModels.length);
|
|
430
|
-
|
|
433
|
+
|
|
431
434
|
if (currentIndex >= pageEndIndex - 1) {
|
|
432
435
|
currentIndex = pageStartIndex;
|
|
433
436
|
} else {
|
|
434
437
|
currentIndex++;
|
|
435
438
|
}
|
|
439
|
+
needsRedraw = true;
|
|
436
440
|
} else if (key === '\u001b[5~') {
|
|
437
441
|
// Page Up
|
|
438
442
|
if (currentPage > 0) {
|
|
439
443
|
currentPage--;
|
|
440
444
|
currentIndex = currentPage * visibleItemsCount;
|
|
445
|
+
needsRedraw = true;
|
|
441
446
|
}
|
|
442
447
|
} else if (key === '\u001b[6~') {
|
|
443
448
|
// Page Down
|
|
444
449
|
if (currentPage < totalPages - 1) {
|
|
445
450
|
currentPage++;
|
|
446
451
|
currentIndex = currentPage * visibleItemsCount;
|
|
452
|
+
needsRedraw = true;
|
|
447
453
|
}
|
|
448
454
|
} else if (key === '\t') {
|
|
449
455
|
// Tab - select/deselect current model
|
|
450
456
|
const currentModel = filteredModels[currentIndex];
|
|
451
457
|
let actualModelIndex;
|
|
452
|
-
|
|
458
|
+
|
|
453
459
|
if (currentModel.isRecent) {
|
|
454
460
|
// For recent models, find by matching the original model ID and provider name
|
|
455
|
-
actualModelIndex = allModels.findIndex(model =>
|
|
456
|
-
model.id === currentModel.id &&
|
|
461
|
+
actualModelIndex = allModels.findIndex(model =>
|
|
462
|
+
model.id === currentModel.id &&
|
|
457
463
|
model.providerName === currentModel.providerName &&
|
|
458
464
|
!model.isRecent // Don't match the recent copy, match the original
|
|
459
465
|
);
|
|
460
466
|
} else {
|
|
461
467
|
// For regular models, use the standard matching
|
|
462
|
-
actualModelIndex = allModels.findIndex(model =>
|
|
468
|
+
actualModelIndex = allModels.findIndex(model =>
|
|
463
469
|
model.id === currentModel.id && model.providerName === currentModel.providerName
|
|
464
470
|
);
|
|
465
471
|
}
|
|
466
|
-
|
|
472
|
+
|
|
467
473
|
if (actualModelIndex !== -1) {
|
|
468
474
|
allModels[actualModelIndex].selected = !allModels[actualModelIndex].selected;
|
|
469
475
|
}
|
|
476
|
+
needsRedraw = true;
|
|
470
477
|
// Force immediate screen redraw by continuing to next iteration
|
|
471
478
|
continue;
|
|
472
479
|
} else if (key === '\r') {
|
|
@@ -508,6 +515,7 @@ async function selectModelsCircular() {
|
|
|
508
515
|
allModels[actualModelIndex].selected = true;
|
|
509
516
|
}
|
|
510
517
|
});
|
|
518
|
+
needsRedraw = true;
|
|
511
519
|
} else {
|
|
512
520
|
// If search is active, add 'A' to search query
|
|
513
521
|
searchQuery += key;
|
|
@@ -526,6 +534,7 @@ async function selectModelsCircular() {
|
|
|
526
534
|
allModels[actualModelIndex].selected = false;
|
|
527
535
|
}
|
|
528
536
|
});
|
|
537
|
+
needsRedraw = true;
|
|
529
538
|
} else {
|
|
530
539
|
// If search is active, add 'N' to search query
|
|
531
540
|
searchQuery += key;
|
|
@@ -540,11 +549,11 @@ async function selectModelsCircular() {
|
|
|
540
549
|
if (searchQuery.length === 0 && recentModelObjects.length > 0) {
|
|
541
550
|
// Deselect all models first
|
|
542
551
|
allModels.forEach(model => model.selected = false);
|
|
543
|
-
|
|
552
|
+
|
|
544
553
|
// Select all recent models by finding the original models
|
|
545
554
|
recentModelObjects.forEach(recentModel => {
|
|
546
|
-
const actualModelIndex = allModels.findIndex(model =>
|
|
547
|
-
model.id === recentModel.id &&
|
|
555
|
+
const actualModelIndex = allModels.findIndex(model =>
|
|
556
|
+
model.id === recentModel.id &&
|
|
548
557
|
model.providerName === recentModel.providerName &&
|
|
549
558
|
!model.isRecent // Match the original, not the recent copy
|
|
550
559
|
);
|
|
@@ -552,7 +561,8 @@ async function selectModelsCircular() {
|
|
|
552
561
|
allModels[actualModelIndex].selected = true;
|
|
553
562
|
}
|
|
554
563
|
});
|
|
555
|
-
|
|
564
|
+
|
|
565
|
+
needsRedraw = true;
|
|
556
566
|
// Break out of loop to run benchmark
|
|
557
567
|
break;
|
|
558
568
|
} else {
|