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.
- package/cli.js +150 -28
- package/dist/ai-speedometer +94 -94
- 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
|
-
|
|
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>
|
|
73
|
-
console.log(' --
|
|
74
|
-
console.log(' --
|
|
75
|
-
console.log(' --
|
|
76
|
-
console.log(' --
|
|
77
|
-
console.log(' --
|
|
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 --
|
|
83
|
-
console.log(' ai-speedometer --bench
|
|
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, '-')
|
|
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, '-')
|
|
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, '-')
|
|
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, '-')
|
|
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.
|
|
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
|
-
//
|
|
2178
|
-
|
|
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
|
-
//
|
|
2206
|
-
//
|
|
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
|
-
//
|
|
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(() => {
|