@struktur/benchmarks 2.6.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/LICENSE +110 -0
- package/README.md +108 -0
- package/dist/cache.d.ts +5 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +961 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/datasets/index.d.ts +5 -0
- package/dist/datasets/index.d.ts.map +1 -0
- package/dist/datasets/longdoc.d.ts +34 -0
- package/dist/datasets/longdoc.d.ts.map +1 -0
- package/dist/datasets/source.d.ts +48 -0
- package/dist/datasets/source.d.ts.map +1 -0
- package/dist/datasets/sroie.d.ts +14 -0
- package/dist/datasets/sroie.d.ts.map +1 -0
- package/dist/datasets/synthetic.d.ts +27 -0
- package/dist/datasets/synthetic.d.ts.map +1 -0
- package/dist/datasets.js +392 -0
- package/dist/datasets.js.map +1 -0
- package/dist/expose/gold-score.d.ts +15 -0
- package/dist/expose/gold-score.d.ts.map +1 -0
- package/dist/expose/gold.d.ts +8 -0
- package/dist/expose/gold.d.ts.map +1 -0
- package/dist/expose/instructions.d.ts +2 -0
- package/dist/expose/instructions.d.ts.map +1 -0
- package/dist/expose/metric.d.ts +7 -0
- package/dist/expose/metric.d.ts.map +1 -0
- package/dist/expose/schema.d.ts +304 -0
- package/dist/expose/schema.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +686 -0
- package/dist/index.js.map +1 -0
- package/dist/pricing.d.ts +15 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/runner.d.ts +81 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/scoring/normalize.d.ts +20 -0
- package/dist/scoring/normalize.d.ts.map +1 -0
- package/dist/scoring/score.d.ts +39 -0
- package/dist/scoring/score.d.ts.map +1 -0
- package/dist/tracks.d.ts +16 -0
- package/dist/tracks.d.ts.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var TRACKS = [
|
|
3
|
+
"text",
|
|
4
|
+
"text+embedded",
|
|
5
|
+
"text+screenshots",
|
|
6
|
+
"text+embedded+screenshots"
|
|
7
|
+
];
|
|
8
|
+
function defineCase(c) {
|
|
9
|
+
return c;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
var BENCHMARK_MODEL = "openrouter/deepseek/deepseek-v4-flash-vision-exp";
|
|
14
|
+
var defaultDatasetDir = () => process.env.STRUKTUR_BENCHMARKS_DIR ?? `${process.env.HOME ?? "."}/.struktur/benchmarks`;
|
|
15
|
+
var defaultResultsDir = () => `${defaultDatasetDir()}/results`;
|
|
16
|
+
|
|
17
|
+
// src/pricing.ts
|
|
18
|
+
var PRICING = {
|
|
19
|
+
// OpenRouter: $0.22/M input, $0.66/M output
|
|
20
|
+
"openrouter/deepseek/deepseek-v4-flash-vision-exp": { input: 0.22, output: 0.66 },
|
|
21
|
+
// OpenAI via OpenRouter: $0.15/M input, $0.60/M output
|
|
22
|
+
"openrouter/openai/gpt-4o-mini": { input: 0.15, output: 0.6 }
|
|
23
|
+
};
|
|
24
|
+
var envOverride = (() => {
|
|
25
|
+
const raw = process.env.STRUKTUR_BENCHMARK_PRICES;
|
|
26
|
+
if (!raw) return void 0;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
var normalizeModel = (model) => model.replace(/#.*$/, "");
|
|
34
|
+
function pricingFor(model) {
|
|
35
|
+
const key = normalizeModel(model);
|
|
36
|
+
return envOverride?.[key] ?? PRICING[key];
|
|
37
|
+
}
|
|
38
|
+
function estimateCostUsd(model, usage) {
|
|
39
|
+
const p = pricingFor(model);
|
|
40
|
+
if (!p) return 0;
|
|
41
|
+
return usage.inputTokens / 1e6 * p.input + usage.outputTokens / 1e6 * p.output;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/tracks.ts
|
|
45
|
+
import { parsePdf } from "@struktur/sdk";
|
|
46
|
+
var trackToPdfOptions = (track) => {
|
|
47
|
+
switch (track) {
|
|
48
|
+
case "text":
|
|
49
|
+
return { includeImages: false, screenshots: false };
|
|
50
|
+
case "text+embedded":
|
|
51
|
+
return { includeImages: true, screenshots: false };
|
|
52
|
+
case "text+screenshots":
|
|
53
|
+
return { includeImages: false, screenshots: true };
|
|
54
|
+
case "text+embedded+screenshots":
|
|
55
|
+
return { includeImages: true, screenshots: true };
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
async function materializeTrack(artifacts, track) {
|
|
59
|
+
return Promise.all(
|
|
60
|
+
artifacts.map(async (artifact) => {
|
|
61
|
+
if (artifact.type !== "pdf") return artifact;
|
|
62
|
+
const buffer = await artifact.raw();
|
|
63
|
+
return parsePdf(buffer, trackToPdfOptions(track));
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/scoring/normalize.ts
|
|
69
|
+
var EMPTY = /* @__PURE__ */ Symbol("struktur.benchmarks.empty");
|
|
70
|
+
var stripDiacritics = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
71
|
+
var normalizeString = (s) => stripDiacritics(s.toLowerCase()).replace(/\s+/g, " ").trim();
|
|
72
|
+
var normalizeAggressive = (s) => stripDiacritics(s.toLowerCase()).replace(/[^a-z0-9]+/g, "");
|
|
73
|
+
function normalizeValue(value, metric) {
|
|
74
|
+
if (value === null || value === void 0) return EMPTY;
|
|
75
|
+
if (metric === "exact") {
|
|
76
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
return JSON.stringify(value);
|
|
80
|
+
}
|
|
81
|
+
if (metric === "semantic") {
|
|
82
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
83
|
+
return normalizeAggressive(typeof value === "string" ? value : JSON.stringify(value));
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
86
|
+
return normalizeString(typeof value === "string" ? value : JSON.stringify(value));
|
|
87
|
+
}
|
|
88
|
+
var RELATIVE_TOLERANCE = 1e-6;
|
|
89
|
+
function valuesEqual(a, b, metric) {
|
|
90
|
+
const na = normalizeValue(a, metric);
|
|
91
|
+
const nb = normalizeValue(b, metric);
|
|
92
|
+
if (na === EMPTY || nb === EMPTY) return na === EMPTY && nb === EMPTY;
|
|
93
|
+
if (metric === "tolerance" && typeof na === "number" && typeof nb === "number") {
|
|
94
|
+
const scale = Math.max(1, Math.abs(na), Math.abs(nb));
|
|
95
|
+
return Math.abs(na - nb) <= RELATIVE_TOLERANCE * scale;
|
|
96
|
+
}
|
|
97
|
+
return na === nb;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/scoring/score.ts
|
|
101
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
102
|
+
"the",
|
|
103
|
+
"a",
|
|
104
|
+
"an",
|
|
105
|
+
"and",
|
|
106
|
+
"or",
|
|
107
|
+
"of",
|
|
108
|
+
"to",
|
|
109
|
+
"in",
|
|
110
|
+
"on",
|
|
111
|
+
"at",
|
|
112
|
+
"is",
|
|
113
|
+
"are",
|
|
114
|
+
"it",
|
|
115
|
+
"this",
|
|
116
|
+
"that",
|
|
117
|
+
"with",
|
|
118
|
+
"for",
|
|
119
|
+
"as",
|
|
120
|
+
"by",
|
|
121
|
+
"from",
|
|
122
|
+
"be",
|
|
123
|
+
"was",
|
|
124
|
+
"were",
|
|
125
|
+
"und",
|
|
126
|
+
"der",
|
|
127
|
+
"die",
|
|
128
|
+
"das",
|
|
129
|
+
"ein",
|
|
130
|
+
"eine",
|
|
131
|
+
"einen",
|
|
132
|
+
"dem",
|
|
133
|
+
"den",
|
|
134
|
+
"mit",
|
|
135
|
+
"von",
|
|
136
|
+
"zur",
|
|
137
|
+
"zum",
|
|
138
|
+
"auf",
|
|
139
|
+
"im",
|
|
140
|
+
"in",
|
|
141
|
+
"am",
|
|
142
|
+
"ist",
|
|
143
|
+
"sind",
|
|
144
|
+
"werden",
|
|
145
|
+
"wird",
|
|
146
|
+
"des",
|
|
147
|
+
"sich",
|
|
148
|
+
"auch",
|
|
149
|
+
"nicht",
|
|
150
|
+
"als",
|
|
151
|
+
"bei",
|
|
152
|
+
"f\xFCr",
|
|
153
|
+
"\xFCber",
|
|
154
|
+
"aus",
|
|
155
|
+
"nach",
|
|
156
|
+
"einer",
|
|
157
|
+
"einem",
|
|
158
|
+
"eine",
|
|
159
|
+
"zu",
|
|
160
|
+
"unter",
|
|
161
|
+
"an",
|
|
162
|
+
"sowie",
|
|
163
|
+
"durch",
|
|
164
|
+
"alle",
|
|
165
|
+
"dass",
|
|
166
|
+
"dieser",
|
|
167
|
+
"diese",
|
|
168
|
+
"hier",
|
|
169
|
+
"sein",
|
|
170
|
+
"ihr",
|
|
171
|
+
"wir",
|
|
172
|
+
"man"
|
|
173
|
+
]);
|
|
174
|
+
var tokenizeProse = (s) => {
|
|
175
|
+
if (typeof s !== "string") return [];
|
|
176
|
+
return Array.from(
|
|
177
|
+
new Set(
|
|
178
|
+
s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length >= 3 && !STOPWORDS.has(w))
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
function scoreProse(gold, pred) {
|
|
183
|
+
const s = empty();
|
|
184
|
+
const gt = tokenizeProse(String(gold ?? ""));
|
|
185
|
+
const pt = tokenizeProse(String(pred ?? ""));
|
|
186
|
+
const gs = new Set(gt);
|
|
187
|
+
const ps = new Set(pt);
|
|
188
|
+
const tp = gt.filter((w) => ps.has(w)).length;
|
|
189
|
+
const fp = pt.filter((w) => !gs.has(w)).length;
|
|
190
|
+
const fn = gt.filter((w) => !ps.has(w)).length;
|
|
191
|
+
s.tp = tp;
|
|
192
|
+
s.fp = fp;
|
|
193
|
+
s.fn = fn;
|
|
194
|
+
s.totalGold = gt.length;
|
|
195
|
+
s.totalPred = pt.length;
|
|
196
|
+
return finalize(s);
|
|
197
|
+
}
|
|
198
|
+
var empty = () => ({
|
|
199
|
+
tp: 0,
|
|
200
|
+
fp: 0,
|
|
201
|
+
fn: 0,
|
|
202
|
+
totalGold: 0,
|
|
203
|
+
totalPred: 0,
|
|
204
|
+
precision: 1,
|
|
205
|
+
recall: 1,
|
|
206
|
+
f1: 1,
|
|
207
|
+
exactMatch: true,
|
|
208
|
+
fieldErrors: []
|
|
209
|
+
});
|
|
210
|
+
var finalize = (s) => {
|
|
211
|
+
s.precision = s.tp + s.fp === 0 ? 1 : s.tp / (s.tp + s.fp);
|
|
212
|
+
s.recall = s.tp + s.fn === 0 ? 1 : s.tp / (s.tp + s.fn);
|
|
213
|
+
s.f1 = s.precision + s.recall === 0 ? 0 : 2 * s.precision * s.recall / (s.precision + s.recall);
|
|
214
|
+
s.exactMatch = s.fp === 0 && s.fn === 0;
|
|
215
|
+
return s;
|
|
216
|
+
};
|
|
217
|
+
var merge = (into, child) => {
|
|
218
|
+
into.tp += child.tp;
|
|
219
|
+
into.fp += child.fp;
|
|
220
|
+
into.fn += child.fn;
|
|
221
|
+
into.totalGold += child.totalGold;
|
|
222
|
+
into.totalPred += child.totalPred;
|
|
223
|
+
into.fieldErrors.push(...child.fieldErrors);
|
|
224
|
+
};
|
|
225
|
+
var isLeaf = (v) => v === null || v === void 0 || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
226
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
227
|
+
var leafCount = (v) => {
|
|
228
|
+
if (Array.isArray(v)) return v.reduce((n, item) => n + leafCount(item), 0);
|
|
229
|
+
if (isObject(v)) {
|
|
230
|
+
return Object.values(v).reduce((n, item) => n + leafCount(item), 0);
|
|
231
|
+
}
|
|
232
|
+
return 1;
|
|
233
|
+
};
|
|
234
|
+
var canonPath = (path3) => path3.replace(/\[[^\]]*\]/g, "[]");
|
|
235
|
+
var metricFor = (spec, path3) => (
|
|
236
|
+
// field overrides are keyed by the leaf field name (e.g. "area"), robust to
|
|
237
|
+
// concrete array keys in the path.
|
|
238
|
+
spec.fields?.[path3.split(".").pop()] ?? spec.default ?? "normalized"
|
|
239
|
+
);
|
|
240
|
+
var alignmentFor = (spec, path3) => spec.arrays?.find((a) => canonPath(a.path) === canonPath(path3)) ?? spec.arrays?.find((a) => canonPath(path3).endsWith(canonPath(a.path)));
|
|
241
|
+
var isSetPath = (spec, path3) => (spec.sets ?? []).some(
|
|
242
|
+
(s) => canonPath(s) === canonPath(path3) || canonPath(path3).endsWith(canonPath(s))
|
|
243
|
+
);
|
|
244
|
+
function groupByKey(items, key) {
|
|
245
|
+
const map = /* @__PURE__ */ new Map();
|
|
246
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
247
|
+
for (const item of items) {
|
|
248
|
+
const k = keys.map((kf) => isObject(item) ? item[kf] : void 0).map((v) => String(normalizeValue(v, "normalized"))).join("|");
|
|
249
|
+
const arr = map.get(k) ?? [];
|
|
250
|
+
arr.push(item);
|
|
251
|
+
map.set(k, arr);
|
|
252
|
+
}
|
|
253
|
+
return map;
|
|
254
|
+
}
|
|
255
|
+
function scoreLeaf(path3, gold, pred, spec) {
|
|
256
|
+
const metric = metricFor(spec, path3);
|
|
257
|
+
if (metric === "prose" && (typeof gold === "string" || typeof pred === "string")) {
|
|
258
|
+
return scoreProse(gold, pred);
|
|
259
|
+
}
|
|
260
|
+
const s = empty();
|
|
261
|
+
s.totalGold = isLeaf(gold) ? 1 : leafCount(gold);
|
|
262
|
+
s.totalPred = isLeaf(pred) ? 1 : leafCount(pred);
|
|
263
|
+
if (valuesEqual(gold, pred, metric)) {
|
|
264
|
+
s.tp = isLeaf(gold) ? 1 : leafCount(gold);
|
|
265
|
+
return finalize(s);
|
|
266
|
+
}
|
|
267
|
+
const goldEmpty = gold === null || gold === void 0;
|
|
268
|
+
const predEmpty = pred === null || pred === void 0;
|
|
269
|
+
if (goldEmpty) s.fp = s.totalPred;
|
|
270
|
+
else if (predEmpty) s.fn = s.totalGold;
|
|
271
|
+
else {
|
|
272
|
+
s.fn = s.totalGold;
|
|
273
|
+
s.fp = s.totalPred;
|
|
274
|
+
}
|
|
275
|
+
s.fieldErrors.push({ path: path3, gold, pred, metric });
|
|
276
|
+
return finalize(s);
|
|
277
|
+
}
|
|
278
|
+
function scoreIndexed(path3, g, p, spec) {
|
|
279
|
+
const s = empty();
|
|
280
|
+
const len = Math.max(g.length, p.length);
|
|
281
|
+
for (let i = 0; i < len; i++) {
|
|
282
|
+
merge(s, scoreAt(`${path3}[${i}]`, g[i] ?? null, p[i] ?? null, spec));
|
|
283
|
+
}
|
|
284
|
+
return finalize(s);
|
|
285
|
+
}
|
|
286
|
+
function scoreAligned(path3, g, p, key, spec) {
|
|
287
|
+
const s = empty();
|
|
288
|
+
const gm = groupByKey(g, key);
|
|
289
|
+
const pm = groupByKey(p, key);
|
|
290
|
+
let matchedAny = false;
|
|
291
|
+
for (const [k, gItems] of gm) {
|
|
292
|
+
const pItems = pm.get(k);
|
|
293
|
+
if (!pItems || pItems.length === 0) {
|
|
294
|
+
for (const gi of gItems) {
|
|
295
|
+
s.fn += leafCount(gi);
|
|
296
|
+
s.totalGold += leafCount(gi);
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
matchedAny = true;
|
|
301
|
+
const pairs = Math.min(gItems.length, pItems.length);
|
|
302
|
+
for (let i = 0; i < pairs; i++) {
|
|
303
|
+
merge(s, scoreAt(`${path3}[${String(k)}]`, gItems[i], pItems[i], spec));
|
|
304
|
+
}
|
|
305
|
+
for (let i = pairs; i < gItems.length; i++) {
|
|
306
|
+
s.fn += leafCount(gItems[i]);
|
|
307
|
+
s.totalGold += leafCount(gItems[i]);
|
|
308
|
+
}
|
|
309
|
+
for (let i = pairs; i < pItems.length; i++) {
|
|
310
|
+
s.fp += leafCount(pItems[i]);
|
|
311
|
+
s.totalPred += leafCount(pItems[i]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const [k, pItems] of pm) {
|
|
315
|
+
if (gm.has(k)) continue;
|
|
316
|
+
for (const pi of pItems) {
|
|
317
|
+
s.fp += leafCount(pi);
|
|
318
|
+
s.totalPred += leafCount(pi);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!matchedAny && g.length > 0 && p.length > 0) {
|
|
322
|
+
return scoreIndexed(path3, g, p, spec);
|
|
323
|
+
}
|
|
324
|
+
return finalize(s);
|
|
325
|
+
}
|
|
326
|
+
function scoreSet(path3, g, p, spec) {
|
|
327
|
+
const s = empty();
|
|
328
|
+
const goldKeyed = /* @__PURE__ */ new Map();
|
|
329
|
+
const predKeyed = /* @__PURE__ */ new Map();
|
|
330
|
+
const metric = metricFor(spec, canonPath(path3));
|
|
331
|
+
const bump = (map, v) => {
|
|
332
|
+
const k = String(normalizeValue(v, metric));
|
|
333
|
+
map.set(k, (map.get(k) ?? 0) + 1);
|
|
334
|
+
};
|
|
335
|
+
for (const v of g) bump(goldKeyed, v);
|
|
336
|
+
for (const v of p) bump(predKeyed, v);
|
|
337
|
+
s.totalGold = g.length;
|
|
338
|
+
s.totalPred = p.length;
|
|
339
|
+
for (const [k, gc] of goldKeyed) {
|
|
340
|
+
const pc = predKeyed.get(k) ?? 0;
|
|
341
|
+
const common = Math.min(gc, pc);
|
|
342
|
+
s.tp += common;
|
|
343
|
+
s.fn += gc - common;
|
|
344
|
+
s.fp += pc - common;
|
|
345
|
+
if (common < gc) {
|
|
346
|
+
s.fieldErrors.push({
|
|
347
|
+
path: path3,
|
|
348
|
+
gold: g.find((v) => String(normalizeValue(v, metric)) === k),
|
|
349
|
+
pred: null,
|
|
350
|
+
metric
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
for (const [k, pc] of predKeyed) {
|
|
355
|
+
const gc = goldKeyed.get(k) ?? 0;
|
|
356
|
+
if (pc > gc) {
|
|
357
|
+
s.fieldErrors.push({
|
|
358
|
+
path: path3,
|
|
359
|
+
gold: null,
|
|
360
|
+
pred: p.find((v) => String(normalizeValue(v, metric)) === k),
|
|
361
|
+
metric
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return finalize(s);
|
|
366
|
+
}
|
|
367
|
+
function scoreAt(path3, gold, pred, spec) {
|
|
368
|
+
if (isLeaf(gold) || isLeaf(pred)) {
|
|
369
|
+
return scoreLeaf(path3, gold, pred, spec);
|
|
370
|
+
}
|
|
371
|
+
if (Array.isArray(gold) || Array.isArray(pred)) {
|
|
372
|
+
const g = Array.isArray(gold) ? gold : [gold];
|
|
373
|
+
const p = Array.isArray(pred) ? pred : [pred];
|
|
374
|
+
if (isSetPath(spec, path3)) {
|
|
375
|
+
return scoreSet(path3, g, p, spec);
|
|
376
|
+
}
|
|
377
|
+
const align = alignmentFor(spec, path3);
|
|
378
|
+
return align ? scoreAligned(path3, g, p, align.key, spec) : scoreIndexed(path3, g, p, spec);
|
|
379
|
+
}
|
|
380
|
+
const s = empty();
|
|
381
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(gold), ...Object.keys(pred)]);
|
|
382
|
+
for (const key of keys) {
|
|
383
|
+
const childPath = path3 === "" ? key : `${path3}.${key}`;
|
|
384
|
+
const gv = gold[key] ?? null;
|
|
385
|
+
const pv = pred[key] ?? null;
|
|
386
|
+
merge(s, scoreAt(childPath, gv, pv, spec));
|
|
387
|
+
}
|
|
388
|
+
return finalize(s);
|
|
389
|
+
}
|
|
390
|
+
function scoreData(gold, pred, spec = {}) {
|
|
391
|
+
return scoreAt("", gold, pred, spec);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/runner.ts
|
|
395
|
+
import { extract, resolveModel, toJsonSchema } from "@struktur/sdk";
|
|
396
|
+
import {
|
|
397
|
+
simple,
|
|
398
|
+
parallel,
|
|
399
|
+
sequential,
|
|
400
|
+
parallelAutoMerge,
|
|
401
|
+
sequentialAutoMerge,
|
|
402
|
+
doublePass,
|
|
403
|
+
doublePassAutoMerge,
|
|
404
|
+
agent
|
|
405
|
+
} from "@struktur/sdk";
|
|
406
|
+
|
|
407
|
+
// src/cache.ts
|
|
408
|
+
import { createHash } from "crypto";
|
|
409
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
410
|
+
import path from "path";
|
|
411
|
+
var stableStringify = (v) => {
|
|
412
|
+
if (v === null) return "null";
|
|
413
|
+
if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
|
|
414
|
+
if (typeof v === "object") {
|
|
415
|
+
const rec = v;
|
|
416
|
+
const keys = Object.keys(rec).sort();
|
|
417
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(rec[k])}`).join(",")}}`;
|
|
418
|
+
}
|
|
419
|
+
return JSON.stringify(v) ?? String(v);
|
|
420
|
+
};
|
|
421
|
+
var hashKey = (input) => createHash("sha256").update(stableStringify(input)).digest("hex").slice(0, 16);
|
|
422
|
+
async function cacheGet(dir, key) {
|
|
423
|
+
try {
|
|
424
|
+
const raw = await readFile(path.join(dir, `${key}.json`), "utf8");
|
|
425
|
+
return JSON.parse(raw);
|
|
426
|
+
} catch {
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function cacheSet(dir, key, value) {
|
|
431
|
+
const file = path.join(dir, `${key}.json`);
|
|
432
|
+
await mkdir(dir, { recursive: true });
|
|
433
|
+
const tmp = `${file}.tmp`;
|
|
434
|
+
await writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
|
|
435
|
+
await rename(tmp, file);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/runner.ts
|
|
439
|
+
var builtins = {
|
|
440
|
+
simple: (model, _spec, instructions) => simple({ model, outputInstructions: instructions }),
|
|
441
|
+
parallel: (model, _spec, instructions) => parallel({ model, mergeModel: model, chunkSize: 1e4, outputInstructions: instructions }),
|
|
442
|
+
sequential: (model, _spec, instructions) => sequential({ model, chunkSize: 1e4, outputInstructions: instructions }),
|
|
443
|
+
parallelAutoMerge: (model, _spec, instructions) => parallelAutoMerge({
|
|
444
|
+
model,
|
|
445
|
+
dedupeModel: model,
|
|
446
|
+
chunkSize: 1e4,
|
|
447
|
+
outputInstructions: instructions
|
|
448
|
+
}),
|
|
449
|
+
sequentialAutoMerge: (model, _spec, instructions) => sequentialAutoMerge({
|
|
450
|
+
model,
|
|
451
|
+
dedupeModel: model,
|
|
452
|
+
chunkSize: 1e4,
|
|
453
|
+
outputInstructions: instructions
|
|
454
|
+
}),
|
|
455
|
+
doublePass: (model, _spec, instructions) => doublePass({ model, mergeModel: model, chunkSize: 1e4, outputInstructions: instructions }),
|
|
456
|
+
doublePassAutoMerge: (model, _spec, instructions) => doublePassAutoMerge({
|
|
457
|
+
model,
|
|
458
|
+
dedupeModel: model,
|
|
459
|
+
chunkSize: 1e4,
|
|
460
|
+
outputInstructions: instructions
|
|
461
|
+
}),
|
|
462
|
+
agent: (_model, modelSpec, instructions) => {
|
|
463
|
+
const [provider, ...rest] = modelSpec.split("/");
|
|
464
|
+
const modelId = rest.join("/");
|
|
465
|
+
if (!provider || !modelId)
|
|
466
|
+
throw new Error(`Agent requires 'provider/model'. Got: ${modelSpec}`);
|
|
467
|
+
return agent({
|
|
468
|
+
provider,
|
|
469
|
+
modelId,
|
|
470
|
+
maxSteps: 50,
|
|
471
|
+
maxIterations: 1,
|
|
472
|
+
vision: true,
|
|
473
|
+
outputInstructions: instructions
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
function resolveStrategy(entry, model, modelSpec) {
|
|
478
|
+
if (typeof entry === "string") {
|
|
479
|
+
const factory = builtins[entry];
|
|
480
|
+
if (!factory) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
`Unknown builtin strategy: ${entry}. Available: ${Object.keys(builtins).join(", ")}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return { name: entry, strategy: factory(model, modelSpec) };
|
|
486
|
+
}
|
|
487
|
+
if (typeof entry === "function") {
|
|
488
|
+
const s = entry(model, modelSpec);
|
|
489
|
+
return { name: s.name, strategy: s };
|
|
490
|
+
}
|
|
491
|
+
if ("strategy" in entry) {
|
|
492
|
+
const inner = entry.strategy;
|
|
493
|
+
if (typeof inner === "string") {
|
|
494
|
+
const factory = builtins[inner];
|
|
495
|
+
if (!factory) throw new Error(`Unknown builtin strategy: ${inner}`);
|
|
496
|
+
return {
|
|
497
|
+
name: entry.label ?? inner,
|
|
498
|
+
strategy: factory(model, modelSpec, entry.instructions)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const s = inner(model, modelSpec, entry.instructions);
|
|
502
|
+
return { name: entry.label ?? s.name, strategy: s };
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
name: entry.name,
|
|
506
|
+
strategy: entry
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
var emptyUsage = () => ({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
|
|
510
|
+
async function runBenchmark(options) {
|
|
511
|
+
const modelSpec = options.model ?? BENCHMARK_MODEL;
|
|
512
|
+
const model = await resolveModel(modelSpec);
|
|
513
|
+
const cacheDir = options.cacheDir ?? defaultResultsDir();
|
|
514
|
+
const useCache = options.cache ?? true;
|
|
515
|
+
const variant = options.variant ?? "";
|
|
516
|
+
const strats = options.strategies.map((s) => resolveStrategy(s, model, modelSpec));
|
|
517
|
+
const cells = [];
|
|
518
|
+
for (const c of options.cases) {
|
|
519
|
+
const tracks = c.tracks ?? [...TRACKS];
|
|
520
|
+
for (const track of tracks) {
|
|
521
|
+
for (const { name, strategy } of strats) {
|
|
522
|
+
const cacheKey = hashKey({
|
|
523
|
+
caseId: c.id,
|
|
524
|
+
track,
|
|
525
|
+
strategy: name,
|
|
526
|
+
model: modelSpec,
|
|
527
|
+
variant,
|
|
528
|
+
schema: toJsonSchema(c.schema),
|
|
529
|
+
gold: c.gold
|
|
530
|
+
});
|
|
531
|
+
const cached = useCache ? await cacheGet(cacheDir, cacheKey) : void 0;
|
|
532
|
+
if (cached) {
|
|
533
|
+
cached.cached = true;
|
|
534
|
+
if (cached.costUsd === void 0)
|
|
535
|
+
cached.costUsd = estimateCostUsd(modelSpec, cached.usage);
|
|
536
|
+
cells.push(cached);
|
|
537
|
+
options.onCell?.({ caseId: c.id, track, strategy: name, cached: true });
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
options.onCell?.({ caseId: c.id, track, strategy: name, cached: false });
|
|
541
|
+
const artifacts = c.artifactsByTrack?.[track] ?? await materializeTrack(c.artifacts, track);
|
|
542
|
+
const start = Date.now();
|
|
543
|
+
let data;
|
|
544
|
+
let usage = emptyUsage();
|
|
545
|
+
let valid = true;
|
|
546
|
+
let error;
|
|
547
|
+
try {
|
|
548
|
+
const result = await extract({ artifacts, schema: c.schema, strategy });
|
|
549
|
+
data = result.data;
|
|
550
|
+
usage = result.usage;
|
|
551
|
+
if (result.error) {
|
|
552
|
+
valid = false;
|
|
553
|
+
error = result.error.message;
|
|
554
|
+
}
|
|
555
|
+
} catch (e) {
|
|
556
|
+
valid = false;
|
|
557
|
+
error = e.message;
|
|
558
|
+
data = null;
|
|
559
|
+
}
|
|
560
|
+
const cell = {
|
|
561
|
+
caseId: c.id,
|
|
562
|
+
track,
|
|
563
|
+
strategy: name,
|
|
564
|
+
model: modelSpec,
|
|
565
|
+
cached: false,
|
|
566
|
+
valid,
|
|
567
|
+
error,
|
|
568
|
+
score: scoreData(c.gold, c.transform ? c.transform(data) : data, c.metrics),
|
|
569
|
+
usage,
|
|
570
|
+
latencyMs: Date.now() - start,
|
|
571
|
+
costUsd: estimateCostUsd(modelSpec, usage),
|
|
572
|
+
data
|
|
573
|
+
};
|
|
574
|
+
if (useCache) await cacheSet(cacheDir, cacheKey, cell);
|
|
575
|
+
cells.push(cell);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return buildReport(modelSpec, cells, variant);
|
|
580
|
+
}
|
|
581
|
+
function buildReport(model, cells, variant) {
|
|
582
|
+
const summary = /* @__PURE__ */ new Map();
|
|
583
|
+
for (const cell of cells) {
|
|
584
|
+
const key = `${cell.strategy}|${cell.track}`;
|
|
585
|
+
let row = summary.get(key);
|
|
586
|
+
if (!row) {
|
|
587
|
+
row = {
|
|
588
|
+
strategy: cell.strategy,
|
|
589
|
+
track: cell.track,
|
|
590
|
+
cases: 0,
|
|
591
|
+
meanF1: 0,
|
|
592
|
+
meanPrecision: 0,
|
|
593
|
+
meanRecall: 0,
|
|
594
|
+
validityRate: 0,
|
|
595
|
+
exactMatchRate: 0,
|
|
596
|
+
totalInputTokens: 0,
|
|
597
|
+
totalOutputTokens: 0,
|
|
598
|
+
totalCostUsd: 0,
|
|
599
|
+
meanCostUsd: 0,
|
|
600
|
+
totalLatencyMs: 0,
|
|
601
|
+
meanLatencyMs: 0
|
|
602
|
+
};
|
|
603
|
+
summary.set(key, row);
|
|
604
|
+
}
|
|
605
|
+
row.cases++;
|
|
606
|
+
row.meanF1 += cell.score.f1;
|
|
607
|
+
row.meanPrecision += cell.score.precision;
|
|
608
|
+
row.meanRecall += cell.score.recall;
|
|
609
|
+
if (cell.valid) row.validityRate++;
|
|
610
|
+
if (cell.score.exactMatch) row.exactMatchRate++;
|
|
611
|
+
row.totalInputTokens += cell.usage.inputTokens;
|
|
612
|
+
row.totalOutputTokens += cell.usage.outputTokens;
|
|
613
|
+
row.totalCostUsd += cell.costUsd ?? estimateCostUsd(model, cell.usage);
|
|
614
|
+
row.totalLatencyMs += cell.latencyMs;
|
|
615
|
+
row.meanLatencyMs += cell.latencyMs;
|
|
616
|
+
}
|
|
617
|
+
for (const row of summary.values()) {
|
|
618
|
+
row.meanF1 /= row.cases;
|
|
619
|
+
row.meanPrecision /= row.cases;
|
|
620
|
+
row.meanRecall /= row.cases;
|
|
621
|
+
row.validityRate /= row.cases;
|
|
622
|
+
row.exactMatchRate /= row.cases;
|
|
623
|
+
row.meanCostUsd = row.cases ? row.totalCostUsd / row.cases : 0;
|
|
624
|
+
row.meanLatencyMs = row.cases ? row.meanLatencyMs / row.cases : 0;
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
model,
|
|
628
|
+
variant,
|
|
629
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
630
|
+
cells,
|
|
631
|
+
summary: [...summary.values()]
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/report.ts
|
|
636
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
637
|
+
import path2 from "path";
|
|
638
|
+
var pct = (v) => `${(v * 100).toFixed(1)}%`;
|
|
639
|
+
function toMarkdownTable(report) {
|
|
640
|
+
const lines = [];
|
|
641
|
+
lines.push(`# Benchmark report`);
|
|
642
|
+
lines.push(``);
|
|
643
|
+
lines.push(`- model: \`${report.model}\``);
|
|
644
|
+
if (report.variant) lines.push(`- variant: \`${report.variant}\``);
|
|
645
|
+
lines.push(`- generated: ${report.generatedAt}`);
|
|
646
|
+
lines.push(``);
|
|
647
|
+
lines.push(
|
|
648
|
+
`| strategy | track | cases | F1 | precision | recall | valid | exact | in tok | out tok | cost (USD) | ms/case | total ms |`
|
|
649
|
+
);
|
|
650
|
+
lines.push(`|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|`);
|
|
651
|
+
for (const row of report.summary) {
|
|
652
|
+
lines.push(
|
|
653
|
+
`| ${row.strategy} | ${row.track} | ${row.cases} | ${pct(row.meanF1)} | ${pct(row.meanPrecision)} | ${pct(row.meanRecall)} | ${pct(row.validityRate)} | ${pct(row.exactMatchRate)} | ${row.totalInputTokens} | ${row.totalOutputTokens} | ${row.totalCostUsd.toFixed(4)} | ${Math.round(row.meanLatencyMs)} | ${row.totalLatencyMs} |`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return lines.join("\n");
|
|
657
|
+
}
|
|
658
|
+
async function saveReport(report, file) {
|
|
659
|
+
await mkdir2(path2.dirname(file), { recursive: true });
|
|
660
|
+
await writeFile2(file, JSON.stringify(report, null, 2), "utf8");
|
|
661
|
+
const mdFile = file.replace(/\.json$/, "") + ".md";
|
|
662
|
+
await writeFile2(mdFile, toMarkdownTable(report), "utf8");
|
|
663
|
+
}
|
|
664
|
+
export {
|
|
665
|
+
BENCHMARK_MODEL,
|
|
666
|
+
EMPTY,
|
|
667
|
+
TRACKS,
|
|
668
|
+
buildReport,
|
|
669
|
+
defaultDatasetDir,
|
|
670
|
+
defaultResultsDir,
|
|
671
|
+
defineCase,
|
|
672
|
+
estimateCostUsd,
|
|
673
|
+
leafCount,
|
|
674
|
+
materializeTrack,
|
|
675
|
+
normalizeAggressive,
|
|
676
|
+
normalizeString,
|
|
677
|
+
normalizeValue,
|
|
678
|
+
pricingFor,
|
|
679
|
+
runBenchmark,
|
|
680
|
+
saveReport,
|
|
681
|
+
scoreData,
|
|
682
|
+
toMarkdownTable,
|
|
683
|
+
trackToPdfOptions,
|
|
684
|
+
valuesEqual
|
|
685
|
+
};
|
|
686
|
+
//# sourceMappingURL=index.js.map
|