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/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
- return !text.includes(search);
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
- const stripped = response.replace(/```json\s*\n?/gi, "").replace(/\n?```/g, "").trim();
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 stripped = response.replace(/```json\s*\n?/gi, "").replace(/\n?```/g, "").trim();
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 ?? 10;
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
- modelLabel,
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
- const modelLabel = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
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
- const modelLabel = r.providerName === PROVIDER_ID ? r.modelId : `${r.providerName}:${r.modelId}`;
373
- lines.push(`<details><summary>${modelLabel} 的回答</summary>`);
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 modelSummary = new Map();
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
- `${pct}%`,
446
+ `${scoreRate(s)}%`,
412
447
  formatDuration(s.totalLatency / s.total),
448
+ tokPerSec(s),
413
449
  costStr(s.totalCost),
414
- s.errors > 0 ? `${((s.errors / s.total) * 100).toFixed(0)}%` : "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
- export async function benchCommand(args) {
424
- const sub = args[0];
425
- if (!sub || sub === "help" || sub === "--help") {
426
- console.log("");
427
- console.log(" u1s1 bench — 模型质量基准测试(带自动评分)");
428
- console.log("");
429
- console.log(" 用法:");
430
- console.log(" u1s1 bench list 列出可用的测试套件");
431
- console.log(" u1s1 bench run <suite> [models..] 跑基准测试");
432
- console.log(" u1s1 bench report <run-id> 查看报告");
433
- console.log(" u1s1 bench compare <run-id1> <run-id2> 对比两次运行");
434
- console.log("");
435
- console.log(" 示例:");
436
- console.log(" u1s1 bench run quick 用当前默认模型跑快速测试");
437
- console.log(" u1s1 bench run full deepseek pro 用多个模型跑全面测试");
438
- console.log(" u1s1 bench report latest 看最近一次报告");
439
- console.log("");
440
- return;
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
- if (sub === "list") {
443
- const suites = listSuites();
444
- console.log("");
445
- console.log(" Bench 套件:");
446
- for (const s of suites) {
447
- const tag = s.builtin ? "(内置)" : "(自定义)";
448
- console.log(` ${s.name.padEnd(12)} ${s.description} ${tag}`);
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
- if (sub === "run") {
456
- const suiteName = args[1] ?? "quick";
457
- const modelQueries = args.slice(2);
458
- const cfg = loadConfig();
459
- if (!cfg.apiKey) {
460
- console.error(" 还没有登录,先 u1s1 login");
461
- process.exit(1);
462
- }
463
- await loadCustomEndpoints(cfg);
464
- let suite;
465
- try {
466
- suite = loadSuite(suiteName);
467
- }
468
- catch (e) {
469
- console.error(` ${e instanceof Error ? e.message : e}`);
470
- process.exit(1);
471
- }
472
- let targets;
473
- function resolveModelTarget(query) {
474
- const lower = query.toLowerCase();
475
- const m = MODELS.find((x) => x.id.toLowerCase() === lower || x.aliases.includes(lower));
476
- if (m)
477
- return { id: m.id, label: m.id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" };
478
- for (const ep of CUSTOM_ENDPOINTS) {
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 (sub === "report") {
609
- const runId = args[1] ?? "latest";
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
- if (sub === "compare" && args[1] && args[2]) {
629
- try {
630
- const run1 = loadRun(args[1]);
631
- const run2 = loadRun(args[2]);
632
- console.log("");
633
- console.log(` 对比 ${run1.suiteName}(${run1.timestamp}) vs ${run2.suiteName}(${run2.timestamp})`);
634
- console.log("");
635
- const qIds1 = new Set(run1.results.map((r) => r.questionId));
636
- const qIds2 = new Set(run2.results.map((r) => r.questionId));
637
- const common = [...qIds1].filter((q) => qIds2.has(q));
638
- for (const qId of common) {
639
- console.log(` ── ${qId} ──`);
640
- const r1 = run1.results.find((r) => r.questionId === qId);
641
- const r2 = run2.results.find((r) => r.questionId === qId);
642
- if (r1 && r2) {
643
- const s1 = r1.score !== undefined ? `[${r1.score}/${r1.maxScore}]` : "";
644
- const s2 = r2.score !== undefined ? `[${r2.score}/${r2.maxScore}]` : "";
645
- console.log(` ${s1} ${r1.modelId}: ${r1.response.slice(0, 200)}`);
646
- console.log(` ${s2} ${r2.modelId}: ${r2.response.slice(0, 200)}`);
647
- console.log("");
648
- }
649
- }
650
- }
651
- catch (e) {
652
- console.error(` ${e instanceof Error ? e.message : e}`);
653
- process.exit(1);
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
- console.log(" 用法: u1s1 bench compare <run-id1> <run-id2>");
659
- return;
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 查看用法");