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/README.md +50 -17
- package/dist/ai-speedometer +58866 -156
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/docs/README.md +26 -0
- package/package.json +13 -23
- package/cli.js +0 -2460
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 };
|