ai-speedometer 1.2.0 → 1.2.2

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
@@ -25,12 +25,16 @@ npm install -g ai-speedometer
25
25
  ```
26
26
 
27
27
  2. **Choose Model Provider**
28
- - Verified providers (OpenAI, Anthropic, Google) - auto-configured
29
- - Custom providers (Ollama, local models) - add your base URL
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 lowercaseQuery = query.toLowerCase();
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 modelNameMatch = model.name.toLowerCase().includes(lowercaseQuery);
274
- const providerNameMatch = model.providerName.toLowerCase().includes(lowercaseQuery);
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
- callback(filterModels(query));
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
- // Build screen content in memory (double buffering)
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
- // 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;
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
- // 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
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
- // 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`;
393
- }
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';
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 {