ai-speedometer 2.1.6 → 2.1.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 (2) hide show
  1. package/dist/ai-speedometer +133 -21
  2. package/package.json +1 -1
@@ -1712,7 +1712,7 @@ var exports_benchmark = {};
1712
1712
  __export(exports_benchmark, {
1713
1713
  benchmarkSingleModelRest: () => benchmarkSingleModelRest
1714
1714
  });
1715
- async function benchmarkSingleModelRest(model) {
1715
+ async function benchmarkSingleModelRest(model, logger) {
1716
1716
  try {
1717
1717
  if (!model.providerConfig || !model.providerConfig.apiKey) {
1718
1718
  throw new Error(`Missing API key for provider ${model.providerName}`);
@@ -1729,6 +1729,7 @@ async function benchmarkSingleModelRest(model) {
1729
1729
  actualModelId = model.name;
1730
1730
  }
1731
1731
  actualModelId = actualModelId.trim();
1732
+ await logger?.logHeader(model.name, model.providerName, model.providerConfig.apiKey);
1732
1733
  const startTime = Date.now();
1733
1734
  let firstTokenTime = null;
1734
1735
  let streamedText = "";
@@ -1801,21 +1802,21 @@ async function benchmarkSingleModelRest(model) {
1801
1802
  const reader = response.body.getReader();
1802
1803
  const decoder = new TextDecoder;
1803
1804
  let buffer = "";
1804
- let isFirstChunk = true;
1805
+ let firstParsedTokenTime = null;
1805
1806
  while (true) {
1806
1807
  const { done, value } = await reader.read();
1807
1808
  if (done)
1808
1809
  break;
1809
- if (isFirstChunk && !firstTokenTime) {
1810
+ if (!firstTokenTime)
1810
1811
  firstTokenTime = Date.now();
1811
- isFirstChunk = false;
1812
- }
1813
1812
  buffer += decoder.decode(value, { stream: true });
1814
1813
  const lines = buffer.split(`
1815
1814
  `);
1816
1815
  buffer = lines.pop() || "";
1817
1816
  for (const line of lines) {
1818
1817
  const trimmedLine = line.trim();
1818
+ if (trimmedLine)
1819
+ await logger?.logRaw(trimmedLine);
1819
1820
  if (!trimmedLine)
1820
1821
  continue;
1821
1822
  try {
@@ -1827,6 +1828,8 @@ async function benchmarkSingleModelRest(model) {
1827
1828
  const chunk = JSON.parse(jsonStr);
1828
1829
  const chunkTyped = chunk;
1829
1830
  if (chunkTyped.type === "content_block_delta" && chunkTyped.delta?.text) {
1831
+ if (!firstParsedTokenTime)
1832
+ firstParsedTokenTime = Date.now();
1830
1833
  streamedText += chunkTyped.delta.text;
1831
1834
  } else if (chunkTyped.type === "message_start" && chunkTyped.message?.usage) {
1832
1835
  inputTokens = chunkTyped.message.usage.input_tokens || 0;
@@ -1841,6 +1844,8 @@ async function benchmarkSingleModelRest(model) {
1841
1844
  } else {
1842
1845
  const chunk = JSON.parse(trimmedLine);
1843
1846
  if (chunk.type === "content_block_delta" && chunk.delta?.text) {
1847
+ if (!firstParsedTokenTime)
1848
+ firstParsedTokenTime = Date.now();
1844
1849
  streamedText += chunk.delta.text;
1845
1850
  } else if (chunk.type === "message_start" && chunk.message?.usage) {
1846
1851
  inputTokens = chunk.message.usage.input_tokens || 0;
@@ -1854,6 +1859,8 @@ async function benchmarkSingleModelRest(model) {
1854
1859
  } else if (model.providerType === "google") {
1855
1860
  const chunk = JSON.parse(trimmedLine);
1856
1861
  if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
1862
+ if (!firstParsedTokenTime)
1863
+ firstParsedTokenTime = Date.now();
1857
1864
  streamedText += chunk.candidates[0].content.parts[0].text;
1858
1865
  }
1859
1866
  if (chunk.usageMetadata?.promptTokenCount)
@@ -1866,10 +1873,15 @@ async function benchmarkSingleModelRest(model) {
1866
1873
  if (jsonStr === "[DONE]")
1867
1874
  continue;
1868
1875
  const chunk = JSON.parse(jsonStr);
1869
- if (chunk.choices?.[0]?.delta?.content)
1876
+ if (chunk.choices?.[0]?.delta?.content) {
1877
+ if (!firstParsedTokenTime)
1878
+ firstParsedTokenTime = Date.now();
1870
1879
  streamedText += chunk.choices[0].delta.content;
1871
- else if (chunk.choices?.[0]?.delta?.reasoning)
1880
+ } else if (chunk.choices?.[0]?.delta?.reasoning) {
1881
+ if (!firstParsedTokenTime)
1882
+ firstParsedTokenTime = Date.now();
1872
1883
  streamedText += chunk.choices[0].delta.reasoning;
1884
+ }
1873
1885
  if (chunk.usage?.prompt_tokens)
1874
1886
  inputTokens = chunk.usage.prompt_tokens;
1875
1887
  if (chunk.usage?.completion_tokens)
@@ -1881,15 +1893,18 @@ async function benchmarkSingleModelRest(model) {
1881
1893
  }
1882
1894
  }
1883
1895
  }
1896
+ await logger?.flush();
1884
1897
  const endTime = Date.now();
1885
1898
  const totalTime = endTime - startTime;
1886
- const timeToFirstToken = firstTokenTime ? firstTokenTime - startTime : totalTime;
1899
+ const effectiveFirstToken = firstParsedTokenTime ?? firstTokenTime;
1900
+ const timeToFirstToken = effectiveFirstToken ? effectiveFirstToken - startTime : totalTime;
1901
+ const generationTime = totalTime - timeToFirstToken;
1887
1902
  const usedEstimateForOutput = !outputTokens;
1888
1903
  const usedEstimateForInput = !inputTokens;
1889
1904
  const finalOutputTokens = outputTokens || Math.round(streamedText.length / 4);
1890
1905
  const finalInputTokens = inputTokens || Math.round(TEST_PROMPT.length / 4);
1891
1906
  const totalTokens = finalInputTokens + finalOutputTokens;
1892
- const tokensPerSecond = totalTime > 0 ? finalOutputTokens / totalTime * 1000 : 0;
1907
+ const tokensPerSecond = generationTime > 0 ? finalOutputTokens / generationTime * 1000 : 0;
1893
1908
  return {
1894
1909
  model: model.name,
1895
1910
  provider: model.providerName,
@@ -1904,6 +1919,7 @@ async function benchmarkSingleModelRest(model) {
1904
1919
  success: true
1905
1920
  };
1906
1921
  } catch (error) {
1922
+ await logger?.flush();
1907
1923
  return {
1908
1924
  model: model.name,
1909
1925
  provider: model.providerName,
@@ -2085,6 +2101,66 @@ var init_headless = __esm(() => {
2085
2101
  init_benchmark();
2086
2102
  });
2087
2103
 
2104
+ // ../core/src/logger.ts
2105
+ var exports_logger = {};
2106
+ __export(exports_logger, {
2107
+ getLogPath: () => getLogPath,
2108
+ createRunId: () => createRunId,
2109
+ createBenchLogger: () => createBenchLogger
2110
+ });
2111
+ import { mkdir, appendFile } from "fs/promises";
2112
+ import { homedir as homedir4 } from "os";
2113
+ import { join } from "path";
2114
+ function generateRunId() {
2115
+ const now = new Date;
2116
+ const date = now.toISOString().slice(0, 10);
2117
+ const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
2118
+ const rand = Math.random().toString(16).slice(2, 6);
2119
+ return `${date}_${time}_${rand}`;
2120
+ }
2121
+ function redactSecrets(line, apiKey) {
2122
+ if (!apiKey)
2123
+ return line;
2124
+ return line.split(apiKey).join("[REDACTED]");
2125
+ }
2126
+ function createRunId() {
2127
+ return generateRunId();
2128
+ }
2129
+ function getLogPath(runId) {
2130
+ return join(homedir4(), ".local", "share", "ai-speedometer", "logs", `${runId}.log`);
2131
+ }
2132
+ async function createBenchLogger(runId) {
2133
+ const logPath = getLogPath(runId);
2134
+ const logDir = join(homedir4(), ".local", "share", "ai-speedometer", "logs");
2135
+ await mkdir(logDir, { recursive: true });
2136
+ let currentApiKey = "";
2137
+ let buffer = "";
2138
+ return {
2139
+ logPath,
2140
+ runId,
2141
+ logHeader: async (modelName, providerName, apiKey = "") => {
2142
+ currentApiKey = apiKey;
2143
+ const ts = new Date().toISOString();
2144
+ buffer = `
2145
+ === ${modelName} | ${providerName} | ${ts} ===
2146
+ `;
2147
+ },
2148
+ logRaw: async (line) => {
2149
+ const safe = redactSecrets(line, currentApiKey);
2150
+ buffer += safe + `
2151
+ `;
2152
+ },
2153
+ flush: async () => {
2154
+ buffer += `
2155
+ ` + "=".repeat(60) + `
2156
+ `;
2157
+ await appendFile(logPath, buffer, "utf8");
2158
+ buffer = "";
2159
+ }
2160
+ };
2161
+ }
2162
+ var init_logger = () => {};
2163
+
2088
2164
  // src/tui/context/AppContext.tsx
2089
2165
  import { createContext, useContext, useReducer, useEffect } from "react";
2090
2166
  import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
@@ -2121,12 +2197,23 @@ function appReducer(state, action) {
2121
2197
  };
2122
2198
  case "BENCH_RESET":
2123
2199
  return { ...state, benchResults: [], selectedModels: [] };
2200
+ case "SET_LOG_INFO":
2201
+ return { ...state, logMode: action.logMode, logPath: action.logPath, runId: action.runId };
2124
2202
  default:
2125
2203
  return state;
2126
2204
  }
2127
2205
  }
2128
- function AppProvider({ children }) {
2206
+ function AppProvider({ children, logMode = false }) {
2129
2207
  const [state, dispatch] = useReducer(appReducer, initialState);
2208
+ useEffect(() => {
2209
+ if (logMode) {
2210
+ Promise.resolve().then(() => (init_logger(), exports_logger)).then(({ createRunId: createRunId2, getLogPath: getLogPath2 }) => {
2211
+ const runId = createRunId2();
2212
+ const logPath = getLogPath2(runId);
2213
+ dispatch({ type: "SET_LOG_INFO", logMode: true, logPath, runId });
2214
+ });
2215
+ }
2216
+ }, [logMode]);
2130
2217
  useEffect(() => {
2131
2218
  let cancelled = false;
2132
2219
  async function loadConfig2() {
@@ -2169,7 +2256,10 @@ var init_AppContext = __esm(() => {
2169
2256
  config: null,
2170
2257
  selectedModels: [],
2171
2258
  benchResults: [],
2172
- isLoadingConfig: true
2259
+ isLoadingConfig: true,
2260
+ logMode: false,
2261
+ logPath: null,
2262
+ runId: null
2173
2263
  };
2174
2264
  AppContext = createContext(null);
2175
2265
  });
@@ -2179,7 +2269,7 @@ var package_default;
2179
2269
  var init_package = __esm(() => {
2180
2270
  package_default = {
2181
2271
  name: "ai-speedometer",
2182
- version: "2.1.6",
2272
+ version: "2.1.7",
2183
2273
  description: "A comprehensive CLI tool for benchmarking AI models across multiple providers with parallel execution and professional metrics",
2184
2274
  bin: {
2185
2275
  "ai-speedometer": "dist/ai-speedometer",
@@ -3005,9 +3095,12 @@ function BenchmarkScreen() {
3005
3095
  setModelStates((prev) => prev.map((s) => ({ ...s, status: "running", startedAt: Date.now() })));
3006
3096
  async function runAll() {
3007
3097
  const { benchmarkSingleModelRest: benchmarkSingleModelRest2 } = await Promise.resolve().then(() => (init_benchmark(), exports_benchmark));
3098
+ const logEnabled = state.logMode && !!state.runId;
3099
+ const { createBenchLogger: createBenchLogger2 } = logEnabled ? await Promise.resolve().then(() => (init_logger(), exports_logger)) : { createBenchLogger: null };
3008
3100
  const promises = models.map(async (model) => {
3101
+ const logger = logEnabled && createBenchLogger2 ? await createBenchLogger2(state.runId) : undefined;
3009
3102
  try {
3010
- const result = await benchmarkSingleModelRest2(model);
3103
+ const result = await benchmarkSingleModelRest2(model, logger);
3011
3104
  if (!result.success) {
3012
3105
  const errMsg = result.error ?? "Request failed";
3013
3106
  setModelStates((prev) => prev.map((s) => s.model.id === model.id && s.model.providerId === model.providerId ? { ...s, status: "error", error: errMsg } : s));
@@ -3428,10 +3521,22 @@ function BenchmarkScreen() {
3428
3521
  navigate("main-menu");
3429
3522
  }
3430
3523
  });
3431
- const statusLine = allDone ? /* @__PURE__ */ jsxDEV10("text", {
3432
- fg: "#9ece6a",
3433
- children: "All done! [Enter]/[q] return [\u2191\u2193/PgUp/PgDn/wheel] scroll"
3434
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV10("box", {
3524
+ const statusLine = allDone ? /* @__PURE__ */ jsxDEV10("box", {
3525
+ flexDirection: "row",
3526
+ children: [
3527
+ /* @__PURE__ */ jsxDEV10("text", {
3528
+ fg: "#9ece6a",
3529
+ children: "All done! [Enter]/[Q] return [\u2191\u2193/PgUp/PgDn/wheel] scroll"
3530
+ }, undefined, false, undefined, this),
3531
+ state.logMode && state.logPath && /* @__PURE__ */ jsxDEV10("text", {
3532
+ fg: "#565f89",
3533
+ children: [
3534
+ " log: ",
3535
+ state.logPath
3536
+ ]
3537
+ }, undefined, true, undefined, this)
3538
+ ]
3539
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV10("box", {
3435
3540
  flexDirection: "row",
3436
3541
  children: [
3437
3542
  running.length > 0 && /* @__PURE__ */ jsxDEV10("text", {
@@ -5100,8 +5205,9 @@ function Shell() {
5100
5205
  ]
5101
5206
  }, undefined, true, undefined, this);
5102
5207
  }
5103
- function App() {
5208
+ function App({ logMode = false }) {
5104
5209
  return /* @__PURE__ */ jsxDEV15(AppProvider, {
5210
+ logMode,
5105
5211
  children: /* @__PURE__ */ jsxDEV15(Shell, {}, undefined, false, undefined, this)
5106
5212
  }, undefined, false, undefined, this);
5107
5213
  }
@@ -5127,7 +5233,7 @@ __export(exports_tui, {
5127
5233
  import { createCliRenderer } from "@opentui/core";
5128
5234
  import { createRoot } from "@opentui/react";
5129
5235
  import { jsxDEV as jsxDEV16 } from "@opentui/react/jsx-dev-runtime";
5130
- async function startTui() {
5236
+ async function startTui(logMode = false) {
5131
5237
  const renderer = await createCliRenderer({
5132
5238
  exitOnCtrlC: false
5133
5239
  });
@@ -5141,7 +5247,9 @@ async function startTui() {
5141
5247
  renderer.destroy();
5142
5248
  process.exit(0);
5143
5249
  });
5144
- createRoot(renderer).render(/* @__PURE__ */ jsxDEV16(App, {}, undefined, false, undefined, this));
5250
+ createRoot(renderer).render(/* @__PURE__ */ jsxDEV16(App, {
5251
+ logMode
5252
+ }, undefined, false, undefined, this));
5145
5253
  }
5146
5254
  var ENABLE_BRACKETED_PASTE = "\x1B[?2004h", DISABLE_BRACKETED_PASTE = "\x1B[?2004l";
5147
5255
  var init_tui = __esm(() => {
@@ -5153,6 +5261,7 @@ function parseCliArgs() {
5153
5261
  const args = process.argv.slice(2);
5154
5262
  const parsed = {
5155
5263
  debug: false,
5264
+ log: false,
5156
5265
  bench: null,
5157
5266
  benchCustom: null,
5158
5267
  apiKey: null,
@@ -5165,6 +5274,8 @@ function parseCliArgs() {
5165
5274
  const arg = args[i];
5166
5275
  if (arg === "--debug")
5167
5276
  parsed.debug = true;
5277
+ else if (arg === "--log")
5278
+ parsed.log = true;
5168
5279
  else if (arg === "--bench")
5169
5280
  parsed.bench = args[++i] ?? null;
5170
5281
  else if (arg === "--bench-custom")
@@ -5197,6 +5308,7 @@ function showHelp() {
5197
5308
  console.log(" --api-key <key> API key for custom provider");
5198
5309
  console.log(" --endpoint-format <format> Endpoint format (default: chat/completions)");
5199
5310
  console.log(" --formatted Format JSON output for human readability");
5311
+ console.log(" --log Log raw SSE streams to ~/.local/share/ai-speedometer/logs/");
5200
5312
  console.log(" --debug Enable debug logging");
5201
5313
  console.log(" --help, -h Show this help message");
5202
5314
  console.log("");
@@ -5214,5 +5326,5 @@ if (cliArgs.help) {
5214
5326
  await runHeadlessBenchmark2(cliArgs);
5215
5327
  } else {
5216
5328
  const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), exports_tui));
5217
- await startTui2();
5329
+ await startTui2(cliArgs.log);
5218
5330
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-speedometer",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
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",