modelfunction-cli 0.5.3 → 0.5.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/README.md +6 -4
- package/SCHEMA.md +11 -5
- package/lib/audit.js +3 -3
- package/lib/cases.js +4 -4
- package/lib/profile.js +53 -19
- package/lib/score.js +32 -4
- package/lib/suite.js +7 -0
- package/mf.js +30 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx -y modelfunction-cli@0.5.
|
|
8
|
+
npx -y modelfunction-cli@0.5.3 --version
|
|
9
9
|
# 或
|
|
10
10
|
npm i -D modelfunction-cli
|
|
11
11
|
npx mf --version
|
|
@@ -126,7 +126,7 @@ mf eval --suite mf.suite.json --models internal-ds \
|
|
|
126
126
|
"mcpServers": {
|
|
127
127
|
"modelfunction": {
|
|
128
128
|
"command": "npx",
|
|
129
|
-
"args": ["-y", "modelfunction-cli@0.5.
|
|
129
|
+
"args": ["-y", "modelfunction-cli@0.5.3", "mcp"]
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -138,7 +138,9 @@ mf eval --suite mf.suite.json --models internal-ds \
|
|
|
138
138
|
|
|
139
139
|
对外 JSON 字段、`check()` 返回结构、MCP 参数与退出码见 [SCHEMA.md](SCHEMA.md),自 0.4.3 起冻结为 v1:v1 内只做加法,重命名或删除字段需 major 并提前两个 minor 或 90 天弃用。`check()` 的类型声明随包发布。
|
|
140
140
|
|
|
141
|
-
`0.5.
|
|
141
|
+
`0.5.3` 已发布到 npm:含凭证与本地反馈、usage `ci` 标记、eval `--progress`/`--max-cost`、`mf audit`、`scan --probe` streamable HTTP、MCP 静态工具 `lint`/`check`/`cluster`/`compare`、`checkMany` 一包批量检查、门禁严重度收敛、静态规则性能(真实 100 能力清单 `checkMany` 471ms→40ms)、`installed` 形状校验与 `profiles` 平台形态兼容。示例统一使用 `@0.5.3`。
|
|
142
|
+
|
|
143
|
+
`0.5.4`(待发布):用例新增 `excludeFromBase` / `excludeFromProfiles`(布尔,排除计分但不省调用);评分卡新增行级 `errorKind` 与 `errors` / `errorsByKind` / `scoredTotal` / `selectionAccuracyExcludingErrors`;挂载对比不再把超时/HTTP 错误记成 jammed,分组表单列 `errors`;`mf audit` 分组表增加 `neg%`。契约见 `SCHEMA.md`。
|
|
142
144
|
|
|
143
145
|
## 数据边界
|
|
144
146
|
|
|
@@ -167,7 +169,7 @@ mf feedback --out out.json # 导出脱敏反馈包(聚合值,不含原始
|
|
|
167
169
|
|
|
168
170
|
```bash
|
|
169
171
|
# 1. 把 mf 挂成 MCP server
|
|
170
|
-
claude mcp add modelfunction -- npx -y modelfunction-cli@0.5.
|
|
172
|
+
claude mcp add modelfunction -- npx -y modelfunction-cli@0.5.3 mcp
|
|
171
173
|
```
|
|
172
174
|
|
|
173
175
|
```text
|
package/SCHEMA.md
CHANGED
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
"note": "",
|
|
52
52
|
"source": "manual",
|
|
53
53
|
"reviewed": true,
|
|
54
|
+
"excludeFromBase": false,
|
|
55
|
+
"excludeFromProfiles": false,
|
|
54
56
|
"tags": []
|
|
55
57
|
}
|
|
56
58
|
```
|
|
@@ -59,6 +61,7 @@
|
|
|
59
61
|
- `expectedArgs` 仅在有 `expectedTool` 时可用;值为断言,支持字面量、`$contains`、`$oneOf`、`$exists`。
|
|
60
62
|
- `toolInput` + `expect` 用于 `--execute`:`expect` 支持 `ok`、`contains`、`equals`、`errorType`、`maxDurationMs`、`maxPeakRssMB`。
|
|
61
63
|
- `reviewed` 缺省为 `true`(手写用例);`mf case add` 缺省写入 `source=agent`、`reviewed=false`。
|
|
64
|
+
- `excludeFromBase` / `excludeFromProfiles` 必须是布尔值。用例仍会实际调用(不省 token),只是不计入对应范围的正式指标;用于「该用例只对某一挂载形态有意义」的情况。
|
|
62
65
|
|
|
63
66
|
### skills[]
|
|
64
67
|
|
|
@@ -89,8 +92,10 @@ skills 只做静态检查,不参与模型评测。
|
|
|
89
92
|
"fingerprint": "sha256:...",
|
|
90
93
|
"execution": { "runtime": "node", "engine": "node-permission", "entry": "index.mjs", "hash": "..." }
|
|
91
94
|
},
|
|
92
|
-
"models": [ { "model": "dsv4-flash", "total": 12, "reviewedTotal": 12, "
|
|
93
|
-
"
|
|
95
|
+
"models": [ { "model": "dsv4-flash", "total": 12, "reviewedTotal": 12, "excludedTotal": 0,
|
|
96
|
+
"selectionAccuracy": 0.83, "selectionAccuracyExcludingErrors": 0.83,
|
|
97
|
+
"argAccuracy": 0.9, "passRate": 0.75, "errors": 0, "errorsByKind": {},
|
|
98
|
+
"scoredTotal": 12, "p50LatencyMs": 900,
|
|
94
99
|
"p95LatencyMs": 1800, "avgCostCny": 0.001, "totalCostCny": 0.012,
|
|
95
100
|
"inputTokens": 1000, "cachedTokens": 0, "outputTokens": 200,
|
|
96
101
|
"rows": [ { "id": "c1", "expectedTool": "...", "actualTool": "...",
|
|
@@ -101,9 +106,10 @@ skills 只做静态检查,不参与模型评测。
|
|
|
101
106
|
}
|
|
102
107
|
```
|
|
103
108
|
|
|
104
|
-
- 正式指标只统计 `reviewed`
|
|
109
|
+
- 正式指标只统计 `reviewed` 且未被 `excludeFromBase` 排除的行;未复核与排除结果单独给出。
|
|
110
|
+
- `errors` 是调用层失败(超时、HTTP、网络)的数量,按 `errorsByKind` 分类;它们仍计入 `selectionAccuracy`(保守口径),同时给出排除错误后的 `selectionAccuracyExcludingErrors` 与可评分行数 `scoredTotal`。
|
|
105
111
|
- `--repeat` 时模型对象带 `stability`(翻转用例与多次结果分布)。
|
|
106
|
-
- `--profile` 时每个组含 `profile`、`mounted`、`allInOne`、`delta`、`lifted`、`jammed
|
|
112
|
+
- `--profile` 时每个组含 `profile`、`mounted`、`allInOne`、`delta`、`lifted`、`jammed`、`errors`;错误行不参与挂载对比(不会被记成 jammed),每个组的 `reviewed` 是可评分行数。
|
|
107
113
|
- `fingerprint` 由工具与用例内容计算;`mf compare` 用它判断两份报告是否可比。
|
|
108
114
|
|
|
109
115
|
## 3. 扫描输出(`mf scan`)
|
|
@@ -338,4 +344,4 @@ const result = checkMany({ manifests: [/* ... */], installed: [/* ... */] });
|
|
|
338
344
|
}
|
|
339
345
|
```
|
|
340
346
|
|
|
341
|
-
`profiles` 为 `null` 表示套件未定义分组。`clusters`/`coverage` 的明细与 §5/§4 同源;`actions` 是给人看的优先级列表。`mf audit` 恒退出 0(advisory
|
|
347
|
+
`profiles` 为 `null` 表示套件未定义分组。`clusters`/`coverage` 的明细与 §5/§4 同源;`actions` 是给人看的优先级列表。`mf audit` 恒退出 0(advisory),不阻断流程;负例占比告警只按整 suite 计算,分组表里的 `negativeRatio` 是该组「正例 + 负例」内的比例,仅供参考。
|
package/lib/audit.js
CHANGED
|
@@ -134,11 +134,11 @@ export function formatAudit(report) {
|
|
|
134
134
|
if (!profiles) {
|
|
135
135
|
lines.push("未定义 profiles(平台可先用 `mf cluster --out <suite>` 生成候选分组)。");
|
|
136
136
|
} else {
|
|
137
|
-
lines.push("group tools cases negatives tools-without-cases");
|
|
138
|
-
lines.push("---------------------- ----- ----- --------- -------------------");
|
|
137
|
+
lines.push("group tools cases negatives neg% tools-without-cases");
|
|
138
|
+
lines.push("---------------------- ----- ----- --------- ---- -------------------");
|
|
139
139
|
for (const row of profiles.groups) {
|
|
140
140
|
lines.push(
|
|
141
|
-
`${row.name.padEnd(22)} ${String(row.tools).padStart(5)} ${String(row.cases).padStart(5)} ${String(row.negatives).padStart(9)} ${row.missing.length ? row.missing.join(", ") : "-"}`
|
|
141
|
+
`${row.name.padEnd(22)} ${String(row.tools).padStart(5)} ${String(row.cases).padStart(5)} ${String(row.negatives).padStart(9)} ${`${Math.round((row.negativeRatio ?? 0) * 100)}%`.padStart(4)} ${row.missing.length ? row.missing.join(", ") : "-"}`
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
144
|
if (profiles.unmounted.length > 0) lines.push("", `未挂载工具:${profiles.unmounted.join(", ")}`);
|
package/lib/cases.js
CHANGED
|
@@ -185,12 +185,12 @@ export function formatCoverage(report) {
|
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
187
|
if (report.profiles && report.profiles.length > 0) {
|
|
188
|
-
lines.push("", "profiles
|
|
189
|
-
lines.push("group tools cases neg tools-without-cases");
|
|
190
|
-
lines.push("---------------------- ----- ----- --- -------------------");
|
|
188
|
+
lines.push("", "profiles(分组后可评分的用例;负例每组都跑,比例按组内正例+负例计):");
|
|
189
|
+
lines.push("group tools cases neg neg% tools-without-cases");
|
|
190
|
+
lines.push("---------------------- ----- ----- --- ---- -------------------");
|
|
191
191
|
for (const row of report.profiles) {
|
|
192
192
|
lines.push(
|
|
193
|
-
`${row.name.padEnd(22)} ${String(row.tools).padStart(5)} ${String(row.cases).padStart(5)} ${String(row.negatives).padStart(3)} ${row.missing.length ? row.missing.join(", ") : "-"}`
|
|
193
|
+
`${row.name.padEnd(22)} ${String(row.tools).padStart(5)} ${String(row.cases).padStart(5)} ${String(row.negatives).padStart(3)} ${`${Math.round((row.negativeRatio ?? 0) * 100)}%`.padStart(4)} ${row.missing.length ? row.missing.join(", ") : "-"}`
|
|
194
194
|
);
|
|
195
195
|
}
|
|
196
196
|
}
|
package/lib/profile.js
CHANGED
|
@@ -54,7 +54,11 @@ export function selectProfiles(profiles, wanted) {
|
|
|
54
54
|
// A case belongs to a group when its expected tool is mounted there. Negative
|
|
55
55
|
// cases ("no tool applies") hold everywhere: the group's agent still gets them.
|
|
56
56
|
export function selectProfileCases(cases, mountedTools) {
|
|
57
|
-
return cases.filter(
|
|
57
|
+
return cases.filter(
|
|
58
|
+
(testCase) =>
|
|
59
|
+
testCase.excludeFromProfiles !== true &&
|
|
60
|
+
(testCase.expectedTool === null || mountedTools.has(testCase.expectedTool))
|
|
61
|
+
);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export function profileWarnings(profiles, suite) {
|
|
@@ -76,11 +80,12 @@ export function profileWarnings(profiles, suite) {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
export function profileCaseCoverage(profiles, cases) {
|
|
79
|
-
const
|
|
83
|
+
const considered = cases.filter((testCase) => testCase.excludeFromProfiles !== true);
|
|
84
|
+
const negatives = considered.filter((testCase) => testCase.expectedTool === null).length;
|
|
80
85
|
return profiles.map((profile) => {
|
|
81
86
|
const mounted = new Set(profile.tools);
|
|
82
87
|
const counts = new Map(profile.tools.map((name) => [name, 0]));
|
|
83
|
-
for (const testCase of
|
|
88
|
+
for (const testCase of considered) {
|
|
84
89
|
if (testCase.expectedTool !== null && mounted.has(testCase.expectedTool)) {
|
|
85
90
|
counts.set(testCase.expectedTool, counts.get(testCase.expectedTool) + 1);
|
|
86
91
|
}
|
|
@@ -88,7 +93,14 @@ export function profileCaseCoverage(profiles, cases) {
|
|
|
88
93
|
const missing = [...counts.entries()].filter(([, count]) => count === 0).map(([name]) => name);
|
|
89
94
|
let total = 0;
|
|
90
95
|
for (const count of counts.values()) total += count;
|
|
91
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
name: profile.name,
|
|
98
|
+
tools: profile.tools.length,
|
|
99
|
+
cases: total,
|
|
100
|
+
negatives,
|
|
101
|
+
negativeRatio: total + negatives > 0 ? negatives / (total + negatives) : 0,
|
|
102
|
+
missing
|
|
103
|
+
};
|
|
92
104
|
});
|
|
93
105
|
}
|
|
94
106
|
|
|
@@ -99,18 +111,21 @@ export function analyzeMount(profile, mounted, allInOne) {
|
|
|
99
111
|
for (const before of allInOne.rows) {
|
|
100
112
|
const row = after.get(before.id);
|
|
101
113
|
if (!row || before.reviewed === false || row.reviewed === false) continue;
|
|
114
|
+
// A provider error is not evidence about mounting; score it separately.
|
|
115
|
+
if (before.error || row.error) continue;
|
|
102
116
|
if (!before.selectionCorrect && row.selectionCorrect) lifted.push(changeOf(before, row));
|
|
103
117
|
else if (before.selectionCorrect && !row.selectionCorrect) jammed.push(changeOf(before, row));
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
const mountedMetrics = metricsOf(mounted);
|
|
107
121
|
const allMetrics = metricsOf(allInOne);
|
|
108
|
-
const reviewed =
|
|
122
|
+
const reviewed = mountedMetrics.scored;
|
|
109
123
|
return {
|
|
110
124
|
profile: profile.name,
|
|
111
125
|
mountedTools: profile.tools.length,
|
|
112
126
|
cases: mounted.total,
|
|
113
127
|
reviewed,
|
|
128
|
+
errors: mountedMetrics.errors,
|
|
114
129
|
mounted: mountedMetrics,
|
|
115
130
|
allInOne: allMetrics,
|
|
116
131
|
delta: {
|
|
@@ -128,32 +143,44 @@ export function analyzeMount(profile, mounted, allInOne) {
|
|
|
128
143
|
|
|
129
144
|
export function modelTotals(entries) {
|
|
130
145
|
let reviewed = 0;
|
|
146
|
+
let errors = 0;
|
|
131
147
|
let mountedCorrect = 0;
|
|
132
148
|
let allCorrect = 0;
|
|
149
|
+
const usage = { inputTokens: 0, cachedTokens: 0, outputTokens: 0, costCny: 0, estimated: false };
|
|
133
150
|
for (const entry of entries) {
|
|
134
151
|
reviewed += entry.reviewed;
|
|
152
|
+
errors += entry.errors ?? 0;
|
|
135
153
|
mountedCorrect += entry.mounted.correct;
|
|
136
154
|
allCorrect += entry.allInOne.correct;
|
|
155
|
+
usage.inputTokens += entry.usage?.inputTokens ?? 0;
|
|
156
|
+
usage.cachedTokens += entry.usage?.cachedTokens ?? 0;
|
|
157
|
+
usage.outputTokens += entry.usage?.outputTokens ?? 0;
|
|
158
|
+
usage.costCny += entry.usage?.costCny ?? 0;
|
|
159
|
+
usage.estimated ||= Boolean(entry.usage?.estimated);
|
|
137
160
|
}
|
|
138
161
|
if (reviewed === 0) {
|
|
139
162
|
return {
|
|
140
163
|
reviewed: 0,
|
|
164
|
+
errors,
|
|
141
165
|
mountedCorrect: 0,
|
|
142
166
|
allCorrect: 0,
|
|
143
167
|
mountedAccuracy: null,
|
|
144
168
|
allAccuracy: null,
|
|
145
|
-
delta: null
|
|
169
|
+
delta: null,
|
|
170
|
+
usage
|
|
146
171
|
};
|
|
147
172
|
}
|
|
148
173
|
const mountedAccuracy = mountedCorrect / reviewed;
|
|
149
174
|
const allAccuracy = allCorrect / reviewed;
|
|
150
175
|
return {
|
|
151
176
|
reviewed,
|
|
177
|
+
errors,
|
|
152
178
|
mountedCorrect,
|
|
153
179
|
allCorrect,
|
|
154
180
|
mountedAccuracy,
|
|
155
181
|
allAccuracy,
|
|
156
|
-
delta: mountedAccuracy - allAccuracy
|
|
182
|
+
delta: mountedAccuracy - allAccuracy,
|
|
183
|
+
usage
|
|
157
184
|
};
|
|
158
185
|
}
|
|
159
186
|
|
|
@@ -169,15 +196,16 @@ export function formatProfileReport(report, { showCases = false } = {}) {
|
|
|
169
196
|
entry.profile,
|
|
170
197
|
String(entry.mountedTools),
|
|
171
198
|
String(entry.cases),
|
|
199
|
+
String(entry.errors ?? 0),
|
|
172
200
|
pct(entry.mounted.selectionAccuracy),
|
|
173
201
|
pct(entry.allInOne.selectionAccuracy),
|
|
174
202
|
signedPoints(entry.delta.selection)
|
|
175
203
|
]);
|
|
176
204
|
lines.push(
|
|
177
205
|
...renderTable(
|
|
178
|
-
["profile", "tools", "cases", "mounted", "all-in-one", "delta"],
|
|
206
|
+
["profile", "tools", "cases", "errors", "mounted", "all-in-one", "delta"],
|
|
179
207
|
rows,
|
|
180
|
-
[1, 2, 3, 4, 5]
|
|
208
|
+
[1, 2, 3, 4, 5, 6]
|
|
181
209
|
)
|
|
182
210
|
);
|
|
183
211
|
const totals = model.totals;
|
|
@@ -185,7 +213,8 @@ export function formatProfileReport(report, { showCases = false } = {}) {
|
|
|
185
213
|
lines.push(
|
|
186
214
|
` 合计: 分组 ${pct(totals.mountedAccuracy)}(${totals.mountedCorrect}/${totals.reviewed})` +
|
|
187
215
|
` / 全量 ${pct(totals.allAccuracy)}(${totals.allCorrect}/${totals.reviewed})` +
|
|
188
|
-
`,${signedPoints(totals.delta)}`
|
|
216
|
+
`,${signedPoints(totals.delta)}` +
|
|
217
|
+
(totals.errors > 0 ? `;${totals.errors} 次调用错误不计入对比` : "")
|
|
189
218
|
);
|
|
190
219
|
}
|
|
191
220
|
const unstable = unstableOf(model.profiles);
|
|
@@ -224,12 +253,12 @@ export function formatProfileMarkdown(report) {
|
|
|
224
253
|
lines.push(
|
|
225
254
|
`### ${model.model}`,
|
|
226
255
|
"",
|
|
227
|
-
"| group | tools | cases | mounted | all-in-one | delta |",
|
|
228
|
-
"| --- | --- | --- | --- | --- | --- |"
|
|
256
|
+
"| group | tools | cases | errors | mounted | all-in-one | delta |",
|
|
257
|
+
"| --- | --- | --- | --- | --- | --- | --- |"
|
|
229
258
|
);
|
|
230
259
|
for (const entry of model.profiles) {
|
|
231
260
|
lines.push(
|
|
232
|
-
`| ${entry.profile} | ${entry.mountedTools} | ${entry.cases} | ${pct(entry.mounted.selectionAccuracy)} | ${pct(entry.allInOne.selectionAccuracy)} | ${signedPoints(entry.delta.selection)} |`
|
|
261
|
+
`| ${entry.profile} | ${entry.mountedTools} | ${entry.cases} | ${entry.errors ?? 0} | ${pct(entry.mounted.selectionAccuracy)} | ${pct(entry.allInOne.selectionAccuracy)} | ${signedPoints(entry.delta.selection)} |`
|
|
233
262
|
);
|
|
234
263
|
}
|
|
235
264
|
const totals = model.totals;
|
|
@@ -237,7 +266,8 @@ export function formatProfileMarkdown(report) {
|
|
|
237
266
|
if (totals.reviewed > 0) {
|
|
238
267
|
lines.push(
|
|
239
268
|
`- 合计:分组 ${pct(totals.mountedAccuracy)}(${totals.mountedCorrect}/${totals.reviewed}),` +
|
|
240
|
-
`全量 ${pct(totals.allAccuracy)}(${totals.allCorrect}/${totals.reviewed}),${signedPoints(totals.delta)}`
|
|
269
|
+
`全量 ${pct(totals.allAccuracy)}(${totals.allCorrect}/${totals.reviewed}),${signedPoints(totals.delta)}` +
|
|
270
|
+
(totals.errors > 0 ? `;${totals.errors} 次调用错误不计入对比` : "")
|
|
241
271
|
);
|
|
242
272
|
} else {
|
|
243
273
|
lines.push("- 合计:没有已复核用例,无法比较");
|
|
@@ -267,14 +297,18 @@ function changeOf(before, after) {
|
|
|
267
297
|
|
|
268
298
|
function metricsOf(summary) {
|
|
269
299
|
const reviewedRows = summary.rows.filter((row) => row.reviewed !== false);
|
|
270
|
-
const
|
|
300
|
+
const errorRows = reviewedRows.filter((row) => row.error);
|
|
301
|
+
const viableRows = reviewedRows.filter((row) => !row.error);
|
|
302
|
+
const correct = viableRows.filter((row) => row.selectionCorrect).length;
|
|
271
303
|
return {
|
|
272
|
-
selectionAccuracy:
|
|
304
|
+
selectionAccuracy: viableRows.length ? correct / viableRows.length : null,
|
|
273
305
|
argAccuracy: summary.argAccuracy,
|
|
274
|
-
passRate:
|
|
275
|
-
?
|
|
306
|
+
passRate: viableRows.length
|
|
307
|
+
? viableRows.filter((row) => row.passed).length / viableRows.length
|
|
276
308
|
: null,
|
|
277
|
-
correct
|
|
309
|
+
correct,
|
|
310
|
+
errors: errorRows.length,
|
|
311
|
+
scored: viableRows.length
|
|
278
312
|
};
|
|
279
313
|
}
|
|
280
314
|
|
package/lib/score.js
CHANGED
|
@@ -89,7 +89,7 @@ export function costCnyOf(usage, pricing) {
|
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export function scoreModel({ model, cases, results, pricing }) {
|
|
92
|
+
export function scoreModel({ model, cases, results, pricing, scope = "base" }) {
|
|
93
93
|
const rows = cases.map((testCase, index) => {
|
|
94
94
|
const result = results[index];
|
|
95
95
|
const actualTool = result.ok ? (result.toolCalls?.[0]?.name ?? null) : null;
|
|
@@ -98,6 +98,7 @@ export function scoreModel({ model, cases, results, pricing }) {
|
|
|
98
98
|
? matchArgs(result.toolCalls?.[0]?.arguments, testCase.expectedArgs)
|
|
99
99
|
: { total: 0, matched: 0, failures: [] };
|
|
100
100
|
const passed = Boolean(result.ok) && selectionCorrect && args.failures.length === 0;
|
|
101
|
+
const error = result.error ?? null;
|
|
101
102
|
return {
|
|
102
103
|
id: testCase.id,
|
|
103
104
|
expectedTool: testCase.expectedTool,
|
|
@@ -106,21 +107,35 @@ export function scoreModel({ model, cases, results, pricing }) {
|
|
|
106
107
|
args,
|
|
107
108
|
passed,
|
|
108
109
|
reviewed: testCase.reviewed !== false,
|
|
110
|
+
...(testCase.excludeFromBase ? { excludeFromBase: true } : {}),
|
|
111
|
+
...(testCase.excludeFromProfiles ? { excludeFromProfiles: true } : {}),
|
|
109
112
|
source: testCase.source ?? "manual",
|
|
110
113
|
latencyMs: result.latencyMs ?? 0,
|
|
111
114
|
usage: result.usage ?? { input: 0, output: 0 },
|
|
112
|
-
error
|
|
115
|
+
error,
|
|
116
|
+
...(error ? { errorKind: classifyError(error) } : {})
|
|
113
117
|
};
|
|
114
118
|
});
|
|
115
119
|
|
|
116
120
|
const total = rows.length;
|
|
117
|
-
|
|
121
|
+
// An excluded case still runs (so profile / all-in-one views keep their rows);
|
|
122
|
+
// it just does not contribute to this scope's official metrics.
|
|
123
|
+
const excluded = (row) => (scope === "profiles" ? row.excludeFromProfiles : row.excludeFromBase);
|
|
124
|
+
const reviewedRows = rows.filter((row) => row.reviewed && !excluded(row));
|
|
118
125
|
const unreviewedRows = rows.filter((row) => !row.reviewed);
|
|
126
|
+
const excludedRows = rows.filter((row) => row.reviewed && excluded(row));
|
|
119
127
|
const selectionHits = reviewedRows.filter((row) => row.selectionCorrect).length;
|
|
120
128
|
const argFields = reviewedRows.reduce((sum, row) => sum + row.args.total, 0);
|
|
121
129
|
const argHits = reviewedRows.reduce((sum, row) => sum + row.args.matched, 0);
|
|
122
130
|
const passed = reviewedRows.filter((row) => row.passed).length;
|
|
123
|
-
const
|
|
131
|
+
const errorRows = reviewedRows.filter((row) => row.error);
|
|
132
|
+
const errors = errorRows.length;
|
|
133
|
+
const errorsByKind = errorRows.reduce((counts, row) => {
|
|
134
|
+
counts[row.errorKind] = (counts[row.errorKind] ?? 0) + 1;
|
|
135
|
+
return counts;
|
|
136
|
+
}, {});
|
|
137
|
+
const scoredTotal = reviewedRows.length - errors;
|
|
138
|
+
const selectionHitsWithoutErrors = reviewedRows.filter((row) => !row.error && row.selectionCorrect).length;
|
|
124
139
|
const latencies = rows.filter((row) => row.latencyMs > 0).map((row) => row.latencyMs);
|
|
125
140
|
const inputTokens = rows.reduce((sum, row) => sum + (row.usage.input ?? 0), 0);
|
|
126
141
|
const outputTokens = rows.reduce((sum, row) => sum + (row.usage.output ?? 0), 0);
|
|
@@ -135,14 +150,18 @@ export function scoreModel({ model, cases, results, pricing }) {
|
|
|
135
150
|
model,
|
|
136
151
|
total,
|
|
137
152
|
reviewedTotal,
|
|
153
|
+
excludedTotal: excludedRows.length,
|
|
138
154
|
unreviewedTotal: unreviewedRows.length,
|
|
139
155
|
unreviewedSelectionAccuracy: unreviewedRows.length
|
|
140
156
|
? unreviewedRows.filter((row) => row.selectionCorrect).length / unreviewedRows.length
|
|
141
157
|
: null,
|
|
142
158
|
selectionAccuracy: reviewedTotal ? selectionHits / reviewedTotal : 0,
|
|
159
|
+
selectionAccuracyExcludingErrors: scoredTotal ? selectionHitsWithoutErrors / scoredTotal : null,
|
|
143
160
|
argAccuracy: argFields ? argHits / argFields : null,
|
|
144
161
|
passRate: reviewedTotal ? passed / reviewedTotal : 0,
|
|
145
162
|
errors,
|
|
163
|
+
errorsByKind,
|
|
164
|
+
scoredTotal,
|
|
146
165
|
p50LatencyMs: percentile(latencies, 0.5),
|
|
147
166
|
p95LatencyMs: percentile(latencies, 0.95),
|
|
148
167
|
avgCostCny: total ? costCny / total : 0,
|
|
@@ -154,6 +173,15 @@ export function scoreModel({ model, cases, results, pricing }) {
|
|
|
154
173
|
};
|
|
155
174
|
}
|
|
156
175
|
|
|
176
|
+
export function classifyError(error) {
|
|
177
|
+
const text = String(error ?? "");
|
|
178
|
+
if (/timeout|timed out/i.test(text)) return "timeout";
|
|
179
|
+
if (/^HTTP 429\b/.test(text)) return "rate-limit";
|
|
180
|
+
if (/^HTTP \d{3}\b/.test(text)) return "http";
|
|
181
|
+
if (/ECONN|ENOTFOUND|EAI_AGAIN|fetch failed|network/i.test(text)) return "network";
|
|
182
|
+
return "other";
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
function percentile(values, ratio) {
|
|
158
186
|
if (values.length === 0) return 0;
|
|
159
187
|
const sorted = [...values].sort((a, b) => a - b);
|
package/lib/suite.js
CHANGED
|
@@ -95,6 +95,11 @@ function normalizeCase(entry, index, toolNames) {
|
|
|
95
95
|
if (toolInput && expectedTool === null) {
|
|
96
96
|
throw new Error(`case ${id}: toolInput requires an expectedTool`);
|
|
97
97
|
}
|
|
98
|
+
for (const key of ["excludeFromBase", "excludeFromProfiles"]) {
|
|
99
|
+
if (entry[key] !== undefined && typeof entry[key] !== "boolean") {
|
|
100
|
+
throw new Error(`case ${id}: "${key}" must be a boolean`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
98
103
|
return {
|
|
99
104
|
id,
|
|
100
105
|
input: entry.input,
|
|
@@ -103,6 +108,8 @@ function normalizeCase(entry, index, toolNames) {
|
|
|
103
108
|
note: entry.note ?? "",
|
|
104
109
|
source: typeof entry.source === "string" && entry.source ? entry.source : "manual",
|
|
105
110
|
reviewed: entry.reviewed === undefined ? true : Boolean(entry.reviewed),
|
|
111
|
+
...(entry.excludeFromBase ? { excludeFromBase: true } : {}),
|
|
112
|
+
...(entry.excludeFromProfiles ? { excludeFromProfiles: true } : {}),
|
|
106
113
|
tags: Array.isArray(entry.tags) ? entry.tags.map(String) : [],
|
|
107
114
|
toolInput,
|
|
108
115
|
expect:
|
package/mf.js
CHANGED
|
@@ -153,7 +153,8 @@ export async function runEval(options, io = console) {
|
|
|
153
153
|
model: adapter.id,
|
|
154
154
|
cases,
|
|
155
155
|
results: evaluated.results,
|
|
156
|
-
pricing
|
|
156
|
+
pricing,
|
|
157
|
+
scope: "base"
|
|
157
158
|
});
|
|
158
159
|
if (evaluated.stability) summary.stability = evaluated.stability;
|
|
159
160
|
runs.push({ adapter, results: evaluated.results, summary });
|
|
@@ -250,6 +251,15 @@ export async function runEval(options, io = console) {
|
|
|
250
251
|
if (summary.unreviewedTotal > 0) {
|
|
251
252
|
io.log(`note: ${summary.unreviewedTotal} unreviewed case(s) excluded from select/args/pass`);
|
|
252
253
|
}
|
|
254
|
+
if (summary.excludedTotal > 0) {
|
|
255
|
+
io.log(`note: ${summary.excludedTotal} case(s) marked excludeFromBase, excluded from base score`);
|
|
256
|
+
}
|
|
257
|
+
if (summary.errors > 0) {
|
|
258
|
+
const kinds = Object.entries(summary.errorsByKind)
|
|
259
|
+
.map(([kind, count]) => `${kind} ${count}`)
|
|
260
|
+
.join(", ");
|
|
261
|
+
io.log(`note: ${summary.errors} call error(s) counted as misses (${kinds})`);
|
|
262
|
+
}
|
|
253
263
|
if (summary.exec) {
|
|
254
264
|
const parts = [
|
|
255
265
|
`exec: ${summary.exec.passed}/${summary.exec.executed} passed`,
|
|
@@ -915,6 +925,14 @@ async function evaluateProfiles({
|
|
|
915
925
|
const selected = selectProfiles(profiles, selection);
|
|
916
926
|
const indexById = new Map(cases.map((testCase, index) => [testCase.id, index]));
|
|
917
927
|
const warnings = profileWarnings(selected, suite);
|
|
928
|
+
const excludedFromProfiles = cases.filter((testCase) => testCase.excludeFromProfiles === true);
|
|
929
|
+
if (excludedFromProfiles.length > 0) {
|
|
930
|
+
warnings.push(
|
|
931
|
+
`${excludedFromProfiles.length} 条用例标记 excludeFromProfiles,不参与分组评分: ${excludedFromProfiles
|
|
932
|
+
.map((testCase) => testCase.id)
|
|
933
|
+
.join(", ")}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
918
936
|
|
|
919
937
|
const groups = [];
|
|
920
938
|
const caseSets = selected.map((profile) => {
|
|
@@ -946,15 +964,24 @@ async function evaluateProfiles({
|
|
|
946
964
|
model: run.adapter.id,
|
|
947
965
|
cases: profileCases,
|
|
948
966
|
results: mountedEval.results,
|
|
949
|
-
pricing
|
|
967
|
+
pricing,
|
|
968
|
+
scope: "profiles"
|
|
950
969
|
});
|
|
951
970
|
const allInOne = scoreModel({
|
|
952
971
|
model: run.adapter.id,
|
|
953
972
|
cases: profileCases,
|
|
954
973
|
results: profileCases.map((testCase) => run.results[indexById.get(testCase.id)]),
|
|
955
|
-
pricing
|
|
974
|
+
pricing,
|
|
975
|
+
scope: "profiles"
|
|
956
976
|
});
|
|
957
977
|
const entry = analyzeMount(profile, mounted, allInOne);
|
|
978
|
+
entry.usage = {
|
|
979
|
+
inputTokens: mounted.inputTokens,
|
|
980
|
+
cachedTokens: mounted.cachedTokens,
|
|
981
|
+
outputTokens: mounted.outputTokens,
|
|
982
|
+
costCny: mounted.totalCostCny,
|
|
983
|
+
estimated: mounted.rows.some((row) => row.usage?.estimated)
|
|
984
|
+
};
|
|
958
985
|
if (mountedEval.stability) entry.stability = mountedEval.stability;
|
|
959
986
|
entries.push(entry);
|
|
960
987
|
}
|