@velum-labs/routekit-eval-core 1.0.26 → 1.1.0
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/basis-selection.d.ts +36 -0
- package/dist/basis-selection.js +118 -0
- package/dist/classification-metrics.d.ts +20 -0
- package/dist/classification-metrics.js +83 -0
- package/dist/effect-api.d.ts +2 -0
- package/dist/effect-api.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/routing-score.d.ts +19 -3
- package/dist/routing-score.js +15 -15
- package/dist/test/classification-v3.test.d.ts +1 -0
- package/dist/test/classification-v3.test.js +40 -0
- package/dist/test/routing-score.test.js +34 -6
- package/package.json +3 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RoutingBasisV3 } from "@velum-labs/routekit-eval-contracts";
|
|
2
|
+
export type RoutingBasisStructureFindingV3 = Readonly<{
|
|
3
|
+
severity: "hard" | "warning";
|
|
4
|
+
code: string;
|
|
5
|
+
dimensionIds: readonly string[];
|
|
6
|
+
detail: string;
|
|
7
|
+
}>;
|
|
8
|
+
export type RoutingBasisStructureReportV3 = Readonly<{
|
|
9
|
+
basisId: string;
|
|
10
|
+
basisDigest: string;
|
|
11
|
+
eligible: boolean;
|
|
12
|
+
dimensionCount: number;
|
|
13
|
+
definitionBytes: number;
|
|
14
|
+
findings: readonly RoutingBasisStructureFindingV3[];
|
|
15
|
+
}>;
|
|
16
|
+
export type RoutingBasisCandidateMetricsV3 = Readonly<{
|
|
17
|
+
basis: RoutingBasisV3;
|
|
18
|
+
structure: RoutingBasisStructureReportV3;
|
|
19
|
+
activeMae: number;
|
|
20
|
+
unknownMae: number;
|
|
21
|
+
referenceUnknownRate: number;
|
|
22
|
+
activeF1: number;
|
|
23
|
+
topDimensionAgreement: number;
|
|
24
|
+
provisional?: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
export type RoutingBasisCandidateSelectionV3 = Readonly<{
|
|
27
|
+
ranked: readonly Readonly<RoutingBasisCandidateMetricsV3 & {
|
|
28
|
+
selectionScore: number;
|
|
29
|
+
pareto: boolean;
|
|
30
|
+
}>[];
|
|
31
|
+
selectedBasisId?: string;
|
|
32
|
+
paretoBasisIds: readonly string[];
|
|
33
|
+
}>;
|
|
34
|
+
export declare function inspectRoutingBasisStructureV3(basis: RoutingBasisV3, aliasSimilarityThreshold?: number): RoutingBasisStructureReportV3;
|
|
35
|
+
export declare function routingBasisSelectionScoreV3(input: Pick<RoutingBasisCandidateMetricsV3, "activeMae" | "unknownMae" | "referenceUnknownRate">): number;
|
|
36
|
+
export declare function selectRoutingBasisCandidatesV3(candidates: readonly RoutingBasisCandidateMetricsV3[]): RoutingBasisCandidateSelectionV3;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const CATCH_ALL = /\b(other|misc(?:ellaneous)?|general|shared|common|everything else)\b/iu;
|
|
2
|
+
function tokens(dimension) {
|
|
3
|
+
const text = [dimension.name, dimension.description, ...dimension.inclusions]
|
|
4
|
+
.join(" ")
|
|
5
|
+
.toLocaleLowerCase("en-US");
|
|
6
|
+
return new Set(text.split(/[^a-z0-9]+/u).filter((token) => token.length > 2));
|
|
7
|
+
}
|
|
8
|
+
function jaccard(left, right) {
|
|
9
|
+
const intersection = [...left].filter((token) => right.has(token)).length;
|
|
10
|
+
const union = new Set([...left, ...right]).size;
|
|
11
|
+
return union === 0 ? 0 : intersection / union;
|
|
12
|
+
}
|
|
13
|
+
export function inspectRoutingBasisStructureV3(basis, aliasSimilarityThreshold = 0.58) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
for (const dimension of basis.dimensions) {
|
|
16
|
+
if (CATCH_ALL.test(`${dimension.id} ${dimension.name} ${dimension.description}`))
|
|
17
|
+
findings.push({
|
|
18
|
+
severity: "hard",
|
|
19
|
+
code: "catch_all_dimension",
|
|
20
|
+
dimensionIds: [dimension.id],
|
|
21
|
+
detail: "dimension appears to define an open-ended catch-all responsibility"
|
|
22
|
+
});
|
|
23
|
+
if (dimension.inclusions.length === 0 ||
|
|
24
|
+
dimension.exclusions.length === 0 ||
|
|
25
|
+
dimension.boundaryRules.length === 0)
|
|
26
|
+
findings.push({
|
|
27
|
+
severity: "hard",
|
|
28
|
+
code: "empty_semantics",
|
|
29
|
+
dimensionIds: [dimension.id],
|
|
30
|
+
detail: "dimension lacks inclusions, exclusions, or boundary rules"
|
|
31
|
+
});
|
|
32
|
+
if (dimension.repositoryFacets.componentIds.length +
|
|
33
|
+
dimension.repositoryFacets.pathAnchors.length +
|
|
34
|
+
dimension.repositoryFacets.symbolAnchors.length ===
|
|
35
|
+
0 &&
|
|
36
|
+
dimension.codeSummaries.length === 0)
|
|
37
|
+
findings.push({
|
|
38
|
+
severity: "hard",
|
|
39
|
+
code: "unsupported_dimension",
|
|
40
|
+
dimensionIds: [dimension.id],
|
|
41
|
+
detail: "dimension has no repository evidence anchors"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (let leftIndex = 0; leftIndex < basis.dimensions.length; leftIndex += 1) {
|
|
45
|
+
const left = basis.dimensions[leftIndex];
|
|
46
|
+
for (let rightIndex = leftIndex + 1; rightIndex < basis.dimensions.length; rightIndex += 1) {
|
|
47
|
+
const right = basis.dimensions[rightIndex];
|
|
48
|
+
if (left.id.startsWith(`${right.id}-`) || right.id.startsWith(`${left.id}-`))
|
|
49
|
+
findings.push({
|
|
50
|
+
severity: "hard",
|
|
51
|
+
code: "parent_child_pair",
|
|
52
|
+
dimensionIds: [left.id, right.id],
|
|
53
|
+
detail: "dimensions mix parent and child responsibilities at one routing level"
|
|
54
|
+
});
|
|
55
|
+
if (jaccard(tokens(left), tokens(right)) >= aliasSimilarityThreshold)
|
|
56
|
+
findings.push({
|
|
57
|
+
severity: "hard",
|
|
58
|
+
code: "near_alias_dimensions",
|
|
59
|
+
dimensionIds: [left.id, right.id],
|
|
60
|
+
detail: `semantic-token similarity meets the ${String(aliasSimilarityThreshold)} rejection threshold`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
basisId: basis.basisId,
|
|
66
|
+
basisDigest: basis.basisDigest,
|
|
67
|
+
eligible: !findings.some((finding) => finding.severity === "hard"),
|
|
68
|
+
dimensionCount: basis.dimensions.length,
|
|
69
|
+
definitionBytes: Buffer.byteLength(JSON.stringify(basis.dimensions), "utf8"),
|
|
70
|
+
findings
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function routingBasisSelectionScoreV3(input) {
|
|
74
|
+
return input.activeMae + 0.5 * input.unknownMae + 0.15 * input.referenceUnknownRate;
|
|
75
|
+
}
|
|
76
|
+
function dominates(left, right) {
|
|
77
|
+
const leftValues = [
|
|
78
|
+
left.activeMae,
|
|
79
|
+
left.unknownMae,
|
|
80
|
+
left.structure.dimensionCount,
|
|
81
|
+
left.structure.definitionBytes
|
|
82
|
+
];
|
|
83
|
+
const rightValues = [
|
|
84
|
+
right.activeMae,
|
|
85
|
+
right.unknownMae,
|
|
86
|
+
right.structure.dimensionCount,
|
|
87
|
+
right.structure.definitionBytes
|
|
88
|
+
];
|
|
89
|
+
return (leftValues.every((value, index) => value <= rightValues[index]) &&
|
|
90
|
+
leftValues.some((value, index) => value < rightValues[index]));
|
|
91
|
+
}
|
|
92
|
+
export function selectRoutingBasisCandidatesV3(candidates) {
|
|
93
|
+
const eligible = candidates.filter((candidate) => candidate.structure.eligible);
|
|
94
|
+
const pareto = eligible.filter((candidate) => !eligible.some((other) => other !== candidate && dominates(other, candidate)));
|
|
95
|
+
const paretoIds = new Set(pareto.map((candidate) => candidate.basis.basisId));
|
|
96
|
+
const ranked = candidates
|
|
97
|
+
.map((candidate) => ({
|
|
98
|
+
...candidate,
|
|
99
|
+
selectionScore: candidate.structure.eligible
|
|
100
|
+
? routingBasisSelectionScoreV3(candidate)
|
|
101
|
+
: Number.POSITIVE_INFINITY,
|
|
102
|
+
pareto: paretoIds.has(candidate.basis.basisId)
|
|
103
|
+
}))
|
|
104
|
+
.sort((left, right) => left.selectionScore - right.selectionScore ||
|
|
105
|
+
left.structure.dimensionCount - right.structure.dimensionCount ||
|
|
106
|
+
left.structure.definitionBytes - right.structure.definitionBytes ||
|
|
107
|
+
left.basis.basisId.localeCompare(right.basis.basisId));
|
|
108
|
+
return {
|
|
109
|
+
ranked,
|
|
110
|
+
...(ranked.find((candidate) => Number.isFinite(candidate.selectionScore)) === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: {
|
|
113
|
+
selectedBasisId: ranked.find((candidate) => Number.isFinite(candidate.selectionScore))
|
|
114
|
+
.basis.basisId
|
|
115
|
+
}),
|
|
116
|
+
paretoBasisIds: pareto.map((candidate) => candidate.basis.basisId).sort()
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DimensionScoreLabelV3, DimensionScorePredictionV3 } from "@velum-labs/routekit-eval-contracts";
|
|
2
|
+
export type ClassificationMetricOptionsV3 = Readonly<{
|
|
3
|
+
activeDimensionThreshold: number;
|
|
4
|
+
unknownThreshold: number;
|
|
5
|
+
}>;
|
|
6
|
+
export type ClassificationMetricsV3 = Readonly<{
|
|
7
|
+
caseCount: number;
|
|
8
|
+
skippedInvalidCount: number;
|
|
9
|
+
activeMae: number;
|
|
10
|
+
activeF1: number;
|
|
11
|
+
topDimensionAgreement: number;
|
|
12
|
+
unknownMae: number;
|
|
13
|
+
unknownBrier: number;
|
|
14
|
+
unknownAccuracy: number;
|
|
15
|
+
falseUnknownRate: number;
|
|
16
|
+
missedUnknownRate: number;
|
|
17
|
+
referenceUnknownRate: number;
|
|
18
|
+
averageActiveDimensionCount: number;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function evaluateClassificationMetricsV3(labels: readonly DimensionScoreLabelV3[], predictions: readonly DimensionScorePredictionV3[], options: ClassificationMetricOptionsV3): ClassificationMetricsV3;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function unit(value, label) {
|
|
2
|
+
if (!Number.isFinite(value) || value < 0 || value > 1)
|
|
3
|
+
throw new Error(`${label} must be between zero and one`);
|
|
4
|
+
}
|
|
5
|
+
function top(scores) {
|
|
6
|
+
return [...scores].sort((left, right) => right.score - left.score || left.dimensionId.localeCompare(right.dimensionId))[0]?.dimensionId;
|
|
7
|
+
}
|
|
8
|
+
export function evaluateClassificationMetricsV3(labels, predictions, options) {
|
|
9
|
+
unit(options.activeDimensionThreshold, "active dimension threshold");
|
|
10
|
+
unit(options.unknownThreshold, "unknown threshold");
|
|
11
|
+
const predictionByCase = new Map(predictions.map((prediction) => [prediction.caseId, prediction]));
|
|
12
|
+
if (predictionByCase.size !== predictions.length)
|
|
13
|
+
throw new Error("predictions contain duplicate case IDs");
|
|
14
|
+
let absolute = 0, scoreCount = 0, tp = 0, fp = 0, fn = 0, topAgreement = 0;
|
|
15
|
+
let unknownAbsolute = 0, unknownSquared = 0, unknownCorrect = 0, falseUnknown = 0, missedUnknown = 0, referenceUnknown = 0, activeCount = 0;
|
|
16
|
+
for (const label of labels) {
|
|
17
|
+
const prediction = predictionByCase.get(label.caseId);
|
|
18
|
+
if (prediction === undefined)
|
|
19
|
+
throw new Error(`missing prediction for case ${label.caseId}`);
|
|
20
|
+
if (prediction.basisDigest !== label.basisDigest)
|
|
21
|
+
throw new Error(`basis digest mismatch for case ${label.caseId}`);
|
|
22
|
+
const labelIds = label.scores.map((entry) => entry.dimensionId);
|
|
23
|
+
const predictionIds = prediction.scores.map((entry) => entry.dimensionId);
|
|
24
|
+
if (labelIds.length !== predictionIds.length ||
|
|
25
|
+
labelIds.some((id, index) => predictionIds[index] !== id))
|
|
26
|
+
throw new Error(`dimension mismatch for case ${label.caseId}`);
|
|
27
|
+
for (let index = 0; index < label.scores.length; index += 1) {
|
|
28
|
+
const expected = label.scores[index];
|
|
29
|
+
const actual = prediction.scores[index];
|
|
30
|
+
unit(expected.score, "reference score");
|
|
31
|
+
unit(actual.score, "prediction score");
|
|
32
|
+
absolute += Math.abs(expected.score - actual.score);
|
|
33
|
+
scoreCount += 1;
|
|
34
|
+
const expectedActive = expected.score >= options.activeDimensionThreshold;
|
|
35
|
+
const actualActive = actual.score >= options.activeDimensionThreshold;
|
|
36
|
+
if (expectedActive && actualActive)
|
|
37
|
+
tp += 1;
|
|
38
|
+
else if (!expectedActive && actualActive)
|
|
39
|
+
fp += 1;
|
|
40
|
+
else if (expectedActive)
|
|
41
|
+
fn += 1;
|
|
42
|
+
if (actualActive)
|
|
43
|
+
activeCount += 1;
|
|
44
|
+
}
|
|
45
|
+
if (top(label.scores) === top(prediction.scores))
|
|
46
|
+
topAgreement += 1;
|
|
47
|
+
unit(label.unknownProbability, "reference unknown probability");
|
|
48
|
+
unit(prediction.unknownProbability, "predicted unknown probability");
|
|
49
|
+
const delta = prediction.unknownProbability - label.unknownProbability;
|
|
50
|
+
unknownAbsolute += Math.abs(delta);
|
|
51
|
+
unknownSquared += delta * delta;
|
|
52
|
+
const expectedUnknown = label.unknownProbability >= options.unknownThreshold;
|
|
53
|
+
const actualUnknown = prediction.unknownProbability >= options.unknownThreshold;
|
|
54
|
+
if (expectedUnknown)
|
|
55
|
+
referenceUnknown += 1;
|
|
56
|
+
if (expectedUnknown === actualUnknown)
|
|
57
|
+
unknownCorrect += 1;
|
|
58
|
+
else if (actualUnknown)
|
|
59
|
+
falseUnknown += 1;
|
|
60
|
+
else
|
|
61
|
+
missedUnknown += 1;
|
|
62
|
+
}
|
|
63
|
+
if (predictionByCase.size !== labels.length)
|
|
64
|
+
throw new Error("predictions contain cases absent from the reference set");
|
|
65
|
+
const count = labels.length;
|
|
66
|
+
if (count === 0 || scoreCount === 0)
|
|
67
|
+
throw new Error("classification metrics require at least one case and dimension");
|
|
68
|
+
const f1Denominator = 2 * tp + fp + fn;
|
|
69
|
+
return {
|
|
70
|
+
caseCount: count,
|
|
71
|
+
skippedInvalidCount: 0,
|
|
72
|
+
activeMae: absolute / scoreCount,
|
|
73
|
+
activeF1: f1Denominator === 0 ? 1 : (2 * tp) / f1Denominator,
|
|
74
|
+
topDimensionAgreement: topAgreement / count,
|
|
75
|
+
unknownMae: unknownAbsolute / count,
|
|
76
|
+
unknownBrier: unknownSquared / count,
|
|
77
|
+
unknownAccuracy: unknownCorrect / count,
|
|
78
|
+
falseUnknownRate: falseUnknown / count,
|
|
79
|
+
missedUnknownRate: missedUnknown / count,
|
|
80
|
+
referenceUnknownRate: referenceUnknown / count,
|
|
81
|
+
averageActiveDimensionCount: activeCount / count
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/effect-api.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export type { EvalEgressOptions } from "./egress.js";
|
|
2
2
|
export { type RoutingModelAvailability, type RoutingScoreConstraints, type RoutingScoreResult, RoutingScoringError, type RoutingScoringErrorCode, type ScoreRoutingCandidatesInput, scoreRoutingCandidates } from "./routing-score.js";
|
|
3
|
+
export * from "./basis-selection.js";
|
|
4
|
+
export * from "./classification-metrics.js";
|
package/dist/effect-api.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export type { EvalEgressOptions } from "./egress.js";
|
|
2
|
-
export { type RoutingModelAvailability, type RoutingScoreConstraints, type RoutingScoreResult, RoutingScoringError, type RoutingScoringErrorCode, type ScoreRoutingCandidatesInput, scoreRoutingCandidates } from "./routing-score.js";
|
|
2
|
+
export { type RoutingModelAvailability, type RoutingScoreConstraints, type RoutingScoreComposition, type RoutingScoreEvidence, type RoutingScoreResult, RoutingScoringError, type RoutingScoringErrorCode, type ScoreRoutingCandidatesInput, scoreRoutingCandidates } from "./routing-score.js";
|
|
3
|
+
export * from "./basis-selection.js";
|
|
4
|
+
export * from "./classification-metrics.js";
|
package/dist/index.js
CHANGED
package/dist/routing-score.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModelDimensionEvidence, RequestRoutingRequirements, RoutingCandidateDecision, RoutingEndpoint, RoutingObjectivePolicy } from "@velum-labs/routekit-eval-contracts";
|
|
2
2
|
export type RoutingModelAvailability = Readonly<{
|
|
3
3
|
model: string;
|
|
4
4
|
served: boolean;
|
|
@@ -14,9 +14,25 @@ export type RoutingScoreConstraints = Readonly<{
|
|
|
14
14
|
/** Maximum weighted failure rate for a candidate. */
|
|
15
15
|
maximumFailureRate?: number;
|
|
16
16
|
}>;
|
|
17
|
+
/** Version-neutral evidence consumed by deterministic routing. */
|
|
18
|
+
export type RoutingScoreEvidence = Readonly<{
|
|
19
|
+
basisDigest: string;
|
|
20
|
+
dimensionIds: readonly string[];
|
|
21
|
+
candidateModels: readonly string[];
|
|
22
|
+
evidence: readonly ModelDimensionEvidence[];
|
|
23
|
+
}>;
|
|
24
|
+
/** Version-neutral composition consumed by deterministic routing. */
|
|
25
|
+
export type RoutingScoreComposition = Readonly<{
|
|
26
|
+
basisDigest: string;
|
|
27
|
+
weights: readonly Readonly<{
|
|
28
|
+
dimensionId: string;
|
|
29
|
+
weight: number;
|
|
30
|
+
}>[];
|
|
31
|
+
unassignedWeight: number;
|
|
32
|
+
}>;
|
|
17
33
|
export type ScoreRoutingCandidatesInput = Readonly<{
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
routingEvidence: RoutingScoreEvidence;
|
|
35
|
+
composition: RoutingScoreComposition;
|
|
20
36
|
requirements: RequestRoutingRequirements;
|
|
21
37
|
objective: RoutingObjectivePolicy;
|
|
22
38
|
availableModels: readonly RoutingModelAvailability[];
|
package/dist/routing-score.js
CHANGED
|
@@ -40,19 +40,19 @@ function compareNumberDescending(left, right) {
|
|
|
40
40
|
return right - left;
|
|
41
41
|
}
|
|
42
42
|
function activeWeights(input) {
|
|
43
|
-
if (input.
|
|
44
|
-
invalid("dimension definition digest does not match the routing
|
|
43
|
+
if (input.routingEvidence.basisDigest !== input.composition.basisDigest) {
|
|
44
|
+
invalid("dimension definition digest does not match the routing evidence");
|
|
45
45
|
}
|
|
46
|
-
const dimensionIds = input.
|
|
47
|
-
unique(dimensionIds, "
|
|
48
|
-
unique(input.
|
|
46
|
+
const dimensionIds = input.routingEvidence.dimensionIds;
|
|
47
|
+
unique(dimensionIds, "routing evidence dimension IDs");
|
|
48
|
+
unique(input.routingEvidence.candidateModels, "routing evidence candidate models");
|
|
49
49
|
if (dimensionIds.length === 0)
|
|
50
|
-
invalid("routing
|
|
51
|
-
if (input.
|
|
52
|
-
invalid("routing
|
|
50
|
+
invalid("routing evidence must contain at least one dimension");
|
|
51
|
+
if (input.routingEvidence.candidateModels.length === 0) {
|
|
52
|
+
invalid("routing evidence must contain at least one candidate model");
|
|
53
53
|
}
|
|
54
54
|
const weights = new Map();
|
|
55
|
-
for (const entry of input.
|
|
55
|
+
for (const entry of input.composition.weights) {
|
|
56
56
|
finiteUnit(entry.weight, `weight for dimension ${entry.dimensionId}`);
|
|
57
57
|
if (weights.has(entry.dimensionId))
|
|
58
58
|
invalid(`duplicate decomposition dimension: ${entry.dimensionId}`);
|
|
@@ -64,10 +64,10 @@ function activeWeights(input) {
|
|
|
64
64
|
if (!weights.has(dimensionId))
|
|
65
65
|
invalid(`missing decomposition dimension: ${dimensionId}`);
|
|
66
66
|
}
|
|
67
|
-
finiteUnit(input.
|
|
67
|
+
finiteUnit(input.composition.unassignedWeight, "unassigned weight");
|
|
68
68
|
const total = [...weights.values()].reduce((sum, weight) => sum + weight, 0);
|
|
69
|
-
if (Math.abs(total + input.
|
|
70
|
-
invalid("dimension weights and
|
|
69
|
+
if (Math.abs(total + input.composition.unassignedWeight - 1) > VECTOR_TOLERANCE) {
|
|
70
|
+
invalid("dimension weights and unassigned weight must sum to one");
|
|
71
71
|
}
|
|
72
72
|
if (total <= VECTOR_TOLERANCE)
|
|
73
73
|
invalid("routing requires at least one covered dimension");
|
|
@@ -81,7 +81,7 @@ function validateInput(input) {
|
|
|
81
81
|
const availableIds = input.availableModels.map((model) => model.model);
|
|
82
82
|
unique(availableIds, "available model IDs");
|
|
83
83
|
const floors = input.constraints?.minimumDimensionQuality ?? {};
|
|
84
|
-
const knownDimensions = new Set(input.
|
|
84
|
+
const knownDimensions = new Set(input.routingEvidence.dimensionIds);
|
|
85
85
|
for (const [dimensionId, floor] of Object.entries(floors)) {
|
|
86
86
|
if (!knownDimensions.has(dimensionId))
|
|
87
87
|
invalid(`quality floor refers to unknown dimension: ${dimensionId}`);
|
|
@@ -148,7 +148,7 @@ function capabilityReasons(availability, requirements) {
|
|
|
148
148
|
function calculateCandidates(input, active) {
|
|
149
149
|
const availability = new Map(input.availableModels.map((model) => [model.model, model]));
|
|
150
150
|
const floors = input.constraints?.minimumDimensionQuality ?? {};
|
|
151
|
-
return input.
|
|
151
|
+
return input.routingEvidence.candidateModels.map((model) => {
|
|
152
152
|
const reasons = capabilityReasons(availability.get(model), input.requirements);
|
|
153
153
|
let quality = 0;
|
|
154
154
|
let failureRate = 0;
|
|
@@ -158,7 +158,7 @@ function calculateCandidates(input, active) {
|
|
|
158
158
|
let completeDuration = true;
|
|
159
159
|
let completeCost = true;
|
|
160
160
|
for (const { dimensionId, weight } of active) {
|
|
161
|
-
const cells = input.
|
|
161
|
+
const cells = input.routingEvidence.evidence.filter((cell) => cell.model === model && cell.dimensionId === dimensionId);
|
|
162
162
|
if (cells.length === 0) {
|
|
163
163
|
reasons.push(`missing_evidence:${dimensionId}`);
|
|
164
164
|
completeCoreEvidence = false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { evaluateClassificationMetricsV3, routingBasisSelectionScoreV3 } from "../index.js";
|
|
4
|
+
const digest = "a".repeat(64);
|
|
5
|
+
test("classification metrics use independent score and separate unknown errors", () => {
|
|
6
|
+
const labels = [
|
|
7
|
+
{
|
|
8
|
+
caseId: "one",
|
|
9
|
+
basisDigest: digest,
|
|
10
|
+
scores: [
|
|
11
|
+
{ dimensionId: "a", score: 1 },
|
|
12
|
+
{ dimensionId: "b", score: 0 }
|
|
13
|
+
],
|
|
14
|
+
unknownProbability: 0
|
|
15
|
+
}
|
|
16
|
+
];
|
|
17
|
+
const predictions = [
|
|
18
|
+
{
|
|
19
|
+
...labels[0],
|
|
20
|
+
classifierConfigurationDigest: "b".repeat(64),
|
|
21
|
+
scores: [
|
|
22
|
+
{ dimensionId: "a", score: 0.8 },
|
|
23
|
+
{ dimensionId: "b", score: 0.3 }
|
|
24
|
+
],
|
|
25
|
+
unknownProbability: 0.2
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
const metrics = evaluateClassificationMetricsV3(labels, predictions, {
|
|
29
|
+
activeDimensionThreshold: 0.25,
|
|
30
|
+
unknownThreshold: 0.3
|
|
31
|
+
});
|
|
32
|
+
assert.ok(Math.abs(metrics.activeMae - 0.25) < 1e-12);
|
|
33
|
+
assert.equal(metrics.activeF1, 2 / 3);
|
|
34
|
+
assert.equal(metrics.topDimensionAgreement, 1);
|
|
35
|
+
assert.equal(metrics.unknownMae, 0.2);
|
|
36
|
+
assert.ok(Math.abs(metrics.unknownBrier - 0.04) < 1e-12);
|
|
37
|
+
});
|
|
38
|
+
test("basis scalar selection score uses the frozen formula", () => {
|
|
39
|
+
assert.equal(routingBasisSelectionScoreV3({ activeMae: 0.1, unknownMae: 0.2, referenceUnknownRate: 0.4 }), 0.26);
|
|
40
|
+
});
|
|
@@ -62,9 +62,19 @@ function available(model, overrides = {}) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
function run(evidence, objective = { kind: "highest-quality" }, options = {}) {
|
|
65
|
+
const routingSnapshot = snapshot(evidence, options.candidateModels);
|
|
65
66
|
return scoreRoutingCandidates({
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
routingEvidence: {
|
|
68
|
+
basisDigest: routingSnapshot.basisDigest,
|
|
69
|
+
dimensionIds: routingSnapshot.dimensions.map((dimension) => dimension.id),
|
|
70
|
+
candidateModels: routingSnapshot.candidateModels,
|
|
71
|
+
evidence: routingSnapshot.evidence
|
|
72
|
+
},
|
|
73
|
+
composition: {
|
|
74
|
+
basisDigest: (options.requestDecomposition ?? decomposition).basisDigest,
|
|
75
|
+
weights: (options.requestDecomposition ?? decomposition).weights,
|
|
76
|
+
unassignedWeight: (options.requestDecomposition ?? decomposition).unknownWeight
|
|
77
|
+
},
|
|
68
78
|
requirements,
|
|
69
79
|
objective,
|
|
70
80
|
availableModels: options.availableModels ?? models.map((model) => available(model))
|
|
@@ -99,8 +109,17 @@ test("hard requirements exclude unserved and incapable candidates before scoring
|
|
|
99
109
|
]
|
|
100
110
|
});
|
|
101
111
|
const withTools = scoreRoutingCandidates({
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
routingEvidence: {
|
|
113
|
+
basisDigest: snapshot(completeEvidence).basisDigest,
|
|
114
|
+
dimensionIds: snapshot(completeEvidence).dimensions.map((dimension) => dimension.id),
|
|
115
|
+
candidateModels: snapshot(completeEvidence).candidateModels,
|
|
116
|
+
evidence: snapshot(completeEvidence).evidence
|
|
117
|
+
},
|
|
118
|
+
composition: {
|
|
119
|
+
basisDigest: decomposition.basisDigest,
|
|
120
|
+
weights: decomposition.weights,
|
|
121
|
+
unassignedWeight: decomposition.unknownWeight
|
|
122
|
+
},
|
|
104
123
|
requirements: { ...requirements, requiresTools: true },
|
|
105
124
|
objective: { kind: "highest-quality" },
|
|
106
125
|
availableModels: [
|
|
@@ -128,8 +147,17 @@ test("unknown numeric basis limits do not claim a served model is incapable", ()
|
|
|
128
147
|
test("missing evidence and per-dimension quality floors fail candidates closed", () => {
|
|
129
148
|
const missingLastCell = completeEvidence.filter((entry) => !(entry.model === models[0] && entry.dimensionId === "docs"));
|
|
130
149
|
const result = scoreRoutingCandidates({
|
|
131
|
-
|
|
132
|
-
|
|
150
|
+
routingEvidence: {
|
|
151
|
+
basisDigest: snapshot(missingLastCell).basisDigest,
|
|
152
|
+
dimensionIds: snapshot(missingLastCell).dimensions.map((dimension) => dimension.id),
|
|
153
|
+
candidateModels: snapshot(missingLastCell).candidateModels,
|
|
154
|
+
evidence: snapshot(missingLastCell).evidence
|
|
155
|
+
},
|
|
156
|
+
composition: {
|
|
157
|
+
basisDigest: decomposition.basisDigest,
|
|
158
|
+
weights: decomposition.weights,
|
|
159
|
+
unassignedWeight: decomposition.unknownWeight
|
|
160
|
+
},
|
|
133
161
|
requirements,
|
|
134
162
|
objective: { kind: "highest-quality" },
|
|
135
163
|
availableModels: models.map((model) => available(model)),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-eval-core",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"effect": "4.0.0-rc.108",
|
|
34
|
-
"@velum-labs/routekit-eval-contracts": "1.0
|
|
35
|
-
"@velum-labs/routekit-runtime": "1.0
|
|
34
|
+
"@velum-labs/routekit-eval-contracts": "1.1.0",
|
|
35
|
+
"@velum-labs/routekit-runtime": "1.1.0"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"routekit",
|