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.
- package/lib/client.js +130 -68
- package/lib/index.js +116 -66
- 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({
|
|
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 [
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
[
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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("
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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.
|
|
447
|
-
running ?
|
|
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
|
-
|
|
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 ? "
|
|
501
|
+
running ? "有评测运行中" : "无运行中的评测"),
|
|
459
502
|
),
|
|
460
|
-
h("
|
|
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
|
-
|
|
502
|
-
|
|
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
|
|
106
|
-
const MAX_CONCURRENT_RUNS =
|
|
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
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
/**
|
|
507
|
-
|
|
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
|
-
|
|
515
|
-
|
|
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
|
|
549
|
-
if (
|
|
550
|
-
|
|
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
|
-
|
|
560
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
740
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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:
|
|
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 (!/^[
|
|
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
|
-
|
|
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.
|
|
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",
|