ai-speedometer 1.4.2 → 2.0.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/cli.js DELETED
@@ -1,2460 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import readline from 'readline';
4
- import fs from 'fs';
5
- import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
6
- import { createAnthropic } from '@ai-sdk/anthropic';
7
- import { streamText } from 'ai'; // Changed from streamText to generateText
8
- import { testPrompt } from './test-prompt.js';
9
- import { getAllProviders, searchProviders, getModelsForProvider } from './models-dev.js';
10
- import {
11
- getAllAvailableProviders,
12
- addApiKey,
13
- getCustomProviders,
14
- migrateFromOldConfig,
15
- getDebugInfo
16
- } from './opencode-integration.js';
17
- import {
18
- readAIConfig,
19
- getCustomProvidersFromConfig,
20
- getVerifiedProvidersFromConfig,
21
- addCustomProvider,
22
- addModelToCustomProvider,
23
- getAIConfigDebugPaths,
24
- addToRecentModels,
25
- getRecentModels,
26
- cleanupRecentModelsFromConfig
27
- } from './ai-config.js';
28
- import 'dotenv/config';
29
- import Table from 'cli-table3';
30
-
31
- // Parse command line arguments
32
- function parseCliArgs() {
33
- const args = process.argv.slice(2);
34
- const parsed = {
35
- debug: false,
36
- bench: null,
37
- benchCustom: null,
38
- apiKey: null,
39
- baseUrl: null,
40
- endpointFormat: null,
41
- useAiSdk: false,
42
- formatted: false,
43
- help: false
44
- };
45
-
46
- for (let i = 0; i < args.length; i++) {
47
- const arg = args[i];
48
-
49
- if (arg === '--debug') {
50
- parsed.debug = true;
51
- } else if (arg === '--bench') {
52
- parsed.bench = args[++i];
53
- } else if (arg === '--bench-custom') {
54
- parsed.benchCustom = args[++i];
55
- } else if (arg === '--api-key') {
56
- parsed.apiKey = args[++i];
57
- } else if (arg === '--base-url') {
58
- parsed.baseUrl = args[++i];
59
- } else if (arg === '--endpoint-format') {
60
- parsed.endpointFormat = args[++i];
61
- } else if (arg === '--ai-sdk') {
62
- parsed.useAiSdk = true;
63
- } else if (arg === '--formatted') {
64
- parsed.formatted = true;
65
- } else if (arg === '--help' || arg === '-h') {
66
- parsed.help = true;
67
- }
68
- }
69
-
70
- return parsed;
71
- }
72
-
73
- function showHelp() {
74
- console.log(colorText('ai-speedometer - Benchmark AI models', 'cyan'));
75
- console.log('');
76
- console.log(colorText('Usage:', 'yellow'));
77
- console.log(' ai-speedometer ' + colorText('# Interactive mode', 'dim'));
78
- console.log(' ai-speedometer --bench <provider:model> ' + colorText('# Headless benchmark', 'dim'));
79
- console.log(' ai-speedometer --bench-custom <provider:model> ' + colorText('# Custom provider benchmark', 'dim'));
80
- console.log('');
81
- console.log(colorText('Options:', 'yellow'));
82
- console.log(' --bench <provider:model> ' + colorText('Run benchmark in headless mode', 'dim'));
83
- console.log(' --bench-custom <provider:model> ' + colorText('Run custom provider benchmark', 'dim'));
84
- console.log(' --base-url <url> ' + colorText('Base URL for custom provider', 'dim'));
85
- console.log(' --api-key <key> ' + colorText('API key for custom provider', 'dim'));
86
- console.log(' --endpoint-format <format> ' + colorText('Endpoint format (default: chat/completions)', 'dim'));
87
- console.log(' --ai-sdk ' + colorText('Use AI SDK instead of REST API', 'dim'));
88
- console.log(' --formatted ' + colorText('Format JSON output for human readability', 'dim'));
89
- console.log(' --debug ' + colorText('Enable debug logging', 'dim'));
90
- console.log(' --help, -h ' + colorText('Show this help message', 'dim'));
91
- console.log('');
92
- console.log(colorText('Examples:', 'yellow'));
93
- console.log(' ai-speedometer --bench openai:gpt-4');
94
- console.log(' ai-speedometer --bench anthropic:claude-3-opus --api-key "sk-..."');
95
- console.log(' ai-speedometer --bench-custom openai:gpt-4 --base-url "https://api.openai.com/v1" --api-key "sk-..."');
96
- console.log(' ai-speedometer --bench-custom anthropic:claude --base-url "https://api.anthropic.com/v1" --api-key "sk-..." --endpoint-format "messages"');
97
- console.log('');
98
- }
99
-
100
- // Parse provider:model format, handling colons in model IDs
101
- function parseProviderModel(arg) {
102
- const firstColonIndex = arg.indexOf(':');
103
- if (firstColonIndex === -1) {
104
- throw new Error(`Invalid format. Use provider:model (e.g., openai:gpt-4)`);
105
- }
106
-
107
- const provider = arg.substring(0, firstColonIndex);
108
- const model = arg.substring(firstColonIndex + 1);
109
-
110
- return { provider, model };
111
- }
112
-
113
- // Create temporary custom provider from CLI args
114
- function createCustomProviderFromCli(cliArgs) {
115
- const { provider, model } = parseProviderModel(cliArgs.benchCustom);
116
-
117
- // Validate required arguments
118
- if (!cliArgs.baseUrl) {
119
- throw new Error('--base-url is required for custom provider benchmarking');
120
- }
121
- if (!cliArgs.apiKey) {
122
- throw new Error('--api-key is required for custom provider benchmarking');
123
- }
124
-
125
- const endpointFormat = cliArgs.endpointFormat || 'chat/completions';
126
-
127
- return {
128
- id: provider,
129
- name: provider,
130
- type: 'openai-compatible', // Default to OpenAI compatible for custom providers
131
- baseUrl: cliArgs.baseUrl,
132
- apiKey: cliArgs.apiKey,
133
- endpointFormat: endpointFormat,
134
- models: [{ name: model, id: model }]
135
- };
136
- }
137
-
138
- const cliArgs = parseCliArgs();
139
- const debugMode = cliArgs.debug;
140
- let logFile = null;
141
-
142
- function log(message) {
143
- if (!debugMode) return;
144
-
145
- const timestamp = new Date().toISOString();
146
- const logMessage = `[${timestamp}] ${message}\n`;
147
-
148
- if (!logFile) {
149
- logFile = fs.createWriteStream('debug.log', { flags: 'w' });
150
- }
151
-
152
- logFile.write(logMessage);
153
- // Only print to console for important messages
154
- if (message.includes('ERROR') || message.includes('Creating') || message.includes('API Request')) {
155
- console.log(logMessage.trim());
156
- }
157
- }
158
-
159
- // Create a custom Anthropic provider function that supports baseUrl
160
- function createAnthropicProvider(baseUrl, apiKey) {
161
- // Use createAnthropic instead of default anthropic for custom baseURL support
162
- log(`Creating Anthropic provider with baseUrl: ${baseUrl}`);
163
- log(`API Key length: ${apiKey ? apiKey.length : 0}`);
164
-
165
- // Use baseUrl as provided - no automatic normalization needed
166
-
167
- // Try with baseURL parameter (correct according to docs)
168
- const provider = createAnthropic({
169
- apiKey: apiKey,
170
- baseURL: baseUrl,
171
- // Add minimal fetch logging for debugging
172
- fetch: debugMode ? async (input, init) => {
173
- log(`API Request to: ${input}`);
174
- const response = await fetch(input, init);
175
- log(`Response status: ${response.status}`);
176
- return response;
177
- } : undefined
178
- });
179
-
180
- log(`Provider created successfully: ${provider ? 'yes' : 'no'}`);
181
- return provider;
182
- }
183
-
184
- const rl = readline.createInterface({
185
- input: process.stdin,
186
- output: process.stdout
187
- });
188
-
189
- // ANSI color codes
190
- const colors = {
191
- reset: '\x1b[0m',
192
- red: '\x1b[31m',
193
- green: '\x1b[32m',
194
- yellow: '\x1b[33m',
195
- blue: '\x1b[34m',
196
- magenta: '\x1b[35m',
197
- cyan: '\x1b[36m',
198
- white: '\x1b[37m',
199
- bright: '\x1b[1m',
200
- dim: '\x1b[2m',
201
- bgRed: '\x1b[41m',
202
- bgGreen: '\x1b[42m',
203
- bgYellow: '\x1b[43m',
204
- bgBlue: '\x1b[44m',
205
- bgMagenta: '\x1b[45m',
206
- bgCyan: '\x1b[46m',
207
- bgWhite: '\x1b[47m'
208
- };
209
-
210
- function colorText(text, color) {
211
- return `${colors[color]}${text}${colors.reset}`;
212
- }
213
-
214
- // Enable raw mode for keyboard input (if available)
215
- try {
216
- process.stdin.setRawMode(true);
217
- process.stdin.resume();
218
- process.stdin.setEncoding('utf8');
219
- } catch (e) {
220
- // Fallback for environments without raw mode
221
- }
222
-
223
- function question(query) {
224
- return new Promise(resolve => rl.question(query, resolve));
225
- }
226
-
227
- function clearScreen() {
228
- console.clear();
229
- }
230
-
231
- function showHeader() {
232
- console.log(colorText('Ai-speedometer', 'cyan'));
233
- console.log(colorText('=============================', 'cyan'));
234
- console.log('');
235
- }
236
-
237
- // Configuration management - now using ai-benchmark-config.json for custom providers
238
- async function loadConfig(includeAllProviders = false) {
239
- try {
240
- // Check if we need to migrate from old config
241
- const oldConfigFile = 'ai-benchmark-config.json';
242
- if (fs.existsSync(oldConfigFile)) {
243
- console.log(colorText('Migrating from old config format to new format...', 'yellow'));
244
-
245
- try {
246
- const data = fs.readFileSync(oldConfigFile, 'utf8');
247
- const oldConfig = JSON.parse(data);
248
-
249
- const migrationResults = await migrateFromOldConfig(oldConfig);
250
-
251
- console.log(colorText(`Migration complete: ${migrationResults.migrated} items migrated`, 'green'));
252
- if (migrationResults.failed > 0) {
253
- console.log(colorText(`Migration warnings: ${migrationResults.failed} items failed`, 'yellow'));
254
- migrationResults.errors.forEach(error => {
255
- console.log(colorText(` - ${error}`, 'dim'));
256
- });
257
- }
258
-
259
- // Backup old config
260
- fs.renameSync(oldConfigFile, `${oldConfigFile}.backup`);
261
- console.log(colorText('Old config backed up as ai-benchmark-config.json.backup', 'cyan'));
262
-
263
- await question(colorText('Press Enter to continue...', 'yellow'));
264
- } catch (error) {
265
- console.log(colorText('Migration failed: ', 'red') + error.message);
266
- await question(colorText('Press Enter to continue with empty config...', 'yellow'));
267
- }
268
- }
269
-
270
- // Load providers from both auth.json (verified) and ai-benchmark-config.json (custom)
271
- // Include all models.dev providers if includeAllProviders is true (for headless mode)
272
- const providers = await getAllAvailableProviders(includeAllProviders);
273
-
274
- return {
275
- providers,
276
- verifiedProviders: {} // Keep for compatibility but no longer used
277
- };
278
- } catch (error) {
279
- console.log(colorText('Error loading config, starting fresh: ', 'yellow') + error.message);
280
- return { providers: [], verifiedProviders: {} };
281
- }
282
- }
283
-
284
- // Save config - now using ai-benchmark-config.json and auth.json
285
- async function saveConfig(config) {
286
- // Note: This function is kept for compatibility but the actual saving
287
- // is handled by the ai-config.js and opencode-integration.js functions
288
- console.log(colorText('Note: Configuration is now automatically saved to ai-benchmark-config.json and auth.json', 'cyan'));
289
- }
290
-
291
- // Keyboard input handling
292
- function getKeyPress() {
293
- return new Promise(resolve => {
294
- if (process.stdin.isRaw) {
295
- process.stdin.once('data', key => {
296
- if (key === '\u0003') {
297
- process.exit(0);
298
- }
299
- resolve(key);
300
- });
301
- } else {
302
- rl.question(colorText('Press Enter to continue...', 'yellow'), () => {
303
- resolve('\r');
304
- });
305
- }
306
- });
307
- }
308
-
309
- // Circular model selection with arrow keys and pagination
310
- async function selectModelsCircular() {
311
- clearScreen();
312
- showHeader();
313
- console.log(colorText('Select Models for Benchmark', 'magenta'));
314
- console.log('');
315
-
316
- const config = await loadConfig();
317
-
318
- // Clean up recent models from main config and migrate to cache
319
- await cleanupRecentModelsFromConfig();
320
-
321
- if (config.providers.length === 0) {
322
- console.log(colorText('No providers available. Please add a provider first.', 'red'));
323
- await question(colorText('Press Enter to continue...', 'yellow'));
324
- return [];
325
- }
326
-
327
- const allModels = [];
328
- config.providers.forEach(provider => {
329
- provider.models.forEach(model => {
330
- allModels.push({
331
- ...model,
332
- providerName: provider.name,
333
- providerType: provider.type,
334
- providerId: provider.id,
335
- providerConfig: {
336
- ...provider,
337
- apiKey: provider.apiKey || '',
338
- baseUrl: provider.baseUrl || ''
339
- },
340
- selected: false
341
- });
342
- });
343
- });
344
-
345
- // Load recent models
346
- const recentModelsData = await getRecentModels();
347
-
348
- // Create a mapping of recent models to actual model objects
349
- const recentModelObjects = [];
350
- recentModelsData.forEach(recentModel => {
351
- const modelObj = allModels.find(model =>
352
- model.id === recentModel.modelId &&
353
- model.providerName === recentModel.providerName
354
- );
355
- if (modelObj) {
356
- recentModelObjects.push({
357
- ...modelObj,
358
- isRecent: true
359
- });
360
- }
361
- });
362
-
363
- let currentIndex = 0;
364
- let currentPage = 0;
365
- let searchQuery = '';
366
-
367
- // Create a reusable filter function to avoid code duplication
368
- const filterModels = (query) => {
369
- if (!query.trim()) {
370
- // When search is empty, return the combined list with recent models at top
371
- const recentModelIds = new Set(recentModelObjects.map(m => m.id));
372
- const nonRecentModels = allModels.filter(model => !recentModelIds.has(model.id));
373
- return [...recentModelObjects, ...nonRecentModels];
374
- }
375
-
376
- // When searching, search through all models (no recent section) with fuzzy matching
377
- const queryWords = query.toLowerCase().split(/\s+/).filter(word => word.length > 0);
378
- return allModels.filter(model => {
379
- const searchableText = `${model.name} ${model.providerName} ${model.providerId} ${model.providerType}`.toLowerCase();
380
- return queryWords.every(word => searchableText.includes(word));
381
- });
382
- };
383
-
384
- // Initialize filtered models using the filter function
385
- let filteredModels = filterModels('');
386
- let needsRedraw = true;
387
-
388
- // Debounce function to reduce filtering frequency
389
- let searchTimeout;
390
- const debouncedFilter = (query, callback) => {
391
- clearTimeout(searchTimeout);
392
- searchTimeout = setTimeout(() => {
393
- filteredModels = filterModels(query);
394
- needsRedraw = true;
395
- callback(filteredModels);
396
- }, 50); // 50ms debounce delay
397
- };
398
-
399
- while (true) {
400
- // Calculate pagination (needed for key handlers)
401
- const visibleItemsCount = getVisibleItemsCount(12); // Extra space for search bar
402
- const totalPages = Math.ceil(filteredModels.length / visibleItemsCount);
403
-
404
- if (needsRedraw) {
405
- // Build screen content in memory (double buffering)
406
- let screenContent = '';
407
-
408
- // Add header
409
- screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
410
- screenContent += colorText('=============================', 'cyan') + '\n';
411
- screenContent += '\n';
412
-
413
- screenContent += colorText('Select Models for Benchmark', 'magenta') + '\n';
414
- screenContent += colorText('Use ↑↓ arrows to navigate, TAB to select/deselect, ENTER to run benchmark', 'cyan') + '\n';
415
- screenContent += colorText('Type to search (real-time filtering)', 'cyan') + '\n';
416
- screenContent += colorText('Press "A" to select all models, "N" to deselect all', 'cyan') + '\n';
417
- screenContent += colorText('Circle states: ●=Current+Selected ○=Current+Unselected ●=Selected ○=Unselected', 'dim') + '\n';
418
- screenContent += colorText('Quick run: ENTER on any model | Multi-select: TAB then ENTER | Recent: R', 'dim') + '\n';
419
- screenContent += '\n';
420
-
421
- // Search interface - always visible
422
- screenContent += colorText('Search: ', 'yellow') + colorText(searchQuery + '_', 'bright') + '\n';
423
- screenContent += '\n';
424
-
425
- // Ensure current page is valid
426
- if (currentPage >= totalPages) currentPage = totalPages - 1;
427
- if (currentPage < 0) currentPage = 0;
428
-
429
- const startIndex = currentPage * visibleItemsCount;
430
- const endIndex = Math.min(startIndex + visibleItemsCount, filteredModels.length);
431
-
432
- // Display models in a vertical layout with pagination
433
- let hasRecentModelsInCurrentPage = false;
434
- let recentSectionDisplayed = false;
435
- let nonRecentSectionDisplayed = false;
436
-
437
- // Only show recent section when search is empty and we have recent models
438
- const showRecentSection = searchQuery.length === 0 && recentModelObjects.length > 0;
439
-
440
- // Check if current page contains any recent models (only when search is empty)
441
- if (showRecentSection) {
442
- for (let i = startIndex; i < endIndex; i++) {
443
- if (filteredModels[i].isRecent) {
444
- hasRecentModelsInCurrentPage = true;
445
- break;
446
- }
447
- }
448
- }
449
-
450
- // Display models with proper section headers
451
- for (let i = startIndex; i < endIndex; i++) {
452
- const model = filteredModels[i];
453
- const isCurrent = i === currentIndex;
454
- // For recent models, check selection state from the original model
455
- let isSelected;
456
- if (model.isRecent) {
457
- const originalModelIndex = allModels.findIndex(originalModel =>
458
- originalModel.id === model.id &&
459
- originalModel.providerName === model.providerName &&
460
- !originalModel.isRecent
461
- );
462
- isSelected = originalModelIndex !== -1 ? allModels[originalModelIndex].selected : false;
463
- } else {
464
- isSelected = model.selected;
465
- }
466
-
467
- // Show recent section header if we encounter a recent model and haven't shown the header yet
468
- if (model.isRecent && !recentSectionDisplayed && hasRecentModelsInCurrentPage && showRecentSection) {
469
- screenContent += colorText('-------recent--------', 'dim') + '\n';
470
- recentSectionDisplayed = true;
471
- }
472
-
473
- // Show separator between recent and non-recent models
474
- if (!model.isRecent && recentSectionDisplayed && !nonRecentSectionDisplayed && showRecentSection) {
475
- screenContent += colorText('-------recent--------', 'dim') + '\n';
476
- nonRecentSectionDisplayed = true;
477
- }
478
-
479
- // Single circle that shows both current state and selection
480
- let circle;
481
- if (isCurrent && isSelected) {
482
- circle = colorText('●', 'green'); // Current and selected - filled green
483
- } else if (isCurrent && !isSelected) {
484
- circle = colorText('○', 'green'); // Current but not selected - empty green
485
- } else if (!isCurrent && isSelected) {
486
- circle = colorText('●', 'cyan'); // Selected but not current - filled cyan
487
- } else {
488
- circle = colorText('○', 'dim'); // Not current and not selected - empty dim
489
- }
490
-
491
- // Model name highlighting
492
- let modelName = isCurrent ? colorText(model.name, 'bright') : colorText(model.name, 'white');
493
-
494
- // Provider name
495
- let providerName = isCurrent ? colorText(`(${model.providerName})`, 'cyan') : colorText(`(${model.providerName})`, 'dim');
496
-
497
- screenContent += `${circle} ${modelName} ${providerName}\n`;
498
- }
499
-
500
- screenContent += '\n';
501
- screenContent += colorText(`Selected: ${allModels.filter(m => m.selected).length} models`, 'yellow') + '\n';
502
-
503
- // Show pagination info
504
- if (totalPages > 1) {
505
- const pageInfo = colorText(`Page ${currentPage + 1}/${totalPages}`, 'cyan');
506
- const navHint = colorText('Use Page Up/Down to navigate pages', 'dim');
507
- screenContent += `${pageInfo} ${navHint}\n`;
508
-
509
- if (currentPage < totalPages - 1) {
510
- screenContent += colorText('↓ More models below', 'dim') + '\n';
511
- }
512
- }
513
-
514
- // Clear screen and output entire buffer at once
515
- clearScreen();
516
- console.log(screenContent);
517
- needsRedraw = false;
518
- }
519
-
520
- const key = await getKeyPress();
521
-
522
- // Navigation keys - only handle special keys
523
- if (key === '\u001b[A') {
524
- // Up arrow - circular navigation within current page
525
- const pageStartIndex = currentPage * visibleItemsCount;
526
- const pageEndIndex = Math.min(pageStartIndex + visibleItemsCount, filteredModels.length);
527
-
528
- if (currentIndex <= pageStartIndex) {
529
- currentIndex = pageEndIndex - 1;
530
- } else {
531
- currentIndex--;
532
- }
533
- needsRedraw = true;
534
- } else if (key === '\u001b[B') {
535
- // Down arrow - circular navigation within current page
536
- const pageStartIndex = currentPage * visibleItemsCount;
537
- const pageEndIndex = Math.min(pageStartIndex + visibleItemsCount, filteredModels.length);
538
-
539
- if (currentIndex >= pageEndIndex - 1) {
540
- currentIndex = pageStartIndex;
541
- } else {
542
- currentIndex++;
543
- }
544
- needsRedraw = true;
545
- } else if (key === '\u001b[5~') {
546
- // Page Up
547
- if (currentPage > 0) {
548
- currentPage--;
549
- currentIndex = currentPage * visibleItemsCount;
550
- needsRedraw = true;
551
- }
552
- } else if (key === '\u001b[6~') {
553
- // Page Down
554
- if (currentPage < totalPages - 1) {
555
- currentPage++;
556
- currentIndex = currentPage * visibleItemsCount;
557
- needsRedraw = true;
558
- }
559
- } else if (key === '\t') {
560
- // Tab - select/deselect current model
561
- const currentModel = filteredModels[currentIndex];
562
- let actualModelIndex;
563
-
564
- if (currentModel.isRecent) {
565
- // For recent models, find by matching the original model ID and provider name
566
- actualModelIndex = allModels.findIndex(model =>
567
- model.id === currentModel.id &&
568
- model.providerName === currentModel.providerName &&
569
- !model.isRecent // Don't match the recent copy, match the original
570
- );
571
- } else {
572
- // For regular models, use the standard matching
573
- actualModelIndex = allModels.findIndex(model =>
574
- model.id === currentModel.id && model.providerName === currentModel.providerName
575
- );
576
- }
577
-
578
- if (actualModelIndex !== -1) {
579
- allModels[actualModelIndex].selected = !allModels[actualModelIndex].selected;
580
- }
581
- needsRedraw = true;
582
- // Force immediate screen redraw by continuing to next iteration
583
- continue;
584
- } else if (key === '\r') {
585
- // Enter - run benchmark on selected models
586
- const currentModel = filteredModels[currentIndex];
587
- if (currentModel) {
588
- // Check if any models are already selected
589
- const hasSelectedModels = allModels.some(model => model.selected);
590
-
591
- if (!hasSelectedModels) {
592
- // If no models are selected, select just the current model (quick single model)
593
- const actualModelIndex = allModels.indexOf(currentModel);
594
- if (actualModelIndex !== -1) {
595
- allModels[actualModelIndex].selected = true;
596
- }
597
- }
598
- // If models are already selected, keep them as is and run benchmark
599
- break;
600
- }
601
- } else if (key === '\u0003') {
602
- // Ctrl+C
603
- process.exit(0);
604
- } else if (key === '\b' || key === '\x7f') {
605
- // Backspace - delete character from search
606
- if (searchQuery.length > 0) {
607
- searchQuery = searchQuery.slice(0, -1);
608
- debouncedFilter(searchQuery, (newFilteredModels) => {
609
- filteredModels = newFilteredModels;
610
- currentIndex = 0;
611
- currentPage = 0;
612
- });
613
- }
614
- } else if (key === 'A') {
615
- // Select all models - only when search is empty and Shift+A is pressed
616
- if (searchQuery.length === 0) {
617
- filteredModels.forEach(model => {
618
- const actualModelIndex = allModels.indexOf(model);
619
- if (actualModelIndex !== -1) {
620
- allModels[actualModelIndex].selected = true;
621
- }
622
- });
623
- needsRedraw = true;
624
- } else {
625
- // If search is active, add 'A' to search query
626
- searchQuery += key;
627
- debouncedFilter(searchQuery, (newFilteredModels) => {
628
- filteredModels = newFilteredModels;
629
- currentIndex = 0;
630
- currentPage = 0;
631
- });
632
- }
633
- } else if (key === 'N') {
634
- // Deselect all models (None) - only when search is empty and Shift+N is pressed
635
- if (searchQuery.length === 0) {
636
- filteredModels.forEach(model => {
637
- const actualModelIndex = allModels.indexOf(model);
638
- if (actualModelIndex !== -1) {
639
- allModels[actualModelIndex].selected = false;
640
- }
641
- });
642
- needsRedraw = true;
643
- } else {
644
- // If search is active, add 'N' to search query
645
- searchQuery += key;
646
- debouncedFilter(searchQuery, (newFilteredModels) => {
647
- filteredModels = newFilteredModels;
648
- currentIndex = 0;
649
- currentPage = 0;
650
- });
651
- }
652
- } else if (key === 'R' || key === 'r') {
653
- // Run recent models - only when search is empty and we have recent models
654
- if (searchQuery.length === 0 && recentModelObjects.length > 0) {
655
- // Deselect all models first
656
- allModels.forEach(model => model.selected = false);
657
-
658
- // Select all recent models by finding the original models
659
- recentModelObjects.forEach(recentModel => {
660
- const actualModelIndex = allModels.findIndex(model =>
661
- model.id === recentModel.id &&
662
- model.providerName === recentModel.providerName &&
663
- !model.isRecent // Match the original, not the recent copy
664
- );
665
- if (actualModelIndex !== -1) {
666
- allModels[actualModelIndex].selected = true;
667
- }
668
- });
669
-
670
- needsRedraw = true;
671
- // Break out of loop to run benchmark
672
- break;
673
- } else {
674
- // If search is active or no recent models, add 'R' to search query
675
- searchQuery += key;
676
- debouncedFilter(searchQuery, (newFilteredModels) => {
677
- filteredModels = newFilteredModels;
678
- currentIndex = 0;
679
- currentPage = 0;
680
- });
681
- }
682
- } else if (key === 'a' || key === 'n') {
683
- // Lowercase 'a' and 'n' go to search field (not select all/none)
684
- searchQuery += key;
685
- debouncedFilter(searchQuery, (newFilteredModels) => {
686
- filteredModels = newFilteredModels;
687
- currentIndex = 0;
688
- currentPage = 0;
689
- });
690
- } else if (key === ' ' || key.length === 1) {
691
- // Spacebar or regular character - add to search query
692
- searchQuery += key;
693
- debouncedFilter(searchQuery, (newFilteredModels) => {
694
- filteredModels = newFilteredModels;
695
- currentIndex = 0;
696
- currentPage = 0;
697
- });
698
- }
699
- }
700
-
701
- return allModels.filter(m => m.selected);
702
- }
703
-
704
- // Enhanced benchmark with streaming (run in parallel)
705
- async function runStreamingBenchmark(models) {
706
- if (models.length === 0) {
707
- console.log(colorText('No models selected for benchmarking.', 'red'));
708
- return;
709
- }
710
-
711
- clearScreen();
712
- showHeader();
713
- console.log(colorText('Running Benchmark...', 'green'));
714
- console.log(colorText(`Running ${models.length} models in parallel...`, 'cyan'));
715
- console.log('');
716
-
717
- // Create a function to benchmark a single model
718
- const benchmarkModel = async (model) => {
719
- console.log(colorText(`Testing ${model.name} (${model.providerName})...`, 'yellow'));
720
-
721
- try {
722
- let firstTokenTime = null;
723
- let tokenCount = 0;
724
- let startTime = Date.now();
725
-
726
- log(`Model provider type: ${model.providerType}`);
727
- log(`Model provider config: ${JSON.stringify(model.providerConfig, null, 2)}`);
728
-
729
- // Validate required configuration
730
- if (!model.providerConfig || !model.providerConfig.apiKey) {
731
- throw new Error(`Missing API key for provider ${model.providerName}`);
732
- }
733
-
734
- if (!model.providerConfig.baseUrl) {
735
- throw new Error(`Missing base URL for provider ${model.providerName}`);
736
- }
737
-
738
- log(`Model provider config baseUrl: ${model.providerConfig.baseUrl}`);
739
- log(`Model provider config apiKey: ${model.providerConfig.apiKey ? '***' + model.providerConfig.apiKey.slice(-4) : 'missing'}`);
740
-
741
- // Extract the actual model ID for API calls
742
- let actualModelId = model.name;
743
- if (model.id && model.id.includes('_')) {
744
- // For models with provider prefix, extract the actual model ID
745
- actualModelId = model.id.split('_')[1];
746
- log(`Using extracted model ID: ${actualModelId}`);
747
- }
748
-
749
- // Trim any trailing spaces from model names
750
- actualModelId = actualModelId.trim();
751
- log(`Using final model ID: "${actualModelId}"`);
752
-
753
-
754
-
755
- let modelConfig;
756
- if (model.providerType === 'openai-compatible') {
757
- modelConfig = {
758
- model: createOpenAICompatible({
759
- name: model.providerName,
760
- apiKey: model.providerConfig.apiKey,
761
- baseURL: model.providerConfig.baseUrl,
762
- })(actualModelId),
763
- system: "", // Remove system prompt for leaner API calls
764
- };
765
- } else if (model.providerType === 'anthropic') {
766
- modelConfig = {
767
- model: createAnthropicProvider(model.providerConfig.baseUrl, model.providerConfig.apiKey)(actualModelId),
768
- system: "", // Remove system prompt for leaner API calls
769
- };
770
- } else if (model.providerType === 'google') {
771
- // For Google providers, we need to import and use the Google SDK
772
- const { createGoogleGenerativeAI } = await import('@ai-sdk/google');
773
- const googleProvider = createGoogleGenerativeAI({
774
- apiKey: model.providerConfig.apiKey,
775
- baseURL: model.providerConfig.baseUrl,
776
- });
777
- modelConfig = {
778
- model: googleProvider(actualModelId),
779
- system: "", // Remove system prompt for leaner API calls
780
- };
781
- } else {
782
- throw new Error(`Unsupported provider type: ${model.providerType}`);
783
- }
784
-
785
- const result = streamText({
786
- ...modelConfig,
787
- prompt: testPrompt,
788
- maxTokens: 500,
789
- onChunk: ({ chunk }) => {
790
- if (!firstTokenTime && chunk.type === 'text-delta') {
791
- firstTokenTime = Date.now();
792
- }
793
- if (chunk.type === 'text-delta') {
794
- tokenCount++;
795
- }
796
- },
797
- });
798
-
799
- // Consume the stream and count tokens manually
800
- let fullText = '';
801
- try {
802
- for await (const textPart of result.textStream) {
803
- fullText += textPart;
804
- // Manual token count estimation as fallback
805
- tokenCount = Math.round(fullText.length / 4); // Rough estimate
806
- }
807
- log(`Stream completed successfully. Total tokens: ${tokenCount}`);
808
- log(`Full text length: ${fullText.length} characters`);
809
- } catch (error) {
810
- log(`Stream error: ${error.message}`);
811
- log(`Error stack: ${error.stack}`);
812
- throw error;
813
- }
814
-
815
- // Close log file when done
816
- if (debugMode) {
817
- process.on('exit', () => {
818
- if (logFile) logFile.end();
819
- });
820
- }
821
-
822
- const endTime = Date.now();
823
- const totalTime = endTime - startTime;
824
- const timeToFirstToken = firstTokenTime ? firstTokenTime - startTime : totalTime;
825
- const tokensPerSecond = tokenCount > 0 && totalTime > 0 ? (tokenCount / totalTime) * 1000 : 0;
826
-
827
- // Try to get usage, but fallback to manual counting
828
- let usage = null;
829
- try {
830
- usage = await result.usage;
831
- log(`Provider usage data: ${JSON.stringify(usage, null, 2)}`);
832
- } catch (e) {
833
- log(`Usage not available: ${e.message}`);
834
- // Usage might not be available
835
- }
836
-
837
- // Use provider token count if available, otherwise use manual count
838
- const completionTokens = usage?.completionTokens || tokenCount;
839
- const promptTokens = usage?.promptTokens || Math.round(testPrompt.length / 4);
840
- const totalTokens = usage?.totalTokens || (completionTokens + promptTokens);
841
-
842
- console.log(colorText('Completed!', 'green'));
843
- console.log(colorText(` Total Time: ${(totalTime / 1000).toFixed(2)}s`, 'cyan'));
844
- console.log(colorText(` TTFT: ${(timeToFirstToken / 1000).toFixed(2)}s`, 'cyan'));
845
- console.log(colorText(` Tokens/Sec: ${tokensPerSecond.toFixed(1)}`, 'cyan'));
846
- console.log(colorText(` Total Tokens: ${totalTokens}`, 'cyan'));
847
-
848
- return {
849
- model: model.name,
850
- provider: model.providerName,
851
- totalTime,
852
- timeToFirstToken,
853
- tokenCount: completionTokens,
854
- tokensPerSecond,
855
- promptTokens: promptTokens,
856
- totalTokens: totalTokens,
857
- success: true
858
- };
859
-
860
- } catch (error) {
861
- console.log(colorText('Failed: ', 'red') + error.message);
862
- log(`Benchmark failed: ${error.message}`);
863
- log(`Error stack: ${error.stack}`);
864
- return {
865
- model: model.name,
866
- provider: model.providerName,
867
- totalTime: 0,
868
- timeToFirstToken: 0,
869
- tokenCount: 0,
870
- tokensPerSecond: 0,
871
- promptTokens: 0,
872
- totalTokens: 0,
873
- success: false,
874
- error: error.message
875
- };
876
- }
877
- };
878
-
879
- // Run all benchmarks in parallel
880
- console.log(colorText('Starting parallel benchmark execution...', 'cyan'));
881
- const promises = models.map(model => benchmarkModel(model));
882
- const results = await Promise.all(promises);
883
-
884
- console.log('');
885
- console.log(colorText('All benchmarks completed!', 'green'));
886
-
887
- await displayColorfulResults(results, 'AI SDK', models);
888
- }
889
-
890
- // Colorful results display with comprehensive table and enhanced bars
891
- async function displayColorfulResults(results, method = 'AI SDK', models = []) {
892
- clearScreen();
893
- showHeader();
894
- console.log(colorText('BENCHMARK RESULTS', 'magenta'));
895
- console.log(colorText('=========================', 'magenta'));
896
- console.log('');
897
- console.log(colorText('Method: ', 'cyan') + colorText(method, 'yellow'));
898
- console.log('');
899
-
900
- // Filter successful results for table
901
- const successfulResults = results.filter(r => r.success);
902
-
903
- if (successfulResults.length === 0) {
904
- console.log(colorText('No successful benchmarks to display.', 'red'));
905
- await question(colorText('Press Enter to continue...', 'yellow'));
906
- return;
907
- }
908
-
909
- // Create comprehensive table
910
- console.log(colorText('COMPREHENSIVE PERFORMANCE SUMMARY', 'yellow'));
911
-
912
- // Add note about method differences
913
- console.log(colorText('Note: ', 'cyan') + colorText('REST API with streaming now supports TTFT measurement. AI SDK also supports streaming.', 'dim'));
914
- console.log(colorText(' ', 'cyan') + colorText('For thinking models, TTFT will be higher as thinking tokens are processed before output tokens.', 'dim'));
915
- console.log(colorText(' ', 'cyan') + colorText('[est] markers indicate token counts were estimated (API did not provide usage metadata).', 'dim'));
916
- console.log('');
917
-
918
- const table = new Table({
919
- head: [
920
- colorText('Model', 'cyan'),
921
- colorText('Provider', 'cyan'),
922
- colorText('Total Time(s)', 'cyan'),
923
- colorText('TTFT(s)', 'cyan'),
924
- colorText('Tokens/Sec', 'cyan'),
925
- colorText('Output Tokens', 'cyan'),
926
- colorText('Prompt Tokens', 'cyan'),
927
- colorText('Total Tokens', 'cyan')
928
- ],
929
- colWidths: [20, 15, 15, 12, 15, 15, 15, 15],
930
- style: {
931
- head: ['cyan'],
932
- border: ['dim'],
933
- compact: false
934
- }
935
- });
936
-
937
- // Sort results by tokens per second (descending) for table ranking
938
- const sortedResults = [...successfulResults].sort((a, b) => b.tokensPerSecond - a.tokensPerSecond);
939
-
940
- // Add data rows (already ranked by sort order)
941
- sortedResults.forEach((result) => {
942
- const outputTokenDisplay = result.tokenCount.toString() + (result.usedEstimateForOutput ? ' [est]' : '');
943
- const promptTokenDisplay = result.promptTokens.toString() + (result.usedEstimateForInput ? ' [est]' : '');
944
- const totalTokenDisplay = result.totalTokens.toString() + ((result.usedEstimateForInput || result.usedEstimateForOutput) ? ' [est]' : '');
945
-
946
- table.push([
947
- colorText(result.model, 'white'),
948
- colorText(result.provider, 'white'),
949
- colorText((result.totalTime / 1000).toFixed(2), 'green'),
950
- colorText((result.timeToFirstToken / 1000).toFixed(2), 'yellow'),
951
- colorText(result.tokensPerSecond.toFixed(1), 'magenta'),
952
- colorText(outputTokenDisplay, result.usedEstimateForOutput ? 'yellow' : 'blue'),
953
- colorText(promptTokenDisplay, result.usedEstimateForInput ? 'yellow' : 'blue'),
954
- colorText(totalTokenDisplay, (result.usedEstimateForInput || result.usedEstimateForOutput) ? 'yellow' : 'bright')
955
- ]);
956
- });
957
-
958
- console.log(table.toString());
959
- console.log('');
960
-
961
- // Enhanced performance comparison charts with ranking and provider sections
962
- console.log(colorText('PERFORMANCE COMPARISON CHARTS', 'yellow'));
963
- console.log(colorText('─'.repeat(80), 'dim'));
964
- console.log('');
965
-
966
- // Group results by provider
967
- const providerGroups = {};
968
- successfulResults.forEach(result => {
969
- if (!providerGroups[result.provider]) {
970
- providerGroups[result.provider] = [];
971
- }
972
- providerGroups[result.provider].push(result);
973
- });
974
-
975
- // Calculate consistent column widths for both charts
976
- const maxModelLength = Math.max(...successfulResults.map(r => r.model.length));
977
- const maxProviderLength = Math.max(...successfulResults.map(r => r.provider.length));
978
- const maxTimeLength = 8; // "99.99s"
979
- const maxTpsLength = 12; // "999.9 tok/s"
980
- const maxRankLength = 6; // "1st", "2nd", "3rd", "4th", etc.
981
-
982
- // Time comparison chart - ranked by fastest (lowest time first)
983
- console.log(colorText('TOTAL TIME RANKING (fastest at top - lower is better)', 'cyan'));
984
- const timeSortedResults = [...successfulResults].sort((a, b) => a.totalTime - b.totalTime);
985
- const maxTime = Math.max(...successfulResults.map(r => r.totalTime));
986
-
987
- timeSortedResults.forEach((result, index) => {
988
- const rank = index + 1;
989
- const barLength = Math.floor((result.totalTime / maxTime) * 25);
990
- const bar = colorText('█'.repeat(barLength), 'red') + colorText('░'.repeat(25 - barLength), 'dim');
991
- const timeDisplay = (result.totalTime / 1000).toFixed(2) + 's';
992
- const tpsDisplay = result.tokensPerSecond.toFixed(1) + ' tok/s';
993
-
994
- // Rank badges
995
- const rankBadge = rank === 1 ? '1st' : rank === 2 ? '2nd' : rank === 3 ? '3rd' : `${rank}th`;
996
-
997
- console.log(
998
- colorText(rankBadge.padStart(maxRankLength), rank === 1 ? 'yellow' : rank === 2 ? 'white' : rank === 3 ? 'bright' : 'white') +
999
- colorText(' | ', 'dim') +
1000
- colorText(timeDisplay.padStart(maxTimeLength), 'red') +
1001
- colorText(' | ', 'dim') +
1002
- colorText(tpsDisplay.padStart(maxTpsLength), 'magenta') +
1003
- colorText(' | ', 'dim') +
1004
- colorText(result.model.padEnd(maxModelLength), 'white') +
1005
- colorText(' | ', 'dim') +
1006
- colorText(result.provider.padEnd(maxProviderLength), 'cyan') +
1007
- colorText(' | ', 'dim') +
1008
- bar
1009
- );
1010
- });
1011
-
1012
- console.log('');
1013
-
1014
- // Tokens per second comparison - ranked by highest TPS first
1015
- console.log(colorText('TOKENS PER SECOND RANKING (fastest at top - higher is better)', 'cyan'));
1016
- const tpsSortedResults = [...successfulResults].sort((a, b) => b.tokensPerSecond - a.tokensPerSecond);
1017
- const maxTps = Math.max(...successfulResults.map(r => r.tokensPerSecond));
1018
-
1019
- tpsSortedResults.forEach((result, index) => {
1020
- const rank = index + 1;
1021
- const barLength = Math.floor((result.tokensPerSecond / maxTps) * 25);
1022
- const bar = colorText('█'.repeat(barLength), 'green') + colorText('░'.repeat(25 - barLength), 'dim');
1023
- const timeDisplay = (result.totalTime / 1000).toFixed(2) + 's';
1024
- const tpsDisplay = result.tokensPerSecond.toFixed(1) + ' tok/s';
1025
-
1026
- // Rank badges
1027
- const rankBadge = rank === 1 ? '1st' : rank === 2 ? '2nd' : rank === 3 ? '3rd' : `${rank}.`;
1028
-
1029
- console.log(
1030
- colorText(rankBadge.padStart(maxRankLength), rank === 1 ? 'yellow' : rank === 2 ? 'white' : rank === 3 ? 'bright' : 'white') +
1031
- colorText(' | ', 'dim') +
1032
- colorText(tpsDisplay.padStart(maxTpsLength), 'green') +
1033
- colorText(' | ', 'dim') +
1034
- colorText(timeDisplay.padStart(maxTimeLength), 'red') +
1035
- colorText(' | ', 'dim') +
1036
- colorText(result.model.padEnd(maxModelLength), 'white') +
1037
- colorText(' | ', 'dim') +
1038
- colorText(result.provider.padEnd(maxProviderLength), 'cyan') +
1039
- colorText(' | ', 'dim') +
1040
- bar
1041
- );
1042
- });
1043
-
1044
- console.log('');
1045
-
1046
- console.log('');
1047
-
1048
- // Show failed benchmarks
1049
- const failedResults = results.filter(r => !r.success);
1050
- if (failedResults.length > 0) {
1051
- console.log(colorText('FAILED BENCHMARKS', 'red'));
1052
- console.log(colorText('─'.repeat(40), 'dim'));
1053
- failedResults.forEach(result => {
1054
- console.log(colorText(`${result.model} (${result.provider}): ${result.error}`, 'red'));
1055
- });
1056
- console.log('');
1057
- }
1058
-
1059
- // Add successful models to recent models list
1060
- const successfulModels = results
1061
- .filter(r => r.success)
1062
- .map(r => {
1063
- // Find the actual model object that matches this benchmark result
1064
- const modelObj = models.find(model =>
1065
- model.name === r.model && model.providerName === r.provider
1066
- );
1067
-
1068
- return {
1069
- modelId: modelObj ? modelObj.id : r.model, // Use actual ID if found, fallback to name
1070
- modelName: r.model,
1071
- providerName: r.provider
1072
- };
1073
- });
1074
-
1075
- if (successfulModels.length > 0) {
1076
- await addToRecentModels(successfulModels);
1077
- }
1078
-
1079
- console.log(colorText('Benchmark completed!', 'green'));
1080
- await question(colorText('Press Enter to continue...', 'yellow'));
1081
- }
1082
-
1083
- // Helper function to calculate visible items based on terminal height
1084
- function getVisibleItemsCount(headerHeight = 10) {
1085
- const terminalHeight = process.stdout.rows || 24;
1086
- return Math.max(3, terminalHeight - headerHeight);
1087
- }
1088
-
1089
-
1090
-
1091
- // Add a verified provider (saves to both auth.json and ai-benchmark-config.json)
1092
- async function addVerifiedProvider() {
1093
- let searchQuery = '';
1094
- let allProviders = [];
1095
- let filteredProviders = [];
1096
- let currentIndex = 0;
1097
- let currentPage = 0;
1098
-
1099
- // Load providers from models.dev
1100
- try {
1101
- allProviders = await getAllProviders();
1102
- filteredProviders = allProviders;
1103
- } catch (error) {
1104
- clearScreen();
1105
- showHeader();
1106
- console.log(colorText('Add Verified Provider', 'magenta'));
1107
- console.log('');
1108
- console.log(colorText('Error loading providers: ', 'red') + error.message);
1109
- await question(colorText('Press Enter to continue...', 'yellow'));
1110
- return;
1111
- }
1112
-
1113
- while (true) {
1114
- // Build screen content in memory (double buffering)
1115
- let screenContent = '';
1116
-
1117
- // Add header
1118
- screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
1119
- screenContent += colorText('=============================', 'cyan') + '\n';
1120
- screenContent += '\n';
1121
-
1122
- screenContent += colorText('Add Verified Provider', 'magenta') + '\n';
1123
- screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
1124
- screenContent += colorText('Type to search (real-time filtering)', 'cyan') + '\n';
1125
- screenContent += colorText('Navigation is circular', 'dim') + '\n';
1126
- screenContent += '\n';
1127
-
1128
- // Search interface - always visible
1129
- screenContent += colorText('🔍 Search: ', 'yellow') + colorText(searchQuery + '_', 'bright') + '\n';
1130
- screenContent += '\n';
1131
-
1132
- // Calculate pagination
1133
- const visibleItemsCount = getVisibleItemsCount(11); // Account for search bar and header
1134
- const totalItems = filteredProviders.length;
1135
- const totalPages = Math.ceil(totalItems / visibleItemsCount);
1136
-
1137
- // Ensure current page is valid
1138
- if (currentPage >= totalPages) currentPage = totalPages - 1;
1139
- if (currentPage < 0) currentPage = 0;
1140
-
1141
- const startIndex = currentPage * visibleItemsCount;
1142
- const endIndex = Math.min(startIndex + visibleItemsCount, totalItems);
1143
-
1144
- // Display providers with pagination
1145
- screenContent += colorText('Available Verified Providers:', 'cyan') + '\n';
1146
- screenContent += '\n';
1147
-
1148
- // Show current page of providers
1149
- for (let i = startIndex; i < endIndex && i < filteredProviders.length; i++) {
1150
- const provider = filteredProviders[i];
1151
- const isCurrent = i === currentIndex;
1152
- const indicator = isCurrent ? colorText('●', 'green') : colorText('○', 'dim');
1153
- const providerName = isCurrent ? colorText(provider.name, 'bright') : colorText(provider.name, 'white');
1154
- const providerType = isCurrent ? colorText(`(${provider.type})`, 'cyan') : colorText(`(${provider.type})`, 'dim');
1155
-
1156
- screenContent += `${indicator} ${providerName} ${providerType}\n`;
1157
- }
1158
-
1159
- // Show pagination info
1160
- screenContent += '\n';
1161
- if (totalPages > 1) {
1162
- const pageInfo = colorText(`Page ${currentPage + 1}/${totalPages}`, 'cyan');
1163
- const navHint = colorText('Use Page Up/Down to navigate pages', 'dim');
1164
- screenContent += `${pageInfo} ${navHint}\n`;
1165
-
1166
- if (currentPage < totalPages - 1) {
1167
- screenContent += colorText('↓ More items below', 'dim') + '\n';
1168
- }
1169
- }
1170
-
1171
- // Clear screen and output entire buffer at once
1172
- clearScreen();
1173
- console.log(screenContent);
1174
-
1175
- const key = await getKeyPress();
1176
-
1177
- // Navigation keys - only handle special keys
1178
- if (key === '\u001b[A') {
1179
- // Up arrow - circular navigation within current page
1180
- const pageStartIndex = currentPage * visibleItemsCount;
1181
- const pageEndIndex = Math.min(pageStartIndex + visibleItemsCount, totalItems);
1182
-
1183
- if (currentIndex <= pageStartIndex) {
1184
- currentIndex = pageEndIndex - 1;
1185
- } else {
1186
- currentIndex--;
1187
- }
1188
- } else if (key === '\u001b[B') {
1189
- // Down arrow - circular navigation within current page
1190
- const pageStartIndex = currentPage * visibleItemsCount;
1191
- const pageEndIndex = Math.min(pageStartIndex + visibleItemsCount, totalItems);
1192
-
1193
- if (currentIndex >= pageEndIndex - 1) {
1194
- currentIndex = pageStartIndex;
1195
- } else {
1196
- currentIndex++;
1197
- }
1198
- } else if (key === '\u001b[5~') {
1199
- // Page Up
1200
- if (currentPage > 0) {
1201
- currentPage--;
1202
- currentIndex = currentPage * visibleItemsCount;
1203
- }
1204
- } else if (key === '\u001b[6~') {
1205
- // Page Down
1206
- if (currentPage < totalPages - 1) {
1207
- currentPage++;
1208
- currentIndex = currentPage * visibleItemsCount;
1209
- }
1210
- } else if (key === '\r') {
1211
- // Enter - select current provider
1212
- await addVerifiedProviderAuto(filteredProviders[currentIndex]);
1213
- break;
1214
- } else if (key === '\u0003') {
1215
- // Ctrl+C
1216
- process.exit(0);
1217
- } else if (key === '\b' || key === '\x7f') {
1218
- // Backspace - delete character from search
1219
- if (searchQuery.length > 0) {
1220
- searchQuery = searchQuery.slice(0, -1);
1221
- filteredProviders = await searchProviders(searchQuery);
1222
- currentIndex = 0;
1223
- currentPage = 0;
1224
- }
1225
- } else if (key.length === 1) {
1226
- // Regular character - add to search query
1227
- searchQuery += key;
1228
- filteredProviders = await searchProviders(searchQuery);
1229
- currentIndex = 0;
1230
- currentPage = 0;
1231
- }
1232
- }
1233
- }
1234
-
1235
- // Add a verified provider from models.dev with AUTO-ADD all models
1236
- async function addVerifiedProviderAuto(provider) {
1237
- clearScreen();
1238
- showHeader();
1239
- console.log(colorText('Add Verified Provider', 'magenta'));
1240
- console.log('');
1241
-
1242
- console.log(colorText('Provider: ', 'cyan') + colorText(provider.name, 'white'));
1243
- console.log(colorText('Type: ', 'cyan') + colorText(provider.type, 'white'));
1244
- console.log(colorText('Base URL: ', 'cyan') + colorText(provider.baseUrl, 'white'));
1245
- console.log('');
1246
-
1247
- // Get available models for this provider
1248
- const models = await getModelsForProvider(provider.id);
1249
-
1250
- if (models.length === 0) {
1251
- console.log(colorText('No models available for this provider.', 'red'));
1252
- await question(colorText('Press Enter to continue...', 'yellow'));
1253
- return;
1254
- }
1255
-
1256
- console.log(colorText(`Found ${models.length} models for ${provider.name}`, 'cyan'));
1257
- console.log(colorText('All models will be automatically added', 'green'));
1258
- console.log('');
1259
-
1260
- // Show first few models as preview
1261
- console.log(colorText('Models to be added:', 'yellow'));
1262
- const previewCount = Math.min(5, models.length);
1263
- models.slice(0, previewCount).forEach(model => {
1264
- console.log(colorText(` • ${model.name}`, 'white'));
1265
- });
1266
-
1267
- if (models.length > previewCount) {
1268
- console.log(colorText(` ... and ${models.length - previewCount} more`, 'dim'));
1269
- }
1270
-
1271
- console.log('');
1272
-
1273
- // Get API key
1274
- const apiKey = await question(colorText('Enter API key: ', 'cyan'));
1275
-
1276
- if (!apiKey) {
1277
- console.log(colorText('API key is required.', 'red'));
1278
- await question(colorText('Press Enter to continue...', 'yellow'));
1279
- return;
1280
- }
1281
-
1282
- // Add API key to auth.json (for opencode integration)
1283
- const authSuccess = await addApiKey(provider.id, apiKey);
1284
-
1285
- if (!authSuccess) {
1286
- console.log(colorText('Failed to save API key to auth.json', 'red'));
1287
- await question(colorText('Press Enter to continue...', 'yellow'));
1288
- return;
1289
- }
1290
-
1291
- // Also add to ai-benchmark-config.json for consistency
1292
- const { addVerifiedProvider: addVerifiedProviderToConfig } = await import('./ai-config.js');
1293
- const configSuccess = await addVerifiedProviderToConfig(provider.id, apiKey);
1294
-
1295
- if (!configSuccess) {
1296
- console.log(colorText('Warning: Could not save to ai-benchmark-config.json', 'yellow'));
1297
- }
1298
-
1299
- console.log('');
1300
- console.log(colorText('Provider added successfully!', 'green'));
1301
- console.log(colorText(`API key saved to auth.json`, 'cyan'));
1302
- console.log(colorText(`Models will be loaded dynamically from ${provider.name}`, 'cyan'));
1303
- console.log(colorText(`Found ${models.length} available models`, 'cyan'));
1304
-
1305
- await question(colorText('\nPress Enter to continue...', 'yellow'));
1306
- }
1307
-
1308
-
1309
-
1310
- // Add a custom provider (now using ai-benchmark-config.json)
1311
- async function addCustomProviderCLI() {
1312
- clearScreen();
1313
- showHeader();
1314
- console.log(colorText('Add Custom Provider', 'magenta'));
1315
- console.log('');
1316
- console.log(colorText('Note: Custom providers are saved to ai-benchmark-config.json', 'cyan'));
1317
- console.log('');
1318
-
1319
- const providerOptions = [
1320
- { id: 1, text: 'OpenAI Compatible', type: 'openai-compatible' },
1321
- { id: 2, text: 'Anthropic', type: 'anthropic' },
1322
- { id: 3, text: 'Back to Custom Models menu', action: 'back' }
1323
- ];
1324
-
1325
- let currentIndex = 0;
1326
- let selectedChoice = null;
1327
-
1328
- while (true) {
1329
- // Build screen content in memory (double buffering)
1330
- let screenContent = '';
1331
-
1332
- // Add header
1333
- screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
1334
- screenContent += colorText('=============================', 'cyan') + '\n';
1335
- screenContent += '\n';
1336
-
1337
- screenContent += colorText('Add Custom Provider', 'magenta') + '\n';
1338
- screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
1339
- screenContent += colorText('Navigation is circular', 'dim') + '\n';
1340
- screenContent += '\n';
1341
-
1342
- screenContent += colorText('Select provider type:', 'cyan') + '\n';
1343
- screenContent += '\n';
1344
-
1345
- // Display provider options with arrow key navigation
1346
- providerOptions.forEach((option, index) => {
1347
- const isCurrent = index === currentIndex;
1348
- const indicator = isCurrent ? colorText('●', 'green') : colorText('○', 'dim');
1349
- const optionText = isCurrent ? colorText(option.text, 'bright') : colorText(option.text, 'yellow');
1350
-
1351
- screenContent += `${indicator} ${optionText}\n`;
1352
- });
1353
-
1354
- // Clear screen and output entire buffer at once
1355
- clearScreen();
1356
- console.log(screenContent);
1357
-
1358
- const key = await getKeyPress();
1359
-
1360
- if (key === '\u001b[A') {
1361
- // Up arrow - circular navigation
1362
- currentIndex = (currentIndex - 1 + providerOptions.length) % providerOptions.length;
1363
- } else if (key === '\u001b[B') {
1364
- // Down arrow - circular navigation
1365
- currentIndex = (currentIndex + 1) % providerOptions.length;
1366
- } else if (key === '\r') {
1367
- // Enter - select current option
1368
- selectedChoice = providerOptions[currentIndex];
1369
- break;
1370
- } else if (key === '\u0003') {
1371
- // Ctrl+C
1372
- process.exit(0);
1373
- }
1374
- }
1375
-
1376
- if (selectedChoice.action === 'back') return;
1377
-
1378
- const providerId = await question(colorText('Enter provider ID (e.g., my-openai): ', 'cyan'));
1379
- const name = await question(colorText('Enter provider name (e.g., MyOpenAI): ', 'cyan'));
1380
- const baseUrl = await question(colorText('Enter base URL (e.g., https://api.openai.com/v1): ', 'cyan'));
1381
- const apiKey = await question(colorText('Enter API key: ', 'cyan'));
1382
-
1383
- // Ask if user wants to add multiple models
1384
- console.log('');
1385
- console.log(colorText('Do you want to add multiple models?', 'cyan'));
1386
- console.log(colorText('1. Add single model', 'yellow'));
1387
- console.log(colorText('2. Add multiple models', 'yellow'));
1388
-
1389
- const modelChoice = await question(colorText('Enter choice (1 or 2): ', 'cyan'));
1390
-
1391
- let models = [];
1392
-
1393
- if (modelChoice === '2') {
1394
- // Multiple models mode
1395
- console.log('');
1396
- console.log(colorText('Enter model names (one per line, empty line to finish):', 'cyan'));
1397
- console.log(colorText('Examples: gpt-4, gpt-4-turbo, gpt-3.5-turbo', 'dim'));
1398
- console.log('');
1399
-
1400
- while (true) {
1401
- const modelName = await question(colorText('Model name: ', 'cyan'));
1402
- if (!modelName.trim()) break;
1403
-
1404
- const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
1405
- models.push({
1406
- name: modelName.trim(),
1407
- id: modelId
1408
- });
1409
- }
1410
- } else {
1411
- // Single model mode
1412
- const modelName = await question(colorText('Enter model name (e.g., gpt-4): ', 'cyan'));
1413
- if (modelName.trim()) {
1414
- const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
1415
- models.push({
1416
- name: modelName.trim(),
1417
- id: modelId
1418
- });
1419
- }
1420
- }
1421
-
1422
- if (models.length === 0) {
1423
- console.log(colorText('At least one model is required.', 'red'));
1424
- await question(colorText('Press Enter to continue...', 'yellow'));
1425
- return;
1426
- }
1427
-
1428
- // Create provider data for ai-benchmark-config.json format
1429
- const providerData = {
1430
- id: providerId,
1431
- name: name,
1432
- type: selectedChoice.type,
1433
- baseUrl: baseUrl,
1434
- apiKey: apiKey,
1435
- models: models
1436
- };
1437
-
1438
- // Save to ai-benchmark-config.json
1439
- const success = await addCustomProvider(providerData);
1440
-
1441
- if (!success) {
1442
- console.log(colorText('Failed to save custom provider.', 'red'));
1443
- await question(colorText('Press Enter to continue...', 'yellow'));
1444
- return;
1445
- }
1446
-
1447
- console.log(colorText('Custom provider added successfully!', 'green'));
1448
- console.log(colorText(`Added ${models.length} model(s)`, 'cyan'));
1449
- console.log(colorText(`Saved to ai-benchmark-config.json`, 'cyan'));
1450
-
1451
- await question(colorText('\nPress Enter to continue...', 'yellow'));
1452
- }
1453
-
1454
- // Show debug information about config system
1455
- async function showDebugInfo() {
1456
- clearScreen();
1457
- showHeader();
1458
- console.log(colorText('Config System Debug Info', 'magenta'));
1459
- console.log('');
1460
-
1461
- const debugInfo = await getDebugInfo();
1462
-
1463
- console.log(colorText('OpenCode Paths (deprecated):', 'cyan'));
1464
- console.log(colorText(` auth.json: ${debugInfo.opencodePaths.authJson}`, 'white'));
1465
- console.log(colorText(` opencode.json: ${debugInfo.opencodePaths.opencodeJson}`, 'white'));
1466
- console.log('');
1467
-
1468
- console.log(colorText('AI Speedometer Config Paths:', 'cyan'));
1469
- console.log(colorText(` ai-benchmark-config.json: ${debugInfo.aiConfigPaths.configJson}`, 'white'));
1470
- console.log(colorText(` Config directory: ${debugInfo.aiConfigPaths.configDir}`, 'white'));
1471
- console.log('');
1472
-
1473
- console.log(colorText('File Status:', 'cyan'));
1474
- console.log(colorText(` auth.json exists: ${debugInfo.authExists ? 'Yes' : 'No'}`, 'white'));
1475
- console.log(colorText(` opencode.json exists: ${debugInfo.configExists ? 'No' : 'No'}`, 'white'));
1476
- console.log(colorText(` ai-benchmark-config.json exists: ${debugInfo.aiConfigPaths.configExists ? 'Yes' : 'No'}`, 'white'));
1477
- console.log('');
1478
-
1479
- console.log(colorText('Authenticated Providers (auth.json):', 'cyan'));
1480
- if (debugInfo.authData.length === 0) {
1481
- console.log(colorText(' None', 'dim'));
1482
- } else {
1483
- debugInfo.authData.forEach(provider => {
1484
- console.log(colorText(` - ${provider}`, 'white'));
1485
- });
1486
- }
1487
- console.log('');
1488
-
1489
- console.log(colorText('Verified Providers (ai-benchmark-config.json):', 'cyan'));
1490
- if (debugInfo.aiConfigData.verifiedProviders.length === 0) {
1491
- console.log(colorText(' None', 'dim'));
1492
- } else {
1493
- debugInfo.aiConfigData.verifiedProviders.forEach(provider => {
1494
- console.log(colorText(` - ${provider}`, 'white'));
1495
- });
1496
- }
1497
- console.log('');
1498
-
1499
- console.log(colorText('Custom Providers (ai-benchmark-config.json):', 'cyan'));
1500
- if (debugInfo.aiConfigData.customProviders.length === 0) {
1501
- console.log(colorText(' None', 'dim'));
1502
- } else {
1503
- debugInfo.aiConfigData.customProviders.forEach(provider => {
1504
- console.log(colorText(` - ${provider}`, 'white'));
1505
- });
1506
- }
1507
- console.log('');
1508
-
1509
- console.log(colorText('XDG Paths (for OpenCode):', 'cyan'));
1510
- console.log(colorText(` Data: ${debugInfo.xdgPaths.data}`, 'white'));
1511
- console.log(colorText(` Config: ${debugInfo.xdgPaths.config}`, 'white'));
1512
- console.log('');
1513
-
1514
- await question(colorText('Press Enter to continue...', 'yellow'));
1515
- }
1516
-
1517
- async function listProviders() {
1518
- clearScreen();
1519
- showHeader();
1520
- console.log(colorText('Existing Providers', 'magenta'));
1521
- console.log('');
1522
-
1523
- const config = await loadConfig();
1524
-
1525
- if (config.providers.length === 0) {
1526
- console.log(colorText('No providers configured yet.', 'yellow'));
1527
- } else {
1528
- // Separate verified and custom providers
1529
- const verifiedProviders = config.providers.filter(p => {
1530
- // Verified providers come from auth.json via models.dev
1531
- return p.baseUrl && p.baseUrl.includes('api.'); // Simple heuristic
1532
- });
1533
-
1534
- const customProviders = config.providers.filter(p => {
1535
- return !verifiedProviders.includes(p);
1536
- });
1537
-
1538
- // Show verified providers
1539
- if (verifiedProviders.length > 0) {
1540
- console.log(colorText('Verified Providers (from models.dev):', 'green'));
1541
- verifiedProviders.forEach((provider, index) => {
1542
- console.log(colorText(`${index + 1}. ${provider.name} (${provider.type})`, 'cyan'));
1543
-
1544
- if (provider.models.length > 0) {
1545
- console.log(colorText(' Models:', 'dim'));
1546
- provider.models.forEach((model, modelIndex) => {
1547
- console.log(colorText(` ${modelIndex + 1}. ${model.name}`, 'yellow'));
1548
- });
1549
- } else {
1550
- console.log(colorText(' Models: None', 'dim'));
1551
- }
1552
-
1553
- console.log('');
1554
- });
1555
- }
1556
-
1557
- // Show custom providers
1558
- if (customProviders.length > 0) {
1559
- console.log(colorText('Custom Providers:', 'magenta'));
1560
- customProviders.forEach((provider, index) => {
1561
- console.log(colorText(`${index + 1}. ${provider.name} (${provider.type})`, 'cyan'));
1562
-
1563
- if (provider.models.length > 0) {
1564
- console.log(colorText(' Models:', 'dim'));
1565
- provider.models.forEach((model, modelIndex) => {
1566
- console.log(colorText(` ${modelIndex + 1}. ${model.name}`, 'yellow'));
1567
- });
1568
- } else {
1569
- console.log(colorText(' Models: None', 'dim'));
1570
- }
1571
-
1572
- console.log('');
1573
- });
1574
- }
1575
- }
1576
-
1577
- await question(colorText('Press Enter to continue...', 'yellow'));
1578
- }
1579
-
1580
- // Add Custom Models submenu
1581
- async function addCustomModelsMenu() {
1582
- const menuOptions = [
1583
- { id: 1, text: 'Add Models to Existing Provider', action: () => addModelsToExistingProvider() },
1584
- { id: 2, text: 'Add Custom Provider', action: () => addCustomProviderCLI() },
1585
- { id: 3, text: 'Back to Model Management', action: () => 'back' }
1586
- ];
1587
-
1588
- let currentIndex = 0;
1589
-
1590
- while (true) {
1591
- // Build screen content in memory (double buffering)
1592
- let screenContent = '';
1593
-
1594
- // Add header
1595
- screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
1596
- screenContent += colorText('=============================', 'cyan') + '\n';
1597
- screenContent += '\n';
1598
-
1599
- screenContent += colorText('Add Custom Models', 'magenta') + '\n';
1600
- screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
1601
- screenContent += colorText('Navigation is circular', 'dim') + '\n';
1602
- screenContent += '\n';
1603
-
1604
- // Display menu options
1605
- menuOptions.forEach((option, index) => {
1606
- const isCurrent = index === currentIndex;
1607
- const indicator = isCurrent ? colorText('●', 'green') : colorText('○', 'dim');
1608
- const optionText = isCurrent ? colorText(option.text, 'bright') : colorText(option.text, 'yellow');
1609
-
1610
- screenContent += `${indicator} ${optionText}\n`;
1611
- });
1612
-
1613
- // Clear screen and output entire buffer at once
1614
- clearScreen();
1615
- console.log(screenContent);
1616
-
1617
- const key = await getKeyPress();
1618
-
1619
- if (key === '\u001b[A') {
1620
- // Up arrow - circular navigation
1621
- currentIndex = (currentIndex - 1 + menuOptions.length) % menuOptions.length;
1622
- } else if (key === '\u001b[B') {
1623
- // Down arrow - circular navigation
1624
- currentIndex = (currentIndex + 1) % menuOptions.length;
1625
- } else if (key === '\r') {
1626
- // Enter - select current option
1627
- const result = await menuOptions[currentIndex].action();
1628
- if (result === 'back') {
1629
- return;
1630
- }
1631
- } else if (key === '\u0003') {
1632
- // Ctrl+C
1633
- process.exit(0);
1634
- }
1635
- }
1636
- }
1637
-
1638
- // Add models to existing custom provider
1639
- async function addModelsToExistingProvider() {
1640
- clearScreen();
1641
- showHeader();
1642
- console.log(colorText('Add Models to Existing Provider', 'magenta'));
1643
- console.log('');
1644
-
1645
- // Get custom providers from config
1646
- const customProviders = await getCustomProvidersFromConfig();
1647
-
1648
- if (customProviders.length === 0) {
1649
- console.log(colorText('No custom providers available. Please add a custom provider first.', 'red'));
1650
- await question(colorText('Press Enter to continue...', 'yellow'));
1651
- return;
1652
- }
1653
-
1654
- let currentIndex = 0;
1655
-
1656
- while (true) {
1657
- // Build screen content in memory (double buffering)
1658
- let screenContent = '';
1659
-
1660
- // Add header
1661
- screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
1662
- screenContent += colorText('=============================', 'cyan') + '\n';
1663
- screenContent += '\n';
1664
-
1665
- screenContent += colorText('Add Models to Existing Provider', 'magenta') + '\n';
1666
- screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
1667
- screenContent += colorText('Navigation is circular', 'dim') + '\n';
1668
- screenContent += '\n';
1669
-
1670
- screenContent += colorText('Select custom provider:', 'cyan') + '\n';
1671
- screenContent += '\n';
1672
-
1673
- // Display custom providers with arrow key navigation
1674
- customProviders.forEach((provider, index) => {
1675
- const isCurrent = index === currentIndex;
1676
- const indicator = isCurrent ? colorText('●', 'green') : colorText('○', 'dim');
1677
- const providerName = isCurrent ? colorText(provider.name, 'bright') : colorText(provider.name, 'yellow');
1678
- const providerType = isCurrent ? colorText(`(${provider.type})`, 'cyan') : colorText(`(${provider.type})`, 'dim');
1679
-
1680
- screenContent += `${indicator} ${providerName} ${providerType}\n`;
1681
- });
1682
-
1683
- // Clear screen and output entire buffer at once
1684
- clearScreen();
1685
- console.log(screenContent);
1686
-
1687
- const key = await getKeyPress();
1688
-
1689
- if (key === '\u001b[A') {
1690
- // Up arrow - circular navigation
1691
- currentIndex = (currentIndex - 1 + customProviders.length) % customProviders.length;
1692
- } else if (key === '\u001b[B') {
1693
- // Down arrow - circular navigation
1694
- currentIndex = (currentIndex + 1) % customProviders.length;
1695
- } else if (key === '\r') {
1696
- // Enter - select current provider
1697
- break;
1698
- } else if (key === '\u0003') {
1699
- // Ctrl+C
1700
- process.exit(0);
1701
- }
1702
- }
1703
-
1704
- const provider = customProviders[currentIndex];
1705
-
1706
- console.log('');
1707
- console.log(colorText('Selected provider: ', 'cyan') + colorText(provider.name, 'white'));
1708
- console.log('');
1709
-
1710
- // Ask if user wants to add multiple models
1711
- console.log(colorText('Do you want to add multiple models?', 'cyan'));
1712
- console.log(colorText('1. Add single model', 'yellow'));
1713
- console.log(colorText('2. Add multiple models', 'yellow'));
1714
-
1715
- const modelChoice = await question(colorText('Enter choice (1 or 2): ', 'cyan'));
1716
-
1717
- let modelsAdded = 0;
1718
-
1719
- if (modelChoice === '2') {
1720
- // Multiple models mode
1721
- console.log('');
1722
- console.log(colorText('Enter model names (one per line, empty line to finish):', 'cyan'));
1723
- console.log(colorText('Examples: gpt-4, gpt-4-turbo, gpt-3.5-turbo', 'dim'));
1724
- console.log('');
1725
-
1726
- while (true) {
1727
- const modelName = await question(colorText('Model name: ', 'cyan'));
1728
- if (!modelName.trim()) break;
1729
-
1730
- const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
1731
- const modelData = {
1732
- name: modelName.trim(),
1733
- id: modelId
1734
- };
1735
-
1736
- const success = await addModelToCustomProvider(provider.id, modelData);
1737
- if (success) {
1738
- modelsAdded++;
1739
- console.log(colorText(`✓ Added model: ${modelName.trim()}`, 'green'));
1740
- } else {
1741
- console.log(colorText(`✗ Failed to add model: ${modelName.trim()}`, 'red'));
1742
- }
1743
- }
1744
- } else {
1745
- // Single model mode
1746
- const modelName = await question(colorText('Enter model name: ', 'cyan'));
1747
- if (modelName.trim()) {
1748
- const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
1749
- const modelData = {
1750
- name: modelName.trim(),
1751
- id: modelId
1752
- };
1753
-
1754
- const success = await addModelToCustomProvider(provider.id, modelData);
1755
- if (success) {
1756
- modelsAdded = 1;
1757
- console.log(colorText(`✓ Added model: ${modelName.trim()}`, 'green'));
1758
- } else {
1759
- console.log(colorText(`✗ Failed to add model: ${modelName.trim()}`, 'red'));
1760
- }
1761
- }
1762
- }
1763
-
1764
- if (modelsAdded > 0) {
1765
- console.log('');
1766
- console.log(colorText(`Successfully added ${modelsAdded} model(s) to ${provider.name}`, 'green'));
1767
- } else {
1768
- console.log(colorText('No models were added.', 'yellow'));
1769
- }
1770
-
1771
- await question(colorText('\nPress Enter to continue...', 'yellow'));
1772
- }
1773
-
1774
- // Silent benchmark helper (returns raw result without UI)
1775
- async function benchmarkSingleModelRest(model) {
1776
-
1777
-
1778
- try {
1779
- // Validate required configuration
1780
- if (!model.providerConfig || !model.providerConfig.apiKey) {
1781
- throw new Error(`Missing API key for provider ${model.providerName}`);
1782
- }
1783
-
1784
- if (!model.providerConfig.baseUrl) {
1785
- throw new Error(`Missing base URL for provider ${model.providerName}`);
1786
- }
1787
-
1788
- // Extract the actual model ID for API calls (moved before usage)
1789
- let actualModelId;
1790
- if (model.id && model.id.includes('_')) {
1791
- // Strip provider prefix (e.g., "provider_model" -> "model")
1792
- actualModelId = model.id.split('_')[1];
1793
- } else if (model.id) {
1794
- // Use raw model ID directly (e.g., "zai-org/GLM-4.5-FP8")
1795
- actualModelId = model.id;
1796
- } else {
1797
- // Fallback to model name
1798
- actualModelId = model.name;
1799
- }
1800
- actualModelId = actualModelId.trim();
1801
-
1802
- const startTime = Date.now();
1803
- let firstTokenTime = null;
1804
- let streamedText = '';
1805
- let tokenCount = 0;
1806
-
1807
- // Use correct endpoint based on provider type or custom format
1808
- let endpoint;
1809
- if (model.providerConfig.endpointFormat) {
1810
- // Use custom endpoint format
1811
- endpoint = '/' + model.providerConfig.endpointFormat;
1812
- } else if (model.providerType === 'anthropic') {
1813
- endpoint = '/messages';
1814
- } else if (model.providerType === 'google') {
1815
- endpoint = '/models/' + actualModelId + ':streamGenerateContent';
1816
- } else {
1817
- endpoint = '/chat/completions';
1818
- }
1819
-
1820
- // Ensure baseUrl doesn't end with slash and endpoint doesn't start with slash
1821
- const baseUrl = model.providerConfig.baseUrl.replace(/\/$/, '');
1822
- const url = `${baseUrl}${endpoint}`;
1823
-
1824
-
1825
-
1826
- const headers = {
1827
- 'Content-Type': 'application/json',
1828
- 'Authorization': `Bearer ${model.providerConfig.apiKey}`
1829
- };
1830
-
1831
- // Add provider-specific headers
1832
- if (model.providerType === 'anthropic') {
1833
- headers['x-api-key'] = model.providerConfig.apiKey;
1834
- headers['anthropic-version'] = '2023-06-01';
1835
- } else if (model.providerType === 'google') {
1836
- delete headers['Authorization'];
1837
- headers['x-goog-api-key'] = model.providerConfig.apiKey;
1838
- }
1839
-
1840
- const body = {
1841
- model: actualModelId,
1842
- messages: [
1843
- { role: 'user', content: testPrompt }
1844
- ],
1845
- max_tokens: 500,
1846
- temperature: 0.7,
1847
- stream: true
1848
- };
1849
-
1850
- // Adjust for provider-specific formats
1851
- if (model.providerType === 'anthropic') {
1852
- body.max_tokens = 500;
1853
- body.stream = true;
1854
- } else if (model.providerType === 'google') {
1855
- body.contents = [{ parts: [{ text: testPrompt }] }];
1856
- body.generationConfig = {
1857
- maxOutputTokens: 500,
1858
- temperature: 0.7
1859
- };
1860
- delete body.messages;
1861
- delete body.max_tokens;
1862
- delete body.stream;
1863
- }
1864
-
1865
-
1866
-
1867
- const response = await fetch(url, {
1868
- method: 'POST',
1869
- headers: headers,
1870
- body: JSON.stringify(body)
1871
- });
1872
-
1873
-
1874
-
1875
- if (!response.ok) {
1876
- const errorText = await response.text();
1877
- throw new Error(`API request failed: ${response.status} ${response.statusText}`);
1878
- }
1879
-
1880
- // Process streaming response
1881
- const reader = response.body.getReader();
1882
- const decoder = new TextDecoder();
1883
- let buffer = '';
1884
- let inputTokens = 0;
1885
- let outputTokens = 0;
1886
- let isFirstChunk = true;
1887
-
1888
- while (true) {
1889
- const { done, value } = await reader.read();
1890
- if (done) break;
1891
-
1892
- // Capture TTFT on first chunk arrival (network level)
1893
- if (isFirstChunk && !firstTokenTime) {
1894
- firstTokenTime = Date.now();
1895
- isFirstChunk = false;
1896
- // Show live TTFT result (only in interactive mode, not headless)
1897
- const ttftSeconds = ((firstTokenTime - startTime) / 1000).toFixed(2);
1898
- if (!cliArgs.bench && !cliArgs.benchCustom) {
1899
- console.log(colorText(`TTFT received at ${ttftSeconds}s for ${model.name} (${model.providerName})`, 'green'));
1900
- }
1901
- }
1902
-
1903
- buffer += decoder.decode(value, { stream: true });
1904
- const lines = buffer.split('\n');
1905
- buffer = lines.pop() || '';
1906
-
1907
- for (const line of lines) {
1908
- const trimmedLine = line.trim();
1909
- if (!trimmedLine) continue;
1910
-
1911
- try {
1912
- if (model.providerType === 'anthropic') {
1913
- // Anthropic uses newline-delimited JSON with event types
1914
- if (trimmedLine.startsWith('data: ')) {
1915
- const jsonStr = trimmedLine.slice(6);
1916
- if (jsonStr === '[DONE]') break;
1917
-
1918
- const chunk = JSON.parse(jsonStr);
1919
-
1920
- if (chunk.type === 'content_block_delta' && chunk.delta?.text) {
1921
- streamedText += chunk.delta.text;
1922
- } else if (chunk.type === 'message_start' && chunk.message?.usage) {
1923
- inputTokens = chunk.message.usage.input_tokens || 0;
1924
- } else if (chunk.type === 'message_delta') {
1925
- // Capture output tokens from message_delta
1926
- if (chunk.usage?.output_tokens) {
1927
- outputTokens = chunk.usage.output_tokens;
1928
- }
1929
- // Some implementations put input_tokens here too
1930
- if (chunk.usage?.input_tokens && !inputTokens) {
1931
- inputTokens = chunk.usage.input_tokens;
1932
- }
1933
- }
1934
- } else if (trimmedLine.startsWith('event: ')) {
1935
- // Skip event lines (Anthropic SSE format uses separate event and data lines)
1936
- continue;
1937
- } else {
1938
- // Try parsing as raw JSON (some Anthropic-compatible APIs don't use SSE format)
1939
- const chunk = JSON.parse(trimmedLine);
1940
-
1941
- if (chunk.type === 'content_block_delta' && chunk.delta?.text) {
1942
- streamedText += chunk.delta.text;
1943
- } else if (chunk.type === 'message_start' && chunk.message?.usage) {
1944
- inputTokens = chunk.message.usage.input_tokens || 0;
1945
- } else if (chunk.type === 'message_delta') {
1946
- if (chunk.usage?.output_tokens) {
1947
- outputTokens = chunk.usage.output_tokens;
1948
- }
1949
- if (chunk.usage?.input_tokens && !inputTokens) {
1950
- inputTokens = chunk.usage.input_tokens;
1951
- }
1952
- }
1953
- }
1954
- } else if (model.providerType === 'google') {
1955
- // Google streaming format
1956
- const chunk = JSON.parse(trimmedLine);
1957
- if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
1958
- const text = chunk.candidates[0].content.parts[0].text;
1959
- streamedText += text;
1960
- }
1961
- if (chunk.usageMetadata?.promptTokenCount) {
1962
- inputTokens = chunk.usageMetadata.promptTokenCount;
1963
- }
1964
- if (chunk.usageMetadata?.candidatesTokenCount) {
1965
- outputTokens = chunk.usageMetadata.candidatesTokenCount;
1966
- }
1967
- } else {
1968
- // OpenAI-compatible SSE format
1969
- if (trimmedLine.startsWith('data: ')) {
1970
- const jsonStr = trimmedLine.slice(6);
1971
- if (jsonStr === '[DONE]') break;
1972
-
1973
- const chunk = JSON.parse(jsonStr);
1974
-
1975
- if (chunk.choices?.[0]?.delta?.content) {
1976
- streamedText += chunk.choices[0].delta.content;
1977
- } else if (chunk.choices?.[0]?.delta?.reasoning) {
1978
- // Handle NanoGPT reasoning tokens
1979
- streamedText += chunk.choices[0].delta.reasoning;
1980
- }
1981
-
1982
- if (chunk.usage?.prompt_tokens) {
1983
- inputTokens = chunk.usage.prompt_tokens;
1984
- }
1985
- if (chunk.usage?.completion_tokens) {
1986
- outputTokens = chunk.usage.completion_tokens;
1987
- }
1988
- }
1989
- }
1990
- } catch (parseError) {
1991
- // Skip invalid JSON lines
1992
- continue;
1993
- }
1994
- }
1995
- }
1996
-
1997
- const endTime = Date.now();
1998
- const totalTime = endTime - startTime;
1999
- const timeToFirstToken = firstTokenTime ? firstTokenTime - startTime : totalTime;
2000
-
2001
- // Calculate token counts - use provider's count if available, otherwise estimate
2002
- const usedEstimateForOutput = !outputTokens;
2003
- const usedEstimateForInput = !inputTokens;
2004
- const finalOutputTokens = outputTokens || Math.round(streamedText.length / 4);
2005
- const finalInputTokens = inputTokens || Math.round(testPrompt.length / 4);
2006
- const totalTokens = finalInputTokens + finalOutputTokens;
2007
- const tokensPerSecond = totalTime > 0 ? (totalTokens / totalTime) * 1000 : 0;
2008
-
2009
- return {
2010
- model: model.name,
2011
- provider: model.providerName,
2012
- totalTime: totalTime,
2013
- timeToFirstToken: timeToFirstToken,
2014
- tokenCount: finalOutputTokens,
2015
- tokensPerSecond: tokensPerSecond,
2016
- promptTokens: finalInputTokens,
2017
- totalTokens: totalTokens,
2018
- usedEstimateForOutput: usedEstimateForOutput,
2019
- usedEstimateForInput: usedEstimateForInput,
2020
- success: true
2021
- };
2022
-
2023
- } catch (error) {
2024
- return {
2025
- model: model.name,
2026
- provider: model.providerName,
2027
- totalTime: 0,
2028
- timeToFirstToken: 0,
2029
- tokenCount: 0,
2030
- tokensPerSecond: 0,
2031
- promptTokens: 0,
2032
- totalTokens: 0,
2033
- success: false,
2034
- error: error.message
2035
- };
2036
- }
2037
- }
2038
-
2039
- // REST API benchmark function using direct API calls (with UI)
2040
- async function runRestApiBenchmark(models) {
2041
- if (models.length === 0) {
2042
- console.log(colorText('No models selected for benchmarking.', 'red'));
2043
- return;
2044
- }
2045
-
2046
- clearScreen();
2047
- showHeader();
2048
- console.log(colorText('Running REST API Benchmark with Streaming...', 'green'));
2049
- console.log(colorText(`Running ${models.length} models in parallel...`, 'cyan'));
2050
- console.log(colorText('Note: This uses direct REST API calls with streaming support', 'dim'));
2051
- console.log('');
2052
-
2053
- // Run all benchmarks in parallel with UI feedback
2054
- console.log(colorText('Starting parallel REST API benchmark execution...', 'cyan'));
2055
-
2056
- // Start all benchmarks in parallel
2057
- const promises = models.map(model => {
2058
- console.log(colorText(`Testing ${model.name} (${model.providerName}) via REST API with streaming...`, 'yellow'));
2059
- return benchmarkSingleModelRest(model);
2060
- });
2061
-
2062
- const results = await Promise.all(promises);
2063
-
2064
- // Show individual results after all complete
2065
- results.forEach((result, index) => {
2066
- if (result.success) {
2067
- console.log(colorText(`✓ ${result.model} (${result.provider}) completed!`, 'green'));
2068
- console.log(colorText(` Total Time: ${(result.totalTime / 1000).toFixed(2)}s`, 'cyan'));
2069
- console.log(colorText(` TTFT: ${(result.timeToFirstToken / 1000).toFixed(2)}s`, 'cyan'));
2070
- console.log(colorText(` Tokens/Sec: ${result.tokensPerSecond.toFixed(1)}`, 'cyan'));
2071
- } else {
2072
- console.log(colorText(`✗ ${result.model} (${result.provider}) failed: `, 'red') + result.error);
2073
- }
2074
- });
2075
-
2076
- console.log('');
2077
- console.log(colorText('All REST API benchmarks completed!', 'green'));
2078
-
2079
- await displayColorfulResults(results, 'REST API (Streaming)', models);
2080
-
2081
- // Add successful models to recent models list
2082
- const successfulModels = results
2083
- .filter(r => r.success)
2084
- .map(r => {
2085
- // Find the actual model object that matches this benchmark result
2086
- const modelObj = models.find(model =>
2087
- model.name === r.model && model.providerName === r.provider
2088
- );
2089
-
2090
- return {
2091
- modelId: modelObj ? modelObj.id : r.model, // Use actual ID if found, fallback to name
2092
- modelName: r.model,
2093
- providerName: r.provider
2094
- };
2095
- });
2096
-
2097
- if (successfulModels.length > 0) {
2098
- await addToRecentModels(successfulModels);
2099
- }
2100
- }
2101
-
2102
- // Main menu with arrow key navigation
2103
- async function showMainMenu() {
2104
- const menuOptions = [
2105
- { id: 1, text: 'Set Model', action: () => showModelMenu() },
2106
- { id: 2, text: 'Run Benchmark (REST API)', action: async () => {
2107
- const selectedModels = await selectModelsCircular();
2108
- if (selectedModels.length > 0) {
2109
- await runRestApiBenchmark(selectedModels);
2110
- }
2111
- }},
2112
- { id: 3, text: 'Run Benchmark (AI SDK - Legacy)', action: async () => {
2113
- const selectedModels = await selectModelsCircular();
2114
- if (selectedModels.length > 0) {
2115
- await runStreamingBenchmark(selectedModels);
2116
- }
2117
- }},
2118
- { id: 4, text: 'Exit', action: () => {
2119
- console.log(colorText('Goodbye!', 'green'));
2120
- rl.close();
2121
- process.exit(0);
2122
- }}
2123
- ];
2124
-
2125
- let currentIndex = 0;
2126
-
2127
- while (true) {
2128
- // Build screen content in memory (double buffering)
2129
- let screenContent = '';
2130
-
2131
- // Add header
2132
- screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
2133
- screenContent += colorText('=============================', 'cyan') + '\n';
2134
- screenContent += '\n';
2135
-
2136
- screenContent += colorText('Main Menu:', 'cyan') + '\n';
2137
- screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
2138
- screenContent += colorText('Navigation is circular', 'dim') + '\n';
2139
- screenContent += '\n';
2140
-
2141
- // Display menu options
2142
- menuOptions.forEach((option, index) => {
2143
- const isCurrent = index === currentIndex;
2144
- const indicator = isCurrent ? colorText('●', 'green') : colorText('○', 'dim');
2145
- const optionText = isCurrent ? colorText(option.text, 'bright') : colorText(option.text, 'yellow');
2146
-
2147
- screenContent += `${indicator} ${optionText}\n`;
2148
- });
2149
-
2150
- // Clear screen and output entire buffer at once
2151
- clearScreen();
2152
- console.log(screenContent);
2153
-
2154
- const key = await getKeyPress();
2155
-
2156
- if (key === '\u001b[A') {
2157
- // Up arrow - circular navigation
2158
- currentIndex = (currentIndex - 1 + menuOptions.length) % menuOptions.length;
2159
- } else if (key === '\u001b[B') {
2160
- // Down arrow - circular navigation
2161
- currentIndex = (currentIndex + 1) % menuOptions.length;
2162
- } else if (key === '\r') {
2163
- // Enter - select current option
2164
- await menuOptions[currentIndex].action();
2165
- } else if (key === '\u0003') {
2166
- // Ctrl+C
2167
- process.exit(0);
2168
- }
2169
- }
2170
- }
2171
-
2172
- async function showModelMenu() {
2173
- const menuOptions = [
2174
- { id: 1, text: 'Add Verified Provider', action: () => addVerifiedProvider() },
2175
- { id: 2, text: 'Add Custom Models', action: () => addCustomModelsMenu() },
2176
- { id: 3, text: 'List Existing Providers', action: () => listProviders() },
2177
- { id: 4, text: 'Debug Info', action: () => showDebugInfo() },
2178
- { id: 5, text: 'Back to Main Menu', action: () => 'back' }
2179
- ];
2180
-
2181
- let currentIndex = 0;
2182
-
2183
- while (true) {
2184
- // Build screen content in memory (double buffering)
2185
- let screenContent = '';
2186
-
2187
- // Add header
2188
- screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
2189
- screenContent += colorText('=============================', 'cyan') + '\n';
2190
- screenContent += '\n';
2191
-
2192
- screenContent += colorText('Model Management:', 'cyan') + '\n';
2193
- screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
2194
- screenContent += colorText('Navigation is circular', 'dim') + '\n';
2195
- screenContent += '\n';
2196
-
2197
- // Display menu options
2198
- menuOptions.forEach((option, index) => {
2199
- const isCurrent = index === currentIndex;
2200
- const indicator = isCurrent ? colorText('●', 'green') : colorText('○', 'dim');
2201
- const optionText = isCurrent ? colorText(option.text, 'bright') : colorText(option.text, 'yellow');
2202
-
2203
- screenContent += `${indicator} ${optionText}\n`;
2204
- });
2205
-
2206
- // Clear screen and output entire buffer at once
2207
- clearScreen();
2208
- console.log(screenContent);
2209
-
2210
- const key = await getKeyPress();
2211
-
2212
- if (key === '\u001b[A') {
2213
- // Up arrow - circular navigation
2214
- currentIndex = (currentIndex - 1 + menuOptions.length) % menuOptions.length;
2215
- } else if (key === '\u001b[B') {
2216
- // Down arrow - circular navigation
2217
- currentIndex = (currentIndex + 1) % menuOptions.length;
2218
- } else if (key === '\r') {
2219
- // Enter - select current option
2220
- const result = await menuOptions[currentIndex].action();
2221
- if (result === 'back') {
2222
- return;
2223
- }
2224
- } else if (key === '\u0003') {
2225
- // Ctrl+C
2226
- process.exit(0);
2227
- }
2228
- }
2229
- }
2230
-
2231
- // Handle process interruption
2232
- process.on('SIGINT', () => {
2233
- console.log(colorText('\n\nCLI interrupted by user', 'yellow'));
2234
- rl.close();
2235
- process.exit(0);
2236
- });
2237
-
2238
- // Headless benchmark mode
2239
- async function runHeadlessBenchmark(benchSpec, apiKey, useAiSdk, cliArgs = null) {
2240
- try {
2241
- // Check if this is a custom provider benchmark
2242
- if (cliArgs && cliArgs.benchCustom) {
2243
- // Handle custom provider
2244
- const customProvider = createCustomProviderFromCli(cliArgs);
2245
-
2246
- // Create model object for benchmarking
2247
- const modelConfig = {
2248
- ...customProvider.models[0],
2249
- providerName: customProvider.name,
2250
- providerType: customProvider.type,
2251
- providerId: customProvider.id,
2252
- providerConfig: {
2253
- baseUrl: customProvider.baseUrl,
2254
- apiKey: customProvider.apiKey,
2255
- endpointFormat: customProvider.endpointFormat
2256
- },
2257
- selected: true
2258
- };
2259
-
2260
- // Run benchmark silently and get results
2261
- const result = await benchmarkSingleModelRest(modelConfig);
2262
-
2263
- // Output JSON to stdout (same format as regular benchmarks)
2264
- const jsonOutput = {
2265
- provider: customProvider.name,
2266
- providerId: customProvider.id,
2267
- model: customProvider.models[0].name,
2268
- modelId: customProvider.models[0].id,
2269
- method: 'rest-api',
2270
- success: result.success,
2271
- totalTime: result.totalTime,
2272
- totalTimeSeconds: result.totalTime / 1000,
2273
- timeToFirstToken: result.timeToFirstToken,
2274
- timeToFirstTokenSeconds: result.timeToFirstToken / 1000,
2275
- tokensPerSecond: result.tokensPerSecond,
2276
- outputTokens: result.tokenCount,
2277
- promptTokens: result.promptTokens,
2278
- totalTokens: result.totalTokens,
2279
- is_estimated: !!(result.usedEstimateForOutput || result.usedEstimateForInput),
2280
- error: result.error || null
2281
- };
2282
-
2283
- console.log(JSON.stringify(jsonOutput, null, cliArgs?.formatted ? 2 : 0));
2284
- process.exit(result.success ? 0 : 1);
2285
- }
2286
-
2287
- // Handle regular provider:model format
2288
- let providerSpec, modelName;
2289
- const colonIndex = benchSpec.indexOf(':');
2290
- if (colonIndex === -1) {
2291
- console.error(colorText('Error: Invalid --bench format. Use: provider:model', 'red'));
2292
- console.error(colorText('Example: --bench zai-code-anth:glm-4.6', 'yellow'));
2293
- process.exit(1);
2294
- }
2295
-
2296
- providerSpec = benchSpec.substring(0, colonIndex);
2297
- modelName = benchSpec.substring(colonIndex + 1);
2298
-
2299
- // Remove quotes from model name if present
2300
- if ((modelName.startsWith('"') && modelName.endsWith('"')) ||
2301
- (modelName.startsWith("'") && modelName.endsWith("'"))) {
2302
- modelName = modelName.slice(1, -1);
2303
- }
2304
-
2305
- if (!providerSpec || !modelName) {
2306
- console.error(colorText('Error: Invalid --bench format. Use: provider:model', 'red'));
2307
- console.error(colorText('Example: --bench zai-code-anth:glm-4.6', 'yellow'));
2308
- process.exit(1);
2309
- }
2310
-
2311
- // Load all available providers (include all models.dev providers for headless mode)
2312
- const config = await loadConfig(true);
2313
-
2314
- // Find the provider (case-insensitive search)
2315
- const provider = config.providers.find(p =>
2316
- p.id?.toLowerCase() === providerSpec.toLowerCase() ||
2317
- p.name?.toLowerCase() === providerSpec.toLowerCase()
2318
- );
2319
-
2320
- if (!provider) {
2321
- console.error(colorText(`Error: Provider '${providerSpec}' not found`, 'red'));
2322
- console.error(colorText('Available providers:', 'yellow'));
2323
- config.providers.forEach(p => {
2324
- console.error(colorText(` - ${p.id || p.name}`, 'cyan'));
2325
- });
2326
- process.exit(1);
2327
- }
2328
-
2329
- // Find the model
2330
- // First try exact match with the provided model ID
2331
- // Then fall back to legacy matching for compatibility
2332
- const model = provider.models.find(m => {
2333
- const modelIdLower = m.id?.toLowerCase() || '';
2334
- const modelNameLower = m.name?.toLowerCase() || '';
2335
- const searchLower = modelName.toLowerCase();
2336
-
2337
- // Exact ID match first (for quoted model IDs like "hf:moonshotai/Kimi-K2-Instruct-0905")
2338
- if (modelIdLower === searchLower) return true;
2339
-
2340
- // Legacy matching for backward compatibility:
2341
- // Model IDs are prefixed with provider name (e.g., "zai-code-anth_glm-4.6")
2342
- // So we need to check:
2343
- // 1. ID without provider prefix: "glm-4.6"
2344
- // 2. Name match: "GLM-4.6-anth"
2345
-
2346
- // Check ID without provider prefix (strip "provider_" prefix)
2347
- const idWithoutPrefix = modelIdLower.includes('_')
2348
- ? modelIdLower.split('_').slice(1).join('_')
2349
- : modelIdLower;
2350
- if (idWithoutPrefix === searchLower) return true;
2351
-
2352
- // Check name match
2353
- if (modelNameLower === searchLower) return true;
2354
-
2355
- return false;
2356
- });
2357
-
2358
- if (!model) {
2359
- console.error(colorText(`Error: Model '${modelName}' not found in provider '${provider.name}'`, 'red'));
2360
- console.error(colorText('Available models:', 'yellow'));
2361
- provider.models.forEach(m => {
2362
- // Show both name and ID (without provider prefix) for clarity
2363
- const idWithoutPrefix = m.id?.includes('_')
2364
- ? m.id.split('_').slice(1).join('_')
2365
- : m.id;
2366
- console.error(colorText(` - ${m.name} (id: ${idWithoutPrefix})`, 'cyan'));
2367
- });
2368
- process.exit(1);
2369
- }
2370
-
2371
- // If API key provided via flag, use it; otherwise use existing config
2372
- let finalApiKey = apiKey || provider.apiKey;
2373
-
2374
- if (!finalApiKey) {
2375
- console.error(colorText(`Error: No API key found for provider '${provider.name}'`, 'red'));
2376
- console.error(colorText('Please provide --api-key flag or configure the provider first', 'yellow'));
2377
- process.exit(1);
2378
- }
2379
-
2380
- // Create model object with all required config
2381
- const modelConfig = {
2382
- ...model,
2383
- providerName: provider.name,
2384
- providerType: provider.type,
2385
- providerId: provider.id,
2386
- providerConfig: {
2387
- ...provider,
2388
- apiKey: finalApiKey,
2389
- baseUrl: provider.baseUrl || ''
2390
- },
2391
- selected: true
2392
- };
2393
-
2394
- // Run benchmark silently and get results
2395
- let result;
2396
- if (useAiSdk) {
2397
- // TODO: Implement AI SDK silent benchmark
2398
- console.error(colorText('AI SDK headless mode not yet implemented', 'red'));
2399
- process.exit(1);
2400
- } else {
2401
- result = await benchmarkSingleModelRest(modelConfig);
2402
- }
2403
-
2404
- // Output JSON to stdout
2405
- const jsonOutput = {
2406
- provider: provider.name,
2407
- providerId: provider.id,
2408
- model: model.name,
2409
- modelId: model.id,
2410
- method: useAiSdk ? 'ai-sdk' : 'rest-api',
2411
- success: result.success,
2412
- totalTime: result.totalTime,
2413
- totalTimeSeconds: result.totalTime / 1000,
2414
- timeToFirstToken: result.timeToFirstToken,
2415
- timeToFirstTokenSeconds: result.timeToFirstToken / 1000,
2416
- tokensPerSecond: result.tokensPerSecond,
2417
- outputTokens: result.tokenCount,
2418
- promptTokens: result.promptTokens,
2419
- totalTokens: result.totalTokens,
2420
- is_estimated: !!(result.usedEstimateForOutput || result.usedEstimateForInput),
2421
- error: result.error || null
2422
- };
2423
-
2424
- console.log(JSON.stringify(jsonOutput, null, cliArgs?.formatted ? 2 : 0));
2425
- process.exit(result.success ? 0 : 1);
2426
- } catch (error) {
2427
- console.error(colorText('Error: ' + error.message, 'red'));
2428
- if (debugMode) {
2429
- console.error(error.stack);
2430
- }
2431
- process.exit(1);
2432
- }
2433
- }
2434
-
2435
- // Start the CLI
2436
- if (typeof require !== 'undefined' && require.main === module) {
2437
- // Check if help flag
2438
- if (cliArgs.help) {
2439
- showHelp();
2440
- process.exit(0);
2441
- }
2442
-
2443
- // Check if custom provider benchmark mode
2444
- if (cliArgs.benchCustom) {
2445
- runHeadlessBenchmark(cliArgs.benchCustom, cliArgs.apiKey, cliArgs.useAiSdk, cliArgs);
2446
- }
2447
- // Check if headless benchmark mode
2448
- else if (cliArgs.bench) {
2449
- runHeadlessBenchmark(cliArgs.bench, cliArgs.apiKey, cliArgs.useAiSdk, null);
2450
- } else {
2451
- // Interactive mode
2452
- cleanupRecentModelsFromConfig().then(() => {
2453
- showMainMenu();
2454
- }).catch(() => {
2455
- showMainMenu();
2456
- });
2457
- }
2458
- }
2459
-
2460
- export { showMainMenu, listProviders, selectModelsCircular, runStreamingBenchmark, loadConfig, saveConfig };