@velum-labs/routekit-eval-setup 1.0.6 → 1.0.8
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.
|
@@ -127,6 +127,72 @@ export function selectProjectAuthoringSourceFiles(input) {
|
|
|
127
127
|
const DimensionsOutput = Schema.Struct({
|
|
128
128
|
dimensions: Schema.Array(WorkloadDimension)
|
|
129
129
|
});
|
|
130
|
+
function schemaRecord(value) {
|
|
131
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
132
|
+
? value
|
|
133
|
+
: undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Anthropic removes unsupported structured-output constraints from its wire
|
|
137
|
+
* schema. Enforce those deferred constraints against the decoded response so
|
|
138
|
+
* authoring validation remains provider-independent.
|
|
139
|
+
*/
|
|
140
|
+
function assertDeferredSchemaConstraints(schema, value, path = "$") {
|
|
141
|
+
const record = schemaRecord(schema);
|
|
142
|
+
if (record === undefined)
|
|
143
|
+
return;
|
|
144
|
+
if (typeof value === "number") {
|
|
145
|
+
if (typeof record.minimum === "number" && value < record.minimum) {
|
|
146
|
+
throw new Error(`${path} must be greater than or equal to ${String(record.minimum)}`);
|
|
147
|
+
}
|
|
148
|
+
if (typeof record.maximum === "number" && value > record.maximum) {
|
|
149
|
+
throw new Error(`${path} must be less than or equal to ${String(record.maximum)}`);
|
|
150
|
+
}
|
|
151
|
+
if (typeof record.exclusiveMinimum === "number" && value <= record.exclusiveMinimum) {
|
|
152
|
+
throw new Error(`${path} must be greater than ${String(record.exclusiveMinimum)}`);
|
|
153
|
+
}
|
|
154
|
+
if (typeof record.exclusiveMaximum === "number" && value >= record.exclusiveMaximum) {
|
|
155
|
+
throw new Error(`${path} must be less than ${String(record.exclusiveMaximum)}`);
|
|
156
|
+
}
|
|
157
|
+
if (typeof record.multipleOf === "number" &&
|
|
158
|
+
record.multipleOf !== 0 &&
|
|
159
|
+
Math.abs(value / record.multipleOf - Math.round(value / record.multipleOf)) >
|
|
160
|
+
Number.EPSILON * Math.max(1, Math.abs(value / record.multipleOf)) * 8) {
|
|
161
|
+
throw new Error(`${path} must be a multiple of ${String(record.multipleOf)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === "string") {
|
|
165
|
+
const length = [...value].length;
|
|
166
|
+
if (typeof record.minLength === "number" && length < record.minLength) {
|
|
167
|
+
throw new Error(`${path} must contain at least ${String(record.minLength)} characters`);
|
|
168
|
+
}
|
|
169
|
+
if (typeof record.maxLength === "number" && length > record.maxLength) {
|
|
170
|
+
throw new Error(`${path} must contain at most ${String(record.maxLength)} characters`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
if (typeof record.minItems === "number" &&
|
|
175
|
+
record.minItems > 1 &&
|
|
176
|
+
value.length < record.minItems) {
|
|
177
|
+
throw new Error(`${path} must contain at least ${String(record.minItems)} items`);
|
|
178
|
+
}
|
|
179
|
+
if (typeof record.maxItems === "number" && value.length > record.maxItems) {
|
|
180
|
+
throw new Error(`${path} must contain at most ${String(record.maxItems)} items`);
|
|
181
|
+
}
|
|
182
|
+
for (const [index, item] of value.entries()) {
|
|
183
|
+
assertDeferredSchemaConstraints(record.items, item, `${path}[${String(index)}]`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const valueRecord = schemaRecord(value);
|
|
187
|
+
const properties = schemaRecord(record.properties);
|
|
188
|
+
if (valueRecord !== undefined && properties !== undefined) {
|
|
189
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
190
|
+
if (Object.hasOwn(valueRecord, key)) {
|
|
191
|
+
assertDeferredSchemaConstraints(propertySchema, valueRecord[key], `${path}.${key}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
130
196
|
const DIMENSIONS_JSON_SCHEMA = {
|
|
131
197
|
type: "object",
|
|
132
198
|
additionalProperties: false,
|
|
@@ -350,11 +416,14 @@ export const makeEvalProjectAuthor = Effect.gen(function* () {
|
|
|
350
416
|
});
|
|
351
417
|
const decoded = yield* Schema.decodeUnknownEffect(DimensionsOutput)(yield* parseJson("authoring-dimensions", text)).pipe(Effect.mapError((cause) => failure("authoring-dimensions", "dimension proposal failed validation", cause)));
|
|
352
418
|
yield* Effect.try({
|
|
353
|
-
try: () =>
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
419
|
+
try: () => {
|
|
420
|
+
assertDeferredSchemaConstraints(DIMENSIONS_JSON_SCHEMA, decoded);
|
|
421
|
+
assertRoutingBasis({
|
|
422
|
+
version: 2,
|
|
423
|
+
basisDigest: "authoring-validation",
|
|
424
|
+
dimensions: decoded.dimensions
|
|
425
|
+
});
|
|
426
|
+
},
|
|
358
427
|
catch: (cause) => failure("authoring-dimensions", "dimension proposal failed validation", cause)
|
|
359
428
|
});
|
|
360
429
|
return decoded.dimensions;
|
|
@@ -377,6 +446,10 @@ export const makeEvalProjectAuthor = Effect.gen(function* () {
|
|
|
377
446
|
maximumOutputTokens: 16_384
|
|
378
447
|
});
|
|
379
448
|
const suite = yield* Schema.decodeUnknownEffect(EvalDimensionSuiteSchema)(yield* parseJson("authoring-evaluations", text)).pipe(Effect.mapError((cause) => failure("authoring-evaluations", `suite for ${JSON.stringify(dimension.id)} failed validation`, cause)));
|
|
449
|
+
yield* Effect.try({
|
|
450
|
+
try: () => assertDeferredSchemaConstraints(SUITE_JSON_SCHEMA, suite),
|
|
451
|
+
catch: (cause) => failure("authoring-evaluations", `suite for ${JSON.stringify(dimension.id)} failed validation`, cause)
|
|
452
|
+
});
|
|
380
453
|
if (suite.dimensionId !== dimension.id ||
|
|
381
454
|
suite.cases.length !== EVAL_AUTHORING_CASES_PER_DIMENSION ||
|
|
382
455
|
new Set(suite.cases.map((testCase) => testCase.id)).size !== suite.cases.length) {
|
|
@@ -399,6 +472,10 @@ export const makeEvalProjectAuthor = Effect.gen(function* () {
|
|
|
399
472
|
maximumOutputTokens: 16_384
|
|
400
473
|
});
|
|
401
474
|
const decompositionBenchmark = yield* Schema.decodeUnknownEffect(EvalDecompositionBenchmarkSchema)(yield* parseJson("authoring-evaluations", decompositionText)).pipe(Effect.mapError((cause) => failure("authoring-evaluations", "decomposition benchmark failed validation", cause)));
|
|
475
|
+
yield* Effect.try({
|
|
476
|
+
try: () => assertDeferredSchemaConstraints(decompositionBenchmarkJsonSchema(dimensionIds), decompositionBenchmark),
|
|
477
|
+
catch: (cause) => failure("authoring-evaluations", "decomposition benchmark failed validation", cause)
|
|
478
|
+
});
|
|
402
479
|
for (const benchmarkCase of decompositionBenchmark.cases) {
|
|
403
480
|
yield* Effect.try({
|
|
404
481
|
try: () => assertDecompositionResult(benchmarkCase.expected, input.basis),
|
|
@@ -419,6 +496,10 @@ export const makeEvalProjectAuthor = Effect.gen(function* () {
|
|
|
419
496
|
maximumOutputTokens: 16_384
|
|
420
497
|
});
|
|
421
498
|
const compositionSuite = yield* Schema.decodeUnknownEffect(EvalCompositionSuiteSchema)(yield* parseJson("authoring-evaluations", compositionText)).pipe(Effect.mapError((cause) => failure("authoring-evaluations", "composition benchmark failed validation", cause)));
|
|
499
|
+
yield* Effect.try({
|
|
500
|
+
try: () => assertDeferredSchemaConstraints(compositionSuiteJsonSchema(dimensionIds), compositionSuite),
|
|
501
|
+
catch: (cause) => failure("authoring-evaluations", "composition benchmark failed validation", cause)
|
|
502
|
+
});
|
|
422
503
|
for (const compositionCase of compositionSuite.cases) {
|
|
423
504
|
yield* Effect.try({
|
|
424
505
|
try: () => assertDecompositionResult(compositionCase.decomposition, input.basis),
|
|
@@ -6,7 +6,8 @@ import test from "node:test";
|
|
|
6
6
|
import { layer as NodeServicesLayer } from "@effect/platform-node/NodeServices";
|
|
7
7
|
import { Effect, Layer } from "effect";
|
|
8
8
|
import { EvalProjectAuthoringError } from "../errors.js";
|
|
9
|
-
import { EvalAuthoringTransport, EvalProjectAuthor, EvalProjectAuthorLive, readProjectAuthoringSources } from "../project-authoring.js";
|
|
9
|
+
import { EVAL_AUTHORING_CASES_PER_DIMENSION, EvalAuthoringTransport, EvalProjectAuthor, EvalProjectAuthorLive, readProjectAuthoringSources } from "../project-authoring.js";
|
|
10
|
+
import { EVAL_PROJECT_VERSION } from "../project-contracts.js";
|
|
10
11
|
const withRepository = async (use) => {
|
|
11
12
|
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-author-sources-"));
|
|
12
13
|
const outside = await mkdtemp(path.join(os.tmpdir(), "routekit-author-outside-"));
|
|
@@ -52,6 +53,81 @@ const proposeDimensions = (root, output, requests = []) => Effect.runPromise(Eff
|
|
|
52
53
|
return JSON.stringify(output);
|
|
53
54
|
})
|
|
54
55
|
}))), Layer.provide(NodeServicesLayer)))));
|
|
56
|
+
const basis = {
|
|
57
|
+
version: 2,
|
|
58
|
+
basisDigest: "basis-digest",
|
|
59
|
+
dimensions: dimensions(5)
|
|
60
|
+
};
|
|
61
|
+
const dimensionCases = () => Array.from({ length: EVAL_AUTHORING_CASES_PER_DIMENSION }, (_, index) => ({
|
|
62
|
+
id: `dimension-case-${String(index + 1)}`,
|
|
63
|
+
prompt: `Answer production request ${String(index + 1)}.`,
|
|
64
|
+
context: "Reference context.",
|
|
65
|
+
rubric: "State the expected production behavior."
|
|
66
|
+
}));
|
|
67
|
+
const dimensionSuite = (dimensionId = basis.dimensions[0].id) => ({
|
|
68
|
+
version: EVAL_PROJECT_VERSION,
|
|
69
|
+
dimensionId,
|
|
70
|
+
maximumOutputTokens: 256,
|
|
71
|
+
cases: dimensionCases()
|
|
72
|
+
});
|
|
73
|
+
const decompositionWeights = () => basis.dimensions.map((dimension) => ({
|
|
74
|
+
dimensionId: dimension.id,
|
|
75
|
+
weight: 1 / basis.dimensions.length
|
|
76
|
+
}));
|
|
77
|
+
const decompositionBenchmark = () => ({
|
|
78
|
+
maximumVectorL1Error: 0.25,
|
|
79
|
+
cases: Array.from({ length: EVAL_AUTHORING_CASES_PER_DIMENSION }, (_, index) => ({
|
|
80
|
+
id: `decomposition-case-${String(index + 1)}`,
|
|
81
|
+
request: `Classify production request ${String(index + 1)}.`,
|
|
82
|
+
expected: {
|
|
83
|
+
weights: decompositionWeights(),
|
|
84
|
+
unknownWeight: 0
|
|
85
|
+
}
|
|
86
|
+
}))
|
|
87
|
+
});
|
|
88
|
+
const compositionSuite = () => ({
|
|
89
|
+
maximumOutputTokens: 256,
|
|
90
|
+
minimumWinnerScoreGap: 0.05,
|
|
91
|
+
minimumWinnerAgreement: 0.8,
|
|
92
|
+
cases: Array.from({ length: EVAL_AUTHORING_CASES_PER_DIMENSION }, (_, index) => ({
|
|
93
|
+
id: `composition-case-${String(index + 1)}`,
|
|
94
|
+
prompt: `Compose production response ${String(index + 1)}.`,
|
|
95
|
+
context: "Reference context.",
|
|
96
|
+
rubric: "State the expected composed production behavior.",
|
|
97
|
+
decomposition: {
|
|
98
|
+
weights: decompositionWeights(),
|
|
99
|
+
unknownWeight: 0
|
|
100
|
+
},
|
|
101
|
+
requirements: {
|
|
102
|
+
endpoint: "chat",
|
|
103
|
+
requiresTools: false,
|
|
104
|
+
requiresVision: false,
|
|
105
|
+
inputTokens: 128,
|
|
106
|
+
maxOutputTokens: 256
|
|
107
|
+
}
|
|
108
|
+
}))
|
|
109
|
+
});
|
|
110
|
+
const proposeEvaluations = (root, outputs) => Effect.runPromise(Effect.gen(function* () {
|
|
111
|
+
const author = yield* EvalProjectAuthor;
|
|
112
|
+
return yield* author.proposeEvaluations({
|
|
113
|
+
operationId: "eng-833",
|
|
114
|
+
repositoryRoot: root,
|
|
115
|
+
sourceInventory: ["source.md"],
|
|
116
|
+
configuration,
|
|
117
|
+
basis
|
|
118
|
+
});
|
|
119
|
+
}).pipe(Effect.provide(EvalProjectAuthorLive.pipe(Layer.provide(Layer.succeed(EvalAuthoringTransport, EvalAuthoringTransport.of({
|
|
120
|
+
complete: (input) => Effect.sync(() => {
|
|
121
|
+
if (input.schemaName === "routekit_dimension_suite") {
|
|
122
|
+
const dimension = basis.dimensions.find((candidate) => input.operationId.endsWith(`:${candidate.id}`));
|
|
123
|
+
return JSON.stringify(outputs.suite ?? dimensionSuite(dimension?.id ?? basis.dimensions[0].id));
|
|
124
|
+
}
|
|
125
|
+
if (input.schemaName === "routekit_decomposition_benchmark") {
|
|
126
|
+
return JSON.stringify(outputs.decomposition ?? decompositionBenchmark());
|
|
127
|
+
}
|
|
128
|
+
return JSON.stringify(outputs.composition ?? compositionSuite());
|
|
129
|
+
})
|
|
130
|
+
}))), Layer.provide(NodeServicesLayer)))));
|
|
55
131
|
const collectSchemaNumberKeyword = (value, keyword) => {
|
|
56
132
|
if (typeof value !== "object" || value === null)
|
|
57
133
|
return [];
|
|
@@ -142,3 +218,43 @@ test("dimension authoring enforces routing basis counts after structured output
|
|
|
142
218
|
});
|
|
143
219
|
});
|
|
144
220
|
});
|
|
221
|
+
test("evaluation authoring enforces Anthropic-deferred bounds after parsing", async () => {
|
|
222
|
+
await withRepository(async ({ root }) => {
|
|
223
|
+
for (const maximumOutputTokens of [0, 16_385]) {
|
|
224
|
+
await assert.rejects(proposeEvaluations(root, {
|
|
225
|
+
suite: { ...dimensionSuite(), maximumOutputTokens }
|
|
226
|
+
}), (error) => {
|
|
227
|
+
assert.ok(error instanceof EvalProjectAuthoringError);
|
|
228
|
+
assert.match(error.detail, /suite .* failed validation/u);
|
|
229
|
+
assert.match(String(error.cause), /maximumOutputTokens/u);
|
|
230
|
+
return true;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
await assert.rejects(proposeEvaluations(root, {
|
|
234
|
+
suite: {
|
|
235
|
+
...dimensionSuite(),
|
|
236
|
+
cases: dimensionCases().slice(0, EVAL_AUTHORING_CASES_PER_DIMENSION - 1)
|
|
237
|
+
}
|
|
238
|
+
}), (error) => {
|
|
239
|
+
assert.ok(error instanceof EvalProjectAuthoringError);
|
|
240
|
+
assert.match(String(error.cause), /cases must contain at least 20 items/u);
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
await assert.rejects(proposeEvaluations(root, {
|
|
244
|
+
decomposition: { ...decompositionBenchmark(), maximumVectorL1Error: 3 }
|
|
245
|
+
}), (error) => {
|
|
246
|
+
assert.ok(error instanceof EvalProjectAuthoringError);
|
|
247
|
+
assert.equal(error.detail, "decomposition benchmark failed validation");
|
|
248
|
+
assert.match(String(error.cause), /maximumVectorL1Error/u);
|
|
249
|
+
return true;
|
|
250
|
+
});
|
|
251
|
+
await assert.rejects(proposeEvaluations(root, {
|
|
252
|
+
composition: { ...compositionSuite(), minimumWinnerAgreement: 2 }
|
|
253
|
+
}), (error) => {
|
|
254
|
+
assert.ok(error instanceof EvalProjectAuthoringError);
|
|
255
|
+
assert.equal(error.detail, "composition benchmark failed validation");
|
|
256
|
+
assert.match(String(error.cause), /minimumWinnerAgreement/u);
|
|
257
|
+
return true;
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-eval-setup",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.8",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"effect": "4.0.0-rc.108",
|
|
35
|
-
"@velum-labs/routekit-eval-contracts": "1.0.
|
|
36
|
-
"@velum-labs/routekit-runtime": "1.0.
|
|
35
|
+
"@velum-labs/routekit-eval-contracts": "1.0.8",
|
|
36
|
+
"@velum-labs/routekit-runtime": "1.0.8"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
39
39
|
"routekit",
|