@velum-labs/routekit-eval-service 1.0.16 → 1.0.18

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.
@@ -1,7 +1,7 @@
1
1
  export type { CompileDimensionEvidenceMatrixInput, CompiledDimensionEvidenceMatrix, DimensionComparisonEvidenceInput } from "./dimension-evidence.js";
2
2
  export { compileDimensionEvidenceMatrix, DimensionEvidenceCompilationError, wilsonLowerBound95 } from "./dimension-evidence.js";
3
3
  export { EvalServiceComparisonError, EvalServiceConfigurationError, EvalServiceEstimateError, EvalServicePolicyError, EvalServicePublicationError, EvalServiceSpendLimitError, EvalServiceValidationError } from "./errors.js";
4
- export type { RouteKitEvalServiceOptions } from "./production-runner.js";
4
+ export type { RouteKitEvalGatewayCallEvent, RouteKitEvalServiceOptions } from "./production-runner.js";
5
5
  export { EvalServiceCredentialError, makeRouteKitEvalServiceLayer } from "./production-runner.js";
6
6
  export type { DimensionMatrixQualificationInput, DimensionMatrixQualificationResult, DimensionMatrixSuite, EvalComparisonEstimate, EvalComparisonMode, EvalRunConfiguration, EvalServiceConfiguration, EvalServiceError, EvalServiceShape, EvalSuiteInspection } from "./service.js";
7
7
  export { EvalService } from "./service.js";
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { CompileDimensionEvidenceMatrixInput, CompiledDimensionEvidenceMatrix, DimensionComparisonEvidenceInput } from "./dimension-evidence.js";
2
2
  export { compileDimensionEvidenceMatrix, DimensionEvidenceCompilationError, wilsonLowerBound95 } from "./dimension-evidence.js";
3
3
  export { EvalServiceComparisonError, EvalServiceConfigurationError, EvalServiceEstimateError, EvalServicePolicyError, EvalServicePublicationError, EvalServiceSpendLimitError, EvalServiceValidationError } from "./errors.js";
4
- export type { RouteKitEvalServiceOptions } from "./production-runner.js";
4
+ export type { RouteKitEvalGatewayCallEvent, RouteKitEvalServiceOptions } from "./production-runner.js";
5
5
  export { EvalServiceCredentialError, makeRouteKitEvalServiceLayer } from "./production-runner.js";
6
6
  export type { DimensionMatrixQualificationInput, DimensionMatrixQualificationResult, DimensionMatrixSuite, EvalComparisonEstimate, EvalComparisonMode, EvalRunConfiguration, EvalServiceConfiguration, EvalServiceError, EvalServiceShape, EvalSuiteInspection } from "./service.js";
7
7
  export { EvalService } from "./service.js";
@@ -1,4 +1,15 @@
1
1
  import type { NodeTestExecutionOptions } from "@velum-labs/routekit-eval-engine";
