ai-speedometer 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -282
- package/cli.js +503 -268
- package/dist/ai-speedometer +116 -108
- package/docs/models-dev-integration.md +218 -93
- package/docs/publish-and-build.md +68 -0
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -15,6 +15,17 @@ import {
|
|
|
15
15
|
migrateFromOldConfig,
|
|
16
16
|
getDebugInfo
|
|
17
17
|
} from './opencode-integration.js';
|
|
18
|
+
import {
|
|
19
|
+
readAIConfig,
|
|
20
|
+
getCustomProvidersFromConfig,
|
|
21
|
+
getVerifiedProvidersFromConfig,
|
|
22
|
+
addCustomProvider,
|
|
23
|
+
addModelToCustomProvider,
|
|
24
|
+
getAIConfigDebugPaths,
|
|
25
|
+
addToRecentModels,
|
|
26
|
+
getRecentModels,
|
|
27
|
+
cleanupRecentModelsFromConfig
|
|
28
|
+
} from './ai-config.js';
|
|
18
29
|
import 'dotenv/config';
|
|
19
30
|
import Table from 'cli-table3';
|
|
20
31
|
|
|
@@ -45,17 +56,12 @@ function createAnthropicProvider(baseUrl, apiKey) {
|
|
|
45
56
|
log(`Creating Anthropic provider with baseUrl: ${baseUrl}`);
|
|
46
57
|
log(`API Key length: ${apiKey ? apiKey.length : 0}`);
|
|
47
58
|
|
|
48
|
-
//
|
|
49
|
-
let normalizedBaseUrl = baseUrl;
|
|
50
|
-
if (!baseUrl.endsWith('/v1')) {
|
|
51
|
-
normalizedBaseUrl = baseUrl.endsWith('/') ? `${baseUrl}v1` : `${baseUrl}/v1`;
|
|
52
|
-
log(`Normalized base URL to: ${normalizedBaseUrl}`);
|
|
53
|
-
}
|
|
59
|
+
// Use baseUrl as provided - no automatic normalization needed
|
|
54
60
|
|
|
55
61
|
// Try with baseURL parameter (correct according to docs)
|
|
56
62
|
const provider = createAnthropic({
|
|
57
63
|
apiKey: apiKey,
|
|
58
|
-
baseURL:
|
|
64
|
+
baseURL: baseUrl,
|
|
59
65
|
// Add minimal fetch logging for debugging
|
|
60
66
|
fetch: debugMode ? async (input, init) => {
|
|
61
67
|
log(`API Request to: ${input}`);
|
|
@@ -123,13 +129,13 @@ function showHeader() {
|
|
|
123
129
|
console.log('');
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
// Configuration management - now using
|
|
132
|
+
// Configuration management - now using ai-benchmark-config.json for custom providers
|
|
127
133
|
async function loadConfig() {
|
|
128
134
|
try {
|
|
129
135
|
// Check if we need to migrate from old config
|
|
130
136
|
const oldConfigFile = 'ai-benchmark-config.json';
|
|
131
137
|
if (fs.existsSync(oldConfigFile)) {
|
|
132
|
-
console.log(colorText('Migrating from old config format to
|
|
138
|
+
console.log(colorText('Migrating from old config format to new format...', 'yellow'));
|
|
133
139
|
|
|
134
140
|
try {
|
|
135
141
|
const data = fs.readFileSync(oldConfigFile, 'utf8');
|
|
@@ -156,7 +162,7 @@ async function loadConfig() {
|
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
// Load providers from
|
|
165
|
+
// Load providers from both auth.json (verified) and ai-benchmark-config.json (custom)
|
|
160
166
|
const providers = await getAllAvailableProviders();
|
|
161
167
|
|
|
162
168
|
return {
|
|
@@ -169,11 +175,11 @@ async function loadConfig() {
|
|
|
169
175
|
}
|
|
170
176
|
}
|
|
171
177
|
|
|
172
|
-
// Save config - now using
|
|
178
|
+
// Save config - now using ai-benchmark-config.json and auth.json
|
|
173
179
|
async function saveConfig(config) {
|
|
174
180
|
// Note: This function is kept for compatibility but the actual saving
|
|
175
|
-
// is handled by the opencode
|
|
176
|
-
console.log(colorText('Note: Configuration is now automatically saved to
|
|
181
|
+
// is handled by the ai-config.js and opencode-integration.js functions
|
|
182
|
+
console.log(colorText('Note: Configuration is now automatically saved to ai-benchmark-config.json and auth.json', 'cyan'));
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
// Keyboard input handling
|
|
@@ -200,9 +206,12 @@ async function selectModelsCircular() {
|
|
|
200
206
|
showHeader();
|
|
201
207
|
console.log(colorText('Select Models for Benchmark', 'magenta'));
|
|
202
208
|
console.log('');
|
|
203
|
-
|
|
209
|
+
|
|
204
210
|
const config = await loadConfig();
|
|
205
|
-
|
|
211
|
+
|
|
212
|
+
// Clean up recent models from main config and migrate to cache
|
|
213
|
+
await cleanupRecentModelsFromConfig();
|
|
214
|
+
|
|
206
215
|
if (config.providers.length === 0) {
|
|
207
216
|
console.log(colorText('No providers available. Please add a provider first.', 'red'));
|
|
208
217
|
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
@@ -227,16 +236,38 @@ async function selectModelsCircular() {
|
|
|
227
236
|
});
|
|
228
237
|
});
|
|
229
238
|
|
|
239
|
+
// Load recent models
|
|
240
|
+
const recentModelsData = await getRecentModels();
|
|
241
|
+
|
|
242
|
+
// Create a mapping of recent models to actual model objects
|
|
243
|
+
const recentModelObjects = [];
|
|
244
|
+
recentModelsData.forEach(recentModel => {
|
|
245
|
+
const modelObj = allModels.find(model =>
|
|
246
|
+
model.id === recentModel.modelId &&
|
|
247
|
+
model.providerName === recentModel.providerName
|
|
248
|
+
);
|
|
249
|
+
if (modelObj) {
|
|
250
|
+
recentModelObjects.push({
|
|
251
|
+
...modelObj,
|
|
252
|
+
isRecent: true
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
230
257
|
let currentIndex = 0;
|
|
231
258
|
let currentPage = 0;
|
|
232
259
|
let searchQuery = '';
|
|
233
|
-
let filteredModels = [...allModels];
|
|
234
260
|
|
|
235
261
|
// Create a reusable filter function to avoid code duplication
|
|
236
262
|
const filterModels = (query) => {
|
|
237
263
|
if (!query.trim()) {
|
|
238
|
-
return
|
|
264
|
+
// When search is empty, return the combined list with recent models at top
|
|
265
|
+
const recentModelIds = new Set(recentModelObjects.map(m => m.id));
|
|
266
|
+
const nonRecentModels = allModels.filter(model => !recentModelIds.has(model.id));
|
|
267
|
+
return [...recentModelObjects, ...nonRecentModels];
|
|
239
268
|
}
|
|
269
|
+
|
|
270
|
+
// When searching, search through all models (no recent section)
|
|
240
271
|
const lowercaseQuery = query.toLowerCase();
|
|
241
272
|
return allModels.filter(model => {
|
|
242
273
|
const modelNameMatch = model.name.toLowerCase().includes(lowercaseQuery);
|
|
@@ -248,6 +279,9 @@ async function selectModelsCircular() {
|
|
|
248
279
|
});
|
|
249
280
|
};
|
|
250
281
|
|
|
282
|
+
// Initialize filtered models using the filter function
|
|
283
|
+
let filteredModels = filterModels('');
|
|
284
|
+
|
|
251
285
|
// Debounce function to reduce filtering frequency
|
|
252
286
|
let searchTimeout;
|
|
253
287
|
const debouncedFilter = (query, callback) => {
|
|
@@ -272,7 +306,7 @@ async function selectModelsCircular() {
|
|
|
272
306
|
screenContent += colorText('Type to search (real-time filtering)', 'cyan') + '\n';
|
|
273
307
|
screenContent += colorText('Press "A" to select all models, "N" to deselect all', 'cyan') + '\n';
|
|
274
308
|
screenContent += colorText('Circle states: ●=Current+Selected ○=Current+Unselected ●=Selected ○=Unselected', 'dim') + '\n';
|
|
275
|
-
screenContent += colorText('Quick run: ENTER on any model | Multi-select: TAB then ENTER', 'dim') + '\n';
|
|
309
|
+
screenContent += colorText('Quick run: ENTER on any model | Multi-select: TAB then ENTER | Recent: R', 'dim') + '\n';
|
|
276
310
|
screenContent += '\n';
|
|
277
311
|
|
|
278
312
|
// Search interface - always visible
|
|
@@ -291,13 +325,51 @@ async function selectModelsCircular() {
|
|
|
291
325
|
const endIndex = Math.min(startIndex + visibleItemsCount, filteredModels.length);
|
|
292
326
|
|
|
293
327
|
// Display models in a vertical layout with pagination
|
|
294
|
-
|
|
295
|
-
|
|
328
|
+
let hasRecentModelsInCurrentPage = false;
|
|
329
|
+
let recentSectionDisplayed = false;
|
|
330
|
+
let nonRecentSectionDisplayed = false;
|
|
331
|
+
|
|
332
|
+
// Only show recent section when search is empty and we have recent models
|
|
333
|
+
const showRecentSection = searchQuery.length === 0 && recentModelObjects.length > 0;
|
|
334
|
+
|
|
335
|
+
// Check if current page contains any recent models (only when search is empty)
|
|
336
|
+
if (showRecentSection) {
|
|
337
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
338
|
+
if (filteredModels[i].isRecent) {
|
|
339
|
+
hasRecentModelsInCurrentPage = true;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
296
344
|
|
|
345
|
+
// Display models with proper section headers
|
|
297
346
|
for (let i = startIndex; i < endIndex; i++) {
|
|
298
347
|
const model = filteredModels[i];
|
|
299
348
|
const isCurrent = i === currentIndex;
|
|
300
|
-
|
|
349
|
+
// For recent models, check selection state from the original model
|
|
350
|
+
let isSelected;
|
|
351
|
+
if (model.isRecent) {
|
|
352
|
+
const originalModelIndex = allModels.findIndex(originalModel =>
|
|
353
|
+
originalModel.id === model.id &&
|
|
354
|
+
originalModel.providerName === model.providerName &&
|
|
355
|
+
!originalModel.isRecent
|
|
356
|
+
);
|
|
357
|
+
isSelected = originalModelIndex !== -1 ? allModels[originalModelIndex].selected : false;
|
|
358
|
+
} else {
|
|
359
|
+
isSelected = model.selected;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Show recent section header if we encounter a recent model and haven't shown the header yet
|
|
363
|
+
if (model.isRecent && !recentSectionDisplayed && hasRecentModelsInCurrentPage && showRecentSection) {
|
|
364
|
+
screenContent += colorText('-------recent--------', 'dim') + '\n';
|
|
365
|
+
recentSectionDisplayed = true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Show separator between recent and non-recent models
|
|
369
|
+
if (!model.isRecent && recentSectionDisplayed && !nonRecentSectionDisplayed && showRecentSection) {
|
|
370
|
+
screenContent += colorText('-------recent--------', 'dim') + '\n';
|
|
371
|
+
nonRecentSectionDisplayed = true;
|
|
372
|
+
}
|
|
301
373
|
|
|
302
374
|
// Single circle that shows both current state and selection
|
|
303
375
|
let circle;
|
|
@@ -375,10 +447,28 @@ async function selectModelsCircular() {
|
|
|
375
447
|
}
|
|
376
448
|
} else if (key === '\t') {
|
|
377
449
|
// Tab - select/deselect current model
|
|
378
|
-
const
|
|
450
|
+
const currentModel = filteredModels[currentIndex];
|
|
451
|
+
let actualModelIndex;
|
|
452
|
+
|
|
453
|
+
if (currentModel.isRecent) {
|
|
454
|
+
// For recent models, find by matching the original model ID and provider name
|
|
455
|
+
actualModelIndex = allModels.findIndex(model =>
|
|
456
|
+
model.id === currentModel.id &&
|
|
457
|
+
model.providerName === currentModel.providerName &&
|
|
458
|
+
!model.isRecent // Don't match the recent copy, match the original
|
|
459
|
+
);
|
|
460
|
+
} else {
|
|
461
|
+
// For regular models, use the standard matching
|
|
462
|
+
actualModelIndex = allModels.findIndex(model =>
|
|
463
|
+
model.id === currentModel.id && model.providerName === currentModel.providerName
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
379
467
|
if (actualModelIndex !== -1) {
|
|
380
468
|
allModels[actualModelIndex].selected = !allModels[actualModelIndex].selected;
|
|
381
469
|
}
|
|
470
|
+
// Force immediate screen redraw by continuing to next iteration
|
|
471
|
+
continue;
|
|
382
472
|
} else if (key === '\r') {
|
|
383
473
|
// Enter - run benchmark on selected models
|
|
384
474
|
const currentModel = filteredModels[currentIndex];
|
|
@@ -445,6 +535,35 @@ async function selectModelsCircular() {
|
|
|
445
535
|
currentPage = 0;
|
|
446
536
|
});
|
|
447
537
|
}
|
|
538
|
+
} else if (key === 'R' || key === 'r') {
|
|
539
|
+
// Run recent models - only when search is empty and we have recent models
|
|
540
|
+
if (searchQuery.length === 0 && recentModelObjects.length > 0) {
|
|
541
|
+
// Deselect all models first
|
|
542
|
+
allModels.forEach(model => model.selected = false);
|
|
543
|
+
|
|
544
|
+
// Select all recent models by finding the original models
|
|
545
|
+
recentModelObjects.forEach(recentModel => {
|
|
546
|
+
const actualModelIndex = allModels.findIndex(model =>
|
|
547
|
+
model.id === recentModel.id &&
|
|
548
|
+
model.providerName === recentModel.providerName &&
|
|
549
|
+
!model.isRecent // Match the original, not the recent copy
|
|
550
|
+
);
|
|
551
|
+
if (actualModelIndex !== -1) {
|
|
552
|
+
allModels[actualModelIndex].selected = true;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Break out of loop to run benchmark
|
|
557
|
+
break;
|
|
558
|
+
} else {
|
|
559
|
+
// If search is active or no recent models, add 'R' to search query
|
|
560
|
+
searchQuery += key;
|
|
561
|
+
debouncedFilter(searchQuery, (newFilteredModels) => {
|
|
562
|
+
filteredModels = newFilteredModels;
|
|
563
|
+
currentIndex = 0;
|
|
564
|
+
currentPage = 0;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
448
567
|
} else if (key === 'a' || key === 'n') {
|
|
449
568
|
// Lowercase 'a' and 'n' go to search field (not select all/none)
|
|
450
569
|
searchQuery += key;
|
|
@@ -650,11 +769,11 @@ async function runStreamingBenchmark(models) {
|
|
|
650
769
|
console.log('');
|
|
651
770
|
console.log(colorText('All benchmarks completed!', 'green'));
|
|
652
771
|
|
|
653
|
-
await displayColorfulResults(results, 'AI SDK');
|
|
772
|
+
await displayColorfulResults(results, 'AI SDK', models);
|
|
654
773
|
}
|
|
655
774
|
|
|
656
775
|
// Colorful results display with comprehensive table and enhanced bars
|
|
657
|
-
async function displayColorfulResults(results, method = 'AI SDK') {
|
|
776
|
+
async function displayColorfulResults(results, method = 'AI SDK', models = []) {
|
|
658
777
|
clearScreen();
|
|
659
778
|
showHeader();
|
|
660
779
|
console.log(colorText('BENCHMARK RESULTS', 'magenta'));
|
|
@@ -817,23 +936,40 @@ async function displayColorfulResults(results, method = 'AI SDK') {
|
|
|
817
936
|
console.log('');
|
|
818
937
|
}
|
|
819
938
|
|
|
939
|
+
// Add successful models to recent models list
|
|
940
|
+
const successfulModels = results
|
|
941
|
+
.filter(r => r.success)
|
|
942
|
+
.map(r => {
|
|
943
|
+
// Find the actual model object that matches this benchmark result
|
|
944
|
+
const modelObj = models.find(model =>
|
|
945
|
+
model.name === r.model && model.providerName === r.provider
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
return {
|
|
949
|
+
modelId: modelObj ? modelObj.id : r.model, // Use actual ID if found, fallback to name
|
|
950
|
+
modelName: r.model,
|
|
951
|
+
providerName: r.provider
|
|
952
|
+
};
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
if (successfulModels.length > 0) {
|
|
956
|
+
await addToRecentModels(successfulModels);
|
|
957
|
+
}
|
|
958
|
+
|
|
820
959
|
console.log(colorText('Benchmark completed!', 'green'));
|
|
821
960
|
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
822
961
|
}
|
|
823
962
|
|
|
824
963
|
// Helper function to calculate visible items based on terminal height
|
|
825
|
-
function getVisibleItemsCount(headerHeight =
|
|
964
|
+
function getVisibleItemsCount(headerHeight = 10) {
|
|
826
965
|
const terminalHeight = process.stdout.rows || 24;
|
|
827
|
-
return Math.max(
|
|
966
|
+
return Math.max(3, terminalHeight - headerHeight);
|
|
828
967
|
}
|
|
829
968
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
console.log(colorText('Add Provider', 'magenta'));
|
|
835
|
-
console.log('');
|
|
836
|
-
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
// Add a verified provider (saves to both auth.json and ai-benchmark-config.json)
|
|
972
|
+
async function addVerifiedProvider() {
|
|
837
973
|
let searchQuery = '';
|
|
838
974
|
let allProviders = [];
|
|
839
975
|
let filteredProviders = [];
|
|
@@ -845,6 +981,10 @@ async function addProvider() {
|
|
|
845
981
|
allProviders = await getAllProviders();
|
|
846
982
|
filteredProviders = allProviders;
|
|
847
983
|
} catch (error) {
|
|
984
|
+
clearScreen();
|
|
985
|
+
showHeader();
|
|
986
|
+
console.log(colorText('Add Verified Provider', 'magenta'));
|
|
987
|
+
console.log('');
|
|
848
988
|
console.log(colorText('Error loading providers: ', 'red') + error.message);
|
|
849
989
|
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
850
990
|
return;
|
|
@@ -860,19 +1000,19 @@ async function addProvider() {
|
|
|
860
1000
|
screenContent += colorText('Note: opencode uses ai-sdk', 'dim') + '\n';
|
|
861
1001
|
screenContent += '\n';
|
|
862
1002
|
|
|
863
|
-
screenContent += colorText('Add Provider', 'magenta') + '\n';
|
|
1003
|
+
screenContent += colorText('Add Verified Provider', 'magenta') + '\n';
|
|
864
1004
|
screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
|
|
865
1005
|
screenContent += colorText('Type to search (real-time filtering)', 'cyan') + '\n';
|
|
866
1006
|
screenContent += colorText('Navigation is circular', 'dim') + '\n';
|
|
867
1007
|
screenContent += '\n';
|
|
868
1008
|
|
|
869
1009
|
// Search interface - always visible
|
|
870
|
-
screenContent += colorText('Search: ', 'yellow') + colorText(searchQuery + '_', 'bright') + '\n';
|
|
1010
|
+
screenContent += colorText('🔍 Search: ', 'yellow') + colorText(searchQuery + '_', 'bright') + '\n';
|
|
871
1011
|
screenContent += '\n';
|
|
872
1012
|
|
|
873
1013
|
// Calculate pagination
|
|
874
|
-
const visibleItemsCount = getVisibleItemsCount();
|
|
875
|
-
const totalItems = filteredProviders.length
|
|
1014
|
+
const visibleItemsCount = getVisibleItemsCount(11); // Account for search bar and header
|
|
1015
|
+
const totalItems = filteredProviders.length;
|
|
876
1016
|
const totalPages = Math.ceil(totalItems / visibleItemsCount);
|
|
877
1017
|
|
|
878
1018
|
// Ensure current page is valid
|
|
@@ -883,7 +1023,7 @@ async function addProvider() {
|
|
|
883
1023
|
const endIndex = Math.min(startIndex + visibleItemsCount, totalItems);
|
|
884
1024
|
|
|
885
1025
|
// Display providers with pagination
|
|
886
|
-
screenContent += colorText('Available Providers:', 'cyan') + '\n';
|
|
1026
|
+
screenContent += colorText('Available Verified Providers:', 'cyan') + '\n';
|
|
887
1027
|
screenContent += '\n';
|
|
888
1028
|
|
|
889
1029
|
// Show current page of providers
|
|
@@ -897,16 +1037,6 @@ async function addProvider() {
|
|
|
897
1037
|
screenContent += `${indicator} ${providerName} ${providerType}\n`;
|
|
898
1038
|
}
|
|
899
1039
|
|
|
900
|
-
// Show "Add Custom Provider" option if it's on current page
|
|
901
|
-
const customIndex = filteredProviders.length;
|
|
902
|
-
if (customIndex >= startIndex && customIndex < endIndex) {
|
|
903
|
-
const isCustomCurrent = customIndex === currentIndex;
|
|
904
|
-
const customIndicator = isCustomCurrent ? colorText('●', 'green') : colorText('○', 'dim');
|
|
905
|
-
const customText = isCustomCurrent ? colorText('Add Custom Provider', 'bright') : colorText('Add Custom Provider', 'yellow');
|
|
906
|
-
|
|
907
|
-
screenContent += `${customIndicator} ${customText}\n`;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
1040
|
// Show pagination info
|
|
911
1041
|
screenContent += '\n';
|
|
912
1042
|
if (totalPages > 1) {
|
|
@@ -959,14 +1089,8 @@ async function addProvider() {
|
|
|
959
1089
|
currentIndex = currentPage * visibleItemsCount;
|
|
960
1090
|
}
|
|
961
1091
|
} else if (key === '\r') {
|
|
962
|
-
// Enter - select current
|
|
963
|
-
|
|
964
|
-
// Custom provider selected
|
|
965
|
-
await addCustomProvider();
|
|
966
|
-
} else {
|
|
967
|
-
// Verified provider selected - auto-add all models
|
|
968
|
-
await addVerifiedProviderAuto(filteredProviders[currentIndex]);
|
|
969
|
-
}
|
|
1092
|
+
// Enter - select current provider
|
|
1093
|
+
await addVerifiedProviderAuto(filteredProviders[currentIndex]);
|
|
970
1094
|
break;
|
|
971
1095
|
} else if (key === '\u0003') {
|
|
972
1096
|
// Ctrl+C
|
|
@@ -1036,18 +1160,26 @@ async function addVerifiedProviderAuto(provider) {
|
|
|
1036
1160
|
return;
|
|
1037
1161
|
}
|
|
1038
1162
|
|
|
1039
|
-
// Add API key to
|
|
1040
|
-
const
|
|
1163
|
+
// Add API key to auth.json (for opencode integration)
|
|
1164
|
+
const authSuccess = await addApiKey(provider.id, apiKey);
|
|
1041
1165
|
|
|
1042
|
-
if (!
|
|
1043
|
-
console.log(colorText('Failed to save API key to
|
|
1166
|
+
if (!authSuccess) {
|
|
1167
|
+
console.log(colorText('Failed to save API key to auth.json', 'red'));
|
|
1044
1168
|
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
1045
1169
|
return;
|
|
1046
1170
|
}
|
|
1047
1171
|
|
|
1172
|
+
// Also add to ai-benchmark-config.json for consistency
|
|
1173
|
+
const { addVerifiedProvider: addVerifiedProviderToConfig } = await import('./ai-config.js');
|
|
1174
|
+
const configSuccess = await addVerifiedProviderToConfig(provider.id, apiKey);
|
|
1175
|
+
|
|
1176
|
+
if (!configSuccess) {
|
|
1177
|
+
console.log(colorText('Warning: Could not save to ai-benchmark-config.json', 'yellow'));
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1048
1180
|
console.log('');
|
|
1049
1181
|
console.log(colorText('Provider added successfully!', 'green'));
|
|
1050
|
-
console.log(colorText(`API key saved to
|
|
1182
|
+
console.log(colorText(`API key saved to auth.json`, 'cyan'));
|
|
1051
1183
|
console.log(colorText(`Models will be loaded dynamically from ${provider.name}`, 'cyan'));
|
|
1052
1184
|
console.log(colorText(`Found ${models.length} available models`, 'cyan'));
|
|
1053
1185
|
|
|
@@ -1056,19 +1188,19 @@ async function addVerifiedProviderAuto(provider) {
|
|
|
1056
1188
|
|
|
1057
1189
|
|
|
1058
1190
|
|
|
1059
|
-
// Add a custom provider (now
|
|
1060
|
-
async function
|
|
1191
|
+
// Add a custom provider (now using ai-benchmark-config.json)
|
|
1192
|
+
async function addCustomProviderCLI() {
|
|
1061
1193
|
clearScreen();
|
|
1062
1194
|
showHeader();
|
|
1063
1195
|
console.log(colorText('Add Custom Provider', 'magenta'));
|
|
1064
1196
|
console.log('');
|
|
1065
|
-
console.log(colorText('Note: Custom providers are saved to
|
|
1197
|
+
console.log(colorText('Note: Custom providers are saved to ai-benchmark-config.json', 'cyan'));
|
|
1066
1198
|
console.log('');
|
|
1067
1199
|
|
|
1068
1200
|
const providerOptions = [
|
|
1069
1201
|
{ id: 1, text: 'OpenAI Compatible', type: 'openai-compatible' },
|
|
1070
1202
|
{ id: 2, text: 'Anthropic', type: 'anthropic' },
|
|
1071
|
-
{ id: 3, text: 'Back to
|
|
1203
|
+
{ id: 3, text: 'Back to Custom Models menu', action: 'back' }
|
|
1072
1204
|
];
|
|
1073
1205
|
|
|
1074
1206
|
let currentIndex = 0;
|
|
@@ -1125,178 +1257,108 @@ async function addCustomProvider() {
|
|
|
1125
1257
|
|
|
1126
1258
|
if (selectedChoice.action === 'back') return;
|
|
1127
1259
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1260
|
+
const providerId = await question(colorText('Enter provider ID (e.g., my-openai): ', 'cyan'));
|
|
1261
|
+
const name = await question(colorText('Enter provider name (e.g., MyOpenAI): ', 'cyan'));
|
|
1262
|
+
const baseUrl = await question(colorText('Enter base URL (e.g., https://api.openai.com/v1): ', 'cyan'));
|
|
1263
|
+
const apiKey = await question(colorText('Enter API key: ', 'cyan'));
|
|
1264
|
+
|
|
1265
|
+
// Ask if user wants to add multiple models
|
|
1266
|
+
console.log('');
|
|
1267
|
+
console.log(colorText('Do you want to add multiple models?', 'cyan'));
|
|
1268
|
+
console.log(colorText('1. Add single model', 'yellow'));
|
|
1269
|
+
console.log(colorText('2. Add multiple models', 'yellow'));
|
|
1270
|
+
|
|
1271
|
+
const modelChoice = await question(colorText('Enter choice (1 or 2): ', 'cyan'));
|
|
1272
|
+
|
|
1273
|
+
let models = [];
|
|
1274
|
+
|
|
1275
|
+
if (modelChoice === '2') {
|
|
1276
|
+
// Multiple models mode
|
|
1136
1277
|
console.log('');
|
|
1137
|
-
console.log(colorText('
|
|
1138
|
-
console.log(colorText('
|
|
1139
|
-
console.log(colorText('2. Add multiple models', 'yellow'));
|
|
1140
|
-
|
|
1141
|
-
const modelChoice = await question(colorText('Enter choice (1 or 2): ', 'cyan'));
|
|
1142
|
-
|
|
1143
|
-
let models = {};
|
|
1144
|
-
|
|
1145
|
-
if (modelChoice === '2') {
|
|
1146
|
-
// Multiple models mode
|
|
1147
|
-
console.log('');
|
|
1148
|
-
console.log(colorText('Enter model names (one per line, empty line to finish):', 'cyan'));
|
|
1149
|
-
console.log(colorText('Examples: gpt-4, gpt-4-turbo, gpt-3.5-turbo', 'dim'));
|
|
1150
|
-
console.log('');
|
|
1151
|
-
|
|
1152
|
-
while (true) {
|
|
1153
|
-
const modelName = await question(colorText('Model name: ', 'cyan'));
|
|
1154
|
-
if (!modelName.trim()) break;
|
|
1155
|
-
|
|
1156
|
-
const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
1157
|
-
models[modelId] = {
|
|
1158
|
-
name: modelName.trim()
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
} else {
|
|
1162
|
-
// Single model mode
|
|
1163
|
-
const modelName = await question(colorText('Enter model name (e.g., gpt-4): ', 'cyan'));
|
|
1164
|
-
const modelId = modelName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
1165
|
-
models[modelId] = {
|
|
1166
|
-
name: modelName
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
if (Object.keys(models).length === 0) {
|
|
1171
|
-
console.log(colorText('At least one model is required.', 'red'));
|
|
1172
|
-
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Create opencode.json format
|
|
1177
|
-
const { readOpencodeConfig } = await import('./opencode-integration.js');
|
|
1178
|
-
const config = await readOpencodeConfig();
|
|
1179
|
-
|
|
1180
|
-
config.provider = config.provider || {};
|
|
1181
|
-
config.provider[providerId] = {
|
|
1182
|
-
name,
|
|
1183
|
-
options: {
|
|
1184
|
-
apiKey,
|
|
1185
|
-
baseURL: baseUrl
|
|
1186
|
-
},
|
|
1187
|
-
models
|
|
1188
|
-
};
|
|
1189
|
-
|
|
1190
|
-
// Save to opencode.json using the integration module
|
|
1191
|
-
const { writeOpencodeConfig } = await import('./opencode-integration.js');
|
|
1192
|
-
const success = await writeOpencodeConfig(config);
|
|
1193
|
-
|
|
1194
|
-
if (!success) {
|
|
1195
|
-
console.log(colorText('Warning: Could not save to opencode.json', 'yellow'));
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
console.log(colorText('Provider added successfully!', 'green'));
|
|
1199
|
-
console.log(colorText(`Added ${Object.keys(models).length} model(s)`, 'cyan'));
|
|
1200
|
-
console.log(colorText(`Saved to opencode.json`, 'cyan'));
|
|
1201
|
-
|
|
1202
|
-
} else if (selectedChoice.type === 'anthropic') {
|
|
1203
|
-
// Anthropic
|
|
1204
|
-
const providerId = await question(colorText('Enter provider ID (e.g., my-anthropic): ', 'cyan'));
|
|
1205
|
-
const name = await question(colorText('Enter provider name (e.g., MyAnthropic): ', 'cyan'));
|
|
1206
|
-
const baseUrl = await question(colorText('Enter base URL (e.g., https://api.anthropic.com): ', 'cyan'));
|
|
1207
|
-
const apiKey = await question(colorText('Enter Anthropic API key: ', 'cyan'));
|
|
1208
|
-
|
|
1209
|
-
// Ask if user wants to add multiple models
|
|
1278
|
+
console.log(colorText('Enter model names (one per line, empty line to finish):', 'cyan'));
|
|
1279
|
+
console.log(colorText('Examples: gpt-4, gpt-4-turbo, gpt-3.5-turbo', 'dim'));
|
|
1210
1280
|
console.log('');
|
|
1211
|
-
console.log(colorText('Do you want to add multiple models?', 'cyan'));
|
|
1212
|
-
console.log(colorText('1. Add single model', 'yellow'));
|
|
1213
|
-
console.log(colorText('2. Add multiple models', 'yellow'));
|
|
1214
|
-
|
|
1215
|
-
const modelChoice = await question(colorText('Enter choice (1 or 2): ', 'cyan'));
|
|
1216
1281
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
// Multiple models mode
|
|
1221
|
-
console.log('');
|
|
1222
|
-
console.log(colorText('Enter model names (one per line, empty line to finish):', 'cyan'));
|
|
1223
|
-
console.log(colorText('Examples: claude-3-sonnet-20240229, claude-3-haiku-20240307', 'dim'));
|
|
1224
|
-
console.log('');
|
|
1282
|
+
while (true) {
|
|
1283
|
+
const modelName = await question(colorText('Model name: ', 'cyan'));
|
|
1284
|
+
if (!modelName.trim()) break;
|
|
1225
1285
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
models[modelId] = {
|
|
1232
|
-
name: modelName.trim()
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
} else {
|
|
1236
|
-
// Single model mode
|
|
1237
|
-
const modelName = await question(colorText('Enter model name (e.g., claude-3-sonnet-20240229): ', 'cyan'));
|
|
1238
|
-
const modelId = modelName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
1239
|
-
models[modelId] = {
|
|
1240
|
-
name: modelName
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
if (Object.keys(models).length === 0) {
|
|
1245
|
-
console.log(colorText('At least one model is required.', 'red'));
|
|
1246
|
-
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
1247
|
-
return;
|
|
1286
|
+
const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') + '_' + Date.now();
|
|
1287
|
+
models.push({
|
|
1288
|
+
name: modelName.trim(),
|
|
1289
|
+
id: modelId
|
|
1290
|
+
});
|
|
1248
1291
|
}
|
|
1249
|
-
|
|
1250
|
-
//
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
apiKey,
|
|
1259
|
-
baseURL: baseUrl
|
|
1260
|
-
},
|
|
1261
|
-
models
|
|
1262
|
-
};
|
|
1263
|
-
|
|
1264
|
-
// Save to opencode.json using the integration module
|
|
1265
|
-
const { writeOpencodeConfig } = await import('./opencode-integration.js');
|
|
1266
|
-
const success = await writeOpencodeConfig(config);
|
|
1267
|
-
|
|
1268
|
-
if (!success) {
|
|
1269
|
-
console.log(colorText('Warning: Could not save to opencode.json', 'yellow'));
|
|
1292
|
+
} else {
|
|
1293
|
+
// Single model mode
|
|
1294
|
+
const modelName = await question(colorText('Enter model name (e.g., gpt-4): ', 'cyan'));
|
|
1295
|
+
if (modelName.trim()) {
|
|
1296
|
+
const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') + '_' + Date.now();
|
|
1297
|
+
models.push({
|
|
1298
|
+
name: modelName.trim(),
|
|
1299
|
+
id: modelId
|
|
1300
|
+
});
|
|
1270
1301
|
}
|
|
1271
|
-
|
|
1272
|
-
console.log(colorText('Provider added successfully!', 'green'));
|
|
1273
|
-
console.log(colorText(`Added ${Object.keys(models).length} model(s)`, 'cyan'));
|
|
1274
|
-
console.log(colorText(`Saved to opencode.json`, 'cyan'));
|
|
1275
1302
|
}
|
|
1276
1303
|
|
|
1304
|
+
if (models.length === 0) {
|
|
1305
|
+
console.log(colorText('At least one model is required.', 'red'));
|
|
1306
|
+
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Create provider data for ai-benchmark-config.json format
|
|
1311
|
+
const providerData = {
|
|
1312
|
+
id: providerId,
|
|
1313
|
+
name: name,
|
|
1314
|
+
type: selectedChoice.type,
|
|
1315
|
+
baseUrl: baseUrl,
|
|
1316
|
+
apiKey: apiKey,
|
|
1317
|
+
models: models
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
// Save to ai-benchmark-config.json
|
|
1321
|
+
const success = await addCustomProvider(providerData);
|
|
1322
|
+
|
|
1323
|
+
if (!success) {
|
|
1324
|
+
console.log(colorText('Failed to save custom provider.', 'red'));
|
|
1325
|
+
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
console.log(colorText('Custom provider added successfully!', 'green'));
|
|
1330
|
+
console.log(colorText(`Added ${models.length} model(s)`, 'cyan'));
|
|
1331
|
+
console.log(colorText(`Saved to ai-benchmark-config.json`, 'cyan'));
|
|
1332
|
+
|
|
1277
1333
|
await question(colorText('\nPress Enter to continue...', 'yellow'));
|
|
1278
1334
|
}
|
|
1279
1335
|
|
|
1280
|
-
// Show debug information about
|
|
1336
|
+
// Show debug information about config system
|
|
1281
1337
|
async function showDebugInfo() {
|
|
1282
1338
|
clearScreen();
|
|
1283
1339
|
showHeader();
|
|
1284
|
-
console.log(colorText('
|
|
1340
|
+
console.log(colorText('Config System Debug Info', 'magenta'));
|
|
1285
1341
|
console.log('');
|
|
1286
1342
|
|
|
1287
1343
|
const debugInfo = await getDebugInfo();
|
|
1288
1344
|
|
|
1289
|
-
console.log(colorText('
|
|
1290
|
-
console.log(colorText(` auth.json: ${debugInfo.
|
|
1291
|
-
console.log(colorText(` opencode.json: ${debugInfo.
|
|
1345
|
+
console.log(colorText('OpenCode Paths (deprecated):', 'cyan'));
|
|
1346
|
+
console.log(colorText(` auth.json: ${debugInfo.opencodePaths.authJson}`, 'white'));
|
|
1347
|
+
console.log(colorText(` opencode.json: ${debugInfo.opencodePaths.opencodeJson}`, 'white'));
|
|
1348
|
+
console.log('');
|
|
1349
|
+
|
|
1350
|
+
console.log(colorText('AI Speedometer Config Paths:', 'cyan'));
|
|
1351
|
+
console.log(colorText(` ai-benchmark-config.json: ${debugInfo.aiConfigPaths.configJson}`, 'white'));
|
|
1352
|
+
console.log(colorText(` Config directory: ${debugInfo.aiConfigPaths.configDir}`, 'white'));
|
|
1292
1353
|
console.log('');
|
|
1293
1354
|
|
|
1294
1355
|
console.log(colorText('File Status:', 'cyan'));
|
|
1295
1356
|
console.log(colorText(` auth.json exists: ${debugInfo.authExists ? 'Yes' : 'No'}`, 'white'));
|
|
1296
|
-
console.log(colorText(` opencode.json exists: ${debugInfo.configExists ? '
|
|
1357
|
+
console.log(colorText(` opencode.json exists: ${debugInfo.configExists ? 'No' : 'No'}`, 'white'));
|
|
1358
|
+
console.log(colorText(` ai-benchmark-config.json exists: ${debugInfo.aiConfigPaths.configExists ? 'Yes' : 'No'}`, 'white'));
|
|
1297
1359
|
console.log('');
|
|
1298
1360
|
|
|
1299
|
-
console.log(colorText('Authenticated Providers:', 'cyan'));
|
|
1361
|
+
console.log(colorText('Authenticated Providers (auth.json):', 'cyan'));
|
|
1300
1362
|
if (debugInfo.authData.length === 0) {
|
|
1301
1363
|
console.log(colorText(' None', 'dim'));
|
|
1302
1364
|
} else {
|
|
@@ -1306,17 +1368,27 @@ async function showDebugInfo() {
|
|
|
1306
1368
|
}
|
|
1307
1369
|
console.log('');
|
|
1308
1370
|
|
|
1309
|
-
console.log(colorText('
|
|
1310
|
-
if (debugInfo.
|
|
1371
|
+
console.log(colorText('Verified Providers (ai-benchmark-config.json):', 'cyan'));
|
|
1372
|
+
if (debugInfo.aiConfigData.verifiedProviders.length === 0) {
|
|
1373
|
+
console.log(colorText(' None', 'dim'));
|
|
1374
|
+
} else {
|
|
1375
|
+
debugInfo.aiConfigData.verifiedProviders.forEach(provider => {
|
|
1376
|
+
console.log(colorText(` - ${provider}`, 'white'));
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
console.log('');
|
|
1380
|
+
|
|
1381
|
+
console.log(colorText('Custom Providers (ai-benchmark-config.json):', 'cyan'));
|
|
1382
|
+
if (debugInfo.aiConfigData.customProviders.length === 0) {
|
|
1311
1383
|
console.log(colorText(' None', 'dim'));
|
|
1312
1384
|
} else {
|
|
1313
|
-
debugInfo.
|
|
1385
|
+
debugInfo.aiConfigData.customProviders.forEach(provider => {
|
|
1314
1386
|
console.log(colorText(` - ${provider}`, 'white'));
|
|
1315
1387
|
});
|
|
1316
1388
|
}
|
|
1317
1389
|
console.log('');
|
|
1318
1390
|
|
|
1319
|
-
console.log(colorText('XDG Paths:', 'cyan'));
|
|
1391
|
+
console.log(colorText('XDG Paths (for OpenCode):', 'cyan'));
|
|
1320
1392
|
console.log(colorText(` Data: ${debugInfo.xdgPaths.data}`, 'white'));
|
|
1321
1393
|
console.log(colorText(` Config: ${debugInfo.xdgPaths.config}`, 'white'));
|
|
1322
1394
|
console.log('');
|
|
@@ -1335,35 +1407,129 @@ async function listProviders() {
|
|
|
1335
1407
|
if (config.providers.length === 0) {
|
|
1336
1408
|
console.log(colorText('No providers configured yet.', 'yellow'));
|
|
1337
1409
|
} else {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
console.log(colorText(' Models:', 'dim'));
|
|
1343
|
-
provider.models.forEach((model, modelIndex) => {
|
|
1344
|
-
console.log(colorText(` ${modelIndex + 1}. ${model.name}`, 'yellow'));
|
|
1345
|
-
});
|
|
1346
|
-
} else {
|
|
1347
|
-
console.log(colorText(' Models: None', 'dim'));
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
console.log('');
|
|
1410
|
+
// Separate verified and custom providers
|
|
1411
|
+
const verifiedProviders = config.providers.filter(p => {
|
|
1412
|
+
// Verified providers come from auth.json via models.dev
|
|
1413
|
+
return p.baseUrl && p.baseUrl.includes('api.'); // Simple heuristic
|
|
1351
1414
|
});
|
|
1415
|
+
|
|
1416
|
+
const customProviders = config.providers.filter(p => {
|
|
1417
|
+
return !verifiedProviders.includes(p);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// Show verified providers
|
|
1421
|
+
if (verifiedProviders.length > 0) {
|
|
1422
|
+
console.log(colorText('Verified Providers (from models.dev):', 'green'));
|
|
1423
|
+
verifiedProviders.forEach((provider, index) => {
|
|
1424
|
+
console.log(colorText(`${index + 1}. ${provider.name} (${provider.type})`, 'cyan'));
|
|
1425
|
+
|
|
1426
|
+
if (provider.models.length > 0) {
|
|
1427
|
+
console.log(colorText(' Models:', 'dim'));
|
|
1428
|
+
provider.models.forEach((model, modelIndex) => {
|
|
1429
|
+
console.log(colorText(` ${modelIndex + 1}. ${model.name}`, 'yellow'));
|
|
1430
|
+
});
|
|
1431
|
+
} else {
|
|
1432
|
+
console.log(colorText(' Models: None', 'dim'));
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
console.log('');
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// Show custom providers
|
|
1440
|
+
if (customProviders.length > 0) {
|
|
1441
|
+
console.log(colorText('Custom Providers:', 'magenta'));
|
|
1442
|
+
customProviders.forEach((provider, index) => {
|
|
1443
|
+
console.log(colorText(`${index + 1}. ${provider.name} (${provider.type})`, 'cyan'));
|
|
1444
|
+
|
|
1445
|
+
if (provider.models.length > 0) {
|
|
1446
|
+
console.log(colorText(' Models:', 'dim'));
|
|
1447
|
+
provider.models.forEach((model, modelIndex) => {
|
|
1448
|
+
console.log(colorText(` ${modelIndex + 1}. ${model.name}`, 'yellow'));
|
|
1449
|
+
});
|
|
1450
|
+
} else {
|
|
1451
|
+
console.log(colorText(' Models: None', 'dim'));
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
console.log('');
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1352
1457
|
}
|
|
1353
1458
|
|
|
1354
1459
|
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
1355
1460
|
}
|
|
1356
1461
|
|
|
1357
|
-
|
|
1462
|
+
// Add Custom Models submenu
|
|
1463
|
+
async function addCustomModelsMenu() {
|
|
1464
|
+
const menuOptions = [
|
|
1465
|
+
{ id: 1, text: 'Add Models to Existing Provider', action: () => addModelsToExistingProvider() },
|
|
1466
|
+
{ id: 2, text: 'Add Custom Provider', action: () => addCustomProviderCLI() },
|
|
1467
|
+
{ id: 3, text: 'Back to Model Management', action: () => 'back' }
|
|
1468
|
+
];
|
|
1469
|
+
|
|
1470
|
+
let currentIndex = 0;
|
|
1471
|
+
|
|
1472
|
+
while (true) {
|
|
1473
|
+
// Build screen content in memory (double buffering)
|
|
1474
|
+
let screenContent = '';
|
|
1475
|
+
|
|
1476
|
+
// Add header
|
|
1477
|
+
screenContent += colorText('Ai-speedometer', 'cyan') + '\n';
|
|
1478
|
+
screenContent += colorText('=============================', 'cyan') + '\n';
|
|
1479
|
+
screenContent += colorText('Note: opencode uses ai-sdk', 'dim') + '\n';
|
|
1480
|
+
screenContent += '\n';
|
|
1481
|
+
|
|
1482
|
+
screenContent += colorText('Add Custom Models', 'magenta') + '\n';
|
|
1483
|
+
screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
|
|
1484
|
+
screenContent += colorText('Navigation is circular', 'dim') + '\n';
|
|
1485
|
+
screenContent += '\n';
|
|
1486
|
+
|
|
1487
|
+
// Display menu options
|
|
1488
|
+
menuOptions.forEach((option, index) => {
|
|
1489
|
+
const isCurrent = index === currentIndex;
|
|
1490
|
+
const indicator = isCurrent ? colorText('●', 'green') : colorText('○', 'dim');
|
|
1491
|
+
const optionText = isCurrent ? colorText(option.text, 'bright') : colorText(option.text, 'yellow');
|
|
1492
|
+
|
|
1493
|
+
screenContent += `${indicator} ${optionText}\n`;
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
// Clear screen and output entire buffer at once
|
|
1497
|
+
clearScreen();
|
|
1498
|
+
console.log(screenContent);
|
|
1499
|
+
|
|
1500
|
+
const key = await getKeyPress();
|
|
1501
|
+
|
|
1502
|
+
if (key === '\u001b[A') {
|
|
1503
|
+
// Up arrow - circular navigation
|
|
1504
|
+
currentIndex = (currentIndex - 1 + menuOptions.length) % menuOptions.length;
|
|
1505
|
+
} else if (key === '\u001b[B') {
|
|
1506
|
+
// Down arrow - circular navigation
|
|
1507
|
+
currentIndex = (currentIndex + 1) % menuOptions.length;
|
|
1508
|
+
} else if (key === '\r') {
|
|
1509
|
+
// Enter - select current option
|
|
1510
|
+
const result = await menuOptions[currentIndex].action();
|
|
1511
|
+
if (result === 'back') {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
} else if (key === '\u0003') {
|
|
1515
|
+
// Ctrl+C
|
|
1516
|
+
process.exit(0);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Add models to existing custom provider
|
|
1522
|
+
async function addModelsToExistingProvider() {
|
|
1358
1523
|
clearScreen();
|
|
1359
1524
|
showHeader();
|
|
1360
|
-
console.log(colorText('Add
|
|
1525
|
+
console.log(colorText('Add Models to Existing Provider', 'magenta'));
|
|
1361
1526
|
console.log('');
|
|
1362
1527
|
|
|
1363
|
-
|
|
1528
|
+
// Get custom providers from config
|
|
1529
|
+
const customProviders = await getCustomProvidersFromConfig();
|
|
1364
1530
|
|
|
1365
|
-
if (
|
|
1366
|
-
console.log(colorText('No providers available. Please add a provider first.', 'red'));
|
|
1531
|
+
if (customProviders.length === 0) {
|
|
1532
|
+
console.log(colorText('No custom providers available. Please add a custom provider first.', 'red'));
|
|
1367
1533
|
await question(colorText('Press Enter to continue...', 'yellow'));
|
|
1368
1534
|
return;
|
|
1369
1535
|
}
|
|
@@ -1380,21 +1546,22 @@ async function addModelToProvider() {
|
|
|
1380
1546
|
screenContent += colorText('Note: opencode uses ai-sdk', 'dim') + '\n';
|
|
1381
1547
|
screenContent += '\n';
|
|
1382
1548
|
|
|
1383
|
-
screenContent += colorText('Add
|
|
1549
|
+
screenContent += colorText('Add Models to Existing Provider', 'magenta') + '\n';
|
|
1384
1550
|
screenContent += colorText('Use ↑↓ arrows to navigate, ENTER to select', 'cyan') + '\n';
|
|
1385
1551
|
screenContent += colorText('Navigation is circular', 'dim') + '\n';
|
|
1386
1552
|
screenContent += '\n';
|
|
1387
1553
|
|
|
1388
|
-
screenContent += colorText('Select provider:', 'cyan') + '\n';
|
|
1554
|
+
screenContent += colorText('Select custom provider:', 'cyan') + '\n';
|
|
1389
1555
|
screenContent += '\n';
|
|
1390
1556
|
|
|
1391
|
-
// Display providers with arrow key navigation
|
|
1392
|
-
|
|
1557
|
+
// Display custom providers with arrow key navigation
|
|
1558
|
+
customProviders.forEach((provider, index) => {
|
|
1393
1559
|
const isCurrent = index === currentIndex;
|
|
1394
1560
|
const indicator = isCurrent ? colorText('●', 'green') : colorText('○', 'dim');
|
|
1395
1561
|
const providerName = isCurrent ? colorText(provider.name, 'bright') : colorText(provider.name, 'yellow');
|
|
1562
|
+
const providerType = isCurrent ? colorText(`(${provider.type})`, 'cyan') : colorText(`(${provider.type})`, 'dim');
|
|
1396
1563
|
|
|
1397
|
-
screenContent += `${indicator} ${providerName}\n`;
|
|
1564
|
+
screenContent += `${indicator} ${providerName} ${providerType}\n`;
|
|
1398
1565
|
});
|
|
1399
1566
|
|
|
1400
1567
|
// Clear screen and output entire buffer at once
|
|
@@ -1405,10 +1572,10 @@ async function addModelToProvider() {
|
|
|
1405
1572
|
|
|
1406
1573
|
if (key === '\u001b[A') {
|
|
1407
1574
|
// Up arrow - circular navigation
|
|
1408
|
-
currentIndex = (currentIndex - 1 +
|
|
1575
|
+
currentIndex = (currentIndex - 1 + customProviders.length) % customProviders.length;
|
|
1409
1576
|
} else if (key === '\u001b[B') {
|
|
1410
1577
|
// Down arrow - circular navigation
|
|
1411
|
-
currentIndex = (currentIndex + 1) %
|
|
1578
|
+
currentIndex = (currentIndex + 1) % customProviders.length;
|
|
1412
1579
|
} else if (key === '\r') {
|
|
1413
1580
|
// Enter - select current provider
|
|
1414
1581
|
break;
|
|
@@ -1418,31 +1585,73 @@ async function addModelToProvider() {
|
|
|
1418
1585
|
}
|
|
1419
1586
|
}
|
|
1420
1587
|
|
|
1421
|
-
const provider =
|
|
1422
|
-
const modelName = await question(colorText('Enter new model name: ', 'cyan'));
|
|
1588
|
+
const provider = customProviders[currentIndex];
|
|
1423
1589
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1590
|
+
console.log('');
|
|
1591
|
+
console.log(colorText('Selected provider: ', 'cyan') + colorText(provider.name, 'white'));
|
|
1592
|
+
console.log('');
|
|
1593
|
+
|
|
1594
|
+
// Ask if user wants to add multiple models
|
|
1595
|
+
console.log(colorText('Do you want to add multiple models?', 'cyan'));
|
|
1596
|
+
console.log(colorText('1. Add single model', 'yellow'));
|
|
1597
|
+
console.log(colorText('2. Add multiple models', 'yellow'));
|
|
1598
|
+
|
|
1599
|
+
const modelChoice = await question(colorText('Enter choice (1 or 2): ', 'cyan'));
|
|
1600
|
+
|
|
1601
|
+
let modelsAdded = 0;
|
|
1602
|
+
|
|
1603
|
+
if (modelChoice === '2') {
|
|
1604
|
+
// Multiple models mode
|
|
1605
|
+
console.log('');
|
|
1606
|
+
console.log(colorText('Enter model names (one per line, empty line to finish):', 'cyan'));
|
|
1607
|
+
console.log(colorText('Examples: gpt-4, gpt-4-turbo, gpt-3.5-turbo', 'dim'));
|
|
1608
|
+
console.log('');
|
|
1609
|
+
|
|
1610
|
+
while (true) {
|
|
1611
|
+
const modelName = await question(colorText('Model name: ', 'cyan'));
|
|
1612
|
+
if (!modelName.trim()) break;
|
|
1613
|
+
|
|
1614
|
+
const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') + '_' + Date.now();
|
|
1615
|
+
const modelData = {
|
|
1616
|
+
name: modelName.trim(),
|
|
1617
|
+
id: modelId
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
const success = await addModelToCustomProvider(provider.id, modelData);
|
|
1621
|
+
if (success) {
|
|
1622
|
+
modelsAdded++;
|
|
1623
|
+
console.log(colorText(`✓ Added model: ${modelName.trim()}`, 'green'));
|
|
1624
|
+
} else {
|
|
1625
|
+
console.log(colorText(`✗ Failed to add model: ${modelName.trim()}`, 'red'));
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1431
1628
|
} else {
|
|
1432
|
-
//
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1629
|
+
// Single model mode
|
|
1630
|
+
const modelName = await question(colorText('Enter model name: ', 'cyan'));
|
|
1631
|
+
if (modelName.trim()) {
|
|
1632
|
+
const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') + '_' + Date.now();
|
|
1633
|
+
const modelData = {
|
|
1634
|
+
name: modelName.trim(),
|
|
1635
|
+
id: modelId
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
const success = await addModelToCustomProvider(provider.id, modelData);
|
|
1639
|
+
if (success) {
|
|
1640
|
+
modelsAdded = 1;
|
|
1641
|
+
console.log(colorText(`✓ Added model: ${modelName.trim()}`, 'green'));
|
|
1642
|
+
} else {
|
|
1643
|
+
console.log(colorText(`✗ Failed to add model: ${modelName.trim()}`, 'red'));
|
|
1644
|
+
}
|
|
1440
1645
|
}
|
|
1441
|
-
config.customProviders.push(provider);
|
|
1442
1646
|
}
|
|
1443
1647
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1648
|
+
if (modelsAdded > 0) {
|
|
1649
|
+
console.log('');
|
|
1650
|
+
console.log(colorText(`Successfully added ${modelsAdded} model(s) to ${provider.name}`, 'green'));
|
|
1651
|
+
} else {
|
|
1652
|
+
console.log(colorText('No models were added.', 'yellow'));
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1446
1655
|
await question(colorText('\nPress Enter to continue...', 'yellow'));
|
|
1447
1656
|
}
|
|
1448
1657
|
|
|
@@ -1622,7 +1831,27 @@ async function runRestApiBenchmark(models) {
|
|
|
1622
1831
|
console.log('');
|
|
1623
1832
|
console.log(colorText('All REST API benchmarks completed!', 'green'));
|
|
1624
1833
|
|
|
1625
|
-
await displayColorfulResults(results, 'REST API');
|
|
1834
|
+
await displayColorfulResults(results, 'REST API', models);
|
|
1835
|
+
|
|
1836
|
+
// Add successful models to recent models list
|
|
1837
|
+
const successfulModels = results
|
|
1838
|
+
.filter(r => r.success)
|
|
1839
|
+
.map(r => {
|
|
1840
|
+
// Find the actual model object that matches this benchmark result
|
|
1841
|
+
const modelObj = models.find(model =>
|
|
1842
|
+
model.name === r.model && model.providerName === r.provider
|
|
1843
|
+
);
|
|
1844
|
+
|
|
1845
|
+
return {
|
|
1846
|
+
modelId: modelObj ? modelObj.id : r.model, // Use actual ID if found, fallback to name
|
|
1847
|
+
modelName: r.model,
|
|
1848
|
+
providerName: r.provider
|
|
1849
|
+
};
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
if (successfulModels.length > 0) {
|
|
1853
|
+
await addToRecentModels(successfulModels);
|
|
1854
|
+
}
|
|
1626
1855
|
}
|
|
1627
1856
|
|
|
1628
1857
|
// Main menu with arrow key navigation
|
|
@@ -1698,9 +1927,9 @@ async function showMainMenu() {
|
|
|
1698
1927
|
|
|
1699
1928
|
async function showModelMenu() {
|
|
1700
1929
|
const menuOptions = [
|
|
1701
|
-
{ id: 1, text: 'Add Provider', action: () =>
|
|
1702
|
-
{ id: 2, text: '
|
|
1703
|
-
{ id: 3, text: '
|
|
1930
|
+
{ id: 1, text: 'Add Verified Provider', action: () => addVerifiedProvider() },
|
|
1931
|
+
{ id: 2, text: 'Add Custom Models', action: () => addCustomModelsMenu() },
|
|
1932
|
+
{ id: 3, text: 'List Existing Providers', action: () => listProviders() },
|
|
1704
1933
|
{ id: 4, text: 'Debug Info', action: () => showDebugInfo() },
|
|
1705
1934
|
{ id: 5, text: 'Back to Main Menu', action: () => 'back' }
|
|
1706
1935
|
];
|
|
@@ -1767,7 +1996,13 @@ process.on('SIGINT', () => {
|
|
|
1767
1996
|
if (import.meta.url === `file://${process.argv[1]}` ||
|
|
1768
1997
|
process.argv.length === 2 ||
|
|
1769
1998
|
(process.argv.length === 3 && process.argv[2] === '--debug')) {
|
|
1770
|
-
|
|
1999
|
+
|
|
2000
|
+
// Clean up recent models from main config and migrate to cache on startup
|
|
2001
|
+
cleanupRecentModelsFromConfig().then(() => {
|
|
2002
|
+
showMainMenu();
|
|
2003
|
+
}).catch(() => {
|
|
2004
|
+
showMainMenu();
|
|
2005
|
+
});
|
|
1771
2006
|
}
|
|
1772
2007
|
|
|
1773
|
-
export { showMainMenu,
|
|
2008
|
+
export { showMainMenu, listProviders, selectModelsCircular, runStreamingBenchmark, loadConfig, saveConfig };
|