dsh-livebench-panel 0.2.2 → 0.2.3

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 +124 -35
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -427,43 +427,103 @@ function readJsonl(path) {
427
427
  }
428
428
  }
429
429
 
430
+ /**
431
+ * 异步流式读取一个 JSONL 文件,对每行回调。
432
+ * 不整读入内存、不阻塞事件循环 —— coding 类 answer 单行可达数 MB,
433
+ * 同步整读会卡死 node 事件循环(表现为 DSH 断连假死)。
434
+ */
435
+ async function streamJsonl(path, onRow) {
436
+ const fsPromises = await import("node:fs/promises");
437
+ let handle;
438
+ try {
439
+ handle = await fsPromises.open(path, "r");
440
+ } catch {
441
+ return;
442
+ }
443
+ try {
444
+ const decoder = new TextDecoder();
445
+ let buffer = "";
446
+ const chunkSize = 256 * 1024;
447
+ const buf = Buffer.alloc(chunkSize);
448
+ while (true) {
449
+ // 每读一块就让出事件循环,避免大文件连续占用
450
+ const { bytesRead } = await handle.read(buf, 0, chunkSize, null);
451
+ if (bytesRead === 0) break;
452
+ buffer += decoder.decode(buf.subarray(0, bytesRead), { stream: true });
453
+ let idx;
454
+ while ((idx = buffer.indexOf("\n")) >= 0) {
455
+ const line = buffer.slice(0, idx);
456
+ buffer = buffer.slice(idx + 1);
457
+ if (line.trim().length > 0) onRow(line);
458
+ }
459
+ }
460
+ buffer += decoder.decode();
461
+ if (buffer.trim().length > 0) onRow(buffer);
462
+ } catch {
463
+ /* 读取中断:尽力返回已读内容 */
464
+ } finally {
465
+ try { await handle.close(); } catch { /* ignore */ }
466
+ }
467
+ }
468
+
469
+ /**
470
+ * 从一行 answer JSON 提取 (model_id, 是否 $ERROR$)。不整对象解析时仍需 parse,
471
+ * 但只保留必要字段并立即释放 —— 避免大对象堆积。
472
+ */
473
+ function parseAnswerLineLight(line) {
474
+ try {
475
+ const row = JSON.parse(line);
476
+ const turns = row.choices?.[0]?.turns;
477
+ const text = Array.isArray(turns) ? String(turns[0] ?? "") : "";
478
+ return { model: typeof row.model_id === "string" ? row.model_id : null, is_error: text.startsWith("$ERROR$"), raw: row };
479
+ } catch {
480
+ return null;
481
+ }
482
+ }
483
+
430
484
  /**
431
485
  * Compute mean scores per (model, category, task) from ground-truth judgment
432
486
  * files, plus the judged/total question counts per task.
487
+ * 异步流式版:不再同步整读 answer 文件(coding 类单行数 MB,同步读会卡死
488
+ * node 事件循环导致 DSH 断连假死)。
433
489
  */
