@velum-labs/routekit-eval-service 1.0.12 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/layer-options.d.ts
CHANGED
|
@@ -15,4 +15,12 @@ export type RouteKitEvalServiceOptions = Omit<NodeTestExecutionOptions, "bridgeO
|
|
|
15
15
|
* stale or missing timeout.
|
|
16
16
|
*/
|
|
17
17
|
readonly timeoutMs?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Run the node:test child from a temporary copy of the reviewed suite.
|
|
20
|
+
*
|
|
21
|
+
* Qualification enables this so a project-local `routekit/eval` or `ori/eval`
|
|
22
|
+
* package cannot shadow the bundled bridge-aware SDK used by the production
|
|
23
|
+
* execution adapter.
|
|
24
|
+
*/
|
|
25
|
+
readonly isolateExecutionFromProjectSdk?: boolean;
|
|
18
26
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { layer as NodeServicesLayer } from "@effect/platform-node/NodeServices";
|
|
2
2
|
import { EvalEngineExecutionError, EvalExecutionPort, makeEvalEngineLayer, makeRouteKitEvalExecutionPortService } from "@velum-labs/routekit-eval-engine";
|
|
3
|
-
import { Data, Effect, Layer, Stream } from "effect";
|
|
3
|
+
import { Data, Effect, FileSystem, Layer, Path, Stream } from "effect";
|
|
4
4
|
import { HttpClient } from "effect/unstable/http";
|
|
5
5
|
import { makeEvalServiceLayer } from "./service.js";
|
|
6
6
|
export class EvalServiceCredentialError extends Data.TaggedError("EvalServiceCredentialError") {
|
|
@@ -8,18 +8,21 @@ export class EvalServiceCredentialError extends Data.TaggedError("EvalServiceCre
|
|
|
8
8
|
return this.detail;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
const makeProductionExecutionPort = (options) => Effect.
|
|
11
|
+
const makeProductionExecutionPort = (options) => Effect.gen(function* () {
|
|
12
|
+
const fs = yield* FileSystem.FileSystem;
|
|
12
13
|
const bearerCredential = options.bearerCredential?.trim();
|
|
13
14
|
if (bearerCredential === undefined || bearerCredential.length === 0) {
|
|
14
|
-
return {
|
|
15
|
+
return EvalExecutionPort.of({
|
|
15
16
|
execute: () => Stream.fail(new EvalEngineExecutionError({
|
|
16
17
|
cause: new EvalServiceCredentialError({
|
|
17
18
|
detail: "RouteKit Eval comparison execution requires an injected bearer credential."
|
|
18
19
|
}),
|
|
19
20
|
detail: "RouteKit Eval comparison execution requires an injected bearer credential."
|
|
20
21
|
}))
|
|
21
|
-
};
|
|
22
|
+
});
|
|
22
23
|
}
|
|
24
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
25
|
+
const paths = yield* Path.Path;
|
|
23
26
|
const execution = makeRouteKitEvalExecutionPortService({
|
|
24
27
|
bearerCredential,
|
|
25
28
|
...(options.childEnvironment === undefined
|
|
@@ -27,17 +30,39 @@ const makeProductionExecutionPort = (options) => Effect.map(HttpClient.HttpClien
|
|
|
27
30
|
: { childEnvironment: options.childEnvironment }),
|
|
28
31
|
...(options.execPath === undefined ? {} : { execPath: options.execPath })
|
|
29
32
|
}, httpClient);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
return EvalExecutionPort.of({
|
|
34
|
+
execute: (input) => {
|
|
35
|
+
const deadlineInput = options.timeoutMs === undefined
|
|
36
|
+
? input
|
|
37
|
+
: {
|
|
38
|
+
...input,
|
|
39
|
+
request: {
|
|
40
|
+
...input.request,
|
|
41
|
+
timeoutMs: options.timeoutMs
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
if (options.isolateExecutionFromProjectSdk !== true) {
|
|
45
|
+
return execution.execute(deadlineInput);
|
|
38
46
|
}
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
48
|
+
const root = yield* Effect.acquireRelease(fs.makeTempDirectory({ prefix: "routekit-eval-execution-" }), (directory) => fs.remove(directory, { recursive: true }).pipe(Effect.ignore));
|
|
49
|
+
const workingDirectory = paths.join(root, "suite");
|
|
50
|
+
yield* fs.copy(input.discovery.workingDirectory, workingDirectory);
|
|
51
|
+
const discovery = {
|
|
52
|
+
...input.discovery,
|
|
53
|
+
workingDirectory,
|
|
54
|
+
files: input.discovery.files.map((file) => paths.join(workingDirectory, paths.relative(input.discovery.workingDirectory, file)))
|
|
55
|
+
};
|
|
56
|
+
return execution.execute({
|
|
57
|
+
...deadlineInput,
|
|
58
|
+
discovery
|
|
59
|
+
});
|
|
60
|
+
}).pipe(Effect.mapError((cause) => new EvalEngineExecutionError({
|
|
61
|
+
cause,
|
|
62
|
+
detail: "RouteKit Eval could not isolate the reviewed suite from project-local SDK packages."
|
|
63
|
+
})))).pipe(Stream.scoped);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
41
66
|
});
|
|
42
67
|
const makeProductionEvalEngineLayer = (options) => makeEvalEngineLayer().pipe(Layer.provide(Layer.effect(EvalExecutionPort, makeProductionExecutionPort(options))));
|
|
43
68
|
/**
|
|
@@ -40,6 +40,27 @@ const writeManifest = async (root, candidateModels, judgeModel, caseIds) => {
|
|
|
40
40
|
expectedCallCount: caseIds.length * candidateModels.length * 2
|
|
41
41
|
})}\n`);
|
|
42
42
|
};
|
|
43
|
+
const writeShadowEvalSdk = async (root, hiddenTimeoutMs) => {
|
|
44
|
+
for (const name of ["routekit", "ori"]) {
|
|
45
|
+
const directory = path.join(root, "node_modules", name);
|
|
46
|
+
await mkdir(directory, { recursive: true });
|
|
47
|
+
await writeFile(path.join(directory, "package.json"), `${JSON.stringify({
|
|
48
|
+
name,
|
|
49
|
+
private: true,
|
|
50
|
+
type: "module",
|
|
51
|
+
exports: { "./eval": "./eval.js" }
|
|
52
|
+
})}\n`);
|
|
53
|
+
await writeFile(path.join(directory, "eval.js"), [
|
|
54
|
+
"export const setupAgent = () => ({",
|
|
55
|
+
" run: async () => {",
|
|
56
|
+
` await new Promise((resolve) => setTimeout(resolve, ${String(hiddenTimeoutMs)}));`,
|
|
57
|
+
' throw new Error("project SDK hidden timeout");',
|
|
58
|
+
" }",
|
|
59
|
+
"});",
|
|
60
|
+
"export const setupJudge = () => ({ autoEvals: async () => {} });"
|
|
61
|
+
].join("\n"));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
43
64
|
test("production runner estimates the exact imported nested-loop testdrive suite from its manifest", async () => {
|
|
44
65
|
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-estimate-"));
|
|
45
66
|
roots.push(root);
|
|
@@ -263,3 +284,69 @@ test("production runner applies the host timeout at node:test instead of a stale
|
|
|
263
284
|
await new Promise((resolve, reject) => gateway.close((error) => (error === undefined ? resolve() : reject(error))));
|
|
264
285
|
}
|
|
265
286
|
});
|
|
287
|
+
test("production runner isolates qualification from a project SDK with a shorter hidden abort", async () => {
|
|
288
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-isolated-sdk-"));
|
|
289
|
+
roots.push(root);
|
|
290
|
+
await writeShadowEvalSdk(root, 20);
|
|
291
|
+
const suiteRoot = path.join(root, ".routekit", "evals", "plans", "plan", "dimensions", "support");
|
|
292
|
+
await mkdir(suiteRoot, { recursive: true });
|
|
293
|
+
const suite = path.join(suiteRoot, "support.eval.ts");
|
|
294
|
+
await writeFile(suite, [
|
|
295
|
+
'import { test } from "node:test";',
|
|
296
|
+
'import { setupAgent, setupJudge } from "routekit/eval";',
|
|
297
|
+
'const judge = setupJudge({ agent: setupAgent({ model: "openai/judge" }), minScore: 0.8 });',
|
|
298
|
+
'test("support case", async () => {',
|
|
299
|
+
' const run = await setupAgent({ model: "openai/cheap" }).run({ prompt: "Help", caseId: "support-case" });',
|
|
300
|
+
" run.toComplete();",
|
|
301
|
+
' await judge.autoEvals({ criteria: "Helpful", prompt: "Help", run });',
|
|
302
|
+
"});"
|
|
303
|
+
].join("\n"));
|
|
304
|
+
await writeManifest(suiteRoot, ["openai/cheap"], "openai/judge", ["support-case"]);
|
|
305
|
+
const calls = [];
|
|
306
|
+
const gateway = createServer((incoming, outgoing) => {
|
|
307
|
+
void (async () => {
|
|
308
|
+
const body = JSON.parse(await readBody(incoming));
|
|
309
|
+
calls.push(String(body.model));
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
311
|
+
const content = body.model === "openai/judge"
|
|
312
|
+
? JSON.stringify({ pass: true, reason: "helpful", score: 0.9 })
|
|
313
|
+
: "Helpful answer";
|
|
314
|
+
outgoing.writeHead(200, { "content-type": "application/json" });
|
|
315
|
+
outgoing.end(JSON.stringify({
|
|
316
|
+
model: body.model,
|
|
317
|
+
choices: [{ message: { role: "assistant", content } }]
|
|
318
|
+
}));
|
|
319
|
+
})();
|
|
320
|
+
});
|
|
321
|
+
await new Promise((resolve) => gateway.listen(0, "127.0.0.1", resolve));
|
|
322
|
+
const address = gateway.address();
|
|
323
|
+
assert.ok(address !== null && typeof address !== "string");
|
|
324
|
+
try {
|
|
325
|
+
const shadowed = await Effect.runPromise(withEvalService({
|
|
326
|
+
bearerCredential: "parent-only-token",
|
|
327
|
+
timeoutMs: 15_000
|
|
328
|
+
}, (service) => service.runComparison({
|
|
329
|
+
...requestFor(suite, `http://127.0.0.1:${String(address.port)}`),
|
|
330
|
+
candidateModels: ["openai/cheap"],
|
|
331
|
+
timeoutMs: 20
|
|
332
|
+
}, "full")).pipe(Effect.provide(NodeHttpClient.layerUndici), Effect.exit));
|
|
333
|
+
assert.equal(Exit.isFailure(shadowed), true);
|
|
334
|
+
assert.deepEqual(calls, []);
|
|
335
|
+
const started = Date.now();
|
|
336
|
+
const result = await Effect.runPromise(withEvalService({
|
|
337
|
+
bearerCredential: "parent-only-token",
|
|
338
|
+
isolateExecutionFromProjectSdk: true,
|
|
339
|
+
timeoutMs: 15_000
|
|
340
|
+
}, (service) => service.runComparison({
|
|
341
|
+
...requestFor(suite, `http://127.0.0.1:${String(address.port)}`),
|
|
342
|
+
candidateModels: ["openai/cheap"],
|
|
343
|
+
timeoutMs: 20
|
|
344
|
+
}, "full")).pipe(Effect.provide(NodeHttpClient.layerUndici)));
|
|
345
|
+
assert.deepEqual(calls, ["openai/cheap", "openai/judge"]);
|
|
346
|
+
assert.equal(result.models[0]?.cases[0]?.outcome, "passed");
|
|
347
|
+
assert.ok(Date.now() - started >= 75, "the project SDK's 20ms abort must not bound execution");
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
await new Promise((resolve, reject) => gateway.close((error) => (error === undefined ? resolve() : reject(error))));
|
|
351
|
+
}
|
|
352
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-eval-service",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.13",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@effect/platform-node": "4.0.0-rc.108",
|
|
34
34
|
"effect": "4.0.0-rc.108",
|
|
35
|
-
"@velum-labs/routekit-eval-contracts": "1.0.
|
|
36
|
-
"@velum-labs/routekit-eval-engine": "1.0.
|
|
37
|
-
"@velum-labs/routekit-eval-store": "1.0.
|
|
38
|
-
"@velum-labs/routekit-registry": "1.0.
|
|
35
|
+
"@velum-labs/routekit-eval-contracts": "1.0.13",
|
|
36
|
+
"@velum-labs/routekit-eval-engine": "1.0.13",
|
|
37
|
+
"@velum-labs/routekit-eval-store": "1.0.13",
|
|
38
|
+
"@velum-labs/routekit-registry": "1.0.13"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "7.0.2"
|