u1s1-cli 1.2.2 → 1.2.4
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 +17 -2
- package/dist/agent-setup.js +81 -10
- package/dist/api.d.ts +8 -2
- package/dist/api.js +6 -6
- package/dist/bench-report.d.ts +22 -0
- package/dist/bench-report.js +158 -0
- package/dist/bench-scoring.d.ts +8 -0
- package/dist/bench-scoring.js +101 -0
- package/dist/bench-types.d.ts +50 -0
- package/dist/bench-types.js +1 -0
- package/dist/bench.js +101 -367
- package/dist/brand.d.ts +9 -3
- package/dist/brand.js +12 -12
- package/dist/config.d.ts +28 -6
- package/dist/config.js +37 -2
- package/dist/deploy.js +72 -49
- package/dist/import/claude.d.ts +2 -1
- package/dist/import/claude.js +162 -129
- package/dist/import/codex.d.ts +2 -1
- package/dist/import/codex.js +184 -157
- package/dist/import/index.js +62 -47
- package/dist/index.js +16 -13
- package/dist/login.js +2 -2
- package/dist/style.js +7 -1
- package/dist/subagent.d.ts +6 -1
- package/dist/subagent.js +4 -4
- package/dist/tools.d.ts +18 -12
- package/dist/tools.js +22 -16
- package/dist/usage.js +95 -77
- package/dist/web.js +2 -1
- package/dist/workflow/runner.js +36 -26
- package/dist/workflow/tool.js +67 -55
- package/package.json +1 -1
- package/scripts/render-regression.mjs +3 -1
package/dist/agent-setup.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { type CliConfig, type CustomEndpoint, type ModelDef } from "./config.js";
|
|
1
|
+
import { type CliConfig, type CustomEndpoint, type ModelDef, type ThinkingLevel } from "./config.js";
|
|
2
2
|
import type { ShellDoctorResult } from "./shell-doctor.js";
|
|
3
3
|
/** pi auto-appends <agentDir>/APPEND_SYSTEM.md to its system prompt — our branding hook. */
|
|
4
4
|
export declare function ensureBrandPrompt(shell: ShellDoctorResult): void;
|
|
5
|
+
/**
|
|
6
|
+
* Seed per-model defaults without overwriting a user's Ctrl+S choice. The
|
|
7
|
+
* marker records values managed by u1s1, so a later Gateway default can update
|
|
8
|
+
* them while a diverged user value becomes user-owned.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyModelThinkingDefaults(settings: Record<string, unknown>, models: ModelDef[]): boolean;
|
|
5
11
|
/** Defaults that don't overwrite values the user already set. */
|
|
6
|
-
export declare function ensureDefaultSettings(): void;
|
|
12
|
+
export declare function ensureDefaultSettings(models?: ModelDef[]): void;
|
|
7
13
|
/** ≤0.4.0 wrote u1s1-dark/u1s1-light into the pi themes dir; remove them. */
|
|
8
14
|
export declare function cleanupBrandThemes(): void;
|
|
9
15
|
/**
|
|
@@ -54,10 +60,19 @@ export declare function toProviderModels(models: ModelDef[]): {
|
|
|
54
60
|
id: string;
|
|
55
61
|
name: string;
|
|
56
62
|
reasoning: boolean;
|
|
63
|
+
thinkingLevelMap?: Partial<Record<ThinkingLevel, string | null>>;
|
|
57
64
|
input: ("text" | "image")[];
|
|
58
65
|
cost: ModelDef["cost"];
|
|
59
66
|
contextWindow: number;
|
|
60
67
|
maxTokens: number;
|
|
68
|
+
compat?: {
|
|
69
|
+
supportsStore?: boolean;
|
|
70
|
+
supportsDeveloperRole?: boolean;
|
|
71
|
+
supportsReasoningEffort: boolean;
|
|
72
|
+
maxTokensField?: "max_tokens";
|
|
73
|
+
requiresReasoningContentOnAssistantMessages?: boolean;
|
|
74
|
+
thinkingFormat: "openai" | "deepseek" | "qwen";
|
|
75
|
+
};
|
|
61
76
|
}[];
|
|
62
77
|
/**
|
|
63
78
|
* 自定义端点 → pi provider 条目。密钥走环境变量引用(启动器已 set,见
|
package/dist/agent-setup.js
CHANGED
|
@@ -66,8 +66,52 @@ function bundledPiVersion() {
|
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
const THINKING_DEFAULTS_MARKER = "u1s1ModelThinkingDefaults";
|
|
70
|
+
function recordValue(value) {
|
|
71
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
72
|
+
? value
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Seed per-model defaults without overwriting a user's Ctrl+S choice. The
|
|
77
|
+
* marker records values managed by u1s1, so a later Gateway default can update
|
|
78
|
+
* them while a diverged user value becomes user-owned.
|
|
79
|
+
*/
|
|
80
|
+
export function applyModelThinkingDefaults(settings, models) {
|
|
81
|
+
const defaults = models.filter((model) => model.thinking !== undefined);
|
|
82
|
+
if (defaults.length === 0)
|
|
83
|
+
return false;
|
|
84
|
+
const levels = { ...recordValue(settings["modelThinkingLevels"]) };
|
|
85
|
+
const managed = { ...recordValue(settings[THINKING_DEFAULTS_MARKER]) };
|
|
86
|
+
let changed = false;
|
|
87
|
+
for (const model of defaults) {
|
|
88
|
+
const key = `${PROVIDER_ID}/${model.id}`;
|
|
89
|
+
const next = model.thinking.defaultLevel;
|
|
90
|
+
const current = levels[key];
|
|
91
|
+
const previousManaged = managed[key];
|
|
92
|
+
if (current === undefined || current === previousManaged) {
|
|
93
|
+
if (current !== next) {
|
|
94
|
+
levels[key] = next;
|
|
95
|
+
changed = true;
|
|
96
|
+
}
|
|
97
|
+
if (previousManaged !== next) {
|
|
98
|
+
managed[key] = next;
|
|
99
|
+
changed = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (previousManaged !== undefined) {
|
|
103
|
+
delete managed[key];
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (changed) {
|
|
108
|
+
settings["modelThinkingLevels"] = levels;
|
|
109
|
+
settings[THINKING_DEFAULTS_MARKER] = managed;
|
|
110
|
+
}
|
|
111
|
+
return changed;
|
|
112
|
+
}
|
|
69
113
|
/** Defaults that don't overwrite values the user already set. */
|
|
70
|
-
export function ensureDefaultSettings() {
|
|
114
|
+
export function ensureDefaultSettings(models = []) {
|
|
71
115
|
mkdirSync(agentDir, { recursive: true });
|
|
72
116
|
const p = join(agentDir, "settings.json");
|
|
73
117
|
let settings = {};
|
|
@@ -114,6 +158,8 @@ export function ensureDefaultSettings() {
|
|
|
114
158
|
settings["autoUpdate"] = true;
|
|
115
159
|
changed = true;
|
|
116
160
|
}
|
|
161
|
+
if (applyModelThinkingDefaults(settings, models))
|
|
162
|
+
changed = true;
|
|
117
163
|
// ≤0.4.0 shipped branded themes and forced them as default; the files are
|
|
118
164
|
// gone now, so a settings.json still pointing at them must fall back to
|
|
119
165
|
// pi's default theme.
|
|
@@ -333,15 +379,40 @@ export function ensureAuthCredential() {
|
|
|
333
379
|
}
|
|
334
380
|
/** pi provider 条目里的模型形状(models.json 与 registerProvider 共用)。 */
|
|
335
381
|
export function toProviderModels(models) {
|
|
336
|
-
return models.map((m) =>
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
382
|
+
return models.map((m) => {
|
|
383
|
+
let thinkingLevelMap;
|
|
384
|
+
let compat;
|
|
385
|
+
if (m.thinking) {
|
|
386
|
+
const supported = new Set(m.thinking.levels);
|
|
387
|
+
thinkingLevelMap = {};
|
|
388
|
+
for (const level of ["off", "minimal", "low", "medium", "high", "xhigh", "max"]) {
|
|
389
|
+
thinkingLevelMap[level] = supported.has(level) ? (m.thinking.levelMap[level] ?? level) : null;
|
|
390
|
+
}
|
|
391
|
+
compat = {
|
|
392
|
+
supportsReasoningEffort: true,
|
|
393
|
+
thinkingFormat: m.thinking.requestFormat,
|
|
394
|
+
...(m.thinking.requestFormat === "deepseek"
|
|
395
|
+
? {
|
|
396
|
+
supportsStore: false,
|
|
397
|
+
supportsDeveloperRole: false,
|
|
398
|
+
maxTokensField: "max_tokens",
|
|
399
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
400
|
+
}
|
|
401
|
+
: {}),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
id: m.id,
|
|
406
|
+
name: m.name,
|
|
407
|
+
reasoning: m.reasoning,
|
|
408
|
+
thinkingLevelMap,
|
|
409
|
+
input: m.vision ? ["text", "image"] : ["text"],
|
|
410
|
+
cost: m.cost,
|
|
411
|
+
contextWindow: m.contextWindow,
|
|
412
|
+
maxTokens: m.maxTokens,
|
|
413
|
+
compat,
|
|
414
|
+
};
|
|
415
|
+
});
|
|
345
416
|
}
|
|
346
417
|
/**
|
|
347
418
|
* 自定义端点 → pi provider 条目。密钥走环境变量引用(启动器已 set,见
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CliConfig } from "./config.js";
|
|
1
|
+
import { type ApiThinkingCapabilities, type CliConfig } from "./config.js";
|
|
2
2
|
export interface MeResponse {
|
|
3
3
|
email: string | null;
|
|
4
4
|
signup_credit_usd?: number;
|
|
@@ -35,6 +35,8 @@ export interface ApiModel {
|
|
|
35
35
|
id: string;
|
|
36
36
|
name: string;
|
|
37
37
|
reasoning: boolean;
|
|
38
|
+
/** Older gateways omit model-specific thinking metadata. */
|
|
39
|
+
thinking?: ApiThinkingCapabilities | null;
|
|
38
40
|
/** Older gateways omit this field; missing means text-only. */
|
|
39
41
|
vision?: boolean;
|
|
40
42
|
context_length: number;
|
|
@@ -103,7 +105,11 @@ export interface SearchResponse {
|
|
|
103
105
|
results: SearchResult[];
|
|
104
106
|
}
|
|
105
107
|
/** 联网搜索走网关代理(上游 key 只在服务端)。maxResults 不传时由服务端决定默认值。 */
|
|
106
|
-
export declare function searchWeb(cfg: Pick<CliConfig, "baseUrl" | "apiKey">,
|
|
108
|
+
export declare function searchWeb(cfg: Pick<CliConfig, "baseUrl" | "apiKey">, input: {
|
|
109
|
+
query: string;
|
|
110
|
+
maxResults?: number;
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
}): Promise<SearchResponse>;
|
|
107
113
|
/** web_fetch 直连失败时的回退:网关用 Cloudflare Browser Rendering 渲染后转 markdown。 */
|
|
108
114
|
export declare function renderPage(cfg: Pick<CliConfig, "baseUrl" | "apiKey">, url: string, signal?: AbortSignal): Promise<{
|
|
109
115
|
url: string;
|
package/dist/api.js
CHANGED
|
@@ -56,7 +56,7 @@ export async function loadCustomEndpoints(cfg) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
/** 联网搜索走网关代理(上游 key 只在服务端)。maxResults 不传时由服务端决定默认值。 */
|
|
59
|
-
export async function searchWeb(cfg,
|
|
59
|
+
export async function searchWeb(cfg, input) {
|
|
60
60
|
if (!cfg.apiKey)
|
|
61
61
|
throw new Error("没有配置 API Key");
|
|
62
62
|
let resp;
|
|
@@ -67,8 +67,8 @@ export async function searchWeb(cfg, query, maxResults, signal) {
|
|
|
67
67
|
"x-u1s1-version": VERSION,
|
|
68
68
|
"content-type": "application/json",
|
|
69
69
|
},
|
|
70
|
-
body: JSON.stringify({ query, max_results: maxResults }),
|
|
71
|
-
signal: signal ?? AbortSignal.timeout(60_000),
|
|
70
|
+
body: JSON.stringify({ query: input.query, max_results: input.maxResults }),
|
|
71
|
+
signal: input.signal ?? AbortSignal.timeout(60_000),
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
catch {
|
|
@@ -171,10 +171,10 @@ export async function generateImage(cfg, req, signal) {
|
|
|
171
171
|
signal: signal ?? AbortSignal.timeout(180_000),
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
|
-
catch (
|
|
174
|
+
catch (error) {
|
|
175
175
|
if (signal?.aborted)
|
|
176
|
-
throw
|
|
177
|
-
throw new Error(`连不上 ${cfg.baseUrl}
|
|
176
|
+
throw error;
|
|
177
|
+
throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`, { cause: error });
|
|
178
178
|
}
|
|
179
179
|
if (resp.status === 401)
|
|
180
180
|
throw new Error("登录已失效,请重新运行 u1s1 login");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type BenchResult, type BenchRunMeta } from "./bench-types.js";
|
|
2
|
+
export interface ModelSummary {
|
|
3
|
+
totalScore: number;
|
|
4
|
+
totalMax: number;
|
|
5
|
+
totalLatency: number;
|
|
6
|
+
totalCost: number;
|
|
7
|
+
errors: number;
|
|
8
|
+
total: number;
|
|
9
|
+
okTokensOut: number;
|
|
10
|
+
okLatency: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function resultModelLabel(result: BenchResult): string;
|
|
13
|
+
export declare function formatDuration(ms: number): string;
|
|
14
|
+
export declare function costStr(usd: number): string;
|
|
15
|
+
export declare function renderTable(header: string[], rows: string[][]): string;
|
|
16
|
+
export declare function scoreBar(score: number, maxScore: number, width?: number): string;
|
|
17
|
+
export declare function summarizeByModel(results: BenchResult[], keyOf: (result: BenchResult) => string): Map<string, ModelSummary>;
|
|
18
|
+
export declare function sortByScore(entries: [string, ModelSummary][]): [string, ModelSummary][];
|
|
19
|
+
export declare function scoreRate(summary: ModelSummary): string;
|
|
20
|
+
export declare function errorRate(summary: ModelSummary): string;
|
|
21
|
+
export declare function tokPerSec(summary: ModelSummary): string;
|
|
22
|
+
export declare function generateReport(run: BenchRunMeta): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { PROVIDER_ID } from "./config.js";
|
|
2
|
+
import { DEFAULT_MAX_SCORE } from "./bench-types.js";
|
|
3
|
+
const MAX_ANSWER_CHARS = 2_000;
|
|
4
|
+
export function resultModelLabel(result) {
|
|
5
|
+
return result.providerName === PROVIDER_ID
|
|
6
|
+
? result.modelId
|
|
7
|
+
: `${result.providerName}:${result.modelId}`;
|
|
8
|
+
}
|
|
9
|
+
export function formatDuration(ms) {
|
|
10
|
+
return ms < 1_000 ? `${ms.toFixed(0)}ms` : `${(ms / 1_000).toFixed(1)}s`;
|
|
11
|
+
}
|
|
12
|
+
export function costStr(usd) {
|
|
13
|
+
if (usd === 0)
|
|
14
|
+
return "—";
|
|
15
|
+
if (usd < 0.001)
|
|
16
|
+
return "<$0.001";
|
|
17
|
+
return `$${usd.toFixed(4)}`;
|
|
18
|
+
}
|
|
19
|
+
export function renderTable(header, rows) {
|
|
20
|
+
const columnWidths = header.map((heading, index) => Math.max(heading.length, ...rows.map((row) => row[index]?.length ?? 0)));
|
|
21
|
+
const separator = "─".repeat(columnWidths.reduce((sum, width) => sum + width + 3, 1));
|
|
22
|
+
const renderRow = (cells) => ` ${cells.map((cell, index) => cell.padEnd(columnWidths[index])).join(" │ ")} `;
|
|
23
|
+
return [renderRow(header), separator, ...rows.map(renderRow)].join("\n");
|
|
24
|
+
}
|
|
25
|
+
export function scoreBar(score, maxScore, width = 8) {
|
|
26
|
+
const ratio = maxScore > 0 ? Math.min(score / maxScore, 1) : 0;
|
|
27
|
+
const filled = Math.round(ratio * width);
|
|
28
|
+
const bar = "█".repeat(filled) + "░".repeat(width - filled);
|
|
29
|
+
if (ratio >= 0.8)
|
|
30
|
+
return `\x1b[32m${bar}\x1b[0m`;
|
|
31
|
+
if (ratio >= 0.5)
|
|
32
|
+
return `\x1b[33m${bar}\x1b[0m`;
|
|
33
|
+
return `\x1b[31m${bar}\x1b[0m`;
|
|
34
|
+
}
|
|
35
|
+
function emptySummary() {
|
|
36
|
+
return {
|
|
37
|
+
totalScore: 0,
|
|
38
|
+
totalMax: 0,
|
|
39
|
+
totalLatency: 0,
|
|
40
|
+
totalCost: 0,
|
|
41
|
+
errors: 0,
|
|
42
|
+
total: 0,
|
|
43
|
+
okTokensOut: 0,
|
|
44
|
+
okLatency: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function summarizeByModel(results, keyOf) {
|
|
48
|
+
const summaries = new Map();
|
|
49
|
+
for (const result of results) {
|
|
50
|
+
const key = keyOf(result);
|
|
51
|
+
const summary = summaries.get(key) ?? emptySummary();
|
|
52
|
+
summary.totalScore += result.score ?? 0;
|
|
53
|
+
summary.totalMax += result.maxScore ?? DEFAULT_MAX_SCORE;
|
|
54
|
+
summary.totalLatency += result.latencyMs;
|
|
55
|
+
summary.totalCost += result.costUsd;
|
|
56
|
+
if (result.error)
|
|
57
|
+
summary.errors++;
|
|
58
|
+
else {
|
|
59
|
+
summary.okTokensOut += result.tokensOut;
|
|
60
|
+
summary.okLatency += result.latencyMs;
|
|
61
|
+
}
|
|
62
|
+
summary.total++;
|
|
63
|
+
summaries.set(key, summary);
|
|
64
|
+
}
|
|
65
|
+
return summaries;
|
|
66
|
+
}
|
|
67
|
+
export function sortByScore(entries) {
|
|
68
|
+
return entries.sort((left, right) => right[1].totalScore - left[1].totalScore);
|
|
69
|
+
}
|
|
70
|
+
export function scoreRate(summary) {
|
|
71
|
+
return summary.totalMax > 0
|
|
72
|
+
? ((summary.totalScore / summary.totalMax) * 100).toFixed(1)
|
|
73
|
+
: "0.0";
|
|
74
|
+
}
|
|
75
|
+
export function errorRate(summary) {
|
|
76
|
+
return ((summary.errors / summary.total) * 100).toFixed(0);
|
|
77
|
+
}
|
|
78
|
+
export function tokPerSec(summary) {
|
|
79
|
+
return summary.okLatency > 0
|
|
80
|
+
? ((summary.okTokensOut / summary.okLatency) * 1_000).toFixed(1)
|
|
81
|
+
: "—";
|
|
82
|
+
}
|
|
83
|
+
function groupResultsBy(results, keyOf) {
|
|
84
|
+
const groups = new Map();
|
|
85
|
+
for (const result of results) {
|
|
86
|
+
const key = keyOf(result);
|
|
87
|
+
const group = groups.get(key) ?? [];
|
|
88
|
+
group.push(result);
|
|
89
|
+
groups.set(key, group);
|
|
90
|
+
}
|
|
91
|
+
return groups;
|
|
92
|
+
}
|
|
93
|
+
function appendScoreDetails(lines, results, maxScore) {
|
|
94
|
+
const scored = results.filter((result) => result.scoreDetails?.length);
|
|
95
|
+
if (scored.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
lines.push("", "<details><summary>评分明细</summary>", "");
|
|
98
|
+
for (const result of scored) {
|
|
99
|
+
lines.push(`**${resultModelLabel(result)}:** ${result.score}/${maxScore}`);
|
|
100
|
+
for (const detail of result.scoreDetails) {
|
|
101
|
+
lines.push(`- ${detail.passed ? "✅" : "❌"} ${detail.check} (+${detail.earned}/${detail.points})`);
|
|
102
|
+
}
|
|
103
|
+
lines.push("");
|
|
104
|
+
}
|
|
105
|
+
lines.push("</details>", "");
|
|
106
|
+
}
|
|
107
|
+
function appendAnswers(lines, results) {
|
|
108
|
+
for (const result of results) {
|
|
109
|
+
lines.push(`<details><summary>${resultModelLabel(result)} 的回答</summary>`, "", "```");
|
|
110
|
+
lines.push(result.response.slice(0, MAX_ANSWER_CHARS));
|
|
111
|
+
if (result.response.length > MAX_ANSWER_CHARS)
|
|
112
|
+
lines.push("...(截断)");
|
|
113
|
+
lines.push("```", "</details>", "");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function appendQuestion(lines, questionId, results) {
|
|
117
|
+
const first = results[0];
|
|
118
|
+
const maxScore = first.maxScore ?? DEFAULT_MAX_SCORE;
|
|
119
|
+
lines.push(`### ${questionId}`, `> ${first.prompt}`, "");
|
|
120
|
+
const rows = results.map((result) => [
|
|
121
|
+
resultModelLabel(result),
|
|
122
|
+
result.score === undefined ? "—" : `${result.score}/${maxScore}`,
|
|
123
|
+
formatDuration(result.latencyMs),
|
|
124
|
+
String(result.tokensOut),
|
|
125
|
+
result.error ? `❌ ${result.error.slice(0, 40)}` : "✅",
|
|
126
|
+
]);
|
|
127
|
+
lines.push("```", renderTable(["模型", `得分/${maxScore}`, "延迟", "输出tok", "状态"], rows), "```");
|
|
128
|
+
appendScoreDetails(lines, results, maxScore);
|
|
129
|
+
appendAnswers(lines, results);
|
|
130
|
+
}
|
|
131
|
+
function appendSummary(lines, results) {
|
|
132
|
+
const rows = sortByScore([...summarizeByModel(results, resultModelLabel)]).map(([model, summary]) => [
|
|
133
|
+
model,
|
|
134
|
+
`${summary.totalScore}/${summary.totalMax}`,
|
|
135
|
+
`${scoreRate(summary)}%`,
|
|
136
|
+
formatDuration(summary.totalLatency / summary.total),
|
|
137
|
+
tokPerSec(summary),
|
|
138
|
+
costStr(summary.totalCost),
|
|
139
|
+
summary.errors > 0 ? `${errorRate(summary)}%` : "0%",
|
|
140
|
+
]);
|
|
141
|
+
lines.push("## 汇总", "", "```", renderTable(["模型", "总分", "得分率", "平均延迟", "速度(tok/s)", "总花费", "错误率"], rows), "```");
|
|
142
|
+
}
|
|
143
|
+
export function generateReport(run) {
|
|
144
|
+
const lines = [
|
|
145
|
+
`# Bench 报告: ${run.suiteName}`,
|
|
146
|
+
`运行时间: ${run.timestamp}`,
|
|
147
|
+
`模型: ${run.models.join(", ")}`,
|
|
148
|
+
"",
|
|
149
|
+
];
|
|
150
|
+
for (const [category, categoryResults] of groupResultsBy(run.results, (result) => result.category || "未分类")) {
|
|
151
|
+
lines.push(`## ${category}`, "");
|
|
152
|
+
for (const [questionId, questionResults] of groupResultsBy(categoryResults, (result) => result.questionId)) {
|
|
153
|
+
appendQuestion(lines, questionId, questionResults);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
appendSummary(lines, run.results);
|
|
157
|
+
return lines.join("\n");
|
|
158
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BenchCheck, ScoreDetail } from "./bench-types.js";
|
|
2
|
+
/** Execute one declarative benchmark check. */
|
|
3
|
+
export declare function runCheck(response: string, check: BenchCheck): boolean;
|
|
4
|
+
/** Score one response against all declared checks. */
|
|
5
|
+
export declare function scoreResponse(response: string, checks: BenchCheck[], maxScore: number): {
|
|
6
|
+
score: number;
|
|
7
|
+
details: ScoreDetail[];
|
|
8
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Remove Markdown fences before parsing a JSON answer. */
|
|
2
|
+
function stripCodeFences(text) {
|
|
3
|
+
return text.replace(/```json\s*\n?/gi, "").replace(/\n?```/g, "").trim();
|
|
4
|
+
}
|
|
5
|
+
function containsExpectedText(response, check) {
|
|
6
|
+
const needle = String(check.value);
|
|
7
|
+
const text = check.ignoreCase ? response.toLowerCase() : response;
|
|
8
|
+
const search = check.ignoreCase ? needle.toLowerCase() : needle;
|
|
9
|
+
const found = text.includes(search);
|
|
10
|
+
return check.type === "contains" ? found : !found;
|
|
11
|
+
}
|
|
12
|
+
function isValidRegex(response, value) {
|
|
13
|
+
try {
|
|
14
|
+
new RegExp(String(value));
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
const literal = response.match(/\/(.+)\/[gimsuy]*/);
|
|
19
|
+
if (literal) {
|
|
20
|
+
new RegExp(literal[1]);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const cleaned = response.replace(/```[\s\S]*?```/g, "").trim();
|
|
24
|
+
const parts = cleaned.startsWith("/") ? cleaned.match(/^\/(.+)\/([gimsuy]*)$/) : null;
|
|
25
|
+
if (!parts)
|
|
26
|
+
return false;
|
|
27
|
+
new RegExp(parts[1]);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function hasJsonKey(response, value) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(stripCodeFences(response));
|
|
34
|
+
return parsed[String(value)] !== undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function usesAllowedWords(response, allowed) {
|
|
41
|
+
const tokens = response.split(/[\s,。!?、;:""''()[【】\]「」]+/).filter(Boolean);
|
|
42
|
+
return tokens.every((token) => /^[,。!?、;:""''()《》\s.。,,、!?;:…\-—]+$/.test(token) || allowed.includes(token));
|
|
43
|
+
}
|
|
44
|
+
function hasExpectedFiveCharacterLines(response, expected) {
|
|
45
|
+
let count = 0;
|
|
46
|
+
for (const line of response.split("\n")) {
|
|
47
|
+
const chars = line.replace(/[,。!?、;:""''()《》\s,.\-,;:!?\dA-Za-z]/g, "").trim();
|
|
48
|
+
if (/^[\u4e00-\u9fff]{5}$/.test(chars))
|
|
49
|
+
count++;
|
|
50
|
+
}
|
|
51
|
+
return count >= expected;
|
|
52
|
+
}
|
|
53
|
+
/** Execute one declarative benchmark check. */
|
|
54
|
+
export function runCheck(response, check) {
|
|
55
|
+
switch (check.type) {
|
|
56
|
+
case "contains":
|
|
57
|
+
case "not_contains":
|
|
58
|
+
return containsExpectedText(response, check);
|
|
59
|
+
case "contains_code_block":
|
|
60
|
+
return /```[\s\S]*?```/.test(response) || /`[^`]+`/.test(response);
|
|
61
|
+
case "parse_json": {
|
|
62
|
+
try {
|
|
63
|
+
JSON.parse(stripCodeFences(response));
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
case "valid_regex":
|
|
71
|
+
return isValidRegex(response, check.value);
|
|
72
|
+
case "has_json_key":
|
|
73
|
+
return hasJsonKey(response, check.value);
|
|
74
|
+
case "max_length":
|
|
75
|
+
return response.length <= Number(check.value);
|
|
76
|
+
case "min_length":
|
|
77
|
+
return response.length >= Number(check.value);
|
|
78
|
+
case "allowed_words_only":
|
|
79
|
+
return usesAllowedWords(response, check.value);
|
|
80
|
+
case "has_chinese":
|
|
81
|
+
return /[\u4e00-\u9fff\u3400-\u4dbf]/.test(response);
|
|
82
|
+
case "line_count_5chars":
|
|
83
|
+
return hasExpectedFiveCharacterLines(response, Number(check.value));
|
|
84
|
+
default:
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Score one response against all declared checks. */
|
|
89
|
+
export function scoreResponse(response, checks, maxScore) {
|
|
90
|
+
const details = checks.map((check) => {
|
|
91
|
+
const passed = runCheck(response, check);
|
|
92
|
+
return {
|
|
93
|
+
check: check.description,
|
|
94
|
+
passed,
|
|
95
|
+
points: check.points,
|
|
96
|
+
earned: passed ? check.points : 0,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
const total = details.reduce((sum, detail) => sum + detail.earned, 0);
|
|
100
|
+
return { score: Math.min(total, maxScore), details };
|
|
101
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface BenchCheck {
|
|
2
|
+
type: string;
|
|
3
|
+
/** Depends on type: string | number | boolean | string[]. */
|
|
4
|
+
value: unknown;
|
|
5
|
+
points: number;
|
|
6
|
+
description: string;
|
|
7
|
+
ignoreCase?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface BenchQuestion {
|
|
10
|
+
id: string;
|
|
11
|
+
category: string;
|
|
12
|
+
prompt: string;
|
|
13
|
+
maxScore?: number;
|
|
14
|
+
checks?: BenchCheck[];
|
|
15
|
+
}
|
|
16
|
+
export interface BenchSuite {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
questions: BenchQuestion[];
|
|
20
|
+
}
|
|
21
|
+
export interface ScoreDetail {
|
|
22
|
+
check: string;
|
|
23
|
+
passed: boolean;
|
|
24
|
+
points: number;
|
|
25
|
+
earned: number;
|
|
26
|
+
}
|
|
27
|
+
export interface BenchResult {
|
|
28
|
+
questionId: string;
|
|
29
|
+
category: string;
|
|
30
|
+
prompt: string;
|
|
31
|
+
modelId: string;
|
|
32
|
+
providerName: string;
|
|
33
|
+
response: string;
|
|
34
|
+
latencyMs: number;
|
|
35
|
+
tokensIn: number;
|
|
36
|
+
tokensOut: number;
|
|
37
|
+
costUsd: number;
|
|
38
|
+
error?: string;
|
|
39
|
+
score?: number;
|
|
40
|
+
maxScore?: number;
|
|
41
|
+
scoreDetails?: ScoreDetail[];
|
|
42
|
+
}
|
|
43
|
+
export interface BenchRunMeta {
|
|
44
|
+
id: string;
|
|
45
|
+
timestamp: string;
|
|
46
|
+
suiteName: string;
|
|
47
|
+
models: string[];
|
|
48
|
+
results: BenchResult[];
|
|
49
|
+
}
|
|
50
|
+
export declare const DEFAULT_MAX_SCORE = 10;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_MAX_SCORE = 10;
|