ai-speedometer 2.1.0 → 2.1.2

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.
@@ -1431,7 +1431,7 @@ var getXDGPaths = () => ({
1431
1431
  const errors = [];
1432
1432
  const parsed = parse2(data, errors, { allowTrailingComma: true });
1433
1433
  if (errors.length > 0) {
1434
- console.warn("Warning: JSONC parsing errors in auth.json");
1434
+ console.warn("Warning: JSONC parsing errors in auth.json:", errors.map((e) => e.error).join(", "));
1435
1435
  return parsed || {};
1436
1436
  }
1437
1437
  return parsed;
@@ -1475,7 +1475,9 @@ var getXDGPaths = () => ({
1475
1475
  provider: { ...merged.provider ?? {}, ...parsed.provider ?? {} }
1476
1476
  };
1477
1477
  }
1478
- } catch {}
1478
+ } catch (error) {
1479
+ console.warn(`Warning: Could not read opencode config file ${filename}:`, error.message);
1480
+ }
1479
1481
  }
1480
1482
  return merged;
1481
1483
  }, getOpencodeGlobalConfigProviders = async () => {
@@ -1938,7 +1940,8 @@ async function loadConfig(includeAll) {
1938
1940
  try {
1939
1941
  const providers = await getAllAvailableProviders(includeAll);
1940
1942
  return { providers };
1941
- } catch {
1943
+ } catch (error) {
1944
+ console.error("Error: Failed to load providers:", error.message);
1942
1945
  return { providers: [] };
1943
1946
  }
1944
1947
  }
@@ -1981,6 +1984,8 @@ async function runHeadlessBenchmark(cliArgs) {
1981
1984
  }
1982
1985
  };
1983
1986
  const result2 = await benchmarkSingleModelRest(modelConfig2);
1987
+ if (!result2.success && result2.error)
1988
+ console.error(`Error: Benchmark failed: ${result2.error}`);
1984
1989
  console.log(buildJsonOutput(customProvider.name, customProvider.id, modelDef.name, modelDef.id, result2, cliArgs.formatted));
1985
1990
  process.exit(result2.success ? 0 : 1);
1986
1991
  }
@@ -2048,6 +2053,8 @@ async function runHeadlessBenchmark(cliArgs) {
2048
2053
  }
2049
2054
  };
2050
2055
  const result = await benchmarkSingleModelRest(modelConfig);
2056
+ if (!result.success && result.error)
2057
+ console.error(`Error: Benchmark failed: ${result.error}`);
2051
2058
  console.log(buildJsonOutput(provider.name, provider.id, model.name, model.id, result, cliArgs.formatted));
2052
2059
  process.exit(result.success ? 0 : 1);
