dsh-livebench-panel 0.2.10 → 0.2.12

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 (3) hide show
  1. package/lib/client.js +22 -15
  2. package/lib/index.js +126 -41
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -587,21 +587,28 @@ window.__ModuleLoader__.load({
587
587
  h("td", null, model),
588
588
  matrix.tasks.map((taskKeyCol) => {
589
589
  const row = matrix.cells.get(`${model}__@__${taskKeyCol}`);
590
- let cellText = "×";
591
- let title = "该模型未执行此任务";
592
- if (row) {
593
- if (row.answered > 0 && row.errors >= row.answered) {
594
- cellText = `E (${row.errors})`;
595
- title = "该任务全部回答均 API 失败($ERROR$),计 0 ";
596
- } else if (row.judged === 0) {
597
- cellText = "未判分";
598
- title = "有答案但尚未判分";
599
- } else {
600
- cellText = `${row.score.toFixed(1)} (${row.judged}/${row.total})`;
601
- if (row.errors > 0) title += ` · 其中 ${row.errors} 题 API 失败计 0 分`;
602
- }
590
+ // 单元格:上方正确率,下方 (-没做/设定/总)。
591
+ // 没做/没做完(访问失败、网络中断)不计入正确率分母。
592
+ const subStyle = { fontSize: "10.5px", color: "var(--dsw-alias-label-tertiary)", fontWeight: "400" };
593
+ const sub = `(-${row ? (row.notDone ?? 0) : 0}/${row ? (row.configured ?? 0) : 0}/${row ? row.total : 0})`;
594
+ if (!row) {
595
+ return h("td", { key: taskKeyCol, className: c("score"), title: "该模型未执行此任务(无任何记录)" },
596
+ h("div", null, "×"),
597
+ h("div", { style: subStyle }, sub));
603
598
  }
604
- return h("td", { key: taskKeyCol, className: c("score"), title }, cellText);
599
+ const allFailed = row.answered > 0 && row.errors >= row.answered;
600
+ const title = allFailed
601
+ ? `该任务 ${row.errors} 题全部访问失败(未做完),不计入正确率`
602
+ : `做完 ${row.done ?? 0} 题,没做/失败 ${row.notDone ?? 0} 题` +
603
+ (row.errors > 0 ? `(其中 ${row.errors} 题 API 失败)` : "");
604
+ if (allFailed || row.judged === 0) {
605
+ return h("td", { key: taskKeyCol, className: c("score"), title },
606
+ h("div", null, "—"),
607
+ h("div", { style: subStyle }, sub));
608
+ }
609
+ return h("td", { key: taskKeyCol, className: c("score"), title },
610
+ h("div", null, row.score.toFixed(1)),
611
+ h("div", { style: subStyle }, sub));
605
612
  }),
606
613
  h("td", null, h("button", {
607
614
  className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
@@ -658,7 +665,7 @@ window.__ModuleLoader__.load({
658
665
  "提示:回答为 $ERROR$ 表示该题 API 调用失败(网络/鉴权/参数问题)计 0 分,可在上方日志区查看具体错误;zebra_puzzle(逻辑谜题)是公认最难的任务,低分属正常现象。"),
659
666
  ),
660
667
  h("p", { className: c("hint") },
661
- "评分读取 LiveBench ground_truth_judgment.jsonl;正式榜单可用 release 2024-11-25。多选题集请在「分类/任务」中缩小范围,避免长时间运行。"),
668
+ "格式说明:单元格上方为正确率(= 做对 / 做完),下方括号 (-没做/设定/总):没做或没做完的题(访问失败、网络中断、未跑到)不计入正确率。× 表示该模型完全没跑过该任务。正式榜单可用 release 2024-11-25"),
662
669
  );
663
670
  }
664
671
  /**
package/lib/index.js CHANGED
@@ -307,6 +307,22 @@ function readDirSafe(dir) {
307
307
  }
308
308
  }
309
309
 
310
+ /** 列出目录下的【文件】(可按扩展名过滤)——注意 readDirSafe 只列目录。 */
311
+ function readFilesSafe(dir, ext) {
312
+ try {
313
+ return readdirSync(dir).filter((f) => {
314
+ try {
315
+ if (!statSync(join(dir, f)).isFile()) return false;
316
+ return ext === undefined || f.endsWith(ext);
317
+ } catch {
318
+ return false;
319
+ }
320
+ });
321
+ } catch {
322
+ return [];
323
+ }
324
+ }
325
+
310
326
  function readdirNames(dir) {
311
327
  try {
312
328
  return readdirSync(dir).filter((entry) => {
@@ -485,56 +501,83 @@ function parseAnswerLineLight(line) {
485
501
 
486
502
  /**
487
503
  * 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 断连假死)。
504
+ * files, plus judged/total question counts per task.
505
+ *
506
+ * 性能:数据目录可能上百 MB(每次评测生成独立答案文件)。这里:
507
+ * - answer 文件用【文件名】当模型名,只做行数与 "$ERROR$" 子串统计 —— 不解析大 JSON 行;
508
+ * - question.jsonl 只数行数;
509
+ * - 结果缓存 5 秒,避免前端频繁刷新时重复全量扫描。
491
510
  */
511
+ let resultsCache = { at: 0, data: null };
512
+
513
+ async function readTextFileAsync(path) {
514
+ const fsPromises = await import("node:fs/promises");
515
+ try {
516
+ return await fsPromises.readFile(path, "utf8");
517
+ } catch {
518
+ return null;
519
+ }
520
+ }
521
+
492
522
  async function computeResults(dataDir) {
523
+ const now = Date.now();
524
+ if (resultsCache.data !== null && now - resultsCache.at < 5000) return resultsCache.data;
493
525
  const judged = new Map(); // `${model}\u0000${task}` -> {model, category, task, sum, n, time}
494
526
  const totals = new Map(); // task -> question count
495
527
  const answers = new Map(); // `${model}\u0000${task}` -> {answered, errors}
496
- const catByTask = new Map(); // task -> category(rows 阶段查分类用)
528
+ const catByTask = new Map();
497
529
  if (!existsSync(dataDir)) return { rows: [], taskTotals: {} };
530
+ // 读取每次评测的配置范围(displayName -> {benchNames, begin, end})
531
+ const runMeta = new Map();
532
+ try {
533
+ const metaDir = join(dataDir, ".dsh_runs");
534
+ for (const f of readdirSync(metaDir)) {
535
+ if (!f.endsWith(".json")) continue;
536
+ try {
537
+ runMeta.set(f.slice(0, -5), JSON.parse(readFileSync(join(metaDir, f), "utf8")));
538
+ } catch { /* 单个元数据损坏忽略 */ }
539
+ }
540
+ } catch { /* 无元数据目录(历史数据) */ }
498
541
  for (const category of readDirSafe(dataDir)) {
499
542
  const catDir = join(dataDir, category);
500
543
  for (const task of readDirSafe(catDir)) {
501
544
  const taskDir = join(catDir, task);
502
545
  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 失败)
546
+ const qText = await readTextFileAsync(join(taskDir, "question.jsonl"));
547
+ totals.set(task, qText === null ? 0 : qText.split("\n").reduce((n, l) => n + (l.trim().length > 0 ? 1 : 0), 0));
507
548
  const answerDir = join(taskDir, "model_answer");
508
- for (const file of readDirSafe(answerDir)) {
509
- 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 };
549
+ for (const file of readFilesSafe(answerDir, ".jsonl")) {
550
+ const model = file.slice(0, -".jsonl".length);
551
+ const text = await readTextFileAsync(join(answerDir, file));
552
+ if (text === null) continue;
553
+ const key = `${model}\u0000${task}`;
554
+ const stat = answers.get(key) ?? { answered: 0, errors: 0 };
555
+ for (const line of text.split("\n")) {
556
+ if (line.trim().length === 0) continue;
517
557
  stat.answered += 1;
518
- if (parsed.is_error) stat.errors += 1;
519
- answers.set(key, stat);
520
- });
558
+ if (line.includes('"$ERROR$"')) stat.errors += 1;
559
+ }
560
+ answers.set(key, stat);
521
561
  }
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 实时响应
562
+ const jText = await readTextFileAsync(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"));
563
+ if (jText !== null) {
564
+ for (const line of jText.split("\n")) {
565
+ if (line.trim().length === 0) continue;
566
+ let row;
567
+ try { row = JSON.parse(line); } catch { continue; }
568
+ const model = typeof row.model === "string" ? row.model : null;
569
+ const score = typeof row.score === "number" ? row.score : Number(row.score);
570
+ if (model === null || !Number.isFinite(score) || score < 0) continue;
571
+ const key = `${model}\u0000${task}`;
572
+ const entry = judged.get(key) ?? { model, category, task, sum: 0, n: 0, time: 0 };
573
+ entry.sum += score;
574
+ entry.n += 1;
575
+ const tstamp = Number(row.tstamp);
576
+ if (Number.isFinite(tstamp) && tstamp > entry.time) entry.time = tstamp;
577
+ judged.set(key, entry);
578
+ }
579
+ }
580
+ // 让出事件循环,保证 HTTP/websocket 实时响应
538
581
  await new Promise((resolve) => setImmediate(resolve));
539
582
  }
540
583
  }
@@ -544,20 +587,48 @@ async function computeResults(dataDir) {
544
587
  const stat = answers.get(key) ?? { answered: 0, errors: 0 };
545
588
  const [model, task] = key.split("\u0000");
546
589
  const category = entry?.category ?? catByTask.get(task) ?? "";
590
+ const total = totals.get(task) ?? 0;
591
+ const judgedCount = entry?.n ?? 0;
592
+ // 做完的题 = 实际产出的有效答案(尝试数 - 访问失败数)
593
+ const done = Math.max(0, stat.answered - stat.errors);
594
+ // 设定题数:优先用评测元数据的范围(end 含端点),否则用尝试/判分中的较大值
595
+ let configured = Math.max(stat.answered, judgedCount);
596
+ const meta = runMeta.get(model);
597
+ if (meta !== undefined) {
598
+ const inScope = (meta.benchNames || []).some((bn) => {
599
+ const parts = String(bn).split("/").filter((s) => s.length > 0);
600
+ if (parts.length >= 3) return parts[1] === category && parts[2] === task;
601
+ if (parts.length === 2) return parts[1] === category;
602
+ return true;
603
+ });
604
+ if (inScope) {
605
+ const hasRange = meta.begin !== null && meta.begin !== undefined && meta.end !== null && meta.end !== undefined;
606
+ configured = hasRange ? Math.min(total, Number(meta.end) - Number(meta.begin) + 1) : total;
607
+ }
608
+ }
609
+ if (configured < stat.answered) configured = stat.answered;
610
+ const notDone = Math.max(0, configured - done);
611
+ // 正确率 = 做对的题 / 已判分的做完题(访问失败/未做的题不计入分母)
612
+ const judgedDone = Math.max(0, judgedCount - stat.errors);
613
+ const score = entry && judgedDone > 0 ? (entry.sum / judgedDone) * 100 : null;
547
614
  return {
548
615
  model,
549
616
  category,
550
617
  task,
551
- score: entry && entry.n > 0 ? (entry.sum / entry.n) * 100 : null,
618
+ score,
552
619
  judged: entry?.n ?? 0,
553
- total: totals.get(task) ?? 0,
620
+ total,
554
621
  time: entry && entry.time > 0 ? entry.time : null,
555
622
  answered: stat.answered,
556
623
  errors: stat.errors,
624
+ done,
625
+ configured,
626
+ notDone,
557
627
  };
558
628
  });
559
- const taskTotals = Object.fromEntries(totals);
560
- return { rows, taskTotals };
629
+ const result = { rows, taskTotals: Object.fromEntries(totals) };
630
+ resultsCache = { at: Date.now(), data: result };
631
+ return result;
561
632
  }
562
633
 
563
634
  /**
@@ -790,6 +861,19 @@ function apply(ctx) {
790
861
  }
791
862
  }
792
863
 
864
+ // 记录本次评测的配置范围:/results 用它计算每个任务的"设定题数"
865
+ // (未做/失败的题数 = 设定题数 - 实际产出的有效答案数)。
866
+ try {
867
+ const metaDir = join(layout.dataDir, ".dsh_runs");
868
+ if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true });
869
+ writeFileSync(join(metaDir, displayName + ".json"), JSON.stringify({
870
+ benchNames: benchNames !== null ? benchNames : [benchParts.join("/")],
871
+ begin: body.begin ?? null,
872
+ end: body.end ?? null,
873
+ startedAt: new Date().toISOString(),
874
+ }), "utf8");
875
+ } catch { /* 元数据写入失败不影响评测 */ }
876
+
793
877
  const child = spawn(layout.pythonExe, args, {
794
878
  cwd: layout.livebenchDir,
795
879
  env,
@@ -847,7 +931,7 @@ function apply(ctx) {
847
931
  startRun(retryBody, record.autoRetries + 1).catch((e) => {
848
932
  appendLog(record, `[自动补跑启动失败] ${e.message}`);
849
933
  });
850
- }, 15000);
934
+ }, [15000, 60000, 180000][Math.min(record.autoRetries, 2)]);
851
935
  }
852
936
  } catch (e) {
853
937
  record.log.push(`[自动补跑检查失败] ${e.message}`);
@@ -1101,6 +1185,7 @@ function apply(ctx) {
1101
1185
  removed += filterJsonlByModel(join(taskDir, "model_answer", `${model}.jsonl`), "model_id", model);
1102
1186
  removed += filterJsonlByModel(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"), "model", model);
1103
1187
  }
1188
+ resultsCache = { at: 0, data: null };
1104
1189
  sendJson(res, 200, { ok: true, removed });
1105
1190
  },
1106
1191
  }), `${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.12",
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",