@velum-labs/routekit-eval-service 1.0.11 → 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.
@@ -7,4 +7,20 @@ export type RouteKitEvalServiceOptions = Omit<NodeTestExecutionOptions, "bridgeO
7
7
  * execution fails before starting a child when it is absent.
8
8
  */
9
9
  readonly bearerCredential?: string;
10
+ /**
11
+ * Host-owned per-test deadline.
12
+ *
13
+ * When set, the production execution adapter applies this value at the
14
+ * node:test boundary even if an intermediate comparison request carries a
15
+ * stale or missing timeout.
16
+ */
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;
10
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,25 +8,61 @@ 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
  }
23
- return makeRouteKitEvalExecutionPortService({
24
+ const httpClient = yield* HttpClient.HttpClient;
25
+ const paths = yield* Path.Path;
26
+ const execution = makeRouteKitEvalExecutionPortService({
24
27
  bearerCredential,
25
28
  ...(options.childEnvironment === undefined
26
29
  ? {}
27
30
  : { childEnvironment: options.childEnvironment }),
28
31
  ...(options.execPath === undefined ? {} : { execPath: options.execPath })
29
32
  }, httpClient);
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);
46
+ }
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
+ });
30
66
  });
31
67
  const makeProductionEvalEngineLayer = (options) => makeEvalEngineLayer().pipe(Layer.provide(Layer.effect(EvalExecutionPort, makeProductionExecutionPort(options))));
32
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);
@@ -216,3 +237,116 @@ test("production runner executes candidate and judge traffic through the live en
216
237
  await new Promise((resolve, reject) => gateway.close((error) => (error === undefined ? resolve() : reject(error))));
217
238
  }
218
239
  });
240
+ test("production runner applies the host timeout at node:test instead of a stale request deadline", async () => {
241
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-timeout-"));
242
+ roots.push(root);
243
+ const suite = path.join(root, "support.eval.ts");
244
+ await writeFile(suite, [
245
+ 'import { test } from "node:test";',
246
+ 'import { setupAgent, setupJudge } from "routekit/eval";',
247
+ 'const judge = setupJudge({ agent: setupAgent({ model: "openai/judge" }), minScore: 0.8 });',
248
+ 'test("support case", async () => {',
249
+ ' const run = await setupAgent({ model: "openai/cheap" }).run({ prompt: "Help", caseId: "support-case" });',
250
+ " run.toComplete();",
251
+ ' await judge.autoEvals({ criteria: "Helpful", prompt: "Help", run });',
252
+ "});"
253
+ ].join("\n"));
254
+ await writeManifest(root, ["openai/cheap"], "openai/judge", ["support-case"]);
255
+ const gateway = createServer((incoming, outgoing) => {
256
+ void (async () => {
257
+ const body = JSON.parse(await readBody(incoming));
258
+ await new Promise((resolve) => setTimeout(resolve, 25));
259
+ const content = body.model === "openai/judge"
260
+ ? JSON.stringify({ pass: true, reason: "helpful", score: 0.9 })
261
+ : "Helpful answer";
262
+ outgoing.writeHead(200, { "content-type": "application/json" });
263
+ outgoing.end(JSON.stringify({
264
+ model: body.model,
265
+ choices: [{ message: { role: "assistant", content } }]
266
+ }));
267
+ })();
268
+ });
269
+ await new Promise((resolve) => gateway.listen(0, "127.0.0.1", resolve));
270
+ const address = gateway.address();
271
+ assert.ok(address !== null && typeof address !== "string");
272
+ try {
273
+ const result = await Effect.runPromise(withEvalService({
274
+ bearerCredential: "parent-only-token",
275
+ timeoutMs: 15_000
276
+ }, (service) => service.runComparison({
277
+ ...requestFor(suite, `http://127.0.0.1:${String(address.port)}`),
278
+ candidateModels: ["openai/cheap"],
279
+ timeoutMs: 5
280
+ }, "full")).pipe(Effect.provide(NodeHttpClient.layerUndici)));
281
+ assert.equal(result.models[0]?.cases[0]?.outcome, "passed");
282
+ }
283
+ finally {
284
+ await new Promise((resolve, reject) => gateway.close((error) => (error === undefined ? resolve() : reject(error))));
285
+ }
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.11",
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.11",
36
- "@velum-labs/routekit-eval-engine": "1.0.11",
37
- "@velum-labs/routekit-eval-store": "1.0.11",
38
- "@velum-labs/routekit-registry": "1.0.11"
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"