@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.
@@ -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.map(HttpClient.HttpClient, (httpClient) => {
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
- if (options.timeoutMs === undefined)
31
- return execution;
32
- return {
33
- execute: (input) => execution.execute({
34
- ...input,
35
- request: {
36
- ...input.request,
37
- timeoutMs: options.timeoutMs
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.12",
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.12",
36
- "@velum-labs/routekit-eval-engine": "1.0.12",
37
- "@velum-labs/routekit-eval-store": "1.0.12",
38
- "@velum-labs/routekit-registry": "1.0.12"
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"