@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.
Files changed (51) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +108 -0
  3. package/dist/cache.d.ts +5 -0
  4. package/dist/cache.d.ts.map +1 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +961 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/config.d.ts +13 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/datasets/index.d.ts +5 -0
  12. package/dist/datasets/index.d.ts.map +1 -0
  13. package/dist/datasets/longdoc.d.ts +34 -0
  14. package/dist/datasets/longdoc.d.ts.map +1 -0
  15. package/dist/datasets/source.d.ts +48 -0
  16. package/dist/datasets/source.d.ts.map +1 -0
  17. package/dist/datasets/sroie.d.ts +14 -0
  18. package/dist/datasets/sroie.d.ts.map +1 -0
  19. package/dist/datasets/synthetic.d.ts +27 -0
  20. package/dist/datasets/synthetic.d.ts.map +1 -0
  21. package/dist/datasets.js +392 -0
  22. package/dist/datasets.js.map +1 -0
  23. package/dist/expose/gold-score.d.ts +15 -0
  24. package/dist/expose/gold-score.d.ts.map +1 -0
  25. package/dist/expose/gold.d.ts +8 -0
  26. package/dist/expose/gold.d.ts.map +1 -0
  27. package/dist/expose/instructions.d.ts +2 -0
  28. package/dist/expose/instructions.d.ts.map +1 -0
  29. package/dist/expose/metric.d.ts +7 -0
  30. package/dist/expose/metric.d.ts.map +1 -0
  31. package/dist/expose/schema.d.ts +304 -0
  32. package/dist/expose/schema.d.ts.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +686 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/pricing.d.ts +15 -0
  38. package/dist/pricing.d.ts.map +1 -0
  39. package/dist/report.d.ts +6 -0
  40. package/dist/report.d.ts.map +1 -0
  41. package/dist/runner.d.ts +81 -0
  42. package/dist/runner.d.ts.map +1 -0
  43. package/dist/scoring/normalize.d.ts +20 -0
  44. package/dist/scoring/normalize.d.ts.map +1 -0
  45. package/dist/scoring/score.d.ts +39 -0
  46. package/dist/scoring/score.d.ts.map +1 -0
  47. package/dist/tracks.d.ts +16 -0
  48. package/dist/tracks.d.ts.map +1 -0
  49. package/dist/types.d.ts +88 -0
  50. package/dist/types.d.ts.map +1 -0
  51. 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