2053
2060
  } catch (error) {
@@ -2154,11 +2161,12 @@ var package_default;
2154
2161
  var init_package = __esm(() => {
2155
2162
  package_default = {
2156
2163
  name: "ai-speedometer",
2157
- version: "2.1.0",
2164
+ version: "2.1.2",
2158
2165
  description: "A comprehensive CLI tool for benchmarking AI models across multiple providers with parallel execution and professional metrics",
2159
2166
  bin: {
2160
2167
  "ai-speedometer": "dist/ai-speedometer",
2161
- aispeed: "dist/ai-speedometer"
2168
+ aispeed: "dist/ai-speedometer",
2169
+ "ai-speedometer-headless": "dist/ai-speedometer-headless"
2162
2170
  },
2163
2171
  engines: {
2164
2172
  bun: ">=1.0.0",
@@ -2171,7 +2179,7 @@ var init_package = __esm(() => {
2171
2179
  "test:watch": "bun test --watch",
2172
2180
  "test:update": "bun test --update-snapshots",
2173
2181
  typecheck: "bun tsc --noEmit",
2174
- build: "bun build src/index.ts --outdir dist --target bun --external '@opentui/core' --external '@opentui/react' --external 'react' --external 'react-reconciler' && cat scripts/shebang dist/index.js > dist/ai-speedometer && chmod +x dist/ai-speedometer && rm dist/index.js",
2182
+ build: "bun build src/index.ts --outdir dist --target bun --external '@opentui/core' --external '@opentui/react' --external 'react' --external 'react-reconciler' && cat scripts/shebang dist/index.js > dist/ai-speedometer && chmod +x dist/ai-speedometer && rm dist/index.js && bun build src/headless-entry.ts --outdir dist --target node --external 'jsonc-parser' && mv dist/headless-entry.js dist/ai-speedometer-headless && chmod +x dist/ai-speedometer-headless",
2175
2183
  prepublishOnly: "bun run build"
2176
2184
  },
2177
2185
  keywords: [
@@ -0,0 +1,941 @@
1
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
2
+
3
+ // src/ai-config.ts
4
+ import fs2 from "fs";
5
+ import path2 from "path";
6
+ import { homedir as homedir2 } from "os";
7
+ var getAIConfigPaths = () => {
8
+ const aiConfigDir = process.env.XDG_CONFIG_HOME || path2.join(homedir2(), ".config");
9
+ const aiSpeedometerConfigDir = path2.join(aiConfigDir, "ai-speedometer");
10
+ return {
11
+ configDir: aiSpeedometerConfigDir,
12
+ configJson: path2.join(aiSpeedometerConfigDir, "ai-benchmark-config.json"),
13
+ recentModelsCache: path2.join(aiSpeedometerConfigDir, "recent-models.json")
14
+ };
15
+ }, readAIConfig = async () => {
16
+ const { configJson } = getAIConfigPaths();
17
+ try {
18
+ if (!fs2.existsSync(configJson)) {
19
+ return { verifiedProviders: {}, customProviders: [] };
20
+ }
21
+ const data = fs2.readFileSync(configJson, "utf8");
22
+ return JSON.parse(data);
23
+ } catch (error) {
24
+ console.warn("Warning: Could not read ai-benchmark-config.json:", error.message);
25
+ return { verifiedProviders: {}, customProviders: [] };
26
+ }
27
+ }, getCustomProvidersFromConfig = async () => {
28
+ const config = await readAIConfig();
29
+ return config.customProviders || [];
30
+ };
31
+ var init_ai_config = () => {};
32
+
33
+ // src/opencode-integration.ts
34
+ import fs3 from "fs";
35
+ import path3 from "path";
36
+ import { homedir as homedir3 } from "os";
37
+
38
+ // src/models-dev.ts
39
+ import fs from "fs";
40
+ import path from "path";
41
+ import { homedir } from "os";
42
+
43
+ // custom-verified-providers.json
44
+ var custom_verified_providers_default = {
45
+ "custom-verified-providers": {
46
+ "zai-code-anth": {
47
+ id: "zai-code-anth",
48
+ name: "zai-code-anth",
49
+ baseUrl: "https://api.z.ai/api/anthropic/v1",
50
+ type: "anthropic",
51
+ models: {
52
+ "glm-4-5": {
53
+ id: "glm-4.5",
54
+ name: "GLM-4.5-anth"
55
+ },
56
+ "glm-4-5-air": {
57
+ id: "glm-4.5-air",
58
+ name: "GLM-4.5-air-anth"
59
+ },
60
+ "glm-4-6": {
61
+ id: "glm-4.6",
62
+ name: "GLM-4.6-anth"
63
+ }
64
+ }
65
+ },
66
+ "zai-china-anth-base": {
67
+ id: "zai-china-anth-base",
68
+ name: "zai-china-anth-base",
69
+ baseUrl: "https://open.bigmodel.cn/api/anthropic/v1",
70
+ type: "anthropic",
71
+ models: {
72
+ "glm-4-5": {
73
+ id: "glm-4.5",
74
+ name: "GLM-4.5-china-anth"
75
+ },
76
+ "glm-4-5-air": {
77
+ id: "glm-4.5-air",
78
+ name: "GLM-4.5-air-china-anth"
79
+ },
80
+ "glm-4-6": {
81
+ id: "glm-4.6",
82
+ name: "GLM-4.6-china-anth"
83
+ }
84
+ }
85
+ },
86
+ "extra-models-dev": {
87
+ chutes: {
88
+ "deepseek-ai/DeepSeek-V3.1-Terminus": {
89
+ id: "deepseek-ai/DeepSeek-V3.1-Terminus",
90
+ name: "DeepSeek V3.1 Terminus"
91
+ },
92
+ "meituan-longcat/LongCat-Flash-Thinking-FP8": {
93
+ id: "meituan-longcat/LongCat-Flash-Thinking-FP8",
94
+ name: "LongCat Flash Thinking FP8"
95
+ }
96
+ }
97
+ },
98
+ "nanogpt-plan": {
99
+ id: "nanogpt-plan",
100
+ name: "nanogpt-plan",
101
+ baseUrl: "https://nano-gpt.com/api/v1",
102
+ type: "openai-compatible",
103
+ models: {
104
+ "deepseek-ai/DeepSeek-V3.1-Terminus": {
105
+ id: "deepseek-ai/DeepSeek-V3.1-Terminus",
106
+ name: "DeepSeek V3.1 Terminus"
107
+ },
108
+ "deepseek-ai/DeepSeek-V3.1": {
109
+ id: "deepseek-ai/DeepSeek-V3.1",
110
+ name: "DeepSeek V3.1"
111
+ },
112
+ "deepseek-ai/DeepSeek-V3.1-Terminus:thinking": {
113
+ id: "deepseek-ai/DeepSeek-V3.1-Terminus:thinking",
114
+ name: "DeepSeek V3.1 Terminus Thinking"
115
+ },
116
+ "zai-org/GLM-4.5-FP8": {
117
+ id: "zai-org/GLM-4.5-FP8",
118
+ name: "GLM 4.5 FP8"
119
+ },
120
+ "zai-org/GLM-4.5-FP8:thinking": {
121
+ id: "zai-org/GLM-4.5-FP8:thinking",
122
+ name: "GLM 4.5 FP8 Thinking"
123
+ },
124
+ "zai-org/GLM-4.5-Air": {
125
+ id: "zai-org/GLM-4.5-Air",
126
+ name: "GLM 4.5 Air"
127
+ },
128
+ "moonshotai/Kimi-K2-Instruct": {
129
+ id: "moonshotai/Kimi-K2-Instruct",
130
+ name: "Kimi K2 Instruct"
131
+ },
132
+ "moonshotai/Kimi-K2-Instruct-0905": {
133
+ id: "moonshotai/Kimi-K2-Instruct-0905",
134
+ name: "Kimi K2 Instruct 0905"
135
+ },
136
+ "moonshotai/kimi-k2-thinking": {
137
+ id: "moonshotai/kimi-k2-thinking",
138
+ name: "Kimi K2 Thinking"
139
+ },
140
+ "deepseek-ai/deepseek-v3.2-exp": {
141
+ id: "deepseek-ai/deepseek-v3.2-exp",
142
+ name: "DeepSeek V3.2 Exp"
143
+ },
144
+ "z-ai/glm-4.6": {
145
+ id: "z-ai/glm-4.6",
146
+ name: "GLM 4.6"
147
+ },
148
+ "z-ai/glm-4.6:thinking": {
149
+ id: "z-ai/glm-4.6:thinking",
150
+ name: "GLM 4.6 Thinking"
151
+ },
152
+ "qwen3-vl-235b-a22b-instruct": {
153
+ id: "qwen3-vl-235b-a22b-instruct",
154
+ name: "Qwen3 VL 235B A22B Instruct"
155
+ },
156
+ "MiniMax-M2": {
157
+ id: "MiniMax-M2",
158
+ name: "MiniMax-M2"
159
+ }
160
+ }
161
+ }
162
+ }
163
+ };
164
+
165
+ // src/models-dev.ts
166
+ var CACHE_DIR = path.join(homedir(), ".cache", "ai-speedometer");
167
+ var CACHE_FILE = path.join(CACHE_DIR, "models.json");
168
+ var FALLBACK_PROVIDERS = [
169
+ {
170
+ id: "openai",
171
+ name: "OpenAI",
172
+ baseUrl: "https://api.openai.com/v1",
173
+ type: "openai-compatible",
174
+ models: [
175
+ { id: "gpt-4o", name: "GPT-4o" },
176
+ { id: "gpt-4o-mini", name: "GPT-4o Mini" },
177
+ { id: "gpt-4-turbo", name: "GPT-4 Turbo" },
178
+ { id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" }
179
+ ]
180
+ },
181
+ {
182
+ id: "anthropic",
183
+ name: "Anthropic",
184
+ baseUrl: "https://api.anthropic.com",
185
+ type: "anthropic",
186
+ models: [
187
+ { id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet" },
188
+ { id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku" },
189
+ { id: "claude-3-opus-20240229", name: "Claude 3 Opus" }
190
+ ]
191
+ },
192
+ {
193
+ id: "openrouter",
194
+ name: "OpenRouter",
195
+ baseUrl: "https://openrouter.ai/api/v1",
196
+ type: "openai-compatible",
197
+ models: [
198
+ { id: "anthropic/claude-3.5-sonnet", name: "Claude 3.5 Sonnet" },
199
+ { id: "openai/gpt-4o", name: "GPT-4o" },
200
+ { id: "openai/gpt-4o-mini", name: "GPT-4o Mini" }
201
+ ]
202
+ }
203
+ ];
204
+ function ensureCacheDir() {
205
+ try {
206
+ if (!fs.existsSync(CACHE_DIR)) {
207
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
208
+ }
209
+ } catch (error) {
210
+ console.warn("Warning: Could not create cache directory:", error.message);
211
+ }
212
+ }
213
+ function isCacheExpired(cacheData) {
214
+ if (!cacheData.timestamp)
215
+ return true;
216
+ return Date.now() - cacheData.timestamp > 60 * 60 * 1000;
217
+ }
218
+ function loadCache() {
219
+ try {
220
+ if (fs.existsSync(CACHE_FILE)) {
221
+ const data = fs.readFileSync(CACHE_FILE, "utf8");
222
+ const parsed = JSON.parse(data);
223
+ if (isCacheExpired(parsed))
224
+ return null;
225
+ return parsed;
226
+ }
227
+ } catch (error) {
228
+ console.warn("Warning: Could not load cache:", error.message);
229
+ }
230
+ return null;
231
+ }
232
+ function saveCache(data) {
233
+ try {
234
+ ensureCacheDir();
235
+ const cacheData = { ...data, timestamp: Date.now() };
236
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cacheData, null, 2));
237
+ } catch (error) {
238
+ console.warn("Warning: Could not save cache:", error.message);
239
+ }
240
+ }
241
+ async function fetchFromAPI() {
242
+ try {
243
+ const response = await fetch("https://models.dev/api.json");
244
+ if (!response.ok)
245
+ throw new Error(`HTTP error! status: ${response.status}`);
246
+ return await response.json();
247
+ } catch (error) {
248
+ console.warn("Warning: Could not fetch from models.dev API:", error.message);
249
+ return null;
250
+ }
251
+ }
252
+ function getCustomProvidersJson() {
253
+ let data = custom_verified_providers_default;
254
+ const customProvidersPath = path.join(process.cwd(), "custom-verified-providers.json");
255
+ if (fs.existsSync(customProvidersPath)) {
256
+ try {
257
+ data = JSON.parse(fs.readFileSync(customProvidersPath, "utf8"));
258
+ } catch (fileError) {
259
+ console.warn("Warning: Could not load custom providers from file, using embedded data:", fileError.message);
260
+ }
261
+ }
262
+ return data;
263
+ }
264
+ function loadCustomVerifiedProviders() {
265
+ try {
266
+ const customProviders = getCustomProvidersJson();
267
+ const providers = [];
268
+ if (customProviders["custom-verified-providers"]) {
269
+ for (const [, providerData] of Object.entries(customProviders["custom-verified-providers"])) {
270
+ const entry = providerData;
271
+ if (entry.id === "extra-models-dev")
272
+ continue;
273
+ if (entry.id && entry.name && entry.models) {
274
+ providers.push({
275
+ id: entry.id,
276
+ name: entry.name,
277
+ baseUrl: entry.baseUrl || "",
278
+ type: entry.type || "openai-compatible",
279
+ models: Object.values(entry.models).map((m) => ({ id: m.id, name: m.name }))
280
+ });
281
+ }
282
+ }
283
+ }
284
+ return providers;
285
+ } catch (error) {
286
+ console.warn("Warning: Could not load custom verified providers:", error.message);
287
+ return [];
288
+ }
289
+ }
290
+ function loadExtraModels() {
291
+ try {
292
+ const customProviders = getCustomProvidersJson();
293
+ const extraModels = {};
294
+ const section = customProviders["custom-verified-providers"]?.["extra-models-dev"];
295
+ if (section) {
296
+ for (const [providerId, models] of Object.entries(section)) {
297
+ extraModels[providerId] = Object.values(models).map((m) => ({ id: m.id, name: m.name }));
298
+ }
299
+ }
300
+ return extraModels;
301
+ } catch (error) {
302
+ console.warn("Warning: Could not load extra models:", error.message);
303
+ return {};
304
+ }
305
+ }
306
+ function transformModelsDevData(apiData) {
307
+ const providers = [];
308
+ const customProviders = loadCustomVerifiedProviders();
309
+ providers.push(...customProviders);
310
+ const extraModels = loadExtraModels();
311
+ if (apiData) {
312
+ for (const [, providerData] of Object.entries(apiData)) {
313
+ if (providerData.id && providerData.name && providerData.models) {
314
+ const models = Object.values(providerData.models).map((m) => ({ id: m.id, name: m.name }));
315
+ if (extraModels[providerData.id]) {
316
+ models.push(...extraModels[providerData.id]);
317
+ }
318
+ providers.push({
319
+ id: providerData.id,
320
+ name: providerData.name,
321
+ baseUrl: providerData.api || providerData.baseUrl || "",
322
+ type: providerData.npm ? providerData.npm.includes("anthropic") ? "anthropic" : "openai-compatible" : "openai-compatible",
323
+ models
324
+ });
325
+ }
326
+ }
327
+ }
328
+ return providers.length === 0 ? FALLBACK_PROVIDERS : providers;
329
+ }
330
+ async function getAllProviders() {
331
+ const cachedData = loadCache();
332
+ if (cachedData?.providers) {
333
+ const customVerifiedProviders = loadCustomVerifiedProviders();
334
+ const existingIds = new Set(cachedData.providers.map((p) => p.id));
335
+ const missing = customVerifiedProviders.filter((p) => !existingIds.has(p.id));
336
+ if (missing.length > 0)
337
+ cachedData.providers.push(...missing);
338
+ return cachedData.providers;
339
+ }
340
+ const apiData = await fetchFromAPI();
341
+ if (apiData) {
342
+ const transformed = transformModelsDevData(apiData);
343
+ saveCache({ providers: transformed });
344
+ return transformed;
345
+ }
346
+ const fallback = [...FALLBACK_PROVIDERS];
347
+ const extraModels = loadExtraModels();
348
+ const customVerified = loadCustomVerifiedProviders();
349
+ fallback.forEach((p) => {
350
+ if (extraModels[p.id])
351
+ p.models.push(...extraModels[p.id]);
352
+ });
353
+ fallback.push(...customVerified);
354
+ return fallback;
355
+ }
356
+
357
+ // src/opencode-integration.ts
358
+ init_ai_config();
359
+ import { parse as parseJsonc } from "jsonc-parser";
360
+ var getXDGPaths = () => ({
361
+ data: path3.join(process.env.XDG_DATA_HOME || path3.join(homedir3(), ".local", "share"), "opencode"),
362
+ config: path3.join(process.env.XDG_CONFIG_HOME || path3.join(homedir3(), ".config"), "opencode")
363
+ });
364
+ var getFilePaths = () => {
365
+ const paths = getXDGPaths();
366
+ return {
367
+ authJson: path3.join(paths.data, "auth.json"),
368
+ opencodeJson: path3.join(paths.config, "opencode.json")
369
+ };
370
+ };
371
+ var readAuthJson = async () => {
372
+ const { authJson } = getFilePaths();
373
+ try {
374
+ if (!fs3.existsSync(authJson))
375
+ return {};
376
+ const data = fs3.readFileSync(authJson, "utf8");
377
+ const errors = [];
378
+ const parsed = parseJsonc(data, errors, { allowTrailingComma: true });
379
+ if (errors.length > 0) {
380
+ console.warn("Warning: JSONC parsing errors in auth.json:", errors.map((e) => e.error).join(", "));
381
+ return parsed || {};
382
+ }
383
+ return parsed;
384
+ } catch (error) {
385
+ console.warn("Warning: Could not read auth.json:", error.message);
386
+ return {};
387
+ }
388
+ };
389
+ var readOpencodeGlobalConfig = () => {
390
+ const configDir = path3.join(process.env.XDG_CONFIG_HOME || path3.join(homedir3(), ".config"), "opencode");
391
+ const candidates = ["config.json", "opencode.json", "opencode.jsonc"];
392
+ let merged = {};
393
+ for (const filename of candidates) {
394
+ const filePath = path3.join(configDir, filename);
395
+ try {
396
+ if (!fs3.existsSync(filePath))
397
+ continue;
398
+ const text = fs3.readFileSync(filePath, "utf8");
399
+ const errors = [];
400
+ const parsed = parseJsonc(text, errors, { allowTrailingComma: true });
401
+ if (parsed && typeof parsed === "object") {
402
+ merged = {
403
+ ...merged,
404
+ ...parsed,
405
+ provider: { ...merged.provider ?? {}, ...parsed.provider ?? {} }
406
+ };
407
+ }
408
+ } catch (error) {
409
+ console.warn(`Warning: Could not read opencode config file ${filename}:`, error.message);
410
+ }
411
+ }
412
+ return merged;
413
+ };
414
+ var getAuthenticatedProviders = async () => {
415
+ try {
416
+ const [allModelsDevProviders, authData, globalConfig] = await Promise.all([
417
+ getAllProviders(),
418
+ readAuthJson(),
419
+ Promise.resolve(readOpencodeGlobalConfig())
420
+ ]);
421
+ const database = new Map;
422
+ for (const mdProvider of allModelsDevProviders) {
423
+ const modelMap = new Map;
424
+ for (const m of mdProvider.models) {
425
+ modelMap.set(`${mdProvider.id}_${m.id}`, { id: `${mdProvider.id}_${m.id}`, name: m.name });
426
+ }
427
+ database.set(mdProvider.id, {
428
+ id: mdProvider.id,
429
+ name: mdProvider.name,
430
+ type: mdProvider.type,
431
+ baseUrl: mdProvider.baseUrl,
432
+ models: modelMap
433
+ });
434
+ }
435
+ for (const [providerID, entry] of Object.entries(globalConfig.provider ?? {})) {
436
+ const existing = database.get(providerID);
437
+ const modelMap = existing ? new Map(existing.models) : new Map;
438
+ for (const [modelKey, m] of Object.entries(entry.models ?? {})) {
439
+ const resolvedId = `${providerID}_${m.id ?? modelKey}`;
440
+ modelMap.set(resolvedId, { id: resolvedId, name: m.name ?? m.id ?? modelKey });
441
+ }
442
+ database.set(providerID, {
443
+ id: providerID,
444
+ name: entry.name ?? existing?.name ?? providerID,
445
+ type: existing?.type ?? "openai-compatible",
446
+ baseUrl: entry.options?.baseURL ?? entry.api ?? existing?.baseUrl ?? "",
447
+ models: modelMap,
448
+ npm: entry.npm
449
+ });
450
+ }
451
+ const providerMap = new Map;
452
+ for (const [providerID, authInfo] of Object.entries(authData)) {
453
+ if (authInfo.type !== "api" || !authInfo.key)
454
+ continue;
455
+ const dbEntry = database.get(providerID);
456
+ if (!dbEntry)
457
+ continue;
458
+ const configEntry = globalConfig.provider?.[providerID];
459
+ const npm = dbEntry.npm ?? configEntry?.npm;
460
+ const type = npm?.includes("anthropic") ? "anthropic" : dbEntry.type;
461
+ providerMap.set(providerID, {
462
+ id: providerID,
463
+ name: dbEntry.name,
464
+ type,
465
+ baseUrl: dbEntry.baseUrl,
466
+ apiKey: authInfo.key,
467
+ models: Array.from(dbEntry.models.values())
468
+ });
469
+ }
470
+ for (const [providerID, entry] of Object.entries(globalConfig.provider ?? {})) {
471
+ if (!entry.options?.apiKey)
472
+ continue;
473
+ const dbEntry = database.get(providerID);
474
+ if (!dbEntry)
475
+ continue;
476
+ const npm = dbEntry.npm ?? entry.npm;
477
+ const type = npm?.includes("anthropic") ? "anthropic" : dbEntry.type;
478
+ providerMap.set(providerID, {
479
+ id: providerID,
480
+ name: dbEntry.name,
481
+ type,
482
+ baseUrl: dbEntry.baseUrl,
483
+ apiKey: entry.options.apiKey,
484
+ models: Array.from(dbEntry.models.values())
485
+ });
486
+ }
487
+ return Array.from(providerMap.values());
488
+ } catch (error) {
489
+ console.warn("Warning: Could not load providers:", error.message);
490
+ return [];
491
+ }
492
+ };
493
+ var getAllAvailableProviders = async (includeAllProviders = false) => {
494
+ const [opencodeProviders, customProvidersFromConfig, customVerifiedProviders] = await Promise.all([
495
+ getAuthenticatedProviders(),
496
+ (async () => {
497
+ try {
498
+ return await getCustomProvidersFromConfig();
499
+ } catch (error) {
500
+ console.warn("Warning: Could not load custom providers:", error.message);
501
+ return [];
502
+ }
503
+ })(),
504
+ (async () => {
505
+ try {
506
+ return loadCustomVerifiedProviders();
507
+ } catch (error) {
508
+ console.warn("Warning: Could not load custom verified providers:", error.message);
509
+ return [];
510
+ }
511
+ })()
512
+ ]);
513
+ const providerMap = new Map;
514
+ customVerifiedProviders.forEach((p) => providerMap.set(p.id, p));
515
+ opencodeProviders.forEach((p) => providerMap.set(p.id, p));
516
+ customProvidersFromConfig.forEach((p) => providerMap.set(p.id, p));
517
+ if (includeAllProviders) {
518
+ try {
519
+ const allModelsDevProviders = await getAllProviders();
520
+ const authenticatedIds = new Set(opencodeProviders.map((p) => p.id));
521
+ const customIds = new Set(customProvidersFromConfig.map((p) => p.id));
522
+ const customVerifiedIds = new Set(customVerifiedProviders.map((p) => p.id));
523
+ allModelsDevProviders.forEach((provider) => {
524
+ if (!authenticatedIds.has(provider.id) && !customIds.has(provider.id) && !customVerifiedIds.has(provider.id)) {
525
+ providerMap.set(provider.id, {
526
+ ...provider,
527
+ type: provider.type,
528
+ apiKey: "",
529
+ models: provider.models.map((m) => ({ ...m, id: `${provider.id}_${m.id}` }))
530
+ });
531
+ }
532
+ });
533
+ } catch (error) {
534
+ console.warn("Warning: Could not load all models.dev providers:", error.message);
535
+ }
536
+ }
537
+ return Array.from(providerMap.values());
538
+ };
539
+
540
+ // src/constants.ts
541
+ var TEST_PROMPT = `make a 300 word story`;
542
+
543
+ // src/benchmark.ts
544
+ async function benchmarkSingleModelRest(model) {
545
+ try {
546
+ if (!model.providerConfig || !model.providerConfig.apiKey) {
547
+ throw new Error(`Missing API key for provider ${model.providerName}`);
548
+ }
549
+ if (!model.providerConfig.baseUrl) {
550
+ throw new Error(`Missing base URL for provider ${model.providerName}`);
551
+ }
552
+ let actualModelId;
553
+ if (model.id && model.id.includes("_")) {
554
+ actualModelId = model.id.split("_")[1];
555
+ } else if (model.id) {
556
+ actualModelId = model.id;
557
+ } else {
558
+ actualModelId = model.name;
559
+ }
560
+ actualModelId = actualModelId.trim();
561
+ const startTime = Date.now();
562
+ let firstTokenTime = null;
563
+ let streamedText = "";
564
+ let inputTokens = 0;
565
+ let outputTokens = 0;
566
+ let endpoint;
567
+ if (model.providerConfig.endpointFormat) {
568
+ endpoint = "/" + model.providerConfig.endpointFormat;
569
+ } else if (model.providerType === "anthropic") {
570
+ endpoint = "/messages";
571
+ } else if (model.providerType === "google") {
572
+ endpoint = "/models/" + actualModelId + ":streamGenerateContent";
573
+ } else {
574
+ endpoint = "/chat/completions";
575
+ }
576
+ const baseUrl = model.providerConfig.baseUrl.replace(/\/$/, "");
577
+ const url = `${baseUrl}${endpoint}`;
578
+ const headers = {
579
+ "Content-Type": "application/json",
580
+ Authorization: `Bearer ${model.providerConfig.apiKey}`
581
+ };
582
+ if (model.providerType === "anthropic") {
583
+ headers["x-api-key"] = model.providerConfig.apiKey;
584
+ headers["anthropic-version"] = "2023-06-01";
585
+ } else if (model.providerType === "google") {
586
+ delete headers["Authorization"];
587
+ headers["x-goog-api-key"] = model.providerConfig.apiKey;
588
+ }
589
+ const body = {
590
+ model: actualModelId,
591
+ messages: [{ role: "user", content: TEST_PROMPT }],
592
+ max_tokens: 500,
593
+ temperature: 0.7,
594
+ stream: true
595
+ };
596
+ if (model.providerType === "google") {
597
+ body["contents"] = [{ parts: [{ text: TEST_PROMPT }] }];
598
+ body["generationConfig"] = { maxOutputTokens: 500, temperature: 0.7 };
599
+ delete body["messages"];
600
+ delete body["max_tokens"];
601
+ delete body["stream"];
602
+ }
603
+ const response = await fetch(url, {
604
+ method: "POST",
605
+ headers,
606
+ body: JSON.stringify(body)
607
+ });
608
+ if (!response.ok) {
609
+ await response.text();
610
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
611
+ }
612
+ const reader = response.body.getReader();
613
+ const decoder = new TextDecoder;
614
+ let buffer = "";
615
+ let isFirstChunk = true;
616
+ while (true) {
617
+ const { done, value } = await reader.read();
618
+ if (done)
619
+ break;
620
+ if (isFirstChunk && !firstTokenTime) {
621
+ firstTokenTime = Date.now();
622
+ isFirstChunk = false;
623
+ }
624
+ buffer += decoder.decode(value, { stream: true });
625
+ const lines = buffer.split(`
626
+ `);
627
+ buffer = lines.pop() || "";
628
+ for (const line of lines) {
629
+ const trimmedLine = line.trim();
630
+ if (!trimmedLine)
631
+ continue;
632
+ try {
633
+ if (model.providerType === "anthropic") {
634
+ if (trimmedLine.startsWith("data: ")) {
635
+ const jsonStr = trimmedLine.slice(6);
636
+ if (jsonStr === "[DONE]")
637
+ break;
638
+ const chunk = JSON.parse(jsonStr);
639
+ const chunkTyped = chunk;
640
+ if (chunkTyped.type === "content_block_delta" && chunkTyped.delta?.text) {
641
+ streamedText += chunkTyped.delta.text;
642
+ } else if (chunkTyped.type === "message_start" && chunkTyped.message?.usage) {
643
+ inputTokens = chunkTyped.message.usage.input_tokens || 0;
644
+ } else if (chunkTyped.type === "message_delta") {
645
+ if (chunkTyped.usage?.output_tokens)
646
+ outputTokens = chunkTyped.usage.output_tokens;
647
+ if (chunkTyped.usage?.input_tokens && !inputTokens)
648
+ inputTokens = chunkTyped.usage.input_tokens;
649
+ }
650
+ } else if (trimmedLine.startsWith("event: ")) {
651
+ continue;
652
+ } else {
653
+ const chunk = JSON.parse(trimmedLine);
654
+ if (chunk.type === "content_block_delta" && chunk.delta?.text) {
655
+ streamedText += chunk.delta.text;
656
+ } else if (chunk.type === "message_start" && chunk.message?.usage) {
657
+ inputTokens = chunk.message.usage.input_tokens || 0;
658
+ } else if (chunk.type === "message_delta") {
659
+ if (chunk.usage?.output_tokens)
660
+ outputTokens = chunk.usage.output_tokens;
661
+ if (chunk.usage?.input_tokens && !inputTokens)
662
+ inputTokens = chunk.usage.input_tokens;
663
+ }
664
+ }
665
+ } else if (model.providerType === "google") {
666
+ const chunk = JSON.parse(trimmedLine);
667
+ if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
668
+ streamedText += chunk.candidates[0].content.parts[0].text;
669
+ }
670
+ if (chunk.usageMetadata?.promptTokenCount)
671
+ inputTokens = chunk.usageMetadata.promptTokenCount;
672
+ if (chunk.usageMetadata?.candidatesTokenCount)
673
+ outputTokens = chunk.usageMetadata.candidatesTokenCount;
674
+ } else {
675
+ if (trimmedLine.startsWith("data: ")) {
676
+ const jsonStr = trimmedLine.slice(6);
677
+ if (jsonStr === "[DONE]")
678
+ break;
679
+ const chunk = JSON.parse(jsonStr);
680
+ if (chunk.choices?.[0]?.delta?.content)
681
+ streamedText += chunk.choices[0].delta.content;
682
+ else if (chunk.choices?.[0]?.delta?.reasoning)
683
+ streamedText += chunk.choices[0].delta.reasoning;
684
+ if (chunk.usage?.prompt_tokens)
685
+ inputTokens = chunk.usage.prompt_tokens;
686
+ if (chunk.usage?.completion_tokens)
687
+ outputTokens = chunk.usage.completion_tokens;
688
+ }
689
+ }
690
+ } catch {
691
+ continue;
692
+ }
693
+ }
694
+ }
695
+ const endTime = Date.now();
696
+ const totalTime = endTime - startTime;
697
+ const timeToFirstToken = firstTokenTime ? firstTokenTime - startTime : totalTime;
698
+ const usedEstimateForOutput = !outputTokens;
699
+ const usedEstimateForInput = !inputTokens;
700
+ const finalOutputTokens = outputTokens || Math.round(streamedText.length / 4);
701
+ const finalInputTokens = inputTokens || Math.round(TEST_PROMPT.length / 4);
702
+ const totalTokens = finalInputTokens + finalOutputTokens;
703
+ const tokensPerSecond = totalTime > 0 ? finalOutputTokens / totalTime * 1000 : 0;
704
+ return {
705
+ model: model.name,
706
+ provider: model.providerName,
707
+ totalTime,
708
+ timeToFirstToken,
709
+ tokenCount: finalOutputTokens,
710
+ tokensPerSecond,
711
+ promptTokens: finalInputTokens,
712
+ totalTokens,
713
+ usedEstimateForOutput,
714
+ usedEstimateForInput,
715
+ success: true
716
+ };
717
+ } catch (error) {
718
+ return {
719
+ model: model.name,
720
+ provider: model.providerName,
721
+ totalTime: 0,
722
+ timeToFirstToken: 0,
723
+ tokenCount: 0,
724
+ tokensPerSecond: 0,
725
+ promptTokens: 0,
726
+ totalTokens: 0,
727
+ usedEstimateForOutput: true,
728
+ usedEstimateForInput: true,
729
+ success: false,
730
+ error: error.message
731
+ };
732
+ }
733
+ }
734
+
735
+ // src/headless.ts
736
+ function parseProviderModel(arg) {
737
+ const firstColonIndex = arg.indexOf(":");
738
+ if (firstColonIndex === -1) {
739
+ throw new Error(`Invalid format. Use provider:model (e.g., openai:gpt-4)`);
740
+ }
741
+ return {
742
+ provider: arg.substring(0, firstColonIndex),
743
+ model: arg.substring(firstColonIndex + 1)
744
+ };
745
+ }
746
+ function createCustomProviderFromCli(cliArgs) {
747
+ const { provider, model } = parseProviderModel(cliArgs.benchCustom);
748
+ if (!cliArgs.baseUrl)
749
+ throw new Error("--base-url is required for custom provider benchmarking");
750
+ if (!cliArgs.apiKey)
751
+ throw new Error("--api-key is required for custom provider benchmarking");
752
+ const endpointFormat = cliArgs.endpointFormat || "chat/completions";
753
+ return {
754
+ id: provider,
755
+ name: provider,
756
+ type: "openai-compatible",
757
+ baseUrl: cliArgs.baseUrl,
758
+ apiKey: cliArgs.apiKey,
759
+ endpointFormat,
760
+ models: [{ name: model, id: model }]
761
+ };
762
+ }
763
+ async function loadConfig(includeAll) {
764
+ try {
765
+ const providers = await getAllAvailableProviders(includeAll);
766
+ return { providers };
767
+ } catch (error) {
768
+ console.error("Error: Failed to load providers:", error.message);
769
+ return { providers: [] };
770
+ }
771
+ }
772
+ function buildJsonOutput(providerName, providerId, modelName, modelId, result, formatted) {
773
+ const jsonOutput = {
774
+ provider: providerName,
775
+ providerId,
776
+ model: modelName,
777
+ modelId,
778
+ method: "rest-api",
779
+ success: result.success,
780
+ totalTime: result.totalTime,
781
+ totalTimeSeconds: result.totalTime / 1000,
782
+ timeToFirstToken: result.timeToFirstToken,
783
+ timeToFirstTokenSeconds: result.timeToFirstToken / 1000,
784
+ tokensPerSecond: result.tokensPerSecond,
785
+ outputTokens: result.tokenCount,
786
+ promptTokens: result.promptTokens,
787
+ totalTokens: result.totalTokens,
788
+ is_estimated: !!(result.usedEstimateForOutput || result.usedEstimateForInput),
789
+ error: result.error || null
790
+ };
791
+ return JSON.stringify(jsonOutput, null, formatted ? 2 : 0);
792
+ }
793
+ async function runHeadlessBenchmark(cliArgs) {
794
+ try {
795
+ if (cliArgs.benchCustom) {
796
+ const customProvider = createCustomProviderFromCli(cliArgs);
797
+ const modelDef = customProvider.models[0];
798
+ const modelConfig2 = {
799
+ id: modelDef.id,
800
+ name: modelDef.name,
801
+ providerName: customProvider.name,
802
+ providerType: customProvider.type,
803
+ providerId: customProvider.id,
804
+ providerConfig: {
805
+ baseUrl: customProvider.baseUrl,
806
+ apiKey: customProvider.apiKey,
807
+ endpointFormat: customProvider.endpointFormat
808
+ }
809
+ };
810
+ const result2 = await benchmarkSingleModelRest(modelConfig2);
811
+ if (!result2.success && result2.error)
812
+ console.error(`Error: Benchmark failed: ${result2.error}`);
813
+ console.log(buildJsonOutput(customProvider.name, customProvider.id, modelDef.name, modelDef.id, result2, cliArgs.formatted));
814
+ process.exit(result2.success ? 0 : 1);
815
+ }
816
+ const benchSpec = cliArgs.bench;
817
+ const colonIndex = benchSpec.indexOf(":");
818
+ if (colonIndex === -1) {
819
+ console.error("Error: Invalid --bench format. Use: provider:model");
820
+ process.exit(1);
821
+ }
822
+ const providerSpec = benchSpec.substring(0, colonIndex);
823
+ let modelName = benchSpec.substring(colonIndex + 1);
824
+ if (modelName.startsWith('"') && modelName.endsWith('"') || modelName.startsWith("'") && modelName.endsWith("'")) {
825
+ modelName = modelName.slice(1, -1);
826
+ }
827
+ if (!providerSpec || !modelName) {
828
+ console.error("Error: Invalid --bench format. Use: provider:model");
829
+ process.exit(1);
830
+ }
831
+ const config = await loadConfig(true);
832
+ const provider = config.providers.find((p) => p.id?.toLowerCase() === providerSpec.toLowerCase() || p.name?.toLowerCase() === providerSpec.toLowerCase());
833
+ if (!provider) {
834
+ console.error(`Error: Provider '${providerSpec}' not found`);
835
+ console.error("Available providers:");
836
+ config.providers.forEach((p) => console.error(` - ${p.id || p.name}`));
837
+ process.exit(1);
838
+ }
839
+ const model = provider.models.find((m) => {
840
+ const modelIdLower = m.id?.toLowerCase() || "";
841
+ const modelNameLower = m.name?.toLowerCase() || "";
842
+ const searchLower = modelName.toLowerCase();
843
+ if (modelIdLower === searchLower)
844
+ return true;
845
+ const idWithoutPrefix = modelIdLower.includes("_") ? modelIdLower.split("_").slice(1).join("_") : modelIdLower;
846
+ if (idWithoutPrefix === searchLower)
847
+ return true;
848
+ if (modelNameLower === searchLower)
849
+ return true;
850
+ return false;
851
+ });
852
+ if (!model) {
853
+ console.error(`Error: Model '${modelName}' not found in provider '${provider.name}'`);
854
+ console.error("Available models:");
855
+ provider.models.forEach((m) => {
856
+ const idWithoutPrefix = m.id?.includes("_") ? m.id.split("_").slice(1).join("_") : m.id;
857
+ console.error(` - ${m.name} (id: ${idWithoutPrefix})`);
858
+ });
859
+ process.exit(1);
860
+ }
861
+ const finalApiKey = cliArgs.apiKey || provider.apiKey;
862
+ if (!finalApiKey) {
863
+ console.error(`Error: No API key found for provider '${provider.name}'`);
864
+ console.error("Please provide --api-key flag or configure the provider first");
865
+ process.exit(1);
866
+ }
867
+ const modelConfig = {
868
+ id: model.id,
869
+ name: model.name,
870
+ providerName: provider.name,
871
+ providerType: provider.type,
872
+ providerId: provider.id,
873
+ providerConfig: {
874
+ ...provider,
875
+ apiKey: finalApiKey,
876
+ baseUrl: provider.baseUrl || ""
877
+ }
878
+ };
879
+ const result = await benchmarkSingleModelRest(modelConfig);
880
+ if (!result.success && result.error)
881
+ console.error(`Error: Benchmark failed: ${result.error}`);
882
+ console.log(buildJsonOutput(provider.name, provider.id, model.name, model.id, result, cliArgs.formatted));
883
+ process.exit(result.success ? 0 : 1);
884
+ } catch (error) {
885
+ console.error("Error: " + error.message);
886
+ process.exit(1);
887
+ }
888
+ }
889
+
890
+ // src/headless-entry.ts
891
+ var args = process.argv.slice(2);
892
+ var parsed = {
893
+ debug: false,
894
+ bench: null,
895
+ benchCustom: null,
896
+ apiKey: null,
897
+ baseUrl: null,
898
+ endpointFormat: null,
899
+ formatted: false,
900
+ help: false
901
+ };
902
+ for (let i = 0;i < args.length; i++) {
903
+ const arg = args[i];
904
+ if (arg === "--debug")
905
+ parsed.debug = true;
906
+ else if (arg === "--bench")
907
+ parsed.bench = args[++i] ?? null;
908
+ else if (arg === "--bench-custom")
909
+ parsed.benchCustom = args[++i] ?? null;
910
+ else if (arg === "--api-key")
911
+ parsed.apiKey = args[++i] ?? null;
912
+ else if (arg === "--base-url")
913
+ parsed.baseUrl = args[++i] ?? null;
914
+ else if (arg === "--endpoint-format")
915
+ parsed.endpointFormat = args[++i] ?? null;
916
+ else if (arg === "--formatted")
917
+ parsed.formatted = true;
918
+ else if (arg === "--help" || arg === "-h") {
919
+ console.log("ai-speedometer headless - Benchmark AI models (Node.js/Bun compatible)");
920
+ console.log("");
921
+ console.log("Usage:");
922
+ console.log(" ai-speedometer-headless --bench <provider:model>");
923
+ console.log(" ai-speedometer-headless --bench-custom <provider:model> --base-url <url> --api-key <key>");
924
+ console.log("");
925
+ console.log("Options:");
926
+ console.log(" --bench <provider:model> Run benchmark in headless mode");
927
+ console.log(" --bench-custom <provider:model> Run custom provider benchmark");
928
+ console.log(" --base-url <url> Base URL for custom provider");
929
+ console.log(" --api-key <key> API key");
930
+ console.log(" --endpoint-format <format> Endpoint format (default: chat/completions)");
931
+ console.log(" --formatted Pretty-print JSON output");
932
+ console.log(" --help, -h Show this help message");
933
+ process.exit(0);
934
+ }
935
+ }
936
+ if (!parsed.bench && !parsed.benchCustom) {
937
+ console.error("Error: --bench or --bench-custom is required");
938
+ console.error("Run with --help for usage");
939
+ process.exit(1);
940
+ }
941
+ await runHeadlessBenchmark(parsed);
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "ai-speedometer",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "A comprehensive CLI tool for benchmarking AI models across multiple providers with parallel execution and professional metrics",
5
5
  "bin": {
6
6
  "ai-speedometer": "dist/ai-speedometer",
7
- "aispeed": "dist/ai-speedometer"
7
+ "aispeed": "dist/ai-speedometer",
8
+ "ai-speedometer-headless": "dist/ai-speedometer-headless"
8
9
  },
9
10
  "engines": {
10
11
  "bun": ">=1.0.0",
@@ -17,7 +18,7 @@
17
18
  "test:watch": "bun test --watch",
18
19
  "test:update": "bun test --update-snapshots",
19
20
  "typecheck": "bun tsc --noEmit",
20
- "build": "bun build src/index.ts --outdir dist --target bun --external '@opentui/core' --external '@opentui/react' --external 'react' --external 'react-reconciler' && cat scripts/shebang dist/index.js > dist/ai-speedometer && chmod +x dist/ai-speedometer && rm dist/index.js",
21
+ "build": "bun build src/index.ts --outdir dist --target bun --external '@opentui/core' --external '@opentui/react' --external 'react' --external 'react-reconciler' && cat scripts/shebang dist/index.js > dist/ai-speedometer && chmod +x dist/ai-speedometer && rm dist/index.js && bun build src/headless-entry.ts --outdir dist --target node --external 'jsonc-parser' && mv dist/headless-entry.js dist/ai-speedometer-headless && chmod +x dist/ai-speedometer-headless",
21
22
  "prepublishOnly": "bun run build"
22
23
  },
23
24
  "keywords": [