2
+ import type { Effect } from "effect";
3
+ export type RouteKitEvalGatewayCallEvent = {
4
+ readonly observationId: string;
5
+ readonly phase: "issued";
6
+ readonly role: "candidate" | "judge";
7
+ } | {
8
+ readonly callId?: string;
9
+ readonly observationId: string;
10
+ readonly phase: "completed";
11
+ readonly role: "candidate" | "judge";
12
+ };
2
13
  export type RouteKitEvalServiceOptions = Omit<NodeTestExecutionOptions, "bridgeOrigin"> & {
3
14
  /**
4
15
  * Injected RouteKit data-plane credential.
@@ -15,6 +26,12 @@ export type RouteKitEvalServiceOptions = Omit<NodeTestExecutionOptions, "bridgeO
15
26
  * stale or missing timeout.
16
27
  */
17
28
  readonly timeoutMs?: number;
29
+ /**
30
+ * Observe the exact parent-owned HTTP client used by the child gateway
31
+ * bridge. The callback runs when an attributed request is issued and again
32
+ * when a model call id is available from its response.
33
+ */
34
+ readonly observeGatewayCall?: (event: RouteKitEvalGatewayCallEvent) => Effect.Effect<void>;
18
35
  /**
19
36
  * Run the node:test child from a temporary copy of the reviewed suite.
20
37
  *
@@ -1,8 +1,8 @@
1
1
  import { Layer } from "effect";
2
2
  import { HttpClient } from "effect/unstable/http";
3
- import type { RouteKitEvalServiceOptions } from "./layer-options.js";
3
+ import type { RouteKitEvalGatewayCallEvent, RouteKitEvalServiceOptions } from "./layer-options.js";
4
4
  import { EvalService, type EvalServiceConfiguration } from "./service.js";
5
- export type { RouteKitEvalServiceOptions };
5
+ export type { RouteKitEvalGatewayCallEvent, RouteKitEvalServiceOptions };
6
6
  declare const EvalServiceCredentialError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
7
7
  readonly _tag: "EvalServiceCredentialError";
8
8
  } & Readonly<A>;
@@ -1,4 +1,5 @@
1
1
  import { layer as NodeServicesLayer } from "@effect/platform-node/NodeServices";
2
+ import { EVAL_ATTRIBUTION_HEADER } from "@velum-labs/routekit-eval-contracts";
2
3
  import { EvalEngineExecutionError, EvalExecutionPort, makeEvalEngineLayer, makeRouteKitEvalExecutionPortService } from "@velum-labs/routekit-eval-engine";
3
4
  import { Data, Effect, FileSystem, Layer, Path, Stream } from "effect";
4
5
  import { HttpClient } from "effect/unstable/http";
@@ -8,11 +9,46 @@ export class EvalServiceCredentialError extends Data.TaggedError("EvalServiceCre
8
9
  return this.detail;
9
10
  }
10
11
  }
12
+ const MODEL_CALL_ID_HEADER = "x-routekit-model-call-id";
13
+ const gatewayCallRole = (raw) => {
14
+ if (raw === undefined)
15
+ return undefined;
16
+ try {
17
+ const decoded = JSON.parse(raw);
18
+ return decoded.role === "candidate" || decoded.role === "judge" ? decoded.role : undefined;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ };
24
+ const observeGatewayCalls = (client, observe) => {
25
+ let nextObservationId = 0;
26
+ return HttpClient.transform(client, (responseEffect, request) => {
27
+ const role = gatewayCallRole(request.headers[EVAL_ATTRIBUTION_HEADER]);
28
+ if (role === undefined)
29
+ return responseEffect;
30
+ return Effect.gen(function* () {
31
+ nextObservationId += 1;
32
+ const observationId = `gateway-call-${String(nextObservationId)}`;
33
+ yield* observe({ observationId, phase: "issued", role });
34
+ const response = yield* responseEffect;
35
+ const rawCallId = response.headers[MODEL_CALL_ID_HEADER]?.trim();
36
+ yield* observe({
37
+ observationId,
38
+ phase: "completed",
39
+ role,
40
+ ...(rawCallId === undefined || rawCallId.length === 0 ? {} : { callId: rawCallId })
41
+ });
42
+ return response;
43
+ });
44
+ });
45
+ };
11
46
  const makeProductionExecutionPort = (options) => Effect.gen(function* () {
12
47
  const fs = yield* FileSystem.FileSystem;
13
48
  const bearerCredential = options.bearerCredential?.trim();
14
49
  if (bearerCredential === undefined || bearerCredential.length === 0) {
15
50
  return EvalExecutionPort.of({
51
+ nodeTestExecPath: options.execPath ?? globalThis.process.execPath,
16
52
  execute: () => Stream.fail(new EvalEngineExecutionError({
17
53
  cause: new EvalServiceCredentialError({
18
54
  detail: "RouteKit Eval comparison execution requires an injected bearer credential."
@@ -21,7 +57,10 @@ const makeProductionExecutionPort = (options) => Effect.gen(function* () {
21
57
  }))
22
58
  });
23
59
  }
24
- const httpClient = yield* HttpClient.HttpClient;
60
+ const baseHttpClient = yield* HttpClient.HttpClient;
61
+ const httpClient = options.observeGatewayCall === undefined
62
+ ? baseHttpClient
63
+ : observeGatewayCalls(baseHttpClient, options.observeGatewayCall);
25
64
  const paths = yield* Path.Path;
26
65
  const execution = makeRouteKitEvalExecutionPortService({
27
66
  bearerCredential,
@@ -31,6 +70,7 @@ const makeProductionExecutionPort = (options) => Effect.gen(function* () {
31
70
  ...(options.execPath === undefined ? {} : { execPath: options.execPath })
32
71
  }, httpClient);
33
72
  return EvalExecutionPort.of({
73
+ nodeTestExecPath: execution.nodeTestExecPath,
34
74
  execute: (input) => {
35
75
  const deadlineInput = options.timeoutMs === undefined
36
76
  ? input
@@ -201,6 +201,7 @@ test("production runner executes candidate and judge traffic through the live en
201
201
  ].join("\n"));
202
202
  await writeManifest(root, ["openai/cheap"], "openai/judge", ["support-case"]);
203
203
  const calls = [];
204
+ const observed = [];
204
205
  const gateway = createServer((incoming, outgoing) => {
205
206
  void (async () => {
206
207
  const body = JSON.parse(await readBody(incoming));
@@ -212,7 +213,10 @@ test("production runner executes candidate and judge traffic through the live en
212
213
  const content = body.model === "openai/judge"
213
214
  ? JSON.stringify({ pass: true, reason: "helpful", score: 0.9 })
214
215
  : "Helpful answer";
215
- outgoing.writeHead(200, { "content-type": "application/json" });
216
+ outgoing.writeHead(200, {
217
+ "content-type": "application/json",
218
+ "x-routekit-model-call-id": `model-call-${String(calls.length)}`
219
+ });
216
220
  outgoing.end(JSON.stringify({
217
221
  model: body.model,
218
222
  choices: [{ message: { role: "assistant", content } }]
@@ -223,13 +227,38 @@ test("production runner executes candidate and judge traffic through the live en
223
227
  const address = gateway.address();
224
228
  assert.ok(address !== null && typeof address !== "string");
225
229
  try {
226
- const result = await Effect.runPromise(withEvalService({ bearerCredential: "parent-only-token" }, (service) => service.runComparison({
230
+ const result = await Effect.runPromise(withEvalService({
231
+ bearerCredential: "parent-only-token",
232
+ observeGatewayCall: (event) => Effect.sync(() => {
233
+ observed.push({
234
+ phase: event.phase,
235
+ role: event.role,
236
+ ...(event.phase === "completed" && event.callId !== undefined
237
+ ? { callId: event.callId }
238
+ : {})
239
+ });
240
+ })
241
+ }, (service) => service.runComparison({
227
242
  ...requestFor(suite, `http://127.0.0.1:${String(address.port)}`),
228
243
  candidateModels: ["openai/cheap"]
229
244
  }, "pilot")).pipe(Effect.provide(NodeHttpClient.layerUndici)));
230
245
  assert.deepEqual(calls.map(({ model }) => model), ["openai/cheap", "openai/judge"]);
231
246
  assert.equal(calls.every(({ authorization }) => authorization === "Bearer parent-only-token"), true);
232
247
  assert.equal(calls.every(({ maxOutputTokens }) => maxOutputTokens === 1_024), true);
248
+ assert.deepEqual(observed, [
249
+ { phase: "issued", role: "candidate" },
250
+ {
251
+ callId: "model-call-1",
252
+ phase: "completed",
253
+ role: "candidate"
254
+ },
255
+ { phase: "issued", role: "judge" },
256
+ {
257
+ callId: "model-call-2",
258
+ phase: "completed",
259
+ role: "judge"
260
+ }
261
+ ]);
233
262
  assert.deepEqual(result.models.map(({ model }) => model), ["openai/cheap"]);
234
263
  assert.equal(JSON.stringify(result).includes("parent-only-token"), false);
235
264
  }
@@ -284,6 +313,38 @@ test("production runner applies the host timeout at node:test instead of a stale
284
313
  await new Promise((resolve, reject) => gateway.close((error) => (error === undefined ? resolve() : reject(error))));
285
314
  }
286
315
  });
316
+ test("production runner uses its selected Node runtime for validation before observation", async () => {
317
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-spawn-failure-"));
318
+ roots.push(root);
319
+ const suite = path.join(root, "support.eval.ts");
320
+ await writeFile(suite, [
321
+ 'import { test } from "node:test";',
322
+ 'import { setupAgent } from "routekit/eval";',
323
+ 'test("support case", async () => {',
324
+ ' await setupAgent({ model: "openai/cheap" }).run({ prompt: "Help", caseId: "support-case" });',
325
+ "});"
326
+ ].join("\n"));
327
+ await writeManifest(root, ["openai/cheap"], "openai/judge", ["support-case"]);
328
+ const observed = [];
329
+ const exit = await Effect.runPromise(withEvalService({
330
+ bearerCredential: "parent-only-token",
331
+ execPath: path.join(root, "missing-node"),
332
+ observeGatewayCall: (event) => Effect.sync(() => {
333
+ observed.push(event);
334
+ }),
335
+ timeoutMs: 600_000
336
+ }, (service) => service.runComparison({
337
+ ...requestFor(suite),
338
+ candidateModels: ["openai/cheap"]
339
+ }, "full")).pipe(Effect.provide(NodeHttpClient.layerUndici), Effect.exit));
340
+ assert.equal(Exit.isFailure(exit), true);
341
+ assert.deepEqual(observed, []);
342
+ if (Exit.isFailure(exit)) {
343
+ assert.match(String(exit.cause), /validation failed/iu);
344
+ assert.match(String(exit.cause), /could not be loaded safely through node:test/iu);
345
+ assert.match(String(exit.cause), /missing-node/iu);
346
+ }
347
+ });
287
348
  test("production runner isolates qualification from a project SDK with a shorter hidden abort", async () => {
288
349
  const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-isolated-sdk-"));
289
350
  roots.push(root);
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.16",
4
+ "version": "1.0.18",
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.16",
36
- "@velum-labs/routekit-eval-engine": "1.0.16",
37
- "@velum-labs/routekit-eval-store": "1.0.16",
38
- "@velum-labs/routekit-registry": "1.0.16"
35
+ "@velum-labs/routekit-eval-contracts": "1.0.18",
36
+ "@velum-labs/routekit-eval-engine": "1.0.18",
37
+ "@velum-labs/routekit-eval-store": "1.0.18",
38
+ "@velum-labs/routekit-registry": "1.0.18"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "7.0.2"