434
- function computeResults(dataDir) {
490
+ async function computeResults(dataDir) {
435
491
  const judged = new Map(); // `${model}\u0000${task}` -> {model, category, task, sum, n, time}
436
492
  const totals = new Map(); // task -> question count
437
493
  const answers = new Map(); // `${model}\u0000${task}` -> {answered, errors}
494
+ const catByTask = new Map(); // task -> category(rows 阶段查分类用)
438
495
  if (!existsSync(dataDir)) return { rows: [], taskTotals: {} };
439
496
  for (const category of readDirSafe(dataDir)) {
440
497
  const catDir = join(dataDir, category);
441
498
  for (const task of readDirSafe(catDir)) {
442
499
  const taskDir = join(catDir, task);
443
- const questions = readJsonl(join(taskDir, "question.jsonl"));
444
- totals.set(task, questions.length);
445
- // answer side: how many answers each model produced, and how many of
446
- // them were $ERROR$ (API call failures) — distinguishes "didn't run"
447
- // from "ran and failed" from "ran and scored".
500
+ catByTask.set(task, category);
501
+ let questionCount = 0;
502
+ await streamJsonl(join(taskDir, "question.jsonl"), () => { questionCount += 1; });
503
+ totals.set(task, questionCount);
504
+ // answer 侧:各模型产出了多少答案、其中多少 $ERROR$(API 失败)
448
505
  const answerDir = join(taskDir, "model_answer");
449
506
  for (const file of readDirSafe(answerDir)) {
450
507
  if (!file.endsWith(".jsonl")) continue;
451
- for (const line of readJsonl(join(answerDir, file))) {
452
- const model = typeof line.model_id === "string" ? line.model_id : file.slice(0, -".jsonl".length);
453
- const turns = line.choices?.[0]?.turns;
454
- const text = Array.isArray(turns) ? String(turns[0] ?? "") : "";
508
+ const fallbackModel = file.slice(0, -".jsonl".length);
509
+ await streamJsonl(join(answerDir, file), (line) => {
510
+ const parsed = parseAnswerLineLight(line);
511
+ if (!parsed) return;
512
+ const model = parsed.model ?? fallbackModel;
455
513
  const key = `${model}\u0000${task}`;
456
514
  const stat = answers.get(key) ?? { answered: 0, errors: 0 };
457
515
  stat.answered += 1;
458
- if (text.startsWith("$ERROR$")) stat.errors += 1;
516
+ if (parsed.is_error) stat.errors += 1;
459
517
  answers.set(key, stat);
460
- }
518
+ });
461
519
  }
462
- const judgments = readJsonl(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"));
463
- for (const row of judgments) {
520
+ // judgment 侧:判分与时间戳
521
+ await streamJsonl(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"), (line) => {
522
+ let row;
523
+ try { row = JSON.parse(line); } catch { return; }
464
524
  const model = typeof row.model === "string" ? row.model : null;
465
525
  const score = typeof row.score === "number" ? row.score : Number(row.score);
466
- if (model === null || !Number.isFinite(score) || score < 0) continue;
526
+ if (model === null || !Number.isFinite(score) || score < 0) return;
467
527
  const key = `${model}\u0000${task}`;
468
528
  const entry = judged.get(key) ?? { model, category, task, sum: 0, n: 0, time: 0 };
469
529
  entry.sum += score;
@@ -471,7 +531,9 @@ function computeResults(dataDir) {
471
531
  const tstamp = Number(row.tstamp);
472
532
  if (Number.isFinite(tstamp) && tstamp > entry.time) entry.time = tstamp;
473
533
  judged.set(key, entry);
474
- }
534
+ });
535
+ // 让出事件循环:任务之间穿插执行,保证 HTTP/websocket 实时响应
536
+ await new Promise((resolve) => setImmediate(resolve));
475
537
  }
476
538
  }
477
539
  const keys = new Set([...judged.keys(), ...answers.keys()]);
@@ -479,15 +541,10 @@ function computeResults(dataDir) {
479
541
  const entry = judged.get(key);
480
542
  const stat = answers.get(key) ?? { answered: 0, errors: 0 };
481
543
  const [model, task] = key.split("\u0000");
482
- let category = entry?.category;
483
- if (!category) {
484
- for (const cat of readDirSafe(dataDir)) {
485
- if (existsSync(join(dataDir, cat, task))) { category = cat; break; }
486
- }
487
- }
544
+ const category = entry?.category ?? catByTask.get(task) ?? "";
488
545
  return {
489
546
  model,
490
- category: category ?? "",
547
+ category,
491
548
  task,
492
549
  score: entry && entry.n > 0 ? (entry.sum / entry.n) * 100 : null,
493
550
  judged: entry?.n ?? 0,
@@ -501,6 +558,31 @@ function computeResults(dataDir) {
501
558
  return { rows, taskTotals };
502
559
  }
503
560
 
561
+ /**
562
+ * 异步统计某模型在指定 bench 范围内的 $ERROR$ 答案数(用于自动补跑判断)。
563
+ * 只扫 body.benchNames 涉及的任务目录,流式读取不阻塞事件循环。
564
+ */
565
+ async function countErrorAnswersAsync(dataDir, displayName, body) {
566
+ if (!/^[A-Za-z0-9._@-]{1,160}$/.test(displayName)) return 0;
567
+ const benches = Array.isArray(body?.benchNames) ? body.benchNames : [];
568
+ let errors = 0;
569
+ for (const bn of benches) {
570
+ const parts = bn.split("/").filter((s) => s.length > 0); // live_bench/<cat>[/<task>]
571
+ if (parts.length < 2) continue;
572
+ const category = parts[1];
573
+ const tasks = parts.length >= 3 ? [parts[2]] : readDirSafe(join(dataDir, category));
574
+ for (const task of tasks) {
575
+ if (!/^[A-Za-z0-9_]{1,80}$/.test(task)) continue;
576
+ const file = join(dataDir, category, task, "model_answer", displayName + ".jsonl");
577
+ await streamJsonl(file, (line) => {
578
+ const parsed = parseAnswerLineLight(line);
579
+ if (parsed && parsed.is_error) errors += 1;
580
+ });
581
+ }
582
+ }
583
+ return errors;
584
+ }
585
+
504
586
  /**
505
587
  * Remove every JSONL line of one model from a file (matched on the given
506
588
  * field), rewriting the file only when something changed.
@@ -740,18 +822,25 @@ function apply(ctx) {
740
822
  record.exitCode = code === null ? -1 : code;
741
823
  record.log.push(`[exit ${record.exitCode}]`);
742
824
  // 自动补跑:正常结束但存在 $ERROR$ 答案时,用 --resume --retry-failures
743
- // 只重跑失败题(最多 2 轮)。网络瞬断/超时导致的失败会被自动修复。
825
+ // 只重跑失败题(最多 2 轮)。统计改为异步流式(同步版曾阻塞事件循环,
826
+ // 且函数缺失会在 close 回调抛未捕获异常 → DSH 假死断连)。
744
827
  if (record.exitCode === 0 && record.autoRetries < 2 && record.body) {
745
- const errors = countErrorAnswers(layout.dataDir, record.displayName);
746
- if (errors > 0) {
747
- record.log.push(`[自动补跑] 检测到 ${errors} 条失败答案,自动重试(第 ${record.autoRetries + 1}/2 轮)`);
748
- const retryBody = { ...record.body, resume: true, retryFailures: true };
749
- setTimeout(() => {
750
- startRun(retryBody, record.autoRetries + 1).catch((e) => {
751
- appendLog(record, `[自动补跑启动失败] ${e.message}`);
752
- });
753
- }, 3000);
754
- }
828
+ (async () => {
829
+ try {
830
+ const errors = await countErrorAnswersAsync(layout.dataDir, record.displayName, record.body);
831
+ if (errors > 0) {
832
+ record.log.push(`[自动补跑] 检测到 ${errors} 条失败答案,自动重试(第 ${record.autoRetries + 1}/2 轮)`);
833
+ const retryBody = { ...record.body, resume: true, retryFailures: true };
834
+ setTimeout(() => {
835
+ startRun(retryBody, record.autoRetries + 1).catch((e) => {
836
+ appendLog(record, `[自动补跑启动失败] ${e.message}`);
837
+ });
838
+ }, 3000);
839
+ }
840
+ } catch (e) {
841
+ record.log.push(`[自动补跑检查失败] ${e.message}`);
842
+ }
843
+ })();
755
844
  }
756
845
  });
757
846
 
@@ -920,7 +1009,7 @@ function apply(ctx) {
920
1009
  return;
921
1010
  }
922
1011
  const layout = livebenchLayout();
923
- const { rows, taskTotals } = computeResults(layout.dataDir);
1012
+ const { rows, taskTotals } = await computeResults(layout.dataDir);
924
1013
  sendJson(res, 200, { ok: true, rows, taskTotals });
925
1014
  },
926
1015
  }), `${name}: results route`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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",