ai-speedometer 1.3.5 → 1.3.7

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.
Files changed (3) hide show
  1. package/cli.js +125 -21
  2. package/dist/ai-speedometer +70 -70
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -34,7 +34,10 @@ function parseCliArgs() {
34
34
  const parsed = {
35
35
  debug: false,
36
36
  bench: null,
37
+ benchCustom: null,
37
38
  apiKey: null,
39
+ baseUrl: null,
40
+ endpointFormat: null,
38
41
  useAiSdk: false,
39
42
  formatted: false,
40
43
  help: false
@@ -47,8 +50,14 @@ function parseCliArgs() {
47
50
  parsed.debug = true;
48
51
  } else if (arg === '--bench') {
49
52
  parsed.bench = args[++i];
53
+ } else if (arg === '--bench-custom') {
54
+ parsed.benchCustom = args[++i];
50
55
  } else if (arg === '--api-key') {
51
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];
52
61
  } else if (arg === '--ai-sdk') {
53
62
  parsed.useAiSdk = true;
54
63
  } else if (arg === '--formatted') {
@@ -58,7 +67,7 @@ function parseCliArgs() {
58
67
  }
59
68
  }
60
69
 
61
- return parsed;
70
+ return parsed;
62
71
  }
63
72
 
64
73
  function showHelp() {
@@ -67,23 +76,65 @@ function showHelp() {
67
76
  console.log(colorText('Usage:', 'yellow'));
68
77
  console.log(' ai-speedometer ' + colorText('# Interactive mode', 'dim'));
69
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'));
70
80
  console.log('');
71
81
  console.log(colorText('Options:', 'yellow'));
72
- console.log(' --bench <provider:model> ' + colorText('Run benchmark in headless mode', 'dim'));
73
- console.log(' --api-key <key> ' + colorText('Override API key (optional)', 'dim'));
74
- console.log(' --ai-sdk ' + colorText('Use AI SDK instead of REST API', 'dim'));
75
- console.log(' --formatted ' + colorText('Format JSON output for human readability', 'dim'));
76
- console.log(' --debug ' + colorText('Enable debug logging', 'dim'));
77
- console.log(' --help, -h ' + colorText('Show this help message', 'dim'));
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'));
78
91
  console.log('');
79
92
  console.log(colorText('Examples:', 'yellow'));
80
93
  console.log(' ai-speedometer --bench openai:gpt-4');
81
94
  console.log(' ai-speedometer --bench anthropic:claude-3-opus --api-key "sk-..."');
82
- console.log(' ai-speedometer --bench openai:gpt-4 --ai-sdk');
83
- console.log(' ai-speedometer --bench openai:gpt-4 --formatted');
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"');
84
97
  console.log('');
85
98
  }
86
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
+
87
138
  const cliArgs = parseCliArgs();
88
139
  const debugMode = cliArgs.debug;
89
140
  let logFile = null;
@@ -1350,7 +1401,7 @@ async function addCustomProviderCLI() {
1350
1401
  const modelName = await question(colorText('Model name: ', 'cyan'));
1351
1402
  if (!modelName.trim()) break;
1352
1403
 
1353
- const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') + '_' + Date.now();
1404
+ const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
1354
1405
  models.push({
1355
1406
  name: modelName.trim(),
1356
1407
  id: modelId
@@ -1360,7 +1411,7 @@ async function addCustomProviderCLI() {
1360
1411
  // Single model mode
1361
1412
  const modelName = await question(colorText('Enter model name (e.g., gpt-4): ', 'cyan'));
1362
1413
  if (modelName.trim()) {
1363
- const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') + '_' + Date.now();
1414
+ const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
1364
1415
  models.push({
1365
1416
  name: modelName.trim(),
1366
1417
  id: modelId
@@ -1676,7 +1727,7 @@ async function addModelsToExistingProvider() {
1676
1727
  const modelName = await question(colorText('Model name: ', 'cyan'));
1677
1728
  if (!modelName.trim()) break;
1678
1729
 
1679
- const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') + '_' + Date.now();
1730
+ const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
1680
1731
  const modelData = {
1681
1732
  name: modelName.trim(),
1682
1733
  id: modelId
@@ -1694,7 +1745,7 @@ async function addModelsToExistingProvider() {
1694
1745
  // Single model mode
1695
1746
  const modelName = await question(colorText('Enter model name: ', 'cyan'));
1696
1747
  if (modelName.trim()) {
1697
- const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') + '_' + Date.now();
1748
+ const modelId = modelName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
1698
1749
  const modelData = {
1699
1750
  name: modelName.trim(),
1700
1751
  id: modelId
@@ -1746,9 +1797,12 @@ async function benchmarkSingleModelRest(model) {
1746
1797
  let streamedText = '';
1747
1798
  let tokenCount = 0;
1748
1799
 
1749
- // Use correct endpoint based on provider type
1800
+ // Use correct endpoint based on provider type or custom format
1750
1801
  let endpoint;
1751
- if (model.providerType === 'anthropic') {
1802
+ if (model.providerConfig.endpointFormat) {
1803
+ // Use custom endpoint format
1804
+ endpoint = '/' + model.providerConfig.endpointFormat;
1805
+ } else if (model.providerType === 'anthropic') {
1752
1806
  endpoint = '/messages';
1753
1807
  } else if (model.providerType === 'google') {
1754
1808
  endpoint = '/models/' + actualModelId + ':streamGenerateContent';
@@ -1834,7 +1888,7 @@ async function benchmarkSingleModelRest(model) {
1834
1888
  isFirstChunk = false;
1835
1889
  // Show live TTFT result (only in interactive mode, not headless)
1836
1890
  const ttftSeconds = ((firstTokenTime - startTime) / 1000).toFixed(2);
1837
- if (!cliArgs.bench) {
1891
+ if (!cliArgs.bench && !cliArgs.benchCustom) {
1838
1892
  console.log(colorText(`TTFT received at ${ttftSeconds}s for ${model.name}`, 'green'));
1839
1893
  }
1840
1894
  }
@@ -2172,9 +2226,55 @@ process.on('SIGINT', () => {
2172
2226
  });
2173
2227
 
2174
2228
  // Headless benchmark mode
2175
- async function runHeadlessBenchmark(benchSpec, apiKey, useAiSdk) {
2229
+ async function runHeadlessBenchmark(benchSpec, apiKey, useAiSdk, cliArgs = null) {
2176
2230
  try {
2177
- // Parse provider:model format, handling quoted model IDs
2231
+ // Check if this is a custom provider benchmark
2232
+ if (cliArgs && cliArgs.benchCustom) {
2233
+ // Handle custom provider
2234
+ const customProvider = createCustomProviderFromCli(cliArgs);
2235
+
2236
+ // Create model object for benchmarking
2237
+ const modelConfig = {
2238
+ ...customProvider.models[0],
2239
+ providerName: customProvider.name,
2240
+ providerType: customProvider.type,
2241
+ providerId: customProvider.id,
2242
+ providerConfig: {
2243
+ baseUrl: customProvider.baseUrl,
2244
+ apiKey: customProvider.apiKey,
2245
+ endpointFormat: customProvider.endpointFormat
2246
+ },
2247
+ selected: true
2248
+ };
2249
+
2250
+ // Run benchmark silently and get results
2251
+ const result = await benchmarkSingleModelRest(modelConfig);
2252
+
2253
+ // Output JSON to stdout (same format as regular benchmarks)
2254
+ const jsonOutput = {
2255
+ provider: customProvider.name,
2256
+ providerId: customProvider.id,
2257
+ model: customProvider.models[0].name,
2258
+ modelId: customProvider.models[0].id,
2259
+ method: 'rest-api',
2260
+ success: result.success,
2261
+ totalTime: result.totalTime,
2262
+ totalTimeSeconds: result.totalTime / 1000,
2263
+ timeToFirstToken: result.timeToFirstToken,
2264
+ timeToFirstTokenSeconds: result.timeToFirstToken / 1000,
2265
+ tokensPerSecond: result.tokensPerSecond,
2266
+ outputTokens: result.tokenCount,
2267
+ promptTokens: result.promptTokens,
2268
+ totalTokens: result.totalTokens,
2269
+ is_estimated: !!(result.usedEstimateForOutput || result.usedEstimateForInput),
2270
+ error: result.error || null
2271
+ };
2272
+
2273
+ console.log(JSON.stringify(jsonOutput, null, cliArgs.formatted ? 2 : 0));
2274
+ process.exit(result.success ? 0 : 1);
2275
+ }
2276
+
2277
+ // Handle regular provider:model format
2178
2278
  let providerSpec, modelName;
2179
2279
  const colonIndex = benchSpec.indexOf(':');
2180
2280
  if (colonIndex === -1) {
@@ -2323,16 +2423,20 @@ async function runHeadlessBenchmark(benchSpec, apiKey, useAiSdk) {
2323
2423
  }
2324
2424
 
2325
2425
  // Start the CLI
2326
- if (require.main === module) {
2426
+ if (typeof require !== 'undefined' && require.main === module) {
2327
2427
  // Check if help flag
2328
2428
  if (cliArgs.help) {
2329
2429
  showHelp();
2330
2430
  process.exit(0);
2331
2431
  }
2332
2432
 
2433
+ // Check if custom provider benchmark mode
2434
+ if (cliArgs.benchCustom) {
2435
+ runHeadlessBenchmark(cliArgs.benchCustom, cliArgs.apiKey, cliArgs.useAiSdk, cliArgs);
2436
+ }
2333
2437
  // Check if headless benchmark mode
2334
- if (cliArgs.bench) {
2335
- runHeadlessBenchmark(cliArgs.bench, cliArgs.apiKey, cliArgs.useAiSdk);
2438
+ else if (cliArgs.bench) {
2439
+ runHeadlessBenchmark(cliArgs.bench, cliArgs.apiKey, cliArgs.useAiSdk, null);
2336
2440
  } else {
2337
2441
  // Interactive mode
2338
2442
  cleanupRecentModelsFromConfig().then(() => {