dsh-livebench-panel 0.1.10 → 0.1.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 +130 -68
  2. package/lib/index.js +116 -66
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -92,11 +92,12 @@ window.__ModuleLoader__.load({
92
92
  function LiveBenchView() {
93
93
  const [config, setConfig] = useState(null);
94
94
  const [configError, setConfigError] = useState(null);
95
- const [sel, setSel] = useState({ provider: "", model: "", reasoning: "default", cats: [], tasks: [], release: "2024-11-25", begin: "", end: "", maxTokens: "32000" });
95
+ const [sel, setSel] = useState({ models: [], reasoning: "default", cats: [], tasks: [], release: "2024-11-25", begin: "", end: "", maxTokens: "32000" });
96
+ const [modelDdOpen, setModelDdOpen] = useState(false);
97
+ const modelDdRef = useRef(null);
96
98
  const [busy, setBusy] = useState(false);
97
99
  const [running, setRunning] = useState(false);
98
- const [log, setLog] = useState("");
99
- const [exitCode, setExitCode] = useState(null);
100
+ const [runsList, setRunsList] = useState([]);
100
101
  const [startError, setStartError] = useState(null);
101
102
  const [homeInput, setHomeInput] = useState("");
102
103
  const [homeBusy, setHomeBusy] = useState(false);
@@ -108,6 +109,16 @@ window.__ModuleLoader__.load({
108
109
  const dragKey = useRef(null);
109
110
  const logRef = useRef(null);
110
111
 
112
+ // 点击面板外部时关闭模型下拉
113
+ useEffect(() => {
114
+ if (!modelDdOpen) return;
115
+ const onDocDown = (event) => {
116
+ if (modelDdRef.current && !modelDdRef.current.contains(event.target)) setModelDdOpen(false);
117
+ };
118
+ document.addEventListener("mousedown", onDocDown);
119
+ return () => document.removeEventListener("mousedown", onDocDown);
120
+ }, [modelDdOpen]);
121
+
111
122
  const loadConfig = useCallback(async () => {
112
123
  const payload = await api("/config");
113
124
  if (payload.ok) {
@@ -115,14 +126,12 @@ window.__ModuleLoader__.load({
115
126
  setConfigError(null);
116
127
  setSel((prev) => {
117
128
  const next = { ...prev, release: payload.releases.includes(prev.release) ? prev.release : "2024-11-25" };
118
- const provider = payload.providers.find((p) => p.id === prev.provider) ?? payload.providers.find((p) => p.models.length > 0) ?? null;
119
- if (provider) {
120
- next.provider = provider.id;
121
- if (!provider.models.some((m) => m.id === prev.model)) {
122
- next.model = provider.models[0]?.id ?? "";
123
- }
124
- }
125
- if (!payload.releases.includes(next.release)) next.release = payload.releases[0] ?? "2024-11-25";
129
+ // 保留仍存在的选择,剔除失效项
130
+ next.models = prev.models.filter((v) => {
131
+ const [pid] = v.split("::");
132
+ const p = payload.providers.find((x) => x.id === pid);
133
+ return p ? true : false;
134
+ });
126
135
  return next;
127
136
  });
128
137
  } else {
@@ -138,11 +147,8 @@ window.__ModuleLoader__.load({
138
147
  const refreshStatus = useCallback(async () => {
139
148
  const payload = await api("/status");
140
149
  if (payload.ok) {
150
+ setRunsList(payload.runs ?? []);
141
151
  setRunning(payload.running === true);
142
- if (payload.hasRun) {
143
- setLog(payload.log ?? "");
144
- setExitCode(payload.exitCode);
145
- }
146
152
  return payload.running === true;
147
153
  }
148
154
  return false;
@@ -197,21 +203,31 @@ window.__ModuleLoader__.load({
197
203
  const next = list.includes(value) ? list.filter((v) => v !== value) : [...list, value];
198
204
  return { ...prev, [key]: next };
199
205
  });
200
- const selectedProvider = useMemo(
201
- () => (config ? config.providers.find((p) => p.id === sel.provider) ?? null : null),
202
- [config, sel.provider],
203
- );
204
- const selectedModel = useMemo(
205
- () => selectedProvider?.models.find((m) => m.id === sel.model) ?? null,
206
- [selectedProvider, sel.model],
207
- );
206
+ // 已选模型(provider::model 值列表)的解析与 effort 并集
207
+ const selectedModelEntries = useMemo(() => sel.models.map((value) => {
208
+ const [pid, ...rest] = value.split("::");
209
+ const mid = rest.join("::");
210
+ const provider = config?.providers.find((p) => p.id === pid) ?? null;
211
+ const model = provider?.models.find((m) => m.id === mid) ?? null;
212
+ return { providerId: pid, modelId: mid, efforts: model?.efforts ?? [] };
213
+ }).filter((entry) => entry.modelId !== ""), [sel.models, config]);
208
214
  const effortOptions = useMemo(() => {
209
- const efforts = selectedModel?.efforts ?? [];
210
- return efforts.map((e) => ({ value: e, label: e === "off" ? "off(关闭思考)" : e }));
211
- }, [selectedModel]);
215
+ const seen = new Set();
216
+ const union = [];
217
+ for (const entry of selectedModelEntries) {
218
+ for (const e of entry.efforts) {
219
+ if (!seen.has(e)) { seen.add(e); union.push({ value: e, label: e === "off" ? "off(关闭思考)" : e }); }
220
+ }
221
+ }
222
+ return union;
223
+ }, [selectedModelEntries]);
212
224
 
213
225
  const onStart = async () => {
214
226
  setStartError(null);
227
+ if (selectedModelEntries.length === 0) {
228
+ setStartError("请先在下拉框中至少选择一个模型。");
229
+ return;
230
+ }
215
231
  const benchNames = [];
216
232
  if (sel.cats.length === 0) {
217
233
  benchNames.push("live_bench");
@@ -227,22 +243,28 @@ window.__ModuleLoader__.load({
227
243
  }
228
244
  setBusy(true);
229
245
  try {
230
- const payload = await api("/start", {
231
- method: "POST",
232
- headers: { "content-type": "application/json" },
233
- body: JSON.stringify({
234
- provider: sel.provider,
235
- model: sel.model,
236
- reasoningEffort: sel.reasoning,
237
- benchNames,
238
- release: sel.release,
239
- begin: sel.begin,
240
- end: sel.end,
241
- maxTokens: sel.maxTokens,
242
- }),
246
+ // 每个 selected 模型并发启动一个评测(服务端有并发上限保护)
247
+ const failures = [];
248
+ const launches = selectedModelEntries.map(async (entry) => {
249
+ const payload = await api("/start", {
250
+ method: "POST",
251
+ headers: { "content-type": "application/json" },
252
+ body: JSON.stringify({
253
+ provider: entry.providerId,
254
+ model: entry.modelId,
255
+ reasoningEffort: sel.reasoning,
256
+ benchNames,
257
+ release: sel.release,
258
+ begin: sel.begin,
259
+ end: sel.end,
260
+ maxTokens: sel.maxTokens,
261
+ }),
262
+ });
263
+ if (!payload.ok) failures.push(`${entry.modelId}: ${payload.error ?? "启动失败"}`);
243
264
  });
244
- if (!payload.ok) setStartError(payload.error ?? "启动失败");
245
- else await refreshStatus();
265
+ await Promise.all(launches);
266
+ if (failures.length > 0) setStartError(failures.join(";"));
267
+ await refreshStatus();
246
268
  } catch (error) {
247
269
  setStartError(String(error.message ?? error));
248
270
  } finally {
@@ -275,14 +297,7 @@ window.__ModuleLoader__.load({
275
297
 
276
298
  const setField = (key) => (event) => {
277
299
  const value = event.target.value;
278
- setSel((prev) => {
279
- const next = { ...prev, [key]: value };
280
- if (key === "provider") {
281
- const provider = config?.providers.find((p) => p.id === value) ?? null;
282
- next.model = provider?.models[0]?.id ?? "";
283
- }
284
- return next;
285
- });
300
+ setSel((prev) => ({ ...prev, [key]: value }));
286
301
  };
287
302
 
288
303
  // ---- 成绩矩阵:model 为行标识、category/task 为列标识 ----
@@ -385,18 +400,48 @@ window.__ModuleLoader__.load({
385
400
  configError !== null && h("p", { className: c("error") }, `加载配置失败:${configError}`),
386
401
  h("div", { className: c("card") },
387
402
  h("div", { className: c("grid") },
388
- h("div", { className: c("field") },
389
- h("label", { className: c("label") }, "模型(harness 全部模型)"),
390
- h("select", { className: c("select"), value: sel.provider + "::" + sel.model, onChange: (event) => {
391
- const [providerId, ...rest] = event.target.value.split("::");
392
- const modelId = rest.join("::");
393
- setSel((prev) => ({ ...prev, provider: providerId, model: modelId, reasoning: "default" }));
394
- } }, modelOptions(config?.providers ?? [])),
403
+ h("div", { className: c("field"), ref: modelDdRef, style: { position: "relative" } },
404
+ h("label", { className: c("label") }, "模型(harness 全部模型,可多选)"),
405
+ h("button", {
406
+ type: "button", className: c("select"), style: { textAlign: "left", width: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
407
+ onClick: () => setModelDdOpen((v) => !v),
408
+ }, sel.models.length === 0
409
+ ? "点击选择模型…"
410
+ : sel.models.length === 1
411
+ ? sel.models[0].split("::")[1]
412
+ : `已选 ${sel.models.length} 个模型`),
413
+ modelDdOpen && h("div", {
414
+ style: {
415
+ position: "absolute", zIndex: 30, top: "calc(100% + 4px)", left: 0, right: 0,
416
+ maxHeight: "320px", overflowY: "auto", border: "1px solid var(--dsw-alias-border-l2)",
417
+ borderRadius: "8px", background: "var(--dsw-alias-bg-layer-1)", padding: "6px",
418
+ boxShadow: "var(--dsw-shadow-lv1, 0 4px 16px rgba(0,0,0,.25))",
419
+ },
420
+ },
421
+ (config?.providers ?? []).map((p) => h("div", { key: p.id, style: { marginBottom: "4px" } },
422
+ h("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary)", padding: "3px 4px" } },
423
+ p.name + (p.routable ? "" : " ·未接LiveBench")),
424
+ p.models.map((m) => {
425
+ const value = p.id + "::" + m.id;
426
+ const checked = sel.models.includes(value);
427
+ return h("label", {
428
+ key: value,
429
+ style: { display: "flex", alignItems: "center", gap: "6px", padding: "3px 4px", cursor: "pointer", borderRadius: "6px" },
430
+ },
431
+ h("input", { type: "checkbox", checked, onChange: () => {
432
+ setSel((prev) => {
433
+ const set = new Set(prev.models);
434
+ if (set.has(value)) set.delete(value); else set.add(value);
435
+ return { ...prev, models: [...set] };
436
+ });
437
+ } }),
438
+ h("span", { style: { fontSize: "12.5px" } }, m.name));
439
+ }))),
395
440
  ),
396
441
  h("div", { className: c("field") },
397
442
  h("label", { className: c("label") }, "推理强度"),
398
443
  h("select", { className: c("select"), value: sel.reasoning, onChange: setField("reasoning"), disabled: effortOptions.length === 0 },
399
- h("option", { value: "default" }, effortOptions.length === 0 ? "(该模型不支持)" : "(模型默认)"),
444
+ h("option", { value: "default" }, effortOptions.length === 0 ? "(按模型默认)" : "(模型默认)"),
400
445
  effortOptions.map((e) => h("option", { key: e.value, value: e.value }, e.label))),
401
446
  ),
402
447
  h("div", { className: c("field") },
@@ -443,22 +488,25 @@ window.__ModuleLoader__.load({
443
488
  ),
444
489
  ),
445
490
  h("div", { className: c("row") },
446
- h("button", { className: c("btn"), onClick: onStart, disabled: busy || running || !config?.available || sel.model === "" },
447
- running ? "评测运行中…" : busy ? "启动中…" : "开始评测"),
448
- running && h("button", { className: c("btn") + " " + c("btnDanger"), onClick: onStop }, "停止"),
491
+ h("button", { className: c("btn"), onClick: onStart, disabled: busy || running || !config?.available || sel.models.length === 0 },
492
+ running ? `评测运行中(可另起)…` : busy ? "启动中…" : `开始评测(${sel.models.length} 个模型并发)`),
493
+ running && h("button", { className: c("btn") + " " + c("btnDanger"), onClick: onStop }, "停止全部"),
449
494
  h("button", { className: c("btnGhost") + " " + c("btn"), onClick: () => { loadConfig(); loadResults(); } }, "刷新"),
450
- selectedProvider && !selectedProvider.routable && h("span", { className: c("hint") },
451
- "该 provider 的协议或端点未知,无法自动路由:LiveBench 将按模型名原生尝试,未注册的模型名会失败。"),
452
495
  ),
453
496
  startError !== null && h("p", { className: c("error") }, startError),
454
497
  ),
455
- (log.length > 0 || running) && h("div", { className: c("card") },
498
+ runsList.length > 0 && h("div", { className: c("card") },
456
499
  h("div", { className: c("row") },
457
500
  h("span", { className: c("badge"), "data-ok": running ? "1" : "0" },
458
- running ? "运行中" : exitCode === 0 ? "已完成" : exitCode === null ? "待运行" : `已退出 (${exitCode})`),
501
+ running ? "有评测运行中" : "无运行中的评测"),
459
502
  ),
460
- h("pre", { className: c("log"), ref: logRef }, log || "(暂无输出)"),
461
- ),
503
+ runsList.map((r) => h("details", { key: r.runId, open: r.running },
504
+ h("summary", { style: { cursor: "pointer", fontSize: "12px", color: "var(--dsw-alias-label-secondary)" } },
505
+ h("span", { className: c("badge"), "data-ok": r.running ? "1" : "0" }, r.running ? "运行中" : r.exitCode === 0 ? "完成" : `退出(${r.exitCode})`),
506
+ " ",
507
+ r.displayName),
508
+ h("pre", { className: c("log"), ref: r.running ? logRef : null }, r.log || "(暂无输出)"),
509
+ )))),
462
510
  sortedModels.length > 0 && h("div", { className: c("card") },
463
511
  h("div", { className: c("row") },
464
512
  h("span", { className: c("label") }, "评测成绩(model 为行;分数 = 平均分 ×100;行首可拖动排序,顺序保存在本机)"),
@@ -498,8 +546,22 @@ window.__ModuleLoader__.load({
498
546
  h("td", null, model),
499
547
  matrix.tasks.map((taskKeyCol) => {
500
548
  const row = matrix.cells.get(`${model}\u0000${taskKeyCol}`);
501
- return h("td", { key: taskKeyCol, className: c("score") },
502
- row ? `${row.score.toFixed(1)} (${row.judged}/${row.total})` : "");
549
+ // ×= 该模型未做过此任务;E = 做了但全部 API 失败
550
+ let cellText = "×";
551
+ let title = "该模型未执行此任务";
552
+ if (row) {
553
+ if (row.answered > 0 && row.errors >= row.answered) {
554
+ cellText = `E (${row.errors})`;
555
+ title = "该任务全部回答均 API 失败($ERROR$),计 0 分";
556
+ } else if (row.judged === 0) {
557
+ cellText = "未判分";
558
+ title = "有答案但尚未判分";
559
+ } else {
560
+ cellText = `${row.score.toFixed(1)} (${row.judged}/${row.total})`;
561
+ if (row.errors > 0) title += ` · 其中 ${row.errors} 题 API 失败计 0 分`;
562
+ }
563
+ }
564
+ return h("td", { key: taskKeyCol, className: c("score"), title }, cellText);
503
565
  }),
504
566
  h("td", null, h("button", {
505
567
  className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
package/lib/index.js CHANGED
@@ -102,8 +102,8 @@ 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 LiveBench grading is not cheap. */
106
- const MAX_CONCURRENT_RUNS = 1;
105
+ /** One evaluation at a time per model, but several models may run concurrently. */
106
+ const MAX_CONCURRENT_RUNS = 6;
107
107
 
108
108
  /** Resolve the profile directory from the config-tree anchor (plugin-market pattern). */
109
109
  function resolveProfileDir(ctx) {
@@ -434,6 +434,7 @@ function readJsonl(path) {
434
434
  function computeResults(dataDir) {
435
435
  const judged = new Map(); // `${model}\u0000${task}` -> {model, category, task, sum, n, time}
436
436
  const totals = new Map(); // task -> question count
437
+ const answers = new Map(); // `${model}\u0000${task}` -> {answered, errors}
437
438
  if (!existsSync(dataDir)) return { rows: [], taskTotals: {} };
438
439
  for (const category of readDirSafe(dataDir)) {
439
440
  const catDir = join(dataDir, category);
@@ -441,6 +442,23 @@ function computeResults(dataDir) {
441
442
  const taskDir = join(catDir, task);
442
443
  const questions = readJsonl(join(taskDir, "question.jsonl"));
443
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".
448
+ const answerDir = join(taskDir, "model_answer");
449
+ for (const file of readDirSafe(answerDir)) {
450
+ 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] ?? "") : "";
455
+ const key = `${model}\u0000${task}`;
456
+ const stat = answers.get(key) ?? { answered: 0, errors: 0 };
457
+ stat.answered += 1;
458
+ if (text.startsWith("$ERROR$")) stat.errors += 1;
459
+ answers.set(key, stat);
460
+ }
461
+ }
444
462
  const judgments = readJsonl(join(taskDir, "model_judgment", "ground_truth_judgment.jsonl"));
445
463
  for (const row of judgments) {
446
464
  const model = typeof row.model === "string" ? row.model : null;
@@ -456,15 +474,29 @@ function computeResults(dataDir) {
456
474
  }
457
475
  }
458
476
  }
459
- const rows = [...judged.values()].map((entry) => ({
460
- model: entry.model,
461
- category: entry.category,
462
- task: entry.task,
463
- score: entry.n > 0 ? (entry.sum / entry.n) * 100 : 0,
464
- judged: entry.n,
465
- total: totals.get(entry.task) ?? 0,
466
- time: entry.time > 0 ? entry.time : null,
467
- }));
477
+ const keys = new Set([...judged.keys(), ...answers.keys()]);
478
+ const rows = [...keys].map((key) => {
479
+ const entry = judged.get(key);
480
+ const stat = answers.get(key) ?? { answered: 0, errors: 0 };
481
+ 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
+ }
488
+ return {
489
+ model,
490
+ category: category ?? "",
491
+ task,
492
+ score: entry && entry.n > 0 ? (entry.sum / entry.n) * 100 : null,
493
+ judged: entry?.n ?? 0,
494
+ total: totals.get(task) ?? 0,
495
+ time: entry && entry.time > 0 ? entry.time : null,
496
+ answered: stat.answered,
497
+ errors: stat.errors,
498
+ };
499
+ });
468
500
  const taskTotals = Object.fromEntries(totals);
469
501
  return { rows, taskTotals };
470
502
  }
@@ -502,17 +534,34 @@ function asInt(value, min, max, fallback) {
502
534
  return Math.min(max, Math.max(min, Math.trunc(n)));
503
535
  }
504
536
 
537
+ /**
538
+ * Validate one bench path: "live_bench", "live_bench/<category>" or
539
+ * "live_bench/<category>/<task>". Task names are mixed-case in LiveBench
540
+ * (AMPS_Hard, LCB_generation, …), so segments allow A-Za-z0-9_.
541
+ * @returns {string|null} error description, or null when valid.
542
+ */
543
+ function validateBenchName(bn) {
544
+ const parts = bn.split("/").filter((s) => s.length > 0);
545
+ if (parts[0] !== "live_bench") return "must start with live_bench";
546
+ if (parts.length > 3) return "too deep";
547
+ for (const seg of parts) {
548
+ if (!/^[A-Za-z0-9_]{1,80}$/.test(seg)) return `bad segment "${seg}"`;
549
+ }
550
+ return null;
551
+ }
552
+
505
553
  function apply(ctx) {
506
- /** The single run slot. */
507
- let run = null;
554
+ /** All runs: runId -> record. Several evaluations may run concurrently. */
555
+ const runs = new Map();
508
556
 
509
557
  const startRun = async (body) => {
510
558
  const layout = livebenchLayout();
511
559
  if (!layout.available) {
512
560
  return { status: 409, payload: { ok: false, error: `LiveBench venv not found under ${layout.root}` } };
513
561
  }
514
- if (run && run.exitCode === null) {
515
- return { status: 409, payload: { ok: false, error: "another evaluation is already running" } };
562
+ const runningCount = [...runs.values()].filter((r) => r.exitCode === null).length;
563
+ if (runningCount >= MAX_CONCURRENT_RUNS) {
564
+ return { status: 409, payload: { ok: false, error: `已有 ${runningCount} 个评测在并发运行(上限 ${MAX_CONCURRENT_RUNS}),请等待或停止部分评测` } };
516
565
  }
517
566
  const providerId = typeof body.provider === "string" ? body.provider : "";
518
567
  const modelId = typeof body.model === "string" ? body.model.trim() : "";
@@ -545,22 +594,22 @@ function apply(ctx) {
545
594
  benchNames = [];
546
595
  for (const bn of body.benchNames) {
547
596
  if (typeof bn !== "string") return { status: 400, payload: { ok: false, error: "invalid bench name" } };
548
- const parts = bn.split("/").filter((s) => s.length > 0);
549
- if (parts[0] !== "live_bench" || parts.length > 3) {
550
- return { status: 400, payload: { ok: false, error: `invalid bench path: ${bn}` } };
551
- }
552
- for (const seg of parts) {
553
- if (!/^[a-z0-9_]+$/.test(seg)) return { status: 400, payload: { ok: false, error: `invalid bench path: ${bn}` } };
554
- }
555
- benchNames.push(parts.join("/"));
597
+ const error = validateBenchName(bn);
598
+ if (error) return { status: 400, payload: { ok: false, error: `invalid bench path: ${bn} (${error})` } };
599
+ benchNames.push(bn.split("/").filter((s) => s.length > 0).join("/"));
556
600
  }
557
601
  }
558
602
 
559
- const hasEffortSuffix = reasoningEffort !== null && reasoningEffort !== "off";
560
- const displayName = displayModelName(providerId || "direct", modelId) + (hasEffortSuffix ? "@" + reasoningEffort : "");
603
+ // A global effort only applies to models whose own effort ladder
604
+ // includes it; others silently fall back to their default behaviour.
605
+ const modelMeta = provider?.models.find((m) => m.id === modelId) ?? null;
606
+ const effort = reasoningEffort !== null && modelMeta?.efforts?.length > 0 && !modelMeta.efforts.includes(reasoningEffort)
607
+ ? null
608
+ : reasoningEffort;
609
+ const hasEffortSuffix = effort !== null && effort !== "off";
610
+ const displayName = displayModelName(providerId || "direct", modelId) + (hasEffortSuffix ? "@" + effort : "");
561
611
  let cliModel = modelId;
562
- let writeError = null;
563
- // Anthropic-protocol proxies cannot go through --api-base (that path
612
+ let writeError = null; // Anthropic-protocol proxies cannot go through --api-base (that path
564
613
  // speaks OpenAI Chat Completions). Instead the generated model config
565
614
  // selects LiveBench's native anthropic client and the spawn env points
566
615
  // the SDK at the proxy (ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY).
@@ -576,7 +625,7 @@ function apply(ctx) {
576
625
  writeError = writeGeneratedModelConfig(layout, loadYaml(profileDir), {
577
626
  displayName,
578
627
  modelId,
579
- reasoningEffort: isAnthropicRoute ? null : reasoningEffort,
628
+ reasoningEffort: isAnthropicRoute ? null : effort,
580
629
  protocol: isAnthropicRoute ? "anthropic" : isOpenAIResponsesRoute ? "openai_responses" : "openai",
581
630
  });
582
631
  if (writeError) return { status: 500, payload: { ok: false, error: writeError } };
@@ -646,14 +695,21 @@ function apply(ctx) {
646
695
  });
647
696
 
648
697
  const record = {
649
- id: `${Date.now()}`,
698
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
650
699
  proc: child,
700
+ displayName,
651
701
  exitCode: null,
652
702
  startedAt: new Date().toISOString(),
653
703
  command: [layout.pythonExe, ...args].join(" "),
654
704
  log: [`$ ${[...args].join(" ")}`],
655
705
  };
656
- run = record;
706
+ runs.set(record.id, record);
707
+ // keep history bounded: drop oldest finished runs beyond 20 entries
708
+ if (runs.size > 20) {
709
+ for (const [id, r] of runs) {
710
+ if (r.exitCode !== null && runs.size > 20) runs.delete(id);
711
+ }
712
+ }
657
713
 
658
714
  child.stdout.on("data", (chunk) => appendLog(record, chunk));
659
715
  child.stderr.on("data", (chunk) => appendLog(record, chunk));
@@ -736,8 +792,9 @@ function apply(ctx) {
736
792
  sendJson(res, 400, { ok: false, error: "invalid request body" });
737
793
  return;
738
794
  }
739
- if (run !== null && run.exitCode === null && runsAlive() >= MAX_CONCURRENT_RUNS) {
740
- sendJson(res, 409, { ok: false, error: "another evaluation is already running" });
795
+ const runningCount = [...runs.values()].filter((r) => r.exitCode === null).length;
796
+ if (runningCount >= MAX_CONCURRENT_RUNS) {
797
+ sendJson(res, 409, { ok: false, error: `已有 ${runningCount} 个评测在并发运行(上限 ${MAX_CONCURRENT_RUNS})` });
741
798
  return;
742
799
  }
743
800
  const result = await startRun(body);
@@ -753,20 +810,15 @@ function apply(ctx) {
753
810
  sendJson(res, 405, { ok: false, error: "method not allowed" });
754
811
  return;
755
812
  }
756
- if (run === null) {
757
- sendJson(res, 200, { ok: true, running: false, hasRun: false });
758
- return;
759
- }
760
- sendJson(res, 200, {
761
- ok: true,
762
- running: run.exitCode === null,
763
- hasRun: true,
764
- runId: run.id,
765
- exitCode: run.exitCode,
766
- startedAt: run.startedAt,
767
- command: run.command,
768
- log: run.log.slice(-120).join("\n"),
769
- });
813
+ const list = [...runs.values()].reverse().map((r) => ({
814
+ runId: r.id,
815
+ displayName: r.displayName,
816
+ running: r.exitCode === null,
817
+ exitCode: r.exitCode,
818
+ startedAt: r.startedAt,
819
+ log: r.log.slice(-80).join("\n"),
820
+ }));
821
+ sendJson(res, 200, { ok: true, running: list.some((r) => r.running), runs: list });
770
822
  },
771
823
  }), `${name}: status route`);
772
824
 
@@ -782,24 +834,27 @@ function apply(ctx) {
782
834
  sendJson(res, 403, { ok: false, error: "forbidden origin" });
783
835
  return;
784
836
  }
785
- if (run === null || run.exitCode !== null) {
837
+ const running = [...runs.values()].filter((r) => r.exitCode === null);
838
+ if (running.length === 0) {
786
839
  sendJson(res, 200, { ok: true, stopped: false });
787
840
  return;
788
841
  }
789
- try {
790
- if (process.platform === "win32") {
791
- // proc.kill() only terminates run_livebench.py itself; its child
792
- // (cmd gen_api_answer.py …) would survive. taskkill /T /F
793
- // takes down the whole tree.
794
- spawn("taskkill", ["/PID", String(run.proc.pid), "/T", "/F"], { windowsHide: true });
795
- } else {
796
- run.proc.kill();
842
+ for (const record of running) {
843
+ try {
844
+ if (process.platform === "win32") {
845
+ // proc.kill() only terminates run_livebench.py itself; its child
846
+ // (cmd gen_api_answer.py …) would survive. taskkill /T /F
847
+ // takes down the whole tree.
848
+ spawn("taskkill", ["/PID", String(record.proc.pid), "/T", "/F"], { windowsHide: true });
849
+ } else {
850
+ record.proc.kill();
851
+ }
852
+ } catch (error) {
853
+ sendJson(res, 500, { ok: false, error: String(error.message ?? error) });
854
+ return;
797
855
  }
798
- } catch (error) {
799
- sendJson(res, 500, { ok: false, error: String(error.message ?? error) });
800
- return;
801
856
  }
802
- sendJson(res, 200, { ok: true, stopped: true });
857
+ sendJson(res, 200, { ok: true, stopped: running.length });
803
858
  },
804
859
  }), `${name}: stop route`);
805
860
 
@@ -886,7 +941,7 @@ function apply(ctx) {
886
941
  const task = typeof row.task === "string" ? row.task : "";
887
942
  // path-safety: membership in the scanned task list + charset guards
888
943
  if (!/^[A-Za-z0-9._@-]{1,160}$/.test(model)) continue;
889
- if (!/^[a-z0-9_]{1,60}$/.test(category) || !/^[a-z0-9_]{1,60}$/.test(task)) continue;
944
+ if (!/^[A-Za-z0-9_]{1,80}$/.test(category) || !/^[A-Za-z0-9_]{1,80}$/.test(task)) continue;
890
945
  if (!(tasks[category] && tasks[category][task])) continue;
891
946
  const taskDir = join(layout.dataDir, category, task);
892
947
  removed += filterJsonlByModel(join(taskDir, "model_answer", `${model}.jsonl`), "model_id", model);
@@ -897,9 +952,4 @@ function apply(ctx) {
897
952
  }), `${name}: delete route`);
898
953
  }
899
954
 
900
- /** Count alive runs (at most one today, kept for future parallel lanes). */
901
- function runsAlive() {
902
- return 1;
903
- }
904
-
905
- export { name, inject, apply, writeGeneratedModelConfig, readProviders };
955
+ export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.1.10",
3
+ "version": "0.1.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",