@theokit/sdk 4.4.1 → 4.5.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/CHANGELOG.md +15 -0
- package/claude-template/dot-claude/skills/theokit-eval/SKILL.md +35 -0
- package/dist/eval.cjs +289 -11
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.d.cts +1 -0
- package/dist/eval.d.ts +1 -0
- package/dist/eval.js +288 -12
- package/dist/eval.js.map +1 -1
- package/dist/interactive/index.d.cts +12 -0
- package/dist/interactive/types.d.cts +85 -0
- package/dist/internal/eval/assert.d.ts +26 -0
- package/dist/internal/eval/trials.d.ts +19 -0
- package/dist/internal/scorers/levenshtein.d.ts +11 -0
- package/dist/sandbox/index.cjs +4 -0
- package/dist/sandbox/index.cjs.map +1 -1
- package/dist/sandbox/index.d.cts +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +4 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/types.d.cts +12 -0
- package/dist/sandbox/types.d.ts +12 -0
- package/dist/scorers.d.ts +57 -0
- package/dist/types/eval.d.ts +45 -0
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 283dca0: feat(eval): eval-as-CI-test primitives (SE41). Adds the pieces that turn `@theokit/sdk/eval` into a regression gate you can drop into a pipeline:
|
|
8
|
+
|
|
9
|
+
- **`assertEval(run, thresholds)`** — a pure gate over an `EvalRun` that throws `EvalThresholdError` (carrying the full list of unmet thresholds) when a run misses `minMeanScore`, `minPassRatio`, `maxErrorRatio`, or any `perScorer` floor. Passing returns `void`, so it drops straight into a Vitest `it(...)` or a standalone eval script whose non-zero exit fails CI.
|
|
10
|
+
- **Three new scorers** — `Scorers.levenshtein()` (normalized edit-distance similarity, deterministic), `Scorers.numericDiff()` (relative numeric closeness, deterministic), and `Scorers.embeddingSimilarity()` (cosine of output vs expected embeddings via OpenRouter, or an injected `embed` for other providers/tests). The two deterministic scorers always run in CI with zero token spend.
|
|
11
|
+
- **`EvalOptions.trials`** — repeat each dataset row N times and collapse to one row whose per-scorer score is the mean over the trials (an errored trial contributes 0), smoothing single-model non-determinism. `EvalRowResult.trialCount` records the collapse.
|
|
12
|
+
- A `pnpm eval` script + an OpenRouter-gated `eval` CI workflow run the new `tests/eval/suites/**` eval suites; the deterministic gate also runs on every `pnpm test`.
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 8932068: fix(build): emit `@theokit/sdk/interactive` CJS type declarations (`dist/interactive/index.d.cts`). The subpath was added to `tsconfig.tools-dts.json` (so `.d.ts` shipped) but omitted from `scripts/mirror-dts-to-cts.mjs`, so `exports["./interactive"].require.types` pointed at a file that was never generated — `publint` and `arethetypeswrong` both flagged it ("No types" from CJS). A CJS `require("@theokit/sdk/interactive")` now resolves its types. Added `dist/interactive` to the mirror target list with a note about the drift trap.
|
|
17
|
+
|
|
3
18
|
## 4.2.10
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
@@ -50,6 +50,10 @@ console.log(run.aggregate.durationMsP95); // 1830
|
|
|
50
50
|
| `Scorers.regex(pattern)` | `pattern.test(output)` — test patterns against adversarial output to avoid ReDoS |
|
|
51
51
|
| `Scorers.jsonShape(zodSchema, { strict? })` | `JSON.parse(output)` + Zod validation — caps output at 1 MB before parse |
|
|
52
52
|
| `Scorers.llmJudge({ model, apiKey, criteria, rubric? })` | Second LLM scores against criteria — requires SEPARATE `apiKey` |
|
|
53
|
+
| `Scorers.levenshtein({ threshold?, caseSensitive? })` | Normalized edit-distance similarity `1 - dist/max(len)` — deterministic (no LLM); `threshold` binarizes |
|
|
54
|
+
| `Scorers.numericDiff({ tolerance? })` | Relative numeric closeness `1 - |o-e|/max(|o|,|e|)` — deterministic; `tolerance` binarizes |
|
|
55
|
+
| `Scorers.embeddingSimilarity({ apiKey?, model?, threshold?, embed? })` | Cosine of output vs expected embeddings (OpenRouter by default; inject `embed` for another provider). One embeddings call per row |
|
|
56
|
+
| `Scorers.verifyGate({ sandbox?, repoDir, failToPass, passToPass, command })` | Runs the project's tests in a sandbox; scores 1 iff exit 0 (SWE-bench style) |
|
|
53
57
|
|
|
54
58
|
### Custom scorer
|
|
55
59
|
|
|
@@ -105,6 +109,36 @@ interface EvalAggregate {
|
|
|
105
109
|
|
|
106
110
|
`EvalRun` is plain JSON — `JSON.stringify(run)` works directly.
|
|
107
111
|
|
|
112
|
+
## CI gate — `assertEval`
|
|
113
|
+
|
|
114
|
+
Turn a run into a pass/fail gate. `assertEval` reads only `run.aggregate`,
|
|
115
|
+
collects EVERY unmet threshold, and throws `EvalThresholdError` (with a
|
|
116
|
+
`.failures` list) when any is missed — otherwise returns `void`. Drop it into a
|
|
117
|
+
Vitest `it(...)` or a standalone script whose non-zero exit fails the CI job.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { Eval, Scorers, assertEval, EvalThresholdError } from "@theokit/sdk/eval";
|
|
121
|
+
|
|
122
|
+
const run = await Eval.create({ name: "qa", dataset, scorers, agent }).run();
|
|
123
|
+
|
|
124
|
+
assertEval(run, {
|
|
125
|
+
minMeanScore: 0.8, // aggregate.meanScore >= 0.8
|
|
126
|
+
minPassRatio: 0.9, // aggregate.passRatio >= 0.9
|
|
127
|
+
maxErrorRatio: 0, // errorRows / totalRows <= 0
|
|
128
|
+
perScorer: { "contains-expected": 0.7 }, // per-scorer mean floor (absent scorer = failure)
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Trials (smooth non-determinism)
|
|
133
|
+
|
|
134
|
+
`trials: N` runs each dataset row N times and collapses to ONE row whose
|
|
135
|
+
per-scorer score is the mean over the trials (an errored trial contributes 0).
|
|
136
|
+
`EvalRowResult.trialCount` records the collapse. Range `[1, 100]`.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const run = await Eval.create({ name: "qa", dataset, scorers, agent, trials: 3 }).run();
|
|
140
|
+
```
|
|
141
|
+
|
|
108
142
|
## Concurrency
|
|
109
143
|
|
|
110
144
|
`concurrency` defaults to 4. Allowed range: `[1, 64]` (integer). 0 and
|
|
@@ -141,4 +175,5 @@ costs roughly $3.00 total (base + judge).
|
|
|
141
175
|
| Error | When |
|
|
142
176
|
|---|---|
|
|
143
177
|
| `EvalAlreadyRunningError` | Same `name` already running in this process |
|
|
178
|
+
| `EvalThresholdError` | `assertEval` found one or more unmet thresholds (see `.failures`) |
|
|
144
179
|
| `ConfigurationError` | Invalid concurrency, missing required fields |
|
package/dist/eval.cjs
CHANGED
|
@@ -7256,11 +7256,11 @@ function truncateToBudget(sources, maxTokens) {
|
|
|
7256
7256
|
if (total <= maxTokens) {
|
|
7257
7257
|
return sources.map((src) => ({ name: src.name, tokens: src.tokens }));
|
|
7258
7258
|
}
|
|
7259
|
-
const
|
|
7260
|
-
const remaining = Math.max(0, maxTokens -
|
|
7259
|
+
const floor2 = Math.min(MIN_SOURCE_TOKENS, Math.floor(maxTokens / sources.length));
|
|
7260
|
+
const remaining = Math.max(0, maxTokens - floor2 * sources.length);
|
|
7261
7261
|
return sources.map((src) => {
|
|
7262
7262
|
const proportional = total === 0 ? 0 : Math.floor(src.tokens.length / total * remaining);
|
|
7263
|
-
const allotted = Math.min(src.tokens.length,
|
|
7263
|
+
const allotted = Math.min(src.tokens.length, floor2 + proportional);
|
|
7264
7264
|
return { name: src.name, tokens: src.tokens.slice(0, allotted) };
|
|
7265
7265
|
});
|
|
7266
7266
|
}
|
|
@@ -18248,6 +18248,87 @@ function startEvalRunSpan(attrs) {
|
|
|
18248
18248
|
};
|
|
18249
18249
|
}
|
|
18250
18250
|
|
|
18251
|
+
// src/internal/eval/trials.ts
|
|
18252
|
+
var TRIAL_INDEX_KEY = "__evalRowIndex";
|
|
18253
|
+
var TRIAL_NUM_KEY = "__evalTrial";
|
|
18254
|
+
function expandForTrials(entries, trials) {
|
|
18255
|
+
const out = [];
|
|
18256
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
18257
|
+
const entry = entries[i];
|
|
18258
|
+
if (entry === void 0) continue;
|
|
18259
|
+
for (let t = 0; t < trials; t += 1) {
|
|
18260
|
+
out.push({
|
|
18261
|
+
...entry,
|
|
18262
|
+
metadata: { ...entry.metadata ?? {}, [TRIAL_INDEX_KEY]: i, [TRIAL_NUM_KEY]: t }
|
|
18263
|
+
});
|
|
18264
|
+
}
|
|
18265
|
+
}
|
|
18266
|
+
return out;
|
|
18267
|
+
}
|
|
18268
|
+
function stripReserved(metadata) {
|
|
18269
|
+
if (metadata === void 0) return void 0;
|
|
18270
|
+
const { [TRIAL_INDEX_KEY]: _idx, [TRIAL_NUM_KEY]: _trial, ...rest } = metadata;
|
|
18271
|
+
return Object.keys(rest).length > 0 ? rest : void 0;
|
|
18272
|
+
}
|
|
18273
|
+
function sumDefined(values) {
|
|
18274
|
+
const present = values.filter((v) => typeof v === "number");
|
|
18275
|
+
return present.length > 0 ? present.reduce((acc, v) => acc + v, 0) : void 0;
|
|
18276
|
+
}
|
|
18277
|
+
function meanScoresByScorer(group, trials) {
|
|
18278
|
+
const names = [];
|
|
18279
|
+
const sums = /* @__PURE__ */ new Map();
|
|
18280
|
+
for (const row of group) {
|
|
18281
|
+
for (const s of row.scores) {
|
|
18282
|
+
if (!sums.has(s.name)) {
|
|
18283
|
+
names.push(s.name);
|
|
18284
|
+
sums.set(s.name, 0);
|
|
18285
|
+
}
|
|
18286
|
+
sums.set(s.name, (sums.get(s.name) ?? 0) + s.score);
|
|
18287
|
+
}
|
|
18288
|
+
}
|
|
18289
|
+
return names.map((name) => ({
|
|
18290
|
+
name,
|
|
18291
|
+
score: (sums.get(name) ?? 0) / trials,
|
|
18292
|
+
reason: `mean of ${trials} trials`
|
|
18293
|
+
}));
|
|
18294
|
+
}
|
|
18295
|
+
function collapseGroup(index, group, trials) {
|
|
18296
|
+
const first = group[0];
|
|
18297
|
+
const scores = meanScoresByScorer(group, trials);
|
|
18298
|
+
const allErrored = group.every((r) => r.error !== void 0);
|
|
18299
|
+
const meanScore = allErrored || scores.length === 0 ? 0 : scores.reduce((acc, s) => acc + s.score, 0) / scores.length;
|
|
18300
|
+
const firstOk = group.find((r) => r.error === void 0);
|
|
18301
|
+
const errorMsg = allErrored ? group.find((r) => r.error !== void 0)?.error : void 0;
|
|
18302
|
+
const metadata = stripReserved(first.metadata);
|
|
18303
|
+
const tokensIn = sumDefined(group.map((r) => r.tokensIn));
|
|
18304
|
+
const tokensOut = sumDefined(group.map((r) => r.tokensOut));
|
|
18305
|
+
return {
|
|
18306
|
+
index,
|
|
18307
|
+
input: first.input,
|
|
18308
|
+
output: firstOk?.output ?? first.output,
|
|
18309
|
+
...first.expected !== void 0 ? { expected: first.expected } : {},
|
|
18310
|
+
scores,
|
|
18311
|
+
meanScore,
|
|
18312
|
+
durationMs: group.reduce((acc, r) => acc + r.durationMs, 0),
|
|
18313
|
+
...tokensIn !== void 0 ? { tokensIn } : {},
|
|
18314
|
+
...tokensOut !== void 0 ? { tokensOut } : {},
|
|
18315
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
18316
|
+
...metadata !== void 0 ? { metadata } : {},
|
|
18317
|
+
trialCount: trials
|
|
18318
|
+
};
|
|
18319
|
+
}
|
|
18320
|
+
function collapseTrials(rows, trials) {
|
|
18321
|
+
const groups = /* @__PURE__ */ new Map();
|
|
18322
|
+
for (const row of rows) {
|
|
18323
|
+
const meta = row.metadata;
|
|
18324
|
+
const origIdx = typeof meta?.[TRIAL_INDEX_KEY] === "number" ? meta[TRIAL_INDEX_KEY] : row.index;
|
|
18325
|
+
const arr = groups.get(origIdx) ?? [];
|
|
18326
|
+
arr.push(row);
|
|
18327
|
+
groups.set(origIdx, arr);
|
|
18328
|
+
}
|
|
18329
|
+
return [...groups.entries()].sort((a, b) => a[0] - b[0]).map(([origIdx, group]) => collapseGroup(origIdx, group, trials));
|
|
18330
|
+
}
|
|
18331
|
+
|
|
18251
18332
|
// src/internal/eval/runner.ts
|
|
18252
18333
|
function safeHook(fn) {
|
|
18253
18334
|
try {
|
|
@@ -18456,7 +18537,9 @@ async function runEval(options, runOpts) {
|
|
|
18456
18537
|
const id = crypto.randomUUID();
|
|
18457
18538
|
const startedAt = Date.now();
|
|
18458
18539
|
const entries = await materializeDataset(options.dataset);
|
|
18459
|
-
const
|
|
18540
|
+
const materialized = entries.map((e) => ({ ...e }));
|
|
18541
|
+
const trials = options.trials ?? 1;
|
|
18542
|
+
const indexed = trials > 1 ? expandForTrials(materialized, trials) : materialized;
|
|
18460
18543
|
const scorers = normalizeScorers(options.scorers);
|
|
18461
18544
|
const concurrency = options.concurrency ?? 4;
|
|
18462
18545
|
const signal = runOpts?.signal;
|
|
@@ -18488,7 +18571,8 @@ async function runEval(options, runOpts) {
|
|
|
18488
18571
|
const batchOpts = makeAgentForBatch(options.agent, indexed);
|
|
18489
18572
|
rows = await runRowsViaBatch(indexed, batchOpts, scorers, concurrency, signal, onRow, sink);
|
|
18490
18573
|
}
|
|
18491
|
-
const
|
|
18574
|
+
const finalRows = trials > 1 ? collapseTrials(rows, trials) : rows;
|
|
18575
|
+
const aggregate = computeAggregate(finalRows);
|
|
18492
18576
|
const endedAt = Date.now();
|
|
18493
18577
|
const run = {
|
|
18494
18578
|
id,
|
|
@@ -18497,7 +18581,7 @@ async function runEval(options, runOpts) {
|
|
|
18497
18581
|
endedAt,
|
|
18498
18582
|
durationMs: endedAt - startedAt,
|
|
18499
18583
|
aggregate,
|
|
18500
|
-
rows,
|
|
18584
|
+
rows: finalRows,
|
|
18501
18585
|
...options.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
18502
18586
|
};
|
|
18503
18587
|
safeHook(() => hooks?.afterRun?.(run));
|
|
@@ -18512,6 +18596,59 @@ async function runEval(options, runOpts) {
|
|
|
18512
18596
|
}
|
|
18513
18597
|
}
|
|
18514
18598
|
|
|
18599
|
+
// src/internal/eval/assert.ts
|
|
18600
|
+
var EvalThresholdError = class extends Error {
|
|
18601
|
+
name = "EvalThresholdError";
|
|
18602
|
+
/** The eval's name (`EvalRun.name`). */
|
|
18603
|
+
evalName;
|
|
18604
|
+
/** Every unmet threshold, in check order. */
|
|
18605
|
+
failures;
|
|
18606
|
+
constructor(evalName, failures) {
|
|
18607
|
+
const lines = failures.map(
|
|
18608
|
+
(f) => ` - ${f.metric}: required ${f.threshold}, got ${Number.isNaN(f.actual) ? "n/a (scorer absent)" : f.actual}`
|
|
18609
|
+
);
|
|
18610
|
+
super(
|
|
18611
|
+
`Eval "${evalName}" failed ${failures.length} threshold${failures.length === 1 ? "" : "s"}:
|
|
18612
|
+
${lines.join("\n")}`
|
|
18613
|
+
);
|
|
18614
|
+
this.evalName = evalName;
|
|
18615
|
+
this.failures = failures;
|
|
18616
|
+
}
|
|
18617
|
+
};
|
|
18618
|
+
function floor(metric, actual, threshold) {
|
|
18619
|
+
if (threshold === void 0 || actual >= threshold) return void 0;
|
|
18620
|
+
return { metric, threshold, actual };
|
|
18621
|
+
}
|
|
18622
|
+
function perScorerFailures(perScorer, thresholds) {
|
|
18623
|
+
if (thresholds === void 0) return [];
|
|
18624
|
+
const out = [];
|
|
18625
|
+
for (const [name, min] of Object.entries(thresholds)) {
|
|
18626
|
+
const stats = perScorer[name];
|
|
18627
|
+
if (stats === void 0 || stats.mean < min) {
|
|
18628
|
+
out.push({
|
|
18629
|
+
metric: `perScorer.${name}`,
|
|
18630
|
+
threshold: min,
|
|
18631
|
+
actual: stats === void 0 ? Number.NaN : stats.mean
|
|
18632
|
+
});
|
|
18633
|
+
}
|
|
18634
|
+
}
|
|
18635
|
+
return out;
|
|
18636
|
+
}
|
|
18637
|
+
function assertEval(run, thresholds) {
|
|
18638
|
+
const a = run.aggregate;
|
|
18639
|
+
const errorRatio = a.totalRows > 0 ? a.errorRows / a.totalRows : 0;
|
|
18640
|
+
const ceiling = thresholds.maxErrorRatio !== void 0 && errorRatio > thresholds.maxErrorRatio ? { metric: "errorRatio", threshold: thresholds.maxErrorRatio, actual: errorRatio } : void 0;
|
|
18641
|
+
const failures = [
|
|
18642
|
+
floor("meanScore", a.meanScore, thresholds.minMeanScore),
|
|
18643
|
+
floor("passRatio", a.passRatio, thresholds.minPassRatio),
|
|
18644
|
+
ceiling,
|
|
18645
|
+
...perScorerFailures(a.perScorer, thresholds.perScorer)
|
|
18646
|
+
].filter((f) => f !== void 0);
|
|
18647
|
+
if (failures.length > 0) {
|
|
18648
|
+
throw new EvalThresholdError(run.name, failures);
|
|
18649
|
+
}
|
|
18650
|
+
}
|
|
18651
|
+
|
|
18515
18652
|
// src/sandbox/shell-escape.ts
|
|
18516
18653
|
function shellEscapePosix(arg) {
|
|
18517
18654
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
@@ -18531,6 +18668,27 @@ async function captureArtifact(sandbox, repoDir) {
|
|
|
18531
18668
|
return { diff, applies: check.exitCode === 0 };
|
|
18532
18669
|
}
|
|
18533
18670
|
|
|
18671
|
+
// src/internal/scorers/levenshtein.ts
|
|
18672
|
+
var LEVENSHTEIN_MAX_LEN = 4e3;
|
|
18673
|
+
function nextRow(prev, rowIndex, ai, b) {
|
|
18674
|
+
const curr = [rowIndex];
|
|
18675
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
18676
|
+
const cost = ai === b.charCodeAt(j - 1) ? 0 : 1;
|
|
18677
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
18678
|
+
}
|
|
18679
|
+
return curr;
|
|
18680
|
+
}
|
|
18681
|
+
function levenshteinDistance(a, b) {
|
|
18682
|
+
const m = a.length;
|
|
18683
|
+
const n = b.length;
|
|
18684
|
+
if (m === 0) return n;
|
|
18685
|
+
if (n === 0) return m;
|
|
18686
|
+
let prev = [];
|
|
18687
|
+
for (let j = 0; j <= n; j += 1) prev[j] = j;
|
|
18688
|
+
for (let i = 1; i <= m; i += 1) prev = nextRow(prev, i, a.charCodeAt(i - 1), b);
|
|
18689
|
+
return prev[n] ?? 0;
|
|
18690
|
+
}
|
|
18691
|
+
|
|
18534
18692
|
// src/internal/scorers/llm-judge.ts
|
|
18535
18693
|
init_agent_factory_registry();
|
|
18536
18694
|
function buildPrompt(subject, criteria, rubric, expected) {
|
|
@@ -18691,15 +18849,80 @@ var LocalSandbox = class extends SandboxBackend {
|
|
|
18691
18849
|
|
|
18692
18850
|
// src/scorers.ts
|
|
18693
18851
|
var JSON_SHAPE_MAX_BYTES = 1e6;
|
|
18852
|
+
function cosineSimilarity(a, b) {
|
|
18853
|
+
const n = Math.min(a.length, b.length);
|
|
18854
|
+
let dot = 0;
|
|
18855
|
+
let na = 0;
|
|
18856
|
+
let nb = 0;
|
|
18857
|
+
for (let i = 0; i < n; i += 1) {
|
|
18858
|
+
const av = a[i] ?? 0;
|
|
18859
|
+
const bv = b[i] ?? 0;
|
|
18860
|
+
dot += av * bv;
|
|
18861
|
+
na += av * av;
|
|
18862
|
+
nb += bv * bv;
|
|
18863
|
+
}
|
|
18864
|
+
if (na === 0 || nb === 0) return 0;
|
|
18865
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
18866
|
+
}
|
|
18867
|
+
function normalizeStringInputs(output, expected, caseSensitive) {
|
|
18868
|
+
if (typeof expected !== "string") return { score: 0, reason: "expected_not_string" };
|
|
18869
|
+
if (expected.length === 0) return { score: 0, reason: "expected_empty" };
|
|
18870
|
+
return {
|
|
18871
|
+
o: caseSensitive ? output : output.toLowerCase(),
|
|
18872
|
+
e: caseSensitive ? expected : expected.toLowerCase()
|
|
18873
|
+
};
|
|
18874
|
+
}
|
|
18875
|
+
function scoreLevenshtein(output, expected, caseSensitive, threshold) {
|
|
18876
|
+
const norm = normalizeStringInputs(output, expected, caseSensitive);
|
|
18877
|
+
if (!("o" in norm)) return norm;
|
|
18878
|
+
const { o, e } = norm;
|
|
18879
|
+
if (o.length > LEVENSHTEIN_MAX_LEN || e.length > LEVENSHTEIN_MAX_LEN) {
|
|
18880
|
+
return { score: 0, reason: "input_too_large" };
|
|
18881
|
+
}
|
|
18882
|
+
const sim = 1 - levenshteinDistance(o, e) / Math.max(o.length, e.length, 1);
|
|
18883
|
+
if (threshold === void 0) return { score: sim };
|
|
18884
|
+
return sim >= threshold ? { score: 1 } : { score: 0, reason: `sim=${sim.toFixed(3)}` };
|
|
18885
|
+
}
|
|
18886
|
+
function toFiniteNumber2(value) {
|
|
18887
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
18888
|
+
if (value === void 0 || value === null) return void 0;
|
|
18889
|
+
const n = Number(String(value).trim());
|
|
18890
|
+
return Number.isFinite(n) ? n : void 0;
|
|
18891
|
+
}
|
|
18892
|
+
function scoreNumericDiff(output, expected, tolerance) {
|
|
18893
|
+
const o = toFiniteNumber2(output);
|
|
18894
|
+
if (o === void 0) return { score: 0, reason: "output_not_numeric" };
|
|
18895
|
+
const e = toFiniteNumber2(expected);
|
|
18896
|
+
if (e === void 0) return { score: 0, reason: "expected_not_numeric" };
|
|
18897
|
+
if (tolerance !== void 0) {
|
|
18898
|
+
return Math.abs(o - e) <= tolerance ? { score: 1 } : { score: 0, reason: `abs_diff=${Math.abs(o - e)}` };
|
|
18899
|
+
}
|
|
18900
|
+
const denom = Math.max(Math.abs(o), Math.abs(e));
|
|
18901
|
+
if (denom === 0) return { score: 1 };
|
|
18902
|
+
return { score: Math.max(0, 1 - Math.abs(o - e) / denom) };
|
|
18903
|
+
}
|
|
18904
|
+
function embedderCreateOptions(opts) {
|
|
18905
|
+
return {
|
|
18906
|
+
...opts.model !== void 0 ? { model: opts.model } : {},
|
|
18907
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
18908
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
18909
|
+
};
|
|
18910
|
+
}
|
|
18911
|
+
function makeDefaultEmbedder(opts) {
|
|
18912
|
+
let runtime;
|
|
18913
|
+
return (texts) => {
|
|
18914
|
+
if (runtime === void 0) {
|
|
18915
|
+
runtime = openRouterMemoryEmbeddingProviderAdapter.create(embedderCreateOptions(opts));
|
|
18916
|
+
}
|
|
18917
|
+
return runtime.then((rt) => rt.embed(texts));
|
|
18918
|
+
};
|
|
18919
|
+
}
|
|
18694
18920
|
function makeStringScorer(name, caseSensitive, compare) {
|
|
18695
18921
|
return {
|
|
18696
18922
|
name,
|
|
18697
18923
|
score: (output, expected) => {
|
|
18698
|
-
|
|
18699
|
-
|
|
18700
|
-
const o = caseSensitive ? output : output.toLowerCase();
|
|
18701
|
-
const e = caseSensitive ? expected : expected.toLowerCase();
|
|
18702
|
-
return compare(o, e);
|
|
18924
|
+
const norm = normalizeStringInputs(output, expected, caseSensitive);
|
|
18925
|
+
return "o" in norm ? compare(norm.o, norm.e) : norm;
|
|
18703
18926
|
}
|
|
18704
18927
|
};
|
|
18705
18928
|
}
|
|
@@ -18743,6 +18966,56 @@ var Scorers = {
|
|
|
18743
18966
|
}
|
|
18744
18967
|
};
|
|
18745
18968
|
},
|
|
18969
|
+
/**
|
|
18970
|
+
* SE41 — normalized Levenshtein similarity: `1 - editDistance / max(len)`.
|
|
18971
|
+
* Deterministic (no LLM), so it always runs in CI. `threshold` binarizes.
|
|
18972
|
+
*
|
|
18973
|
+
* Refuses empty/non-string `expected` (EC-1 parity) and caps input at
|
|
18974
|
+
* {@link LEVENSHTEIN_MAX_LEN} chars to bound the O(n*m) cost on adversarial output.
|
|
18975
|
+
*/
|
|
18976
|
+
levenshtein(opts = {}) {
|
|
18977
|
+
const caseSensitive = opts.caseSensitive ?? false;
|
|
18978
|
+
const { threshold } = opts;
|
|
18979
|
+
return {
|
|
18980
|
+
name: threshold !== void 0 ? `levenshtein(>=${threshold})` : "levenshtein",
|
|
18981
|
+
score: (output, expected) => scoreLevenshtein(output, expected, caseSensitive, threshold)
|
|
18982
|
+
};
|
|
18983
|
+
},
|
|
18984
|
+
/**
|
|
18985
|
+
* SE41 — numeric closeness. Parses `output` and `expected` as numbers and
|
|
18986
|
+
* scores continuous relative closeness `1 - |o-e| / max(|o|,|e|)` (both 0 ⇒ 1),
|
|
18987
|
+
* or a binary pass when `tolerance` is set. Deterministic (no LLM).
|
|
18988
|
+
*/
|
|
18989
|
+
numericDiff(opts = {}) {
|
|
18990
|
+
const { tolerance } = opts;
|
|
18991
|
+
return {
|
|
18992
|
+
name: "numeric-diff",
|
|
18993
|
+
score: (output, expected) => scoreNumericDiff(output, expected, tolerance)
|
|
18994
|
+
};
|
|
18995
|
+
},
|
|
18996
|
+
/**
|
|
18997
|
+
* SE41 — semantic similarity via embeddings: cosine of `embed(output)` vs
|
|
18998
|
+
* `embed(expected)`, clamped to `[0, 1]` (negatives → 0). `threshold` binarizes.
|
|
18999
|
+
*
|
|
19000
|
+
* By default routes through OpenRouter's embeddings endpoint
|
|
19001
|
+
* (`OPENROUTER_API_KEY`); inject `embed` to use another provider or to test
|
|
19002
|
+
* deterministically. Each scored row costs one embeddings call.
|
|
19003
|
+
*/
|
|
19004
|
+
embeddingSimilarity(opts) {
|
|
19005
|
+
const { threshold } = opts;
|
|
19006
|
+
const embed = opts.embed ?? makeDefaultEmbedder(opts);
|
|
19007
|
+
return {
|
|
19008
|
+
name: threshold !== void 0 ? `embedding-similarity(>=${threshold})` : "embedding-similarity",
|
|
19009
|
+
score: async (output, expected) => {
|
|
19010
|
+
if (typeof expected !== "string") return { score: 0, reason: "expected_not_string" };
|
|
19011
|
+
const [vo, ve] = await embed([output, expected]);
|
|
19012
|
+
if (vo === void 0 || ve === void 0) return { score: 0, reason: "embed_failed" };
|
|
19013
|
+
const score = Math.max(0, cosineSimilarity(vo, ve));
|
|
19014
|
+
if (threshold === void 0) return { score };
|
|
19015
|
+
return score >= threshold ? { score: 1 } : { score: 0, reason: `cos=${score.toFixed(3)}` };
|
|
19016
|
+
}
|
|
19017
|
+
};
|
|
19018
|
+
},
|
|
18746
19019
|
/**
|
|
18747
19020
|
* Parse `output` as JSON and validate against a Zod schema.
|
|
18748
19021
|
*
|
|
@@ -18848,6 +19121,9 @@ var EvalOptionsSchema = zod.z.object({
|
|
|
18848
19121
|
// EC-3: concurrency MUST be a finite integer in [1, 64]. Bare `z.number()`
|
|
18849
19122
|
// would let Infinity / 0 / negatives slip through.
|
|
18850
19123
|
concurrency: zod.z.number().int("concurrency must be an integer").min(1, "concurrency must be >= 1").max(64, "concurrency must be <= 64").optional(),
|
|
19124
|
+
// SE41: trials MUST be a finite integer in [1, 100]. Guards against 0
|
|
19125
|
+
// (no-op run) and unbounded fanout that would DoS the provider.
|
|
19126
|
+
trials: zod.z.number().int("trials must be an integer").min(1, "trials must be >= 1").max(100, "trials must be <= 100").optional(),
|
|
18851
19127
|
metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
18852
19128
|
hooks: zod.z.object({
|
|
18853
19129
|
beforeRun: zod.z.function().optional(),
|
|
@@ -18880,8 +19156,10 @@ var Eval = class _Eval {
|
|
|
18880
19156
|
|
|
18881
19157
|
exports.Eval = Eval;
|
|
18882
19158
|
exports.EvalAlreadyRunningError = EvalAlreadyRunningError;
|
|
19159
|
+
exports.EvalThresholdError = EvalThresholdError;
|
|
18883
19160
|
exports.JsonlParseError = JsonlParseError;
|
|
18884
19161
|
exports.Scorers = Scorers;
|
|
19162
|
+
exports.assertEval = assertEval;
|
|
18885
19163
|
exports.captureArtifact = captureArtifact;
|
|
18886
19164
|
exports.loadJsonl = loadJsonl;
|
|
18887
19165
|
//# sourceMappingURL=eval.cjs.map
|