dsh-livebench-panel 0.2.10 → 0.2.11

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/lib/index.js +58 -39
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -485,56 +485,73 @@ function parseAnswerLineLight(line) {
485
485
 
486
486
  /**
487
487
  * Compute mean scores per (model, category, task) from ground-truth judgment
488
- * files, plus the judged/total question counts per task.
489
- * 异步流式版:不再同步整读 answer 文件(coding 类单行数 MB,同步读会卡死
490
- * node 事件循环导致 DSH 断连假死)。
488
+ * files, plus judged/total question counts per task.
489
+ *
490
+ * 性能:数据目录可能上百 MB(每次评测生成独立答案文件)。这里:
491
+ * - answer 文件用【文件名】当模型名,只做行数与 "$ERROR$" 子串统计 —— 不解析大 JSON 行;
492
+ * - question.jsonl 只数行数;
493
+ * - 结果缓存 5 秒,避免前端频繁刷新时重复全量扫描。
491
494
  */
495
+ let resultsCache = { at: 0, data: null };
496
+
497
+ async function readTextFileAsync(path) {
498
+ const fsPromises = await import("node:fs/promises");
499
+ try {
500
+ return await fsPromises.readFile(path, "utf8");
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+
492
506
  async function computeResults(dataDir) {
493
- const judged = new Map(); // `${model}\u0000${task}` -> {model, category, task, sum, n, time}
507
+ const now = Date.now();
508
+ if (resultsCache.data !== null && now - resultsCache.at < 5000) return resultsCache.data;
509
+ const judged = new Map(); // `${model}${task}` -> {model, category, task, sum, n, time}
494
510
  const totals = new Map(); // task -> question count
495
- const answers = new Map(); // `${model}\u0000${task}` -> {answered, errors}
496
- const catByTask = new Map(); // task -> category(rows 阶段查分类用)
511
+ const answers = new Map(); // `${model}${task}` -> {answered, errors}
512
+ const catByTask = new Map();
497
513
  if (!existsSync(dataDir)) return { rows: [], taskTotals: {} };
498
514
  for (const category of readDirSafe(dataDir)) {
499
515
  const catDir = join(dataDir, category);
500
516
  for (const task of readDirSafe(catDir)) {
501
517
  const taskDir = join(catDir, task);
502
518
  catByTask.set(task, category);
503
- let questionCount = 0;
504
- await streamJsonl(join(taskDir, "question.jsonl"), () => { questionCount += 1; });
505
- totals.set(task, questionCount);
506
- // answer 侧:各模型产出了多少答案、其中多少 $ERROR$(API 失败)
519
+ const qText = await readTextFileAsync(join(taskDir, "question.jsonl"));
520
+ totals.set(task, qText === null ? 0 : qText.split("\n").reduce((n, l) => n + (l.trim().length > 0 ? 1 : 0), 0));
507
521
  const answerDir = join(taskDir, "model_answer");
508
522
  for (const file of readDirSafe(answerDir)) {
509
523
  if (!file.endsWith(".jsonl")) continue;
510
- const fallbackModel = file.slice(0, -".jsonl".length);
511
- await streamJsonl(join(answerDir, file), (line) => {
512
- const parsed = parseAnswerLineLight(line);
513
- if (!parsed) return;
514
- const model = parsed.model ?? fallbackModel;
515
- const key = `${model}\u0000${task}`;
516
- const stat = answers.get(key) ?? { answered: 0, errors: 0 };
524
+ const model = file.slice(0, -".jsonl".length);
525
+ const text = await readTextFileAsync(join(answerDir, file));
526
+ if (text === null) continue;
527
+ const key = `${model}${task}`;
528
+ const stat = answers.get(key) ?? { answered: 0, errors: 0 };
529
+ for (const line of text.split("\n")) {
530
+ if (line.trim().length === 0) continue;
517
531
  stat.answered += 1;
518
- if (parsed.is_error) stat.errors += 1;
519
- answers.set(key, stat);
520
- });
532
+ if (line.includes('"$ERROR$"')) stat.errors += 1;
533
+ }
534
+ answers.set(key, stat);
521
535
  }
522
- // judgment 侧:判分与时间戳
523
- await streamJsonl(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"), (line) => {
524
- let row;
525
- try { row = JSON.parse(line); } catch { return; }
526
- const model = typeof row.model === "string" ? row.model : null;
527
- const score = typeof row.score === "number" ? row.score : Number(row.score);
528
- if (model === null || !Number.isFinite(score) || score < 0) return;
529
- const key = `${model}\u0000${task}`;
530
- const entry = judged.get(key) ?? { model, category, task, sum: 0, n: 0, time: 0 };
531
- entry.sum += score;
532
- entry.n += 1;
533
- const tstamp = Number(row.tstamp);
534
- if (Number.isFinite(tstamp) && tstamp > entry.time) entry.time = tstamp;
535
- judged.set(key, entry);
536
- });
537
- // 让出事件循环:任务之间穿插执行,保证 HTTP/websocket 实时响应
536
+ const jText = await readTextFileAsync(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"));
537
+ if (jText !== null) {
538
+ for (const line of jText.split("\n")) {
539
+ if (line.trim().length === 0) continue;
540
+ let row;
541
+ try { row = JSON.parse(line); } catch { continue; }
542
+ const model = typeof row.model === "string" ? row.model : null;
543
+ const score = typeof row.score === "number" ? row.score : Number(row.score);
544
+ if (model === null || !Number.isFinite(score) || score < 0) continue;
545
+ const key = `${model}${task}`;
546
+ const entry = judged.get(key) ?? { model, category, task, sum: 0, n: 0, time: 0 };
547
+ entry.sum += score;
548
+ entry.n += 1;
549
+ const tstamp = Number(row.tstamp);
550
+ if (Number.isFinite(tstamp) && tstamp > entry.time) entry.time = tstamp;
551
+ judged.set(key, entry);
552
+ }
553
+ }
554
+ // 让出事件循环,保证 HTTP/websocket 实时响应
538
555
  await new Promise((resolve) => setImmediate(resolve));
539
556
  }
540
557
  }
@@ -542,7 +559,7 @@ async function computeResults(dataDir) {
542
559
  const rows = [...keys].map((key) => {
543
560
  const entry = judged.get(key);
544
561
  const stat = answers.get(key) ?? { answered: 0, errors: 0 };
545
- const [model, task] = key.split("\u0000");
562
+ const [model, task] = key.split("");
546
563
  const category = entry?.category ?? catByTask.get(task) ?? "";
547
564
  return {
548
565
  model,
@@ -556,8 +573,9 @@ async function computeResults(dataDir) {
556
573
  errors: stat.errors,
557
574
  };
558
575
  });
559
- const taskTotals = Object.fromEntries(totals);
560
- return { rows, taskTotals };
576
+ const result = { rows, taskTotals: Object.fromEntries(totals) };
577
+ resultsCache = { at: Date.now(), data: result };
578
+ return result;
561
579
  }
562
580
 
563
581
  /**
@@ -1101,6 +1119,7 @@ function apply(ctx) {
1101
1119
  removed += filterJsonlByModel(join(taskDir, "model_answer", `${model}.jsonl`), "model_id", model);
1102
1120
  removed += filterJsonlByModel(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"), "model", model);
1103
1121
  }
1122
+ resultsCache = { at: 0, data: null };
1104
1123
  sendJson(res, 200, { ok: true, removed });
1105
1124
  },
1106
1125
  }), `${name}: delete route`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "DSH web plugin: a LiveBench tab in the Trajectory view (right of 对话/轨迹). Run LiveBench evaluations against every model configured in the DeepSeek Harness — pick provider/model, category, task, release and question range from dropdowns, watch progress, and read scores in place.",
5
5
  "license": "MIT",
6
6
  "type": "module",