dsh-livebench-panel 0.2.17 → 0.2.20

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.
package/README.md CHANGED
@@ -42,11 +42,43 @@ cd livebench
42
42
  | 模型 | harness 全部 provider 的全部模型(含内置 deepseek-official);分组展示 | 首次验证选小任务 + 便宜模型 |
43
43
  | 推理强度 | 该模型的 reasoning effort(无配置则禁用);编码进条目名,不同强度独立出分 | 日常 `medium`/`high`;对比测试固定同一档 |
44
44
  | 题集 release | LiveBench 题目发布批次 | 公开题目最全的是 `2024-11-25`(推荐) |
45
+ | **Baseline 题库** | 参考模型的实测对错题集,**可脱离分类/任务单独使用**(见下节) | 快速探针:参考模型「做错」的题 |
45
46
  | 分类 | 六大类:coding / math / reasoning / language / data_analysis / instruction_following | 首次验证选 `language` |
46
47
  | 任务 | 分类下的具体任务(如 language/typos 拼写纠错) | 首次验证选 `typos`(短平快) |
47
- | 题目序号范围 | 该任务题目的起止下标(0 起,**含首尾**) | 冒烟测试填 0–1(只跑 2 题) |
48
+ | 题目序号范围 | 起止下标(0 起,**含首尾**);选 Baseline 时序号相对该题库 | 冒烟测试填 0–1(只跑 2 题) |
48
49
  | max-tokens | 单次回答的 token 上限 | 推理模型给 8192+,否则思考被截断判 0 分 |
49
50
 
51
+ ## Baseline 题库(探针)
52
+
53
+ 用**一个参考模型的实测对错**当尺子,快速判断别的模型处在哪个档位。位于「分类 / 任务」之上,
54
+ 三层可选:
55
+
56
+ 1. **参考模型**:`Baseline(glm-5.3-flash@max)`
57
+ 2. **任务**:该参考模型已经跑过全量的任务(如 `olympiad(36)`)
58
+ 3. **做错 / 做对**:各自标注题目数量(如 `做错(8)` / `做对(28)`),可多选
59
+
60
+ 选中后**不需要也不应该再选分类/任务**——"不选 = 全部分类"这条规则对 Baseline 不适用。
61
+ 「题目序号范围」仍然生效,序号相对 Baseline 题库(0 起、含首尾);超出上界不会报错,
62
+ 会按实际题数截断。
63
+
64
+ **为什么默认勾「做错」**:探针的价值在于"参考模型做不出来的题"。参考模型能轻松做对的题,
65
+ 对更强的模型没有分辨力,混进来只会稀释信号。「做对」也保留,用于反向验证
66
+ (例如确认某模型确实强于参考模型)。
67
+
68
+ **当前内置的数据**(2026-09-12 实测,release `2024-11-25`):
69
+
70
+ | 参考模型 | 任务 | 全量 | 做对 | 做错 |
71
+ |---|---|---|---|---|
72
+ | `zai-coding-cn__glm-5.3-flash@max` | `math/olympiad` | 36 | 28 | 8 |
73
+
74
+ 实测效果(跑「做错」那 8 题):`aiportx-gg/grok-4.6@xhigh` 明显强于参考模型;
75
+ `deepseek-official/deepseek-flash@max` 明显更弱。失败的 8 题全部属于 olympiad 的
76
+ **公式还原**题型——把解答里的公式挖空让模型补全,是纯符号推理,很吃"智力"。
77
+
78
+ 后续任务按同样方式补进 `lib/index.js` 的 `BASELINE_SETS.tasks` 即可:
79
+ 只需填 `passed` / `failed` 两组 question id(取自 LiveBench 的 `question.jsonl`),
80
+ 面板用 `--question-id` 直接指定,题目在题库里是否连续都无所谓。
81
+
50
82
  ## 功能
51
83
 
52
84
  - **标签页位置**:`conversation.view` 槽位 `id: "livebench"`、`order: 20`(对话 = 0,轨迹 = 10,LiveBench 排最右)。
