ai-speedometer 1.3.4 → 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 +150 -28
  2. package/dist/ai-speedometer +94 -94
  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,10 +2226,71 @@ 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
2178
- const [providerSpec, modelName] = benchSpec.split(':');
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
2278
+ let providerSpec, modelName;
2279
+ const colonIndex = benchSpec.indexOf(':');
2280
+ if (colonIndex === -1) {
2281
+ console.error(colorText('Error: Invalid --bench format. Use: provider:model', 'red'));
2282
+ console.error(colorText('Example: --bench zai-code-anth:glm-4.6', 'yellow'));
2283
+ process.exit(1);
2284
+ }
2285
+
2286
+ providerSpec = benchSpec.substring(0, colonIndex);
2287
+ modelName = benchSpec.substring(colonIndex + 1);
2288
+
2289
+ // Remove quotes from model name if present
2290
+ if ((modelName.startsWith('"') && modelName.endsWith('"')) ||
2291
+ (modelName.startsWith("'") && modelName.endsWith("'"))) {
2292
+ modelName = modelName.slice(1, -1);
2293
+ }
2179
2294
 
2180
2295
  if (!providerSpec || !modelName) {
2181
2296
  console.error(colorText('Error: Invalid --bench format. Use: provider:model', 'red'));
@@ -2202,19 +2317,22 @@ async function runHeadlessBenchmark(benchSpec, apiKey, useAiSdk) {
2202
2317
  }
2203
2318
 
2204
2319
  // Find the model
2205
- // Model IDs are prefixed with provider name (e.g., "zai-code-anth_glm-4.6")
2206
- // So we need to check:
2207
- // 1. Full ID match: "zai-code-anth_glm-4.6"
2208
- // 2. ID without provider prefix: "glm-4.6"
2209
- // 3. Name match: "GLM-4.6-anth"
2320
+ // First try exact match with the provided model ID
2321
+ // Then fall back to legacy matching for compatibility
2210
2322
  const model = provider.models.find(m => {
2211
2323
  const modelIdLower = m.id?.toLowerCase() || '';
2212
2324
  const modelNameLower = m.name?.toLowerCase() || '';
2213
2325
  const searchLower = modelName.toLowerCase();
2214
2326
 
2215
- // Check full ID match
2327
+ // Exact ID match first (for quoted model IDs like "hf:moonshotai/Kimi-K2-Instruct-0905")
2216
2328
  if (modelIdLower === searchLower) return true;
2217
2329
 
2330
+ // Legacy matching for backward compatibility:
2331
+ // Model IDs are prefixed with provider name (e.g., "zai-code-anth_glm-4.6")
2332
+ // So we need to check:
2333
+ // 1. ID without provider prefix: "glm-4.6"
2334
+ // 2. Name match: "GLM-4.6-anth"
2335
+
2218
2336
  // Check ID without provider prefix (strip "provider_" prefix)
2219
2337
  const idWithoutPrefix = modelIdLower.includes('_')
2220
2338
  ? modelIdLower.split('_').slice(1).join('_')
@@ -2305,16 +2423,20 @@ async function runHeadlessBenchmark(benchSpec, apiKey, useAiSdk) {
2305
2423
  }
2306
2424
 
2307
2425
  // Start the CLI
2308
- if (require.main === module) {
2426
+ if (typeof require !== 'undefined' && require.main === module) {
2309
2427
  // Check if help flag
2310
2428
  if (cliArgs.help) {
2311
2429
  showHelp();
2312
2430
  process.exit(0);
2313
2431
  }
2314
2432
 
2433
+ // Check if custom provider benchmark mode
2434
+ if (cliArgs.benchCustom) {
2435
+ runHeadlessBenchmark(cliArgs.benchCustom, cliArgs.apiKey, cliArgs.useAiSdk, cliArgs);
2436
+ }
2315
2437
  // Check if headless benchmark mode
2316
- if (cliArgs.bench) {
2317
- runHeadlessBenchmark(cliArgs.bench, cliArgs.apiKey, cliArgs.useAiSdk);
2438
+ else if (cliArgs.bench) {
2439
+ runHeadlessBenchmark(cliArgs.bench, cliArgs.apiKey, cliArgs.useAiSdk, null);
2318
2440
  } else {
2319
2441
  // Interactive mode
2320
2442
  cleanupRecentModelsFromConfig().then(() => {