u1s1-cli 0.16.6 → 0.17.0
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/dist/agent-setup.d.ts +5 -0
- package/dist/agent-setup.js +47 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +1 -1
- package/dist/bench.js +317 -268
- package/dist/brand.d.ts +4 -1
- package/dist/brand.js +17 -3
- package/dist/config.js +1 -1
- package/dist/index.js +7 -3
- package/dist/style.d.ts +4 -0
- package/dist/style.js +7 -1
- package/dist/subagent.d.ts +34 -0
- package/dist/subagent.js +87 -0
- package/dist/tools.d.ts +23 -0
- package/dist/tools.js +98 -2
- package/dist/workflow/runner.d.ts +33 -0
- package/dist/workflow/runner.js +262 -0
- package/dist/workflow/tool.d.ts +12 -0
- package/dist/workflow/tool.js +93 -0
- package/package.json +1 -1
package/dist/bench.js
CHANGED
|
@@ -6,6 +6,24 @@ import { CUSTOM_ENDPOINTS, loadConfig, MODELS, PROVIDER_ID, } from "./config.js"
|
|
|
6
6
|
import { loadCustomEndpoints } from "./api.js";
|
|
7
7
|
// ─── 工具 ───
|
|
8
8
|
const BENCH_DIR = join(homedir(), ".u1s1", "bench");
|
|
9
|
+
/** 题目未声明 maxScore 时的默认满分 */
|
|
10
|
+
const DEFAULT_MAX_SCORE = 10;
|
|
11
|
+
/** 报告里单条回答最多保留的字符数 */
|
|
12
|
+
const MAX_ANSWER_CHARS = 2000;
|
|
13
|
+
/** compare 子命令里单条回答的截断长度 */
|
|
14
|
+
const MAX_COMPARE_CHARS = 200;
|
|
15
|
+
/** 去掉回答里的 markdown 代码块围栏,便于 JSON 解析 */
|
|
16
|
+
function stripCodeFences(text) {
|
|
17
|
+
return text.replace(/```json\s*\n?/gi, "").replace(/\n?```/g, "").trim();
|
|
18
|
+
}
|
|
19
|
+
/** 自定义端点的模型 id 常带 org/ 前缀,取斜杠后的短 id */
|
|
20
|
+
function shortModelId(id) {
|
|
21
|
+
return id.includes("/") ? id.split("/")[1] : id;
|
|
22
|
+
}
|
|
23
|
+
/** 结果里模型的显示名:内置模型不带前缀,自定义端点带 provider 前缀 */
|
|
24
|
+
function resultModelLabel(r) {
|
|
25
|
+
return r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
|
|
26
|
+
}
|
|
9
27
|
/** 项目 bench 数据目录 */
|
|
10
28
|
function benchDataDir() {
|
|
11
29
|
const distDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -118,24 +136,19 @@ function scoreResponse(response, checks, maxScore) {
|
|
|
118
136
|
function runCheck(response, check) {
|
|
119
137
|
const val = check.value;
|
|
120
138
|
switch (check.type) {
|
|
121
|
-
case "contains":
|
|
122
|
-
const needle = String(val);
|
|
123
|
-
const text = check.ignoreCase ? response.toLowerCase() : response;
|
|
124
|
-
const search = check.ignoreCase ? needle.toLowerCase() : needle;
|
|
125
|
-
return text.includes(search);
|
|
126
|
-
}
|
|
139
|
+
case "contains":
|
|
127
140
|
case "not_contains": {
|
|
128
141
|
const needle = String(val);
|
|
129
142
|
const text = check.ignoreCase ? response.toLowerCase() : response;
|
|
130
143
|
const search = check.ignoreCase ? needle.toLowerCase() : needle;
|
|
131
|
-
|
|
144
|
+
const found = text.includes(search);
|
|
145
|
+
return check.type === "contains" ? found : !found;
|
|
132
146
|
}
|
|
133
147
|
case "contains_code_block":
|
|
134
148
|
return /```[\s\S]*?```/.test(response) || /`[^`]+`/.test(response);
|
|
135
149
|
case "parse_json": {
|
|
136
150
|
try {
|
|
137
|
-
|
|
138
|
-
JSON.parse(stripped);
|
|
151
|
+
JSON.parse(stripCodeFences(response));
|
|
139
152
|
return true;
|
|
140
153
|
}
|
|
141
154
|
catch {
|
|
@@ -168,8 +181,7 @@ function runCheck(response, check) {
|
|
|
168
181
|
}
|
|
169
182
|
case "has_json_key": {
|
|
170
183
|
try {
|
|
171
|
-
const
|
|
172
|
-
const obj = JSON.parse(stripped);
|
|
184
|
+
const obj = JSON.parse(stripCodeFences(response));
|
|
173
185
|
return obj[String(val)] !== undefined;
|
|
174
186
|
}
|
|
175
187
|
catch {
|
|
@@ -303,6 +315,44 @@ function listRuns() {
|
|
|
303
315
|
}
|
|
304
316
|
return runs;
|
|
305
317
|
}
|
|
318
|
+
/** 把一批结果按模型聚合成分数/延迟/花费/错误数 */
|
|
319
|
+
function summarizeByModel(results, keyOf) {
|
|
320
|
+
const map = new Map();
|
|
321
|
+
for (const r of results) {
|
|
322
|
+
const key = keyOf(r);
|
|
323
|
+
const s = map.get(key) ?? { totalScore: 0, totalMax: 0, totalLatency: 0, totalCost: 0, errors: 0, total: 0, okTokensOut: 0, okLatency: 0 };
|
|
324
|
+
s.totalScore += r.score ?? 0;
|
|
325
|
+
s.totalMax += r.maxScore ?? DEFAULT_MAX_SCORE;
|
|
326
|
+
s.totalLatency += r.latencyMs;
|
|
327
|
+
s.totalCost += r.costUsd;
|
|
328
|
+
if (r.error) {
|
|
329
|
+
s.errors++;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
s.okTokensOut += r.tokensOut;
|
|
333
|
+
s.okLatency += r.latencyMs;
|
|
334
|
+
}
|
|
335
|
+
s.total++;
|
|
336
|
+
map.set(key, s);
|
|
337
|
+
}
|
|
338
|
+
return map;
|
|
339
|
+
}
|
|
340
|
+
/** 按总分从高到低排序 */
|
|
341
|
+
function sortByScore(entries) {
|
|
342
|
+
return entries.sort((a, b) => b[1].totalScore - a[1].totalScore);
|
|
343
|
+
}
|
|
344
|
+
/** 得分率百分数字符串(一位小数) */
|
|
345
|
+
function scoreRate(s) {
|
|
346
|
+
return s.totalMax > 0 ? ((s.totalScore / s.totalMax) * 100).toFixed(1) : "0.0";
|
|
347
|
+
}
|
|
348
|
+
/** 错误率百分数字符串(整数) */
|
|
349
|
+
function errorRate(s) {
|
|
350
|
+
return ((s.errors / s.total) * 100).toFixed(0);
|
|
351
|
+
}
|
|
352
|
+
/** 推理速度:成功题的输出 token / 耗时(tok/s),没有成功题返回 — */
|
|
353
|
+
function tokPerSec(s) {
|
|
354
|
+
return s.okLatency > 0 ? (s.okTokensOut / s.okLatency * 1000).toFixed(1) : "—";
|
|
355
|
+
}
|
|
306
356
|
// ─── 报告生成 ───
|
|
307
357
|
function generateReport(run) {
|
|
308
358
|
const lines = [];
|
|
@@ -328,7 +378,7 @@ function generateReport(run) {
|
|
|
328
378
|
}
|
|
329
379
|
for (const [qId, qResults] of byQuestion) {
|
|
330
380
|
const prompt = qResults[0].prompt;
|
|
331
|
-
const maxScore = qResults[0].maxScore ??
|
|
381
|
+
const maxScore = qResults[0].maxScore ?? DEFAULT_MAX_SCORE;
|
|
332
382
|
lines.push(`### ${qId}`);
|
|
333
383
|
lines.push(`> ${prompt}`);
|
|
334
384
|
lines.push("");
|
|
@@ -336,10 +386,9 @@ function generateReport(run) {
|
|
|
336
386
|
const header = ["模型", `得分/${maxScore}`, "延迟", "输出tok", "状态"];
|
|
337
387
|
const rows = [];
|
|
338
388
|
for (const r of qResults) {
|
|
339
|
-
const modelLabel = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
|
|
340
389
|
const scoreStr = r.score !== undefined ? `${r.score}/${maxScore}` : "—";
|
|
341
390
|
rows.push([
|
|
342
|
-
|
|
391
|
+
resultModelLabel(r),
|
|
343
392
|
scoreStr,
|
|
344
393
|
formatDuration(r.latencyMs),
|
|
345
394
|
String(r.tokensOut),
|
|
@@ -356,8 +405,7 @@ function generateReport(run) {
|
|
|
356
405
|
lines.push("<details><summary>评分明细</summary>");
|
|
357
406
|
lines.push("");
|
|
358
407
|
for (const r of scored) {
|
|
359
|
-
|
|
360
|
-
lines.push(`**${modelLabel}:** ${r.score}/${maxScore}`);
|
|
408
|
+
lines.push(`**${resultModelLabel(r)}:** ${r.score}/${maxScore}`);
|
|
361
409
|
for (const d of r.scoreDetails) {
|
|
362
410
|
const icon = d.passed ? "✅" : "❌";
|
|
363
411
|
lines.push(`- ${icon} ${d.check} (+${d.earned}/${d.points})`);
|
|
@@ -369,8 +417,12 @@ function generateReport(run) {
|
|
|
369
417
|
}
|
|
370
418
|
// 各模型回答
|
|
371
419
|
for (const r of qResults) {
|
|
372
|
-
|
|
373
|
-
lines.push(
|
|
420
|
+
lines.push(`<details><summary>${resultModelLabel(r)} 的回答</summary>`);
|
|
421
|
+
lines.push("");
|
|
422
|
+
lines.push("```");
|
|
423
|
+
lines.push(r.response.slice(0, MAX_ANSWER_CHARS));
|
|
424
|
+
if (r.response.length > MAX_ANSWER_CHARS)
|
|
425
|
+
lines.push("...(截断)");
|
|
374
426
|
lines.push("");
|
|
375
427
|
lines.push("```");
|
|
376
428
|
lines.push(r.response.slice(0, 2000));
|
|
@@ -385,33 +437,17 @@ function generateReport(run) {
|
|
|
385
437
|
// 汇总 — 带分数
|
|
386
438
|
lines.push("## 汇总");
|
|
387
439
|
lines.push("");
|
|
388
|
-
const
|
|
389
|
-
for (const r of run.results) {
|
|
390
|
-
const key = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
|
|
391
|
-
if (!modelSummary.has(key))
|
|
392
|
-
modelSummary.set(key, { totalScore: 0, totalMax: 0, totalLatency: 0, totalCost: 0, errors: 0, total: 0 });
|
|
393
|
-
const s = modelSummary.get(key);
|
|
394
|
-
s.totalScore += r.score ?? 0;
|
|
395
|
-
s.totalMax += r.maxScore ?? 10;
|
|
396
|
-
s.totalLatency += r.latencyMs;
|
|
397
|
-
s.totalCost += r.costUsd;
|
|
398
|
-
if (r.error)
|
|
399
|
-
s.errors++;
|
|
400
|
-
s.total++;
|
|
401
|
-
}
|
|
402
|
-
const header = ["模型", "总分", "得分率", "平均延迟", "总花费", "错误率"];
|
|
440
|
+
const header = ["模型", "总分", "得分率", "平均延迟", "速度(tok/s)", "总花费", "错误率"];
|
|
403
441
|
const rows = [];
|
|
404
|
-
|
|
405
|
-
const sorted = [...modelSummary.entries()].sort((a, b) => b[1].totalScore - a[1].totalScore);
|
|
406
|
-
for (const [model, s] of sorted) {
|
|
407
|
-
const pct = s.totalMax > 0 ? ((s.totalScore / s.totalMax) * 100).toFixed(1) : "0.0";
|
|
442
|
+
for (const [model, s] of sortByScore([...summarizeByModel(run.results, resultModelLabel)])) {
|
|
408
443
|
rows.push([
|
|
409
444
|
model,
|
|
410
445
|
`${s.totalScore}/${s.totalMax}`,
|
|
411
|
-
`${
|
|
446
|
+
`${scoreRate(s)}%`,
|
|
412
447
|
formatDuration(s.totalLatency / s.total),
|
|
448
|
+
tokPerSec(s),
|
|
413
449
|
costStr(s.totalCost),
|
|
414
|
-
s.errors > 0 ? `${(
|
|
450
|
+
s.errors > 0 ? `${errorRate(s)}%` : "0%",
|
|
415
451
|
]);
|
|
416
452
|
}
|
|
417
453
|
lines.push("```");
|
|
@@ -420,243 +456,256 @@ function generateReport(run) {
|
|
|
420
456
|
return lines.join("\n");
|
|
421
457
|
}
|
|
422
458
|
// ─── 主命令 ───
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
459
|
+
/** 打印错误信息并以退出码 1 结束。永不返回。 */
|
|
460
|
+
function failWithError(e) {
|
|
461
|
+
console.error(` ${e instanceof Error ? e.message : e}`);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
function printHelp() {
|
|
465
|
+
console.log("");
|
|
466
|
+
console.log(" u1s1 bench — 模型质量基准测试(带自动评分)");
|
|
467
|
+
console.log("");
|
|
468
|
+
console.log(" 用法:");
|
|
469
|
+
console.log(" u1s1 bench list 列出可用的测试套件");
|
|
470
|
+
console.log(" u1s1 bench run <suite> [models..] 跑基准测试(--delay 秒数:题目间隔,限流渠道用)");
|
|
471
|
+
console.log(" u1s1 bench report <run-id> 查看报告");
|
|
472
|
+
console.log(" u1s1 bench compare <run-id1> <run-id2> 对比两次运行");
|
|
473
|
+
console.log("");
|
|
474
|
+
console.log(" 示例:");
|
|
475
|
+
console.log(" u1s1 bench run quick 用当前默认模型跑快速测试");
|
|
476
|
+
console.log(" u1s1 bench run full deepseek pro 用多个模型跑全面测试");
|
|
477
|
+
console.log(" u1s1 bench report latest 看最近一次报告");
|
|
478
|
+
console.log("");
|
|
479
|
+
}
|
|
480
|
+
function listSuitesCommand() {
|
|
481
|
+
const suites = listSuites();
|
|
482
|
+
console.log("");
|
|
483
|
+
console.log(" Bench 套件:");
|
|
484
|
+
for (const s of suites) {
|
|
485
|
+
const tag = s.builtin ? "(内置)" : "(自定义)";
|
|
486
|
+
console.log(` ${s.name.padEnd(12)} ${s.description} ${tag}`);
|
|
441
487
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
488
|
+
console.log("");
|
|
489
|
+
console.log(" 自定义套件放 ~/.u1s1/bench/<name>.json,格式同内置 suite.json");
|
|
490
|
+
console.log("");
|
|
491
|
+
}
|
|
492
|
+
/** 把模型名解析成走 u1s1 默认端点的调用目标 */
|
|
493
|
+
function u1s1Target(id, cfg) {
|
|
494
|
+
return { id, label: id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" };
|
|
495
|
+
}
|
|
496
|
+
/** 把用户输入的模型名解析成一个可调用的目标 */
|
|
497
|
+
function resolveModelTarget(query, cfg) {
|
|
498
|
+
const lower = query.toLowerCase();
|
|
499
|
+
const m = MODELS.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
|
|
500
|
+
if (m)
|
|
501
|
+
return u1s1Target(m.id, cfg);
|
|
502
|
+
for (const ep of CUSTOM_ENDPOINTS) {
|
|
503
|
+
const em = ep.models.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
|
|
504
|
+
if (em) {
|
|
505
|
+
return { id: shortModelId(em.id), label: `${ep.name}:${em.id}`, baseUrl: ep.baseUrl, apiKey: ep.apiKey ?? "", source: "custom" };
|
|
449
506
|
}
|
|
450
|
-
console.log("");
|
|
451
|
-
console.log(" 自定义套件放 ~/.u1s1/bench/<name>.json,格式同内置 suite.json");
|
|
452
|
-
console.log("");
|
|
453
|
-
return;
|
|
454
507
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const em = ep.models.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
|
|
480
|
-
if (em) {
|
|
481
|
-
const shortId = em.id.includes("/") ? em.id.split("/")[1] : em.id;
|
|
482
|
-
return { id: shortId, label: `${ep.name}:${em.id}`, baseUrl: ep.baseUrl, apiKey: ep.apiKey ?? "", source: "custom" };
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return { id: query, label: query, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" };
|
|
486
|
-
}
|
|
487
|
-
if (modelQueries.length > 0) {
|
|
488
|
-
targets = modelQueries.map((q) => resolveModelTarget(q)).filter((t) => t !== null);
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
const { resolvePreferredModel } = await import("./config.js");
|
|
492
|
-
const pref = resolvePreferredModel(cfg);
|
|
493
|
-
if (pref.provider === PROVIDER_ID) {
|
|
494
|
-
targets = [{ id: pref.id, label: pref.id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" }];
|
|
495
|
-
}
|
|
496
|
-
else {
|
|
497
|
-
const ep = CUSTOM_ENDPOINTS.find((e) => e.id === pref.provider);
|
|
498
|
-
if (ep) {
|
|
499
|
-
const shortId = pref.id.includes("/") ? pref.id.split("/")[1] : pref.id;
|
|
500
|
-
targets = [{ id: shortId, label: `${ep.name}:${pref.id}`, baseUrl: ep.baseUrl, apiKey: ep.apiKey ?? "", source: "custom" }];
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
targets = [{ id: pref.id, label: pref.id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" }];
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
console.log("");
|
|
508
|
-
console.log(` 📊 Bench: ${suite.name}`);
|
|
509
|
-
console.log(` ${suite.description}`);
|
|
510
|
-
console.log(` 题目数: ${suite.questions.length}`);
|
|
511
|
-
console.log(` 模型: ${targets.map((t) => t.label).join(", ")}`);
|
|
512
|
-
console.log("");
|
|
513
|
-
const results = [];
|
|
514
|
-
const total = suite.questions.length * targets.length;
|
|
515
|
-
let done = 0;
|
|
516
|
-
for (const target of targets) {
|
|
517
|
-
for (const q of suite.questions) {
|
|
518
|
-
const label = `${target.label} / ${q.id}`;
|
|
519
|
-
process.stdout.write(` [${++done}/${total}] ${label.padEnd(40)} `);
|
|
520
|
-
const resp = await callModel(target.baseUrl, target.apiKey, target.id, q.prompt);
|
|
521
|
-
const costUsd = estimateCost(target.id, resp.tokensIn, resp.tokensOut);
|
|
522
|
-
// 评分
|
|
523
|
-
let score;
|
|
524
|
-
let maxScore;
|
|
525
|
-
let scoreDetails;
|
|
526
|
-
if (!resp.error && q.checks && q.checks.length > 0) {
|
|
527
|
-
const ms = q.maxScore ?? 10;
|
|
528
|
-
const result = scoreResponse(resp.response, q.checks, ms);
|
|
529
|
-
score = result.score;
|
|
530
|
-
maxScore = ms;
|
|
531
|
-
scoreDetails = result.details;
|
|
532
|
-
}
|
|
533
|
-
results.push({
|
|
534
|
-
questionId: q.id,
|
|
535
|
-
category: q.category,
|
|
536
|
-
prompt: q.prompt,
|
|
537
|
-
modelId: target.label,
|
|
538
|
-
providerName: target.source === "u1s1" ? PROVIDER_ID : target.label,
|
|
539
|
-
response: resp.response,
|
|
540
|
-
latencyMs: resp.latencyMs,
|
|
541
|
-
tokensIn: resp.tokensIn,
|
|
542
|
-
tokensOut: resp.tokensOut,
|
|
543
|
-
costUsd,
|
|
544
|
-
error: resp.error,
|
|
545
|
-
score,
|
|
546
|
-
maxScore,
|
|
547
|
-
scoreDetails,
|
|
548
|
-
});
|
|
549
|
-
if (resp.error) {
|
|
550
|
-
console.log(`❌ ${resp.error.slice(0, 60)}`);
|
|
551
|
-
}
|
|
552
|
-
else if (score !== undefined) {
|
|
553
|
-
const bar = scoreBar(score, maxScore);
|
|
554
|
-
console.log(`✅ ${bar} ${score}/${maxScore} · ${formatDuration(resp.latencyMs)}`);
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
console.log(`✅ ${formatDuration(resp.latencyMs)} · ${resp.tokensIn}→${resp.tokensOut} tok`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
const runId = `bench-${suite.name}-${Date.now()}`;
|
|
562
|
-
const run = {
|
|
563
|
-
id: runId,
|
|
564
|
-
timestamp: new Date().toISOString(),
|
|
565
|
-
suiteName: suite.name,
|
|
566
|
-
models: [...new Set(results.map((r) => r.modelId))],
|
|
567
|
-
results,
|
|
568
|
-
};
|
|
569
|
-
saveRun(run);
|
|
570
|
-
// 简易汇总(带分数)
|
|
571
|
-
console.log("");
|
|
572
|
-
console.log(" 📋 评分汇总:");
|
|
573
|
-
const byModel = new Map();
|
|
574
|
-
for (const r of results) {
|
|
575
|
-
const key = r.modelId;
|
|
576
|
-
if (!byModel.has(key))
|
|
577
|
-
byModel.set(key, { totalScore: 0, totalMax: 0, totalLatency: 0, totalCost: 0, ok: 0, fail: 0 });
|
|
578
|
-
const s = byModel.get(key);
|
|
579
|
-
s.totalScore += r.score ?? 0;
|
|
580
|
-
s.totalMax += r.maxScore ?? 10;
|
|
581
|
-
s.totalLatency += r.latencyMs;
|
|
582
|
-
s.totalCost += r.costUsd;
|
|
583
|
-
if (r.error)
|
|
584
|
-
s.fail++;
|
|
585
|
-
else
|
|
586
|
-
s.ok++;
|
|
587
|
-
}
|
|
588
|
-
const sumHeader = ["模型", "总分", "得分率", "平均延迟", "花费", "状态"];
|
|
589
|
-
const sumRows = [];
|
|
590
|
-
const sorted = [...byModel.entries()].sort((a, b) => b[1].totalScore - a[1].totalScore);
|
|
591
|
-
for (const [model, s] of sorted) {
|
|
592
|
-
const pct = s.totalMax > 0 ? ((s.totalScore / s.totalMax) * 100).toFixed(1) : "0.0";
|
|
593
|
-
sumRows.push([
|
|
594
|
-
model,
|
|
595
|
-
`${s.totalScore}/${s.totalMax}`,
|
|
596
|
-
`${pct}%`,
|
|
597
|
-
formatDuration(s.totalLatency / (s.ok + s.fail)),
|
|
598
|
-
costStr(s.totalCost),
|
|
599
|
-
s.fail > 0 ? `${((s.fail / (s.ok + s.fail)) * 100).toFixed(0)}%失败` : "✅全部通过",
|
|
600
|
-
]);
|
|
601
|
-
}
|
|
602
|
-
console.log(" " + renderTable(sumHeader, sumRows));
|
|
603
|
-
console.log("");
|
|
604
|
-
console.log(` 查看完整报告: u1s1 bench report ${runId}`);
|
|
605
|
-
console.log("");
|
|
606
|
-
return;
|
|
508
|
+
// 兜底:当作原始模型 id 直接走默认端点
|
|
509
|
+
return u1s1Target(query, cfg);
|
|
510
|
+
}
|
|
511
|
+
/** 没指定模型时,用当前偏好的默认模型 */
|
|
512
|
+
async function resolveDefaultTargets(cfg) {
|
|
513
|
+
const { resolvePreferredModel } = await import("./config.js");
|
|
514
|
+
const pref = resolvePreferredModel(cfg);
|
|
515
|
+
if (pref.provider === PROVIDER_ID)
|
|
516
|
+
return [u1s1Target(pref.id, cfg)];
|
|
517
|
+
const ep = CUSTOM_ENDPOINTS.find((e) => e.id === pref.provider);
|
|
518
|
+
if (!ep)
|
|
519
|
+
return [u1s1Target(pref.id, cfg)];
|
|
520
|
+
return [{
|
|
521
|
+
id: shortModelId(pref.id),
|
|
522
|
+
label: `${ep.name}:${pref.id}`,
|
|
523
|
+
baseUrl: ep.baseUrl,
|
|
524
|
+
apiKey: ep.apiKey ?? "",
|
|
525
|
+
source: "custom",
|
|
526
|
+
}];
|
|
527
|
+
}
|
|
528
|
+
/** 打印单题进度行(✅/❌ + 得分条或 token 统计) */
|
|
529
|
+
function printQuestionOutcome(resp, score, maxScore) {
|
|
530
|
+
if (resp.error) {
|
|
531
|
+
console.log(`❌ ${resp.error.slice(0, 60)}`);
|
|
607
532
|
}
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
let run;
|
|
611
|
-
try {
|
|
612
|
-
const id = runId === "latest" ? findLatestRun() : runId;
|
|
613
|
-
if (!id)
|
|
614
|
-
throw new Error("还没有运行记录");
|
|
615
|
-
run = loadRun(id);
|
|
616
|
-
}
|
|
617
|
-
catch (e) {
|
|
618
|
-
console.error(` ${e instanceof Error ? e.message : e}`);
|
|
619
|
-
process.exit(1);
|
|
620
|
-
}
|
|
621
|
-
const report = generateReport(run);
|
|
622
|
-
const reportFile = join(BENCH_DIR, `${run.id}.md`);
|
|
623
|
-
writeFileSync(reportFile, report);
|
|
624
|
-
console.log(report);
|
|
625
|
-
console.log(` 报告已保存: ${reportFile}`);
|
|
626
|
-
return;
|
|
533
|
+
else if (score !== undefined && maxScore !== undefined) {
|
|
534
|
+
console.log(`✅ ${scoreBar(score, maxScore)} ${score}/${maxScore} · ${formatDuration(resp.latencyMs)}`);
|
|
627
535
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
536
|
+
else {
|
|
537
|
+
console.log(`✅ ${formatDuration(resp.latencyMs)} · ${resp.tokensIn}→${resp.tokensOut} tok`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function runBench(args) {
|
|
541
|
+
const suiteName = args[0] ?? "quick";
|
|
542
|
+
const modelQueries = [];
|
|
543
|
+
let delayMs = 0;
|
|
544
|
+
for (let i = 1; i < args.length; i++) {
|
|
545
|
+
const a = args[i];
|
|
546
|
+
if (a === "--delay")
|
|
547
|
+
delayMs = Number(args[++i]) || 0;
|
|
548
|
+
else if (a.startsWith("--delay="))
|
|
549
|
+
delayMs = Number(a.slice("--delay=".length)) || 0;
|
|
550
|
+
else
|
|
551
|
+
modelQueries.push(a);
|
|
552
|
+
}
|
|
553
|
+
if (delayMs > 0)
|
|
554
|
+
console.log(` ⏳ 题目间隔:${(delayMs / 1000).toFixed(0)}s(限流渠道用)`);
|
|
555
|
+
const cfg = loadConfig();
|
|
556
|
+
if (!cfg.apiKey) {
|
|
557
|
+
console.error(" 还没有登录,先 u1s1 login");
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
await loadCustomEndpoints(cfg);
|
|
561
|
+
let suite;
|
|
562
|
+
try {
|
|
563
|
+
suite = loadSuite(suiteName);
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
failWithError(e);
|
|
567
|
+
}
|
|
568
|
+
const targets = modelQueries.length > 0
|
|
569
|
+
? modelQueries.map((q) => resolveModelTarget(q, cfg))
|
|
570
|
+
: await resolveDefaultTargets(cfg);
|
|
571
|
+
console.log("");
|
|
572
|
+
console.log(` 📊 Bench: ${suite.name}`);
|
|
573
|
+
console.log(` ${suite.description}`);
|
|
574
|
+
console.log(` 题目数: ${suite.questions.length}`);
|
|
575
|
+
console.log(` 模型: ${targets.map((t) => t.label).join(", ")}`);
|
|
576
|
+
console.log("");
|
|
577
|
+
const results = [];
|
|
578
|
+
const total = suite.questions.length * targets.length;
|
|
579
|
+
let done = 0;
|
|
580
|
+
for (const target of targets) {
|
|
581
|
+
for (const q of suite.questions) {
|
|
582
|
+
const label = `${target.label} / ${q.id}`;
|
|
583
|
+
process.stdout.write(` [${++done}/${total}] ${label.padEnd(40)} `);
|
|
584
|
+
const resp = await callModel(target.baseUrl, target.apiKey, target.id, q.prompt);
|
|
585
|
+
const costUsd = estimateCost(target.id, resp.tokensIn, resp.tokensOut);
|
|
586
|
+
// 有评分规则且调用没出错时才打分
|
|
587
|
+
const maxScore = q.maxScore ?? DEFAULT_MAX_SCORE;
|
|
588
|
+
const scoring = !resp.error && q.checks && q.checks.length > 0
|
|
589
|
+
? scoreResponse(resp.response, q.checks, maxScore)
|
|
590
|
+
: null;
|
|
591
|
+
results.push({
|
|
592
|
+
questionId: q.id,
|
|
593
|
+
category: q.category,
|
|
594
|
+
prompt: q.prompt,
|
|
595
|
+
modelId: target.label,
|
|
596
|
+
providerName: target.source === "u1s1" ? PROVIDER_ID : target.label,
|
|
597
|
+
response: resp.response,
|
|
598
|
+
latencyMs: resp.latencyMs,
|
|
599
|
+
tokensIn: resp.tokensIn,
|
|
600
|
+
tokensOut: resp.tokensOut,
|
|
601
|
+
costUsd,
|
|
602
|
+
error: resp.error,
|
|
603
|
+
score: scoring?.score,
|
|
604
|
+
maxScore: scoring ? maxScore : undefined,
|
|
605
|
+
scoreDetails: scoring?.details,
|
|
606
|
+
});
|
|
607
|
+
printQuestionOutcome(resp, scoring?.score, scoring ? maxScore : undefined);
|
|
608
|
+
if (delayMs > 0)
|
|
609
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
654
610
|
}
|
|
655
|
-
return;
|
|
656
611
|
}
|
|
612
|
+
const runId = `bench-${suite.name}-${Date.now()}`;
|
|
613
|
+
saveRun({
|
|
614
|
+
id: runId,
|
|
615
|
+
timestamp: new Date().toISOString(),
|
|
616
|
+
suiteName: suite.name,
|
|
617
|
+
models: [...new Set(results.map((r) => r.modelId))],
|
|
618
|
+
results,
|
|
619
|
+
});
|
|
620
|
+
printConsoleSummary(results);
|
|
621
|
+
console.log(` 查看完整报告: u1s1 bench report ${runId}`);
|
|
622
|
+
console.log("");
|
|
623
|
+
}
|
|
624
|
+
/** 控制台里的简易评分汇总表 */
|
|
625
|
+
function printConsoleSummary(results) {
|
|
626
|
+
console.log("");
|
|
627
|
+
console.log(" 📋 评分汇总:");
|
|
628
|
+
const header = ["模型", "总分", "得分率", "平均延迟", "速度", "花费", "状态"];
|
|
629
|
+
const rows = sortByScore([...summarizeByModel(results, (r) => r.modelId)]).map(([model, s]) => [
|
|
630
|
+
model,
|
|
631
|
+
`${s.totalScore}/${s.totalMax}`,
|
|
632
|
+
`${scoreRate(s)}%`,
|
|
633
|
+
formatDuration(s.totalLatency / s.total),
|
|
634
|
+
tokPerSec(s),
|
|
635
|
+
costStr(s.totalCost),
|
|
636
|
+
s.errors > 0 ? `${errorRate(s)}%失败` : "✅全部通过",
|
|
637
|
+
]);
|
|
638
|
+
console.log(" " + renderTable(header, rows));
|
|
639
|
+
console.log("");
|
|
640
|
+
}
|
|
641
|
+
// ─── 报告 & 对比 ───
|
|
642
|
+
function showReport(args) {
|
|
643
|
+
const runIdArg = args[0] ?? "latest";
|
|
644
|
+
let run;
|
|
645
|
+
try {
|
|
646
|
+
const id = runIdArg === "latest" ? findLatestRun() : runIdArg;
|
|
647
|
+
if (!id)
|
|
648
|
+
throw new Error("还没有运行记录");
|
|
649
|
+
run = loadRun(id);
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
failWithError(e);
|
|
653
|
+
}
|
|
654
|
+
const report = generateReport(run);
|
|
655
|
+
const reportFile = join(BENCH_DIR, `${run.id}.md`);
|
|
656
|
+
writeFileSync(reportFile, report);
|
|
657
|
+
console.log(report);
|
|
658
|
+
console.log(` 报告已保存: ${reportFile}`);
|
|
659
|
+
}
|
|
660
|
+
/** 两次运行共同拥有的题目 id */
|
|
661
|
+
function commonQuestionIds(a, b) {
|
|
662
|
+
const idsInB = new Set(b.results.map((r) => r.questionId));
|
|
663
|
+
return [...new Set(a.results.map((r) => r.questionId))].filter((id) => idsInB.has(id));
|
|
664
|
+
}
|
|
665
|
+
/** 单条回答带分数打印(截断到一行能看下的长度) */
|
|
666
|
+
function printAnswerWithScore(r) {
|
|
667
|
+
const s = r.score !== undefined ? `[${r.score}/${r.maxScore}]` : "";
|
|
668
|
+
console.log(` ${s} ${r.modelId}: ${r.response.slice(0, MAX_COMPARE_CHARS)}`);
|
|
669
|
+
}
|
|
670
|
+
function compareRuns(runId1, runId2) {
|
|
671
|
+
let run1;
|
|
672
|
+
let run2;
|
|
673
|
+
try {
|
|
674
|
+
run1 = loadRun(runId1);
|
|
675
|
+
run2 = loadRun(runId2);
|
|
676
|
+
}
|
|
677
|
+
catch (e) {
|
|
678
|
+
failWithError(e);
|
|
679
|
+
}
|
|
680
|
+
console.log("");
|
|
681
|
+
console.log(` 对比 ${run1.suiteName}(${run1.timestamp}) vs ${run2.suiteName}(${run2.timestamp})`);
|
|
682
|
+
console.log("");
|
|
683
|
+
for (const qId of commonQuestionIds(run1, run2)) {
|
|
684
|
+
console.log(` ── ${qId} ──`);
|
|
685
|
+
const r1 = run1.results.find((r) => r.questionId === qId);
|
|
686
|
+
const r2 = run2.results.find((r) => r.questionId === qId);
|
|
687
|
+
printAnswerWithScore(r1);
|
|
688
|
+
printAnswerWithScore(r2);
|
|
689
|
+
console.log("");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
// ─── 入口 ───
|
|
693
|
+
export async function benchCommand(args) {
|
|
694
|
+
const sub = args[0];
|
|
695
|
+
if (!sub || sub === "help" || sub === "--help")
|
|
696
|
+
return printHelp();
|
|
697
|
+
if (sub === "list")
|
|
698
|
+
return listSuitesCommand();
|
|
699
|
+
if (sub === "run")
|
|
700
|
+
return runBench(args.slice(1));
|
|
701
|
+
if (sub === "report")
|
|
702
|
+
return showReport(args.slice(1));
|
|
657
703
|
if (sub === "compare") {
|
|
658
|
-
|
|
659
|
-
|
|
704
|
+
if (!args[1] || !args[2]) {
|
|
705
|
+
console.log(" 用法: u1s1 bench compare <run-id1> <run-id2>");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
return compareRuns(args[1], args[2]);
|
|
660
709
|
}
|
|
661
710
|
console.error(` 未知子命令: ${sub}`);
|
|
662
711
|
console.log(" u1s1 bench help 查看用法");
|