package/lib/client.js CHANGED
@@ -121,7 +121,7 @@ window.__ModuleLoader__.load({
121
121
  function LiveBenchView() {
122
122
  const [config, setConfig] = useState(null);
123
123
  const [configError, setConfigError] = useState(null);
124
- const [sel, setSel] = useState({ models: [], efforts: {}, cats: [], tasks: [], release: "2024-11-25", begin: "", end: "", maxTokens: "32000" });
124
+ const [sel, setSel] = useState({ models: [], efforts: {}, cats: [], tasks: [], release: "2024-11-25", begin: "", end: "", maxTokens: "32000", baseline: null });
125
125
  const [modelDdOpen, setModelDdOpen] = useState(false);
126
126
  const [busy, setBusy] = useState(false);
127
127
  const [running, setRunning] = useState(false);
@@ -131,8 +131,8 @@ window.__ModuleLoader__.load({
131
131
  const [homeBusy, setHomeBusy] = useState(false);
132
132
  const [results, setResults] = useState(null);
133
133
  const [pickedModels, setPickedModels] = useState(() => new Set());
134
- const [sortBy, setSortBy] = useState(null);
135
- const [sortDir, setSortDir] = useState("asc");
134
+ const [sortBy, setSortBy] = useState("time");
135
+ const [sortDir, setSortDir] = useState("desc");
136
136
  const [modelOrder, setModelOrder] = useState(() => {
137
137
  try { return JSON.parse(localStorage.getItem("dlb_model_order_v1")) ?? []; } catch { return []; }
138
138
  });
@@ -243,24 +243,54 @@ window.__ModuleLoader__.load({
243
243
  return { providerId: pid, modelId: mid, modelValue: value, efforts: model?.efforts ?? [] };
244
244
  }), [sel.models, config]);
245
245
 
246
+ // Baseline 三层选择:参考模型 -> 任务 -> 做对/做错
247
+ const baselineObj = useMemo(
248
+ () => (config?.baselines ?? []).find((b) => b.id === sel.baseline) ?? null,
249
+ [config, sel.baseline]);
250
+ const baselineTaskObj = useMemo(
251
+ () => (baselineObj?.tasks ?? []).find((t) => t.task === sel.baselineTask) ?? null,
252
+ [baselineObj, sel.baselineTask]);
253
+ // 当前勾选真正会跑几题:题号范围的上界就是它 - 1
254
+ const baselineSelectedCount = useMemo(() => {
255
+ if (baselineTaskObj === null) return 0;
256
+ let n = 0;
257
+ if (sel.baselinePicks.includes("failed")) n += baselineTaskObj.failedCount;
258
+ if (sel.baselinePicks.includes("passed")) n += baselineTaskObj.passedCount;
259
+ return n;
260
+ }, [baselineTaskObj, sel.baselinePicks]);
261
+
246
262
  const onStart = async () => {
247
263
  setStartError(null);
248
264
  if (selectedModelEntries.length === 0) {
249
265
  setStartError("请先在模型下拉中至少勾选一个模型。");
250
266
  return;
251
267
  }
252
- const benchNames = [];
253
- if (sel.cats.length === 0) {
254
- benchNames.push("live_bench");
255
- } else if (sel.tasks.length === 0) {
256
- for (const cat of sel.cats) benchNames.push(`live_bench/${cat}`);
268
+ // Baseline 题库:绕开分类/任务的"不选 = 全部"推导,
269
+ // 由服务端按「参考模型 + 任务 + 做对/做错」解析出 question id。
270
+ let benchNames = null;
271
+ if (baselineObj !== null) {
272
+ if (baselineTaskObj === null) {
273
+ setStartError("已选 Baseline,请再选择它下面的任务(例如 olympiad)。");
274
+ return;
275
+ }
276
+ if (sel.baselinePicks.length === 0) {
277
+ setStartError("已选 Baseline 任务,请至少勾选「做错」或「做对」之一。");
278
+ return;
279
+ }
257
280
  } else {
258
- for (const t of sel.tasks) benchNames.push(`live_bench/${t}`);
259
- }
260
- const zeroTask = sel.tasks.find((t) => countFor(t.split("/")[0], t.split("/")[1]) === 0);
261
- if (zeroTask) {
262
- setStartError(`任务 ${zeroTask.split("/")[1]} release ${sel.release} 下没有可用题目(该批题目已退役),请取消勾选或换 release。`);
263
- return;
281
+ benchNames = [];
282
+ if (sel.cats.length === 0) {
283
+ benchNames.push("live_bench");
284
+ } else if (sel.tasks.length === 0) {
285
+ for (const cat of sel.cats) benchNames.push(`live_bench/${cat}`);
286
+ } else {
287
+ for (const t of sel.tasks) benchNames.push(`live_bench/${t}`);
288
+ }
289
+ const zeroTask = sel.tasks.find((t) => countFor(t.split("/")[0], t.split("/")[1]) === 0);
290
+ if (zeroTask) {
291
+ setStartError(`任务 ${zeroTask.split("/")[1]} 在 release ${sel.release} 下没有可用题目(该批题目已退役),请取消勾选或换 release。`);
292
+ return;
293
+ }
264
294
  }
265
295
  setBusy(true);
266
296
  try {
@@ -274,7 +304,9 @@ window.__ModuleLoader__.load({
274
304
  provider: entry.providerId,
275
305
  model: entry.modelId,
276
306
  reasoningEffort: sel.efforts[entry.modelValue] ?? "default",
277
- benchNames,
307
+ ...(baselineObj !== null
308
+ ? { baseline: baselineObj.id, baselineTask: sel.baselineTask, baselinePicks: sel.baselinePicks }
309
+ : { benchNames }),
278
310
  release: sel.release,
279
311
  begin: sel.begin,
280
312
  end: sel.end,
@@ -359,15 +391,20 @@ window.__ModuleLoader__.load({
359
391
  const groupStart = new Set(groups.map((g) => g.tasks[0]));
360
392
  const cells = new Map();
361
393
  const startTimes = new Map();
394
+ // 哪些 模型×任务 是 Baseline 子集跑出来的:同一列里会混着"全量"和
395
+ // "题库子集"两种分数,必须在界面上标出来,否则没法解读。
396
+ const baselineCells = new Set();
362
397
  for (const row of results.rows) {
363
- cells.set(`${row.model}__@__${taskKey(row.category, row.task)}`, row);
398
+ const key = `${row.model}__@__${taskKey(row.category, row.task)}`;
399
+ cells.set(key, row);
400
+ if (row.baselineLabel) baselineCells.add(key);
364
401
  const fromMeta = row.runStartedAt ? Math.floor(Date.parse(row.runStartedAt) / 1000) : null;
365
402
  const candidate = (Number.isFinite(fromMeta) ? fromMeta : null) ?? runStampSeconds(row.model) ?? row.time ?? null;
366
403
  const prev = startTimes.get(row.model);
367
404
  if (candidate && (prev === undefined || candidate < prev)) startTimes.set(row.model, candidate);
368
405
  }
369
406
  const taskTotals = (results && results.taskTotals) || {};
370
- return { models, tasks, groups, groupStart, cells, startTimes, taskTotals };
407
+ return { models, tasks, groups, groupStart, cells, startTimes, taskTotals, baselineCells };
371
408
  }, [results]);
372
409
  const cellOf = (model, key) => matrix.cells.get(`${model}__@__${key}`);
373
410
  // 排序:sortBy=null 时用拖拽自定义顺序;点击表头在 正序/倒序 间切换
@@ -380,8 +417,13 @@ window.__ModuleLoader__.load({
380
417
  }
381
418
  if (sortBy === "time") {
382
419
  base.sort((a, b) => {
420
+ // 没有时间戳的排在最后,不管升序降序 —— 否则空值会被当成 0
421
+ // 混在中间/开头,看起来像"最旧的一次评测"。
383
422
  const ta = matrix.startTimes.get(a) ?? 0;
384
423
  const tb = matrix.startTimes.get(b) ?? 0;
424
+ if (ta === 0 && tb === 0) return a.localeCompare(b);
425
+ if (ta === 0) return 1;
426
+ if (tb === 0) return -1;
385
427
  return sortDir === "asc" ? ta - tb : tb - ta;
386
428
  });
387
429
  return base;
@@ -398,7 +440,7 @@ window.__ModuleLoader__.load({
398
440
  // 点击表头:切换排序字段(再次点击同一列则反转方向)
399
441
  const toggleSort = (field) => {
400
442
  if (sortBy === field) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
401
- else { setSortBy(field); setSortDir("asc"); }
443
+ else { setSortBy(field); setSortDir(field === "time" ? "desc" : "asc"); }
402
444
  };
403
445
  const sortMark = (field) => (sortBy === field ? (sortDir === "asc" ? " ↑" : " ↓") : "");
404
446
 
@@ -544,6 +586,67 @@ window.__ModuleLoader__.load({
544
586
  h("input", { className: c("input"), type: "number", min: 0, placeholder: "起(含)", title: "起始题号(0 起,包含该题)", value: sel.begin, onChange: setField("begin"), style: { flex: "1", minWidth: "0" } }),
545
587
  h("input", { className: c("input"), type: "number", min: 0, placeholder: "止(含)", title: "结束题号(包含该题);只填起不填止 = 从该题跑到底", value: sel.end, onChange: setField("end"), style: { flex: "1", minWidth: "0" } }),
546
588
  ),
589
+ // Baseline 题库:位于「分类 / 任务」之上的独立条目,三层选择。
590
+ // 选中后走它自己的任务 + 一组 question id,因此不需要(也不应该)
591
+ // 再受"不选分类 = 全部分类"这条规则约束。
592
+ h("div", { className: c("field") },
593
+ h("label", { className: c("label") }, "Baseline 题库(探针,可单独使用,不必选分类/任务)"),
594
+ // 第 1 层:参考模型
595
+ h("div", { className: c("chips") },
596
+ (config?.baselines ?? []).map((b) => h("button", {
597
+ key: b.id,
598
+ type: "button",
599
+ className: c("chip") + (sel.baseline === b.id ? " " + c("chipOn") : ""),
600
+ title: `参考模型 ${b.referenceModel}\n已实测 ${b.tasks.length} 个任务`,
601
+ onClick: () => setSel((prev) => (prev.baseline === b.id
602
+ ? { ...prev, baseline: null, baselineTask: null, baselinePicks: [] }
603
+ : { ...prev, baseline: b.id, baselineTask: null, baselinePicks: [] })),
604
+ }, b.label)),
605
+ ),
606
+ // 第 2 层:任务(该参考模型已实测过的任务)
607
+ baselineObj !== null && h("div", { className: c("chips"), style: { marginTop: "6px" } },
608
+ h("span", { className: c("hint"), style: { alignSelf: "center" } }, "任务:"),
609
+ (baselineObj.tasks ?? []).map((t) => h("button", {
610
+ key: t.task,
611
+ type: "button",
612
+ className: c("chip") + (sel.baselineTask === t.task ? " " + c("chipOn") : ""),
613
+ title: `${t.task} · release ${t.release}\n该任务全量 ${t.total} 题(做对 ${t.passedCount} / 做错 ${t.failedCount})`,
614
+ onClick: () => setSel((prev) => (prev.baselineTask === t.task
615
+ ? { ...prev, baselineTask: null, baselinePicks: [] }
616
+ // 默认勾「做错」:探针的用途就是拿参考模型做不出来的题去量别人
617
+ : { ...prev, baselineTask: t.task, baselinePicks: ["failed"] })),
618
+ }, `${t.taskName}(${t.total})`)),
619
+ ),
620
+ // 第 3 层:做对 / 做错,各带题目数量
621
+ baselineTaskObj !== null && h("div", { className: c("chips"), style: { marginTop: "6px" } },
622
+ h("span", { className: c("hint"), style: { alignSelf: "center" } }, "题目:"),
623
+ [["failed", "做错"], ["passed", "做对"]].map(([key, text]) => {
624
+ const count = key === "failed" ? baselineTaskObj.failedCount : baselineTaskObj.passedCount;
625
+ const on = sel.baselinePicks.includes(key);
626
+ return h("button", {
627
+ key,
628
+ type: "button",
629
+ className: c("chip") + (on ? " " + c("chipOn") : ""),
630
+ disabled: count === 0,
631
+ title: key === "failed"
632
+ ? `${baselineTaskObj.referenceModel} 得 0 分的 ${count} 题(探针首选:做不出来的题才有分辨力)`
633
+ : `${baselineTaskObj.referenceModel} 判分 ≥0.5 的 ${count} 题(反向验证用)`,
634
+ onClick: () => setSel((prev) => ({
635
+ ...prev,
636
+ baselinePicks: prev.baselinePicks.includes(key)
637
+ ? prev.baselinePicks.filter((p) => p !== key)
638
+ : [...prev.baselinePicks, key],
639
+ })),
640
+ }, `${text}(${count})`);
641
+ }),
642
+ ),
643
+ baselineTaskObj !== null && h("span", { className: c("hint") },
644
+ `已选 ${baselineTaskObj.referenceModel} 的 ${baselineTaskObj.taskName}:`
645
+ + `本次只跑勾选的题(共 ${baselineSelectedCount} 题,有效题号 0–${Math.max(0, baselineSelectedCount - 1)}),`
646
+ + "上面的「分类/任务」被忽略;"
647
+ + "「题目序号范围」仍然生效(序号相对本题库,从 0 起、含首尾;超出上界不会报错,会按实际题数截断);"
648
+ + `release 固定为 ${baselineTaskObj.release}。`),
649
+ ),
547
650
  h("div", { className: c("field") },
548
651
  h("label", { className: c("label") }, "分类(可多选,不选 = 全部分类;括号内为该 release 下有效题数)"),
549
652
  h("div", { className: c("chips") },
@@ -601,7 +704,7 @@ window.__ModuleLoader__.load({
601
704
  h("div", { className: c("row") },
602
705
  h("div", { className: c("cardHead") },
603
706
  h("span", { className: c("label") }, `评测成绩(行 = 模型,列 = 任务)· ${sortedModels.length} 个模型 × ${matrix.tasks.length} 个任务`),
604
- h("span", { className: c("hint") }, "分数 = 该任务平均分 ×100;括号 (-没做或没做完的题数/用户选择的题数,即题目序号范围的题数);× = 未执行该任务;— = 无有效判分(如全部访问失败,不计入正确率);颜色 ≥75 绿 / 40–75 橙 / <40 红;拖换行序(存在本机),点表头按模型名或时间排序。"),
707
+ h("span", { className: c("hint") }, "分数 = 该任务平均分 ×100;括号 (-没做或没做完的题数/用户选择的题数,即题目序号范围的题数);× = 未执行该任务;— = 无有效判分(如全部访问失败,不计入正确率);颜色 ≥75 绿 / 40–75 橙 / <40 红;默认按 time 倒序(最新在前),点表头可在模型名/时间间切换排序,拖 则回到本机保存的自定义行序。"),
605
708
  ),
606
709
  h("button", {
607
710
  className: c("btn") + " " + c("btnDanger"),
@@ -693,11 +796,11 @@ window.__ModuleLoader__.load({
693
796
  }
694
797
  const sub = `(-${row.notDone ?? 0}/${row.configured ?? 0})`;
695
798
  const allFailed = row.answered > 0 && row.errors >= row.answered;
696
- const title = allFailed
799
+ const basePrefix = row.baselineLabel ? `【${row.baselineLabel}】` : "";
800
+ const title = basePrefix + (allFailed
697
801
  ? `本次选择 ${row.configured ?? 0} 题,其中 ${row.errors} 题全部访问失败(未做完),不计入正确率 ${sub}`
698
802
  : `本次选择 ${row.configured ?? 0} 题,做完 ${row.done ?? 0} 题,没做或没做完 ${row.notDone ?? 0} 题 ${sub}` +
699
- (row.errors > 0 ? `(其中 ${row.errors} 题 API 失败)` : "");
700
- if (allFailed || row.judged === 0) {
803
+ (row.errors > 0 ? `(其中 ${row.errors} 题 API 失败)` : "")); if (allFailed || row.judged === 0) {
701
804
  return h("td", { key: taskKeyCol, className: cls, title },
702
805
  h("div", { className: c("dim") }, "—"),
703
806
  h("div", { className: c("cellSub") }, sub));
package/lib/index.js CHANGED
@@ -102,9 +102,64 @@ const RELEASES = [
102
102
  ];
103
103
  /** Sane cap so a runaway run cannot eat memory with its log. */
104
104
  const LOG_MAX_LINES = 600;
105
- /** One evaluation at a time per model, but several models may run concurrently. */
105
+ /** One evaluation at a time per model, but several models may run together. */
106
106
  const MAX_CONCURRENT_RUNS = 9;
107
107
 
108
+ /**
109
+ * 「Baseline」题库:记录某个**参考模型**在各任务上的实测对错,用来给别的模型一把同样的尺子。
110
+ *
111
+ * 结构是三层可选:参考模型(baseline)→ 任务(task)→ 对/错(passed / failed)。
112
+ * 作为"探针"用时通常只跑 failed —— 参考模型做不出来的题,才对更强的模型有分辨力;
113
+ * passed 一并留存,便于反向验证(例如确认某模型确实强于参考模型)。
114
+ * 目前只有 olympiad 做过全量实测,后续任务按同样方式补进 tasks 即可。
115
+ *
116
+ * 实现要点:题目 id 取自 LiveBench 的 question.jsonl。面板用 `--question-id` 直接指定,
117
+ * 因此不要求这些题在题库里连续;`--question-begin/--question-end` 仍然会作用在
118
+ * **id 过滤之后**的列表上,所以「题目序号范围」对 baseline 一样有效。
119
+ */
120
+ const BASELINE_SETS = {
121
+ glm53flash: {
122
+ id: "glm53flash",
123
+ label: "Baseline(glm-5.3-flash@max)",
124
+ referenceModel: "zai-coding-cn__glm-5.3-flash@max",
125
+ // 参考模型在每个任务上的实测结果:passed = 判分 >= 0.5 的题,failed = 判分 0 的题。
126
+ // 作为"探针"用时通常只跑 failed —— 参考模型做不出来的题,才对更强的模型有分辨力;
127
+ // passed 也一并留存,便于反向验证(例如确认某模型确实强于参考模型)。
128
+ // 新增任务时只在这里加一项:id 取自 LiveBench question.jsonl 的 question_id,
129
+ // 面板用 --question-id 直接指定,所以题目在题库里是否连续都无所谓。
130
+ tasks: [
131
+ {
132
+ task: "live_bench/math/olympiad",
133
+ category: "math",
134
+ taskName: "olympiad",
135
+ release: "2024-11-25",
136
+ // 2026-09-12 实测:glm-5.3-flash@max 跑满该任务全量 36 题(并发 4),
137
+ // 通过 28、失败 8;失败的 8 题全部属于"公式还原"题型。
138
+ passed: [
139
+ "ef6c6830ebf0e71d74e7ceb45a242f2b87228933eebfe47db8d798d5738cdaeb", "6e12ab2219a4051c396de5afc4228be52c2d8617c7f5621be8c364f50016c80d",
140
+ "c03217423ccb2181ef9e3106b4c116aaf1bcdbd45b3a97e04110665690177eca", "698d9bd9ad134096358dad82317dae610d9f265255aeee52c644871b83eb7a85",
141
+ "56155e5b8c1dc430cc262691aea0a75d71f7dee78b6e15304607caf0b56ecfbd", "3f55db366e970e1965bbba20e9958a4c0ea52e4224d11d875d390983485dd32b",
142
+ "3649fb05ed87c2f3ed8a933e7c8f032b54a5d6f0d23504cdd578541dd60c0f08", "e5b72d9cdb39c6e5f8798a557f119fff35d98fd165f55659e71bf6ac38b5f178",
143
+ "bc3551b5f643d8920f428909104fe2640a50f6b8a17fff6196aa78f25df28bb6", "2dd75e080f3e276f3dcf304d970e6a03973ddf777537422c9aba59559ddc9594",
144
+ "558c3c463860fcaa2b08edd9d5f0f11071944bf7d9229574862a92e6d91f1a21", "e81b96eed50d6b9b43f041d6a36ad0d9a4d00038c42c47a16efd4465d79d08a4",
145
+ "2191e02b65ac78dd0d28bc13f41786d2596dbaa560ad9d0658a27ae2814faef3", "fc903ff1f1f66a6096c9dea58c278b48f54c80a606b65f82af1730559d73755f",
146
+ "e56e4d8ed64cb6d0e43ce3c8edda0a77d769b4bda5230c1aba57442e27214694", "749116060528890530a40d111c5506f4cdec004a7c52a87ef0a4a01206c187ec",
147
+ "6d9bd0806e27f34ff88209075cad638ca0ed6662f499c74bdd3a256d3511b9fe", "9a199388f2e2bd43deac789cd248363bdcb32611480ac27cf62420b4c351759e",
148
+ "a908da1d873928f9f872dddd33c29620dfa0a54646b79bfde672bd15e6986ec9", "2c17f46f8323e8e692a1827bb18c9a9a43fcff9860b72372cb9551b7cdd532f5",
149
+ "d805a5c9f398cf5126ee84e6899cc9a3d34444a7e2337d15cdf9cc2a949649ba", "f4fdca7e355eaf9362449ebf7df78a366e239091bdb4c4a34b9dfcf9882f23de",
150
+ "aba8388e52d25b4006e78d4010c26cf8848c3e3145b0c050b9d55c8aff211fe8", "95b0921a4de64f6e510ac36b5426ecea550c4d3a6033dfbd0d93dd2213fea714",
151
+ "6df4693c156b7581ffb9a1267a84c055bc9edd8170681b9021bb837f1e9d1167", "1f6132e867a52e5c7b084011484f7fcfec9195f407c9bfb06a3f13f5b709d333",
152
+ "c6675bf6647188f84ee445590a494a8d516635bad77ace98ec61d28746839a8d", "2c5982946e20e2a85531ac00121697b263591e055ac19586db59a890fdaf98b1",
153
+ ],
154
+ failed: [
155
+ "0d3a48049dc5da31531654f59e7866832579f812b892610bd1b178fc4d010d0c", "5b9c56e9392dd999162474f10addf1a3a72f1d8b1c8bafc999ccec6f22194c18",
156
+ "11f95734f602e7d1481f9887ca7fc8bed83258e22fd5c443449ac159a4732115", "2a82215ea19fcbded36fa95df35b6e7c5f6ed28b0b5f6c3460b4223f1904a536",
157
+ "527d5f9f9cf27824b84109a495eeced7c7c097fb324b872212f6813a660e5ee4", "f22ff1f6c067d0e085eb79fe5c2e35df01f9edb2bcc73e59e1e15eefd32b8f07",
158
+ "6dfb6aade6429e2cca0718a497a442299a57199dfd547813471f4cf30ed6c7c7", "0499deda2f068008d488551abf96b4b758c6ed6b79cd2ec6a204d1250b140421",
159
+ ],
160
+ },
161
+ ], },
162
+ };
108
163
  /** Resolve the profile directory from the config-tree anchor (plugin-market pattern). */
109
164
  function resolveProfileDir(ctx) {
110
165
  if (typeof ctx.baseUrl === "string" && ctx.baseUrl.startsWith("file:")) {
@@ -299,6 +354,51 @@ function scanCategoryTasks(dataDir) {
299
354
  return tasks;
300
355
  }
301
356
 
357
+ /**
358
+ * 缓存版的 scanCategoryTasks:/config 每次都要它,/results 也要用它算"某 release 下的
359
+ * 有效题数"。question.jsonl 是静态文件,扫描结果缓存 60s 足够。
360
+ */
361
+ let taskScanCache = { at: 0, data: null };
362
+ function scanCategoryTasksCached(dataDir) {
363
+ const now = Date.now();
364
+ if (taskScanCache.data !== null && now - taskScanCache.at < 60000) return taskScanCache.data;
365
+ const data = scanCategoryTasks(dataDir);
366
+ taskScanCache = { at: now, data };
367
+ return data;
368
+ }
369
+
370
+ /**
371
+ * 某个任务在指定 release 下的**有效题数**。
372
+ * LiveBench 的 --question-begin/--question-end 作用在 release 过滤后的列表上,
373
+ * 所以边界裁剪必须用这个数,而不是 question.jsonl 的原始行数。
374
+ * @returns {number|null} null 表示没有该任务的数据(调用方回退到原始题数)。
375
+ */
376
+ function releaseQuestionCount(tasks, category, task, release) {
377
+ const meta = (tasks[category] ?? {})[task];
378
+ if (!meta || !Array.isArray(meta.buckets) || typeof release !== "string" || release.length === 0) return null;
379
+ let n = 0;
380
+ for (const b of meta.buckets) {
381
+ const releasedOk = b.r !== "" && b.r <= release;
382
+ const notRemoved = b.rm === "" || b.rm > release;
383
+ if (releasedOk && notRemoved) n += b.n;
384
+ }
385
+ return n;
386
+ }
387
+
388
+ /**
389
+ * Python `list[begin:end]` 的长度,含越界裁剪。
390
+ * 面板的「止」是闭区间,所以 end 先 +1 再按切片语义算:
391
+ * len = min(total, end+1) - min(total, begin)
392
+ * 只写 min(total, end-begin+1) 会在 begin 也越界时算多
393
+ * (8 题填 6-9,实际跑 2 题却算成 4 题)。
394
+ */
395
+ function clampedRangeLength(total, begin, end) {
396
+ if (!Number.isFinite(total) || total <= 0) return null;
397
+ const b = Math.max(0, Math.min(total, Number(begin)));
398
+ const e = Math.max(0, Math.min(total, Number(end) + 1));
399
+ return Math.max(0, e - b);
400
+ }
401
+
302
402
  function readDirSafe(dir) {
303
403
  try {
304
404
  return readdirNames(dir);
@@ -608,8 +708,20 @@ async function computeResults(dataDir) {
608
708
  return true;
609
709
  });
610
710
  if (inScope) {
711
+ // 「可选题目总数」的基准,按优先级取:
712
+ // baseline 子集大小 > 该任务在本次 release 下的有效题数 > question.jsonl 原始行数。
713
+ // 基准取错会把越界的范围当成有效选择(题库只有 8 题而用户填 0-8 时算出 9 题),
714
+ // 于是 notDone 永远多 1,全做对也显示 (-1/8)。
715
+ const byRelease = releaseQuestionCount(scanCategoryTasksCached(dataDir), category, task, meta.release);
716
+ const baselineTotal = Number.isFinite(Number(meta.baselineSize)) && Number(meta.baselineSize) > 0
717
+ ? Number(meta.baselineSize)
718
+ : null;
719
+ const scopeTotal = baselineTotal ?? byRelease ?? total;
611
720
  const hasRange = meta.begin !== null && meta.begin !== undefined && meta.end !== null && meta.end !== undefined;
612
- if (hasRange) configured = Math.min(total, Number(meta.end) - Number(meta.begin) + 1);
721
+ if (hasRange) {
722
+ const len = clampedRangeLength(scopeTotal, Number(meta.begin), Number(meta.end));
723
+ if (len !== null) configured = len;
724
+ }
613
725
  }
614
726
  }
615
727
  // 实际尝试数比设定还多(断点重跑等)时,以实际为准
@@ -633,6 +745,8 @@ async function computeResults(dataDir) {
633
745
  notDone,
634
746
  // 本次运行的开始时间(ISO);旧数据没有元数据时为 null,前端退回解析 displayName 里的运行戳
635
747
  runStartedAt: meta !== undefined && typeof meta.startedAt === "string" ? meta.startedAt : null,
748
+ // 非 null 表示这一行跑的是 Baseline 题库子集,不是该任务全量
749
+ baselineLabel: meta !== undefined && typeof meta.baselineLabel === "string" ? meta.baselineLabel : null,
636
750
  };
637
751
  });
638
752
  // taskTotals 同时给出裸任务名与 `category/task` 两种键,便于前端按列取题目总数
@@ -743,7 +857,7 @@ function apply(ctx) {
743
857
  }
744
858
  const providerId = typeof body.provider === "string" ? body.provider : "";
745
859
  const modelId = typeof body.model === "string" ? body.model.trim() : "";
746
- const release = typeof body.release === "string" ? body.release : "2024-11-25";
860
+ let release = typeof body.release === "string" ? body.release : "2024-11-25";
747
861
  const category = typeof body.category === "string" && body.category.length > 0 ? body.category : null;
748
862
  const task = typeof body.task === "string" && body.task.length > 0 ? body.task : null;
749
863
  const reasoningEffort = typeof body.reasoningEffort === "string" && body.reasoningEffort.length > 0 && body.reasoningEffort !== "default" ? body.reasoningEffort : null;
@@ -764,8 +878,44 @@ function apply(ctx) {
764
878
  // Multi-select support: the client resolves its category/task chips into
765
879
  // an explicit bench-path list ("live_bench", "live_bench/<cat>",
766
880
  // "live_bench/<cat>/<task>"); gen_api_answer accepts nargs="+".
881
+ // Baseline 题库则完全绕开分类/任务选择("不选 = 全部分类"不适用),
882
+ // 锁定到它自己的任务 + 一组 question id。
883
+ const baselineKey = typeof body.baseline === "string" && body.baseline.length > 0 ? body.baseline : null;
884
+ const baseline = baselineKey !== null ? (BASELINE_SETS[baselineKey] ?? null) : null;
885
+ if (baselineKey !== null && baseline === null) {
886
+ return { status: 400, payload: { ok: false, error: `unknown baseline: ${baselineKey}` } };
887
+ }
767
888
  let benchNames = null;
768
- if (Array.isArray(body.benchNames) && body.benchNames.length > 0) {
889
+ // Baseline 是三层选择:参考模型 + 任务 + 对/错。服务端负责把选择解析成 question id 列表,
890
+ // 客户端只送选择,不送 id —— 免得界面与题库数据脱节。
891
+ let baselinePick = null;
892
+ if (baseline !== null) {
893
+ const taskKey = typeof body.baselineTask === "string" ? body.baselineTask : "";
894
+ const picks = Array.isArray(body.baselinePicks)
895
+ ? body.baselinePicks.filter((p) => p === "passed" || p === "failed")
896
+ : [];
897
+ const baselineTask = baseline.tasks.find((t) => t.task === taskKey) ?? null;
898
+ if (baselineTask === null) {
899
+ return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}: 未选择任务(或任务不存在):${taskKey}` } };
900
+ }
901
+ if (picks.length === 0) {
902
+ return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}/${taskKey}: 请至少选择「做对」或「做错」之一` } };
903
+ }
904
+ const ids = [];
905
+ const seen = new Set();
906
+ for (const pick of picks) {
907
+ for (const qid of baselineTask[pick]) {
908
+ if (!seen.has(qid)) { seen.add(qid); ids.push(qid); }
909
+ }
910
+ }
911
+ if (ids.length === 0) {
912
+ return { status: 400, payload: { ok: false, error: `baseline ${baselineKey}/${taskKey}: 所选分组没有题目` } };
913
+ }
914
+ // baseline 的 question id 属于特定 release,跟着用户选别的 release 会一题都匹配不到
915
+ release = baselineTask.release;
916
+ benchNames = [baselineTask.task];
917
+ baselinePick = { label: baseline.label, task: baselineTask.task, picks, ids };
918
+ } else if (Array.isArray(body.benchNames) && body.benchNames.length > 0) {
769
919
  if (body.benchNames.length > 24) {
770
920
  return { status: 400, payload: { ok: false, error: "too many bench names (max 24)" } };
771
921
  }
@@ -791,7 +941,6 @@ function apply(ctx) {
791
941
  const _pad = (n) => String(n).padStart(2, "0");
792
942
  const runStamp = `r${_now.getFullYear()}${_pad(_now.getMonth() + 1)}${_pad(_now.getDate())}-${_pad(_now.getHours())}${_pad(_now.getMinutes())}${_pad(_now.getSeconds())}`;
793
943
  const displayName = displayModelName(providerId || "direct", modelId) + (hasEffortSuffix ? "@" + effort : "") + "__" + runStamp;
794
- let cliModel = modelId;
795
944
  let writeError = null; // Anthropic-protocol proxies cannot go through --api-base (that path
796
945
  // speaks OpenAI Chat Completions). Instead the generated model config
797
946
  // selects LiveBench's native anthropic client and the spawn env points
@@ -803,22 +952,24 @@ function apply(ctx) {
803
952
  const isOpenAIResponsesRoute = provider
804
953
  && (provider.api === "openai-responses" || provider.api === "openai_responses")
805
954
  && provider.baseURL;
806
- const needConfig = isAnthropicRoute || isOpenAIResponsesRoute || hasEffortSuffix;
807
- if (needConfig) {
808
- writeError = writeGeneratedModelConfig(layout, loadYaml(profileDir), {
809
- displayName,
810
- modelId,
811
- reasoningEffort: isAnthropicRoute ? null : effort,
812
- protocol: isAnthropicRoute ? "anthropic" : isOpenAIResponsesRoute ? "openai_responses" : "openai",
813
- });
814
- if (writeError) return { status: 500, payload: { ok: false, error: writeError } };
815
- cliModel = displayName;
816
- }
955
+ // 每次都写自己的模型配置,并且 --model 永远用 display-name。
956
+ // 不能图省事把裸模型名交给 LiveBench:它会用 get_model_config(裸名) 去撞它自带的
957
+ // 模型库(例如 "kimi-k3" 命中 moonshotai.yml),命中后就改用那份配置的
958
+ // provider / api_kwargs / max_tokens —— 实测会把 temperature 悄悄变成 1.0、
959
+ // max_tokens 变成 131072,等于用户选的 provider 被换掉。写了自己的配置,
960
+ // 解析结果就是确定的 {local: <modelId>} + --api-base。
961
+ writeError = writeGeneratedModelConfig(layout, loadYaml(profileDir), {
962
+ displayName,
963
+ modelId,
964
+ reasoningEffort: isAnthropicRoute ? null : effort,
965
+ protocol: isAnthropicRoute ? "anthropic" : isOpenAIResponsesRoute ? "openai_responses" : "openai",
966
+ });
967
+ if (writeError) return { status: 500, payload: { ok: false, error: writeError } };
817
968
 
818
969
  const benchParts = ["live_bench", ...(category ? [category] : []), ...(task ? [task] : [])];
819
970
  const args = [
820
971
  "run_livebench.py",
821
- "--model", cliModel,
972
+ "--model", displayName,
822
973
  "--model-display-name", displayName,
823
974
  "--bench-name",
824
975
  ...(benchNames !== null ? benchNames : [benchParts.join("/")]),
@@ -830,6 +981,13 @@ function apply(ctx) {
830
981
  "--stream",
831
982
  "--mode", "single",
832
983
  ];
984
+ // Baseline:直接点名 question id。--question-id 是 nargs="+",后面跟一串 id;
985
+ // 它和后面的 --question-begin/--question-end 叠加时,范围作用在**id 过滤后**的
986
+ // 列表上(gen_api_answer: load_questions(..., question_id) 之后才做 [begin:end]),
987
+ // 所以「题目序号范围」对 baseline 题库同样有效。
988
+ if (baselinePick !== null) {
989
+ args.push("--question-id", ...baselinePick.ids);
990
+ }
833
991
  if (body.begin !== undefined && body.begin !== null && `${body.begin}`.length > 0) {
834
992
  args.push("--question-begin", String(asInt(body.begin, 0, 100000, 0)));
835
993
  }
@@ -896,6 +1054,17 @@ function apply(ctx) {
896
1054
  benchNames: benchNames !== null ? benchNames : [benchParts.join("/")],
897
1055
  begin: body.begin ?? null,
898
1056
  end: body.end ?? null,
1057
+ // 算"本次设定题数"要用 release 过滤后的有效题数来裁剪边界,所以必须记下来
1058
+ release,
1059
+ // baseline 评测标出来:成绩表里同一列会混着"全量"和"题库子集"两种分数,
1060
+ // 不标注就没法解读。
1061
+ baseline: baselinePick !== null ? baselineKey : null,
1062
+ baselineLabel: baselinePick !== null
1063
+ ? `${baselinePick.label} · ${baselinePick.task.split("/").slice(-1)[0]} · ${baselinePick.picks.map((p) => (p === "passed" ? "做对" : "做错")).join("+")}`
1064
+ : null,
1065
+ baselineTask: baselinePick !== null ? baselinePick.task : null,
1066
+ baselinePicks: baselinePick !== null ? baselinePick.picks : null,
1067
+ baselineSize: baselinePick !== null ? baselinePick.ids.length : null,
899
1068
  startedAt: new Date().toISOString(),
900
1069
  }), "utf8");
901
1070
  } catch { /* 元数据写入失败不影响评测 */ }
@@ -1004,7 +1173,22 @@ function apply(ctx) {
1004
1173
  available: layout.available,
1005
1174
  root: layout.root,
1006
1175
  releases: RELEASES,
1007
- tasks: scanCategoryTasks(layout.dataDir),
1176
+ tasks: scanCategoryTasksCached(layout.dataDir),
1177
+ // Baseline 题库:参考模型在各任务上的实测对错,供"探针"式快速评测选用
1178
+ baselines: Object.values(BASELINE_SETS).map((b) => ({
1179
+ id: b.id,
1180
+ label: b.label,
1181
+ referenceModel: b.referenceModel,
1182
+ tasks: b.tasks.map((t) => ({
1183
+ task: t.task,
1184
+ category: t.category,
1185
+ taskName: t.taskName,
1186
+ release: t.release,
1187
+ total: t.passed.length + t.failed.length,
1188
+ passedCount: t.passed.length,
1189
+ failedCount: t.failed.length,
1190
+ })),
1191
+ })),
1008
1192
  providers: providers.map(({ id, name: pname, models, api, baseURL }) => ({
1009
1193
  id,
1010
1194
  name: pname,
@@ -1222,4 +1406,4 @@ function apply(ctx) {
1222
1406
  }), `${name}: delete route`);
1223
1407
  }
1224
1408
 
1225
- export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName };
1409
+ export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName, clampedRangeLength, releaseQuestionCount };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.2.17",
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.",
3
+ "version": "0.2.20",
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. Ships a Baseline probe set built from the questions a reference model cannot solve.",
5
5
  "license": "MIT",
6
6
  "author": "cszr (Vithrive)",
7
7
  "type": "module",