ai-speedometer-headless 2.1.6 → 2.2.0

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.
@@ -542,7 +542,7 @@ var getAllAvailableProviders = async (includeAllProviders = false) => {
542
542
  var TEST_PROMPT = `make a 300 word story`;
543
543
 
544
544
  // ../core/src/benchmark.ts
545
- async function benchmarkSingleModelRest(model) {
545
+ async function benchmarkSingleModelRest(model, logger) {
546
546
  try {
547
547
  if (!model.providerConfig || !model.providerConfig.apiKey) {
548
548
  throw new Error(`Missing API key for provider ${model.providerName}`);
@@ -559,6 +559,7 @@ async function benchmarkSingleModelRest(model) {
559
559
  actualModelId = model.name;
560
560
  }
561
561
  actualModelId = actualModelId.trim();
562
+ await logger?.logHeader(model.name, model.providerName, model.providerConfig.apiKey);
562
563
  const startTime = Date.now();
563
564
  let firstTokenTime = null;
564
565
  let streamedText = "";
@@ -631,33 +632,36 @@ async function benchmarkSingleModelRest(model) {
631
632
  const reader = response.body.getReader();
632
633
  const decoder = new TextDecoder;
633
634
  let buffer = "";
634
- let isFirstChunk = true;
635
+ let firstParsedTokenTime = null;
635
636
  while (true) {
636
637
  const { done, value } = await reader.read();
637
638
  if (done)
638
639
  break;
639
- if (isFirstChunk && !firstTokenTime) {
640
+ if (!firstTokenTime)
640
641
  firstTokenTime = Date.now();
641
- isFirstChunk = false;
642
- }
643
642
  buffer += decoder.decode(value, { stream: true });
644
643
  const lines = buffer.split(`
645
644
  `);
646
645
  buffer = lines.pop() || "";
647
646
  for (const line of lines) {
648
647
  const trimmedLine = line.trim();
648
+ if (trimmedLine)
649
+ await logger?.logRaw(trimmedLine);
649
650
  if (!trimmedLine)
650
651
  continue;
651
652
  try {
652
653
  if (model.providerType === "anthropic") {
653
- if (trimmedLine.startsWith("data: ")) {
654
- const jsonStr = trimmedLine.slice(6);
654
+ const anthropicDataPrefix = trimmedLine.startsWith("data: ") ? 6 : trimmedLine.startsWith("data:") ? 5 : -1;
655
+ if (anthropicDataPrefix !== -1) {
656
+ const jsonStr = trimmedLine.slice(anthropicDataPrefix);
655
657
  if (jsonStr === "[DONE]")
656
658
  continue;
657
659
  const chunk = JSON.parse(jsonStr);
658
660
  const chunkTyped = chunk;
659
- if (chunkTyped.type === "content_block_delta" && chunkTyped.delta?.text) {
660
- streamedText += chunkTyped.delta.text;
661
+ if (chunkTyped.type === "content_block_delta" && (chunkTyped.delta?.text || chunkTyped.delta?.thinking)) {
662
+ if (!firstParsedTokenTime)
663
+ firstParsedTokenTime = Date.now();
664
+ streamedText += chunkTyped.delta?.text || chunkTyped.delta?.thinking || "";
661
665
  } else if (chunkTyped.type === "message_start" && chunkTyped.message?.usage) {
662
666
  inputTokens = chunkTyped.message.usage.input_tokens || 0;
663
667
  } else if (chunkTyped.type === "message_delta") {
@@ -670,8 +674,10 @@ async function benchmarkSingleModelRest(model) {
670
674
  continue;
671
675
  } else {
672
676
  const chunk = JSON.parse(trimmedLine);
673
- if (chunk.type === "content_block_delta" && chunk.delta?.text) {
674
- streamedText += chunk.delta.text;
677
+ if (chunk.type === "content_block_delta" && (chunk.delta?.text || chunk.delta?.thinking)) {
678
+ if (!firstParsedTokenTime)
679
+ firstParsedTokenTime = Date.now();
680
+ streamedText += chunk.delta?.text || chunk.delta?.thinking || "";
675
681
  } else if (chunk.type === "message_start" && chunk.message?.usage) {
676
682
  inputTokens = chunk.message.usage.input_tokens || 0;
677
683
  } else if (chunk.type === "message_delta") {
@@ -684,6 +690,8 @@ async function benchmarkSingleModelRest(model) {
684
690
  } else if (model.providerType === "google") {
685
691
  const chunk = JSON.parse(trimmedLine);
686
692
  if (chunk.candidates?.[0]?.content?.parts?.[0]?.text) {
693
+ if (!firstParsedTokenTime)
694
+ firstParsedTokenTime = Date.now();
687
695
  streamedText += chunk.candidates[0].content.parts[0].text;
688
696
  }
689
697
  if (chunk.usageMetadata?.promptTokenCount)
@@ -691,19 +699,42 @@ async function benchmarkSingleModelRest(model) {
691
699
  if (chunk.usageMetadata?.candidatesTokenCount)
692
700
  outputTokens = chunk.usageMetadata.candidatesTokenCount;
693
701
  } else {
694
- if (trimmedLine.startsWith("data: ")) {
695
- const jsonStr = trimmedLine.slice(6);
696
- if (jsonStr === "[DONE]")
697
- continue;
698
- const chunk = JSON.parse(jsonStr);
699
- if (chunk.choices?.[0]?.delta?.content)
700
- streamedText += chunk.choices[0].delta.content;
701
- else if (chunk.choices?.[0]?.delta?.reasoning)
702
- streamedText += chunk.choices[0].delta.reasoning;
703
- if (chunk.usage?.prompt_tokens)
704
- inputTokens = chunk.usage.prompt_tokens;
705
- if (chunk.usage?.completion_tokens)
706
- outputTokens = chunk.usage.completion_tokens;
702
+ const dataPrefix = trimmedLine.startsWith("data: ") ? 6 : trimmedLine.startsWith("data:") ? 5 : -1;
703
+ if (dataPrefix === -1)
704
+ continue;
705
+ const jsonStr = trimmedLine.slice(dataPrefix);
706
+ if (jsonStr === "[DONE]")
707
+ continue;
708
+ const chunk = JSON.parse(jsonStr);
709
+ if (chunk.choices?.[0]?.delta?.content) {
710
+ if (!firstParsedTokenTime)
711
+ firstParsedTokenTime = Date.now();
712
+ streamedText += chunk.choices[0].delta.content;
713
+ } else if (chunk.choices?.[0]?.delta?.reasoning) {
714
+ if (!firstParsedTokenTime)
715
+ firstParsedTokenTime = Date.now();
716
+ streamedText += chunk.choices[0].delta.reasoning;
717
+ } else if (chunk.choices?.[0]?.delta?.reasoning_content) {
718
+ if (!firstParsedTokenTime)
719
+ firstParsedTokenTime = Date.now();
720
+ streamedText += chunk.choices[0].delta.reasoning_content;
721
+ } else if (chunk.type === "content_block_delta" && chunk.delta?.text) {
722
+ if (!firstParsedTokenTime)
723
+ firstParsedTokenTime = Date.now();
724
+ streamedText += chunk.delta.text;
725
+ } else if (chunk.type === "content_block_delta" && chunk.delta?.thinking) {
726
+ if (!firstParsedTokenTime)
727
+ firstParsedTokenTime = Date.now();
728
+ streamedText += chunk.delta.thinking;
729
+ }
730
+ if (chunk.usage?.prompt_tokens)
731
+ inputTokens = chunk.usage.prompt_tokens;
732
+ if (chunk.usage?.completion_tokens)
733
+ outputTokens = chunk.usage.completion_tokens;
734
+ if (chunk.type === "message_start" && chunk.message?.usage?.input_tokens)
735
+ inputTokens = chunk.message.usage.input_tokens;
736
+ if (chunk.type === "message_delta" && chunk.usage?.output_tokens) {
737
+ outputTokens = chunk.usage.output_tokens;
707
738
  }
708
739
  }
709
740
  } catch {
@@ -711,15 +742,18 @@ async function benchmarkSingleModelRest(model) {
711
742
  }
712
743
  }
713
744
  }
745
+ await logger?.flush();
714
746
  const endTime = Date.now();
715
747
  const totalTime = endTime - startTime;
716
- const timeToFirstToken = firstTokenTime ? firstTokenTime - startTime : totalTime;
748
+ const effectiveFirstToken = firstParsedTokenTime ?? firstTokenTime;
749
+ const timeToFirstToken = effectiveFirstToken ? effectiveFirstToken - startTime : totalTime;
750
+ const generationTime = totalTime - timeToFirstToken;
717
751
  const usedEstimateForOutput = !outputTokens;
718
752
  const usedEstimateForInput = !inputTokens;
719
753
  const finalOutputTokens = outputTokens || Math.round(streamedText.length / 4);
720
754
  const finalInputTokens = inputTokens || Math.round(TEST_PROMPT.length / 4);
721
755
  const totalTokens = finalInputTokens + finalOutputTokens;
722
- const tokensPerSecond = totalTime > 0 ? finalOutputTokens / totalTime * 1000 : 0;
756
+ const tokensPerSecond = generationTime > 0 ? finalOutputTokens / generationTime * 1000 : 0;
723
757
  return {
724
758
  model: model.name,
725
759
  provider: model.providerName,
@@ -734,6 +768,7 @@ async function benchmarkSingleModelRest(model) {
734
768
  success: true
735
769
  };
736
770
  } catch (error) {
771
+ await logger?.flush();
737
772
  return {
738
773
  model: model.name,
739
774
  provider: model.providerName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-speedometer-headless",
3
- "version": "2.1.6",
3
+ "version": "2.2.0",
4
4
  "description": "Headless CLI for benchmarking AI models — runs on Node.js and Bun, no TUI dependencies",
5
5
  "bin": {
6
6
  "ai-speedometer-headless": "dist/ai-speedometer-headless"