@velum-labs/routekit-gateway 1.2.0 → 1.3.1
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/README.md +7 -0
- package/dist/adapters/anthropic-models.js +1 -1
- package/dist/endpoints/responses-endpoint.js +2 -2
- package/dist/http/auth.d.ts +4 -0
- package/dist/http/auth.js +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/routing/eval-policy.d.ts +4 -2
- package/dist/routing/eval-policy.js +86 -20
- package/dist/routing-api.d.ts +1 -0
- package/dist/routing-api.js +1 -0
- package/dist/test/anthropic.test.js +6 -0
- package/dist/test/auth.test.js +18 -0
- package/dist/test/compositional-routing.test.js +94 -2
- package/dist/test/eval-policy.test.js +1 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -26,3 +26,10 @@ API-key providers use registry-defined credentials and URLs. Multi-account
|
|
|
26
26
|
subscription providers and relays are in `@velum-labs/routekit-accounts`; they expose the
|
|
27
27
|
same source interface with per-model account eligibility and quota-aware
|
|
28
28
|
selection. RouteKit hosts wire this package into the singleton daemon gateway.
|
|
29
|
+
|
|
30
|
+
Compositional routing accepts explicit named repository selectors as
|
|
31
|
+
`model: "auto:<name>"`, or as `x-routekit-routing-profile: <name>` with
|
|
32
|
+
`model: "auto"`. The host supplies trusted repository context (the singleton
|
|
33
|
+
uses its repository-bound launcher credential); request headers never select a
|
|
34
|
+
filesystem root. The gateway rejects malformed, unbound, and conflicting
|
|
35
|
+
selectors before routing.
|
|
@@ -62,7 +62,7 @@ export function resolveClaudeModelSelection(requested, modelIds = [], modelRoute
|
|
|
62
62
|
}
|
|
63
63
|
if (requested.startsWith(CLAUDE_PICKER_PREFIX)) {
|
|
64
64
|
const candidate = requested.slice(CLAUDE_PICKER_PREFIX.length);
|
|
65
|
-
if (modelIds.includes(candidate)) {
|
|
65
|
+
if (modelIds.includes(candidate) || candidate.startsWith("auto:")) {
|
|
66
66
|
return {
|
|
67
67
|
status: "resolved",
|
|
68
68
|
model: candidate,
|
|
@@ -5,7 +5,7 @@ import { prepareResponsesReasoningInput, wrapResponsesReasoningResponse } from "
|
|
|
5
5
|
import { handleResponses } from "../adapters/responses.js";
|
|
6
6
|
import { decodeValidatedResponsesRequest, validateResponsesRequest } from "../adapters/validate.js";
|
|
7
7
|
import { gatewayTry } from "../effect/gateway.js";
|
|
8
|
-
import { compositionalRoutingAttribution, evalAutoRouterRejection, evalRequestAttribution, resolveConfiguredAutoRoutingModel } from "../routing/eval-policy.js";
|
|
8
|
+
import { compositionalRoutingAttribution, evalAutoRouterRejection, evalRequestAttribution, isAutoRoutingModel, resolveConfiguredAutoRoutingModel } from "../routing/eval-policy.js";
|
|
9
9
|
import { extractClassifiableRequestText } from "../routing/classifier.js";
|
|
10
10
|
import { UnknownModelError } from "../routing/router.js";
|
|
11
11
|
import { deriveRoutingRequirements } from "../routing/requirements.js";
|
|
@@ -62,7 +62,7 @@ function executeResponsesRequest(dependencies, request) {
|
|
|
62
62
|
});
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
-
if (decodedBody.model
|
|
65
|
+
if (isAutoRoutingModel(decodedBody.model) &&
|
|
66
66
|
decodedBody.previous_response_id != null) {
|
|
67
67
|
context.transport.writeJson(400, {
|
|
68
68
|
error: {
|
package/dist/http/auth.d.ts
CHANGED
package/dist/http/auth.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* so neither content nor length differences are observable through timing.
|
|
5
5
|
*/
|
|
6
6
|
import { createPublicKey, timingSafeEqual, verify } from "node:crypto";
|
|
7
|
+
import { isAbsolute } from "node:path";
|
|
8
|
+
import { ROUTING_PROFILE_NAME_PATTERN } from "@velum-labs/routekit-config-core";
|
|
7
9
|
/** Trusted principal header injected by the switching proxy after auth. */
|
|
8
10
|
export const ROUTEKIT_PRINCIPAL_HEADER = "x-routekit-principal";
|
|
9
11
|
function decodeJwtPart(value) {
|
|
@@ -198,10 +200,28 @@ export function parsePrincipalHeader(value) {
|
|
|
198
200
|
: undefined;
|
|
199
201
|
if (parsed.role === "eval" && evalSession === undefined)
|
|
200
202
|
return undefined;
|
|
203
|
+
const routing = parsed.role !== "eval" &&
|
|
204
|
+
parsed.routing !== undefined &&
|
|
205
|
+
typeof parsed.routing === "object" &&
|
|
206
|
+
parsed.routing !== null &&
|
|
207
|
+
typeof parsed.routing.repositoryRoot === "string" &&
|
|
208
|
+
parsed.routing.repositoryRoot.length > 0 &&
|
|
209
|
+
parsed.routing.repositoryRoot.length <= 4_096 &&
|
|
210
|
+
isAbsolute(parsed.routing.repositoryRoot) &&
|
|
211
|
+
typeof parsed.routing.profileName === "string" &&
|
|
212
|
+
ROUTING_PROFILE_NAME_PATTERN.test(parsed.routing.profileName)
|
|
213
|
+
? {
|
|
214
|
+
repositoryRoot: parsed.routing.repositoryRoot,
|
|
215
|
+
profileName: parsed.routing.profileName
|
|
216
|
+
}
|
|
217
|
+
: undefined;
|
|
218
|
+
if (parsed.routing !== undefined && routing === undefined)
|
|
219
|
+
return undefined;
|
|
201
220
|
return {
|
|
202
221
|
id: parsed.id,
|
|
203
222
|
label: parsed.label,
|
|
204
223
|
role: parsed.role,
|
|
224
|
+
...(routing === undefined ? {} : { routing }),
|
|
205
225
|
...(evalSession === undefined ? {} : { evalSession })
|
|
206
226
|
};
|
|
207
227
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export { endpointHealthProbe, probeEndpointHealth, providerAuthHeaders } from ".
|
|
|
34
34
|
export type { EndpointPipeline } from "./endpoint-pipeline.js";
|
|
35
35
|
export { runEndpointPipeline } from "./endpoint-pipeline.js";
|
|
36
36
|
export type { CompositionalRoutingObservation, CompositionalRoutingPolicyReader, CompositionalRoutingRuntime } from "./routing/eval-policy.js";
|
|
37
|
-
export { AutoRoutingUnavailableError, compositionalRoutingAttribution, compositionalRoutingPolicyReaderFromActivation, EvalAutoRoutingForbiddenError, RoutingPolicyReadError, resolveCompositionalAutoRoutingModel, resolveConfiguredAutoRoutingModel } from "./routing/eval-policy.js";
|
|
37
|
+
export { AutoRoutingUnavailableError, compositionalRoutingAttribution, compositionalRoutingPolicyReaderFromActivation, EvalAutoRoutingForbiddenError, isAutoRoutingModel, RoutingPolicyReadError, resolveCompositionalAutoRoutingModel, resolveConfiguredAutoRoutingModel } from "./routing/eval-policy.js";
|
|
38
38
|
export { invokeObservedModelCall } from "./model-call-service.js";
|
|
39
39
|
export type { OpenAiBackendOptions } from "./providers/openai-backend.js";
|
|
40
40
|
export { OpenAiBackend } from "./providers/openai-backend.js";
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { CompositionalRoutingError, routeCompositionalRequest } from "./routing/
|
|
|
17
17
|
export { DEFAULT_MODEL_PRICING, estimateCost, formatUsd, lookupPricing, meterCall, parseUsage, parseUsageFromSse } from "./observability/cost.js";
|
|
18
18
|
export { endpointHealthProbe, probeEndpointHealth, providerAuthHeaders } from "./endpoint-health-service.js";
|
|
19
19
|
export { runEndpointPipeline } from "./endpoint-pipeline.js";
|
|
20
|
-
export { AutoRoutingUnavailableError, compositionalRoutingAttribution, compositionalRoutingPolicyReaderFromActivation, EvalAutoRoutingForbiddenError, RoutingPolicyReadError, resolveCompositionalAutoRoutingModel, resolveConfiguredAutoRoutingModel } from "./routing/eval-policy.js";
|
|
20
|
+
export { AutoRoutingUnavailableError, compositionalRoutingAttribution, compositionalRoutingPolicyReaderFromActivation, EvalAutoRoutingForbiddenError, isAutoRoutingModel, RoutingPolicyReadError, resolveCompositionalAutoRoutingModel, resolveConfiguredAutoRoutingModel } from "./routing/eval-policy.js";
|
|
21
21
|
export { invokeObservedModelCall } from "./model-call-service.js";
|
|
22
22
|
export { OpenAiBackend } from "./providers/openai-backend.js";
|
|
23
23
|
export { buildModelCallRecord, MODEL_CALL_ID_HEADER, modelCallId, readProducerVersion, resolveProducerGitSha, responseBodyHash, UNKNOWN_GIT_SHA } from "./observability/provenance.js";
|
|
@@ -17,10 +17,10 @@ export declare class RoutingPolicyReadError extends RoutingPolicyReadError_base<
|
|
|
17
17
|
}
|
|
18
18
|
/** Read-only online projection of the authoritative routing activation. */
|
|
19
19
|
export type CompositionalRoutingPolicyReader = Readonly<{
|
|
20
|
-
getActivation(): Effect.Effect<AnyPublishedRoutingActivation | undefined, RoutingPolicyReadError, RouteKitPlatform>;
|
|
20
|
+
getActivation(profileName?: string, repositoryRoot?: string): Effect.Effect<AnyPublishedRoutingActivation | undefined, RoutingPolicyReadError, RouteKitPlatform>;
|
|
21
21
|
}>;
|
|
22
22
|
export declare function compositionalRoutingPolicyReaderFromActivation(activation: AnyPublishedRoutingActivation | undefined): CompositionalRoutingPolicyReader;
|
|
23
|
-
export declare function authoritativeRoutingActivation(reader: CompositionalRoutingPolicyReader): Effect.Effect<AnyPublishedRoutingActivation | undefined, RoutingPolicyReadError, RouteKitPlatform>;
|
|
23
|
+
export declare function authoritativeRoutingActivation(reader: CompositionalRoutingPolicyReader, profileName?: string, repositoryRoot?: string): Effect.Effect<AnyPublishedRoutingActivation | undefined, RoutingPolicyReadError, RouteKitPlatform>;
|
|
24
24
|
declare const AutoRoutingUnavailableError_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 & {
|
|
25
25
|
readonly _tag: "AutoRoutingUnavailableError";
|
|
26
26
|
} & Readonly<A>;
|
|
@@ -58,6 +58,7 @@ export type CompositionalRoutingRuntime = Readonly<{
|
|
|
58
58
|
export declare function compositionalRoutingAttribution(observation: Extract<CompositionalRoutingObservation, {
|
|
59
59
|
status: "decided";
|
|
60
60
|
}>): NonNullable<RequestAttribution["compositional_routing"]>;
|
|
61
|
+
export declare function isAutoRoutingModel(model: unknown): boolean;
|
|
61
62
|
export declare function evalPolicyBypassRequested(headers: IncomingHttpHeaders): boolean;
|
|
62
63
|
export declare function evalRequestAttribution(headers: IncomingHttpHeaders): NonNullable<RequestAttribution["eval"]> | undefined;
|
|
63
64
|
/** Reject eval traffic that would fall through to the auto-router. */
|
|
@@ -76,6 +77,7 @@ export declare function resolveCompositionalAutoRoutingModel(options: Readonly<{
|
|
|
76
77
|
objective: RoutingObjectivePolicy;
|
|
77
78
|
maximumUnknownWeight: number;
|
|
78
79
|
constraints?: RoutingScoreConstraints;
|
|
80
|
+
profileName?: string;
|
|
79
81
|
onDecision?(decision: AutoRoutingDecision, classifierCallId?: string): void;
|
|
80
82
|
}>): Effect.Effect<string | undefined, AutoRoutingUnavailableError | EvalAutoRoutingForbiddenError, RouteKitPlatform>;
|
|
81
83
|
/** Apply the authoritative dimension-decomposition and evidence-matrix auto-router. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ROUTING_PROFILE_NAME_PATTERN } from "@velum-labs/routekit-config-core";
|
|
2
|
+
import { COMPOSITIONAL_ROUTING_VERSION, EVAL_ATTRIBUTION_HEADER, EVAL_POLICY_BYPASS_HEADER, isForbiddenEvalModel, ROUTING_PROFILE_HEADER } from "@velum-labs/routekit-eval-contracts";
|
|
2
3
|
import { Data, Effect } from "effect";
|
|
3
4
|
import { parsePrincipalHeader, ROUTEKIT_PRINCIPAL_HEADER } from "../http/auth.js";
|
|
4
5
|
import { classifyRequestDimensions, validateDecompositionResult } from "../routing/classifier.js";
|
|
@@ -12,8 +13,8 @@ export function compositionalRoutingPolicyReaderFromActivation(activation) {
|
|
|
12
13
|
getActivation: () => Effect.succeed(activation)
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
|
-
export function authoritativeRoutingActivation(reader) {
|
|
16
|
-
return reader.getActivation();
|
|
16
|
+
export function authoritativeRoutingActivation(reader, profileName, repositoryRoot) {
|
|
17
|
+
return reader.getActivation(profileName, repositoryRoot);
|
|
17
18
|
}
|
|
18
19
|
export class AutoRoutingUnavailableError extends Data.TaggedError("AutoRoutingUnavailableError") {
|
|
19
20
|
}
|
|
@@ -91,6 +92,56 @@ function firstHeader(headers, name) {
|
|
|
91
92
|
const normalized = raw?.trim();
|
|
92
93
|
return normalized === undefined || normalized.length === 0 ? undefined : normalized;
|
|
93
94
|
}
|
|
95
|
+
function autoRoutingModelProfile(model) {
|
|
96
|
+
const normalized = model?.trim();
|
|
97
|
+
if (normalized === undefined || normalized.length === 0)
|
|
98
|
+
return { auto: false };
|
|
99
|
+
if (normalized.toLowerCase() === "auto")
|
|
100
|
+
return { auto: true, valid: true };
|
|
101
|
+
if (!normalized.toLowerCase().startsWith("auto:"))
|
|
102
|
+
return { auto: false };
|
|
103
|
+
const profileName = normalized.slice("auto:".length);
|
|
104
|
+
return {
|
|
105
|
+
auto: true,
|
|
106
|
+
profileName,
|
|
107
|
+
valid: ROUTING_PROFILE_NAME_PATTERN.test(profileName)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function isAutoRoutingModel(model) {
|
|
111
|
+
return typeof model === "string" && autoRoutingModelProfile(model).auto;
|
|
112
|
+
}
|
|
113
|
+
function autoRoutingSelector(headers, model) {
|
|
114
|
+
const parsedModel = autoRoutingModelProfile(model);
|
|
115
|
+
if (!parsedModel.auto)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (!parsedModel.valid) {
|
|
118
|
+
throw new Error(`model routing profile must match ${String(ROUTING_PROFILE_NAME_PATTERN)}`);
|
|
119
|
+
}
|
|
120
|
+
const modelProfile = parsedModel.profileName;
|
|
121
|
+
const headerProfile = firstHeader(headers, ROUTING_PROFILE_HEADER);
|
|
122
|
+
const principal = parsePrincipalHeader(firstHeader(headers, ROUTEKIT_PRINCIPAL_HEADER));
|
|
123
|
+
const boundProfile = principal?.routing?.profileName;
|
|
124
|
+
if (headerProfile !== undefined && !ROUTING_PROFILE_NAME_PATTERN.test(headerProfile)) {
|
|
125
|
+
throw new Error(`routing profile header must match ${String(ROUTING_PROFILE_NAME_PATTERN)}`);
|
|
126
|
+
}
|
|
127
|
+
if (modelProfile !== undefined && headerProfile !== undefined && modelProfile !== headerProfile) {
|
|
128
|
+
throw new Error(`model routing profile ${JSON.stringify(modelProfile)} conflicts with header profile ${JSON.stringify(headerProfile)}`);
|
|
129
|
+
}
|
|
130
|
+
const requestedProfile = modelProfile ?? headerProfile;
|
|
131
|
+
if (requestedProfile !== undefined &&
|
|
132
|
+
boundProfile !== undefined &&
|
|
133
|
+
requestedProfile !== boundProfile) {
|
|
134
|
+
throw new Error(`requested routing profile ${JSON.stringify(requestedProfile)} conflicts with launcher profile ${JSON.stringify(boundProfile)}`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
...((requestedProfile ?? boundProfile) === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: { profileName: requestedProfile ?? boundProfile }),
|
|
140
|
+
...(principal?.routing?.repositoryRoot === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { repositoryRoot: principal.routing.repositoryRoot })
|
|
143
|
+
};
|
|
144
|
+
}
|
|
94
145
|
export function evalPolicyBypassRequested(headers) {
|
|
95
146
|
const raw = firstHeader(headers, EVAL_POLICY_BYPASS_HEADER);
|
|
96
147
|
if (raw !== "1" && raw?.toLowerCase() !== "true")
|
|
@@ -135,7 +186,7 @@ export function evalRequestAttribution(headers) {
|
|
|
135
186
|
export function evalAutoRouterRejection(headers, model) {
|
|
136
187
|
if (!evalPolicyBypassRequested(headers))
|
|
137
188
|
return undefined;
|
|
138
|
-
if (typeof model !== "string" || isForbiddenEvalModel(model)) {
|
|
189
|
+
if (typeof model !== "string" || isForbiddenEvalModel(model) || isAutoRoutingModel(model)) {
|
|
139
190
|
return "eval requests must name an explicit provider/model id";
|
|
140
191
|
}
|
|
141
192
|
const principal = evalSessionPrincipal(headers);
|
|
@@ -148,7 +199,7 @@ export function evalAutoRouterRejection(headers, model) {
|
|
|
148
199
|
* Resolve `model: "auto"` against the model-by-dimension evidence matrix.
|
|
149
200
|
*/
|
|
150
201
|
export function resolveCompositionalAutoRoutingModel(options) {
|
|
151
|
-
if (options.model
|
|
202
|
+
if (!isAutoRoutingModel(options.model)) {
|
|
152
203
|
return Effect.succeed(options.model);
|
|
153
204
|
}
|
|
154
205
|
if (evalPolicyBypassRequested(options.headers)) {
|
|
@@ -173,33 +224,35 @@ export function resolveCompositionalAutoRoutingModel(options) {
|
|
|
173
224
|
const classifier = options.classifier;
|
|
174
225
|
return Effect.gen(function* () {
|
|
175
226
|
const readSnapshot = yield* Effect.try({
|
|
176
|
-
try: () => reader.getActivation(),
|
|
227
|
+
try: () => reader.getActivation(options.profileName),
|
|
177
228
|
catch: (cause) => new AutoRoutingUnavailableError({
|
|
178
|
-
profileId:
|
|
229
|
+
profileId: options.profileName,
|
|
179
230
|
message: "failed to read the compositional routing snapshot",
|
|
180
231
|
cause
|
|
181
232
|
})
|
|
182
233
|
});
|
|
183
234
|
const snapshot = yield* readSnapshot.pipe(Effect.mapError((cause) => new AutoRoutingUnavailableError({
|
|
184
|
-
profileId:
|
|
235
|
+
profileId: options.profileName,
|
|
185
236
|
message: "failed to read the compositional routing snapshot",
|
|
186
237
|
cause
|
|
187
238
|
})));
|
|
188
239
|
if (snapshot === undefined) {
|
|
189
240
|
return yield* new AutoRoutingUnavailableError({
|
|
190
|
-
profileId:
|
|
191
|
-
message:
|
|
241
|
+
profileId: options.profileName,
|
|
242
|
+
message: options.profileName === undefined
|
|
243
|
+
? "no compositional routing snapshot is available"
|
|
244
|
+
: `no compositional routing snapshot is available for profile ${JSON.stringify(options.profileName)}`
|
|
192
245
|
});
|
|
193
246
|
}
|
|
194
247
|
if (snapshot.version !== COMPOSITIONAL_ROUTING_VERSION) {
|
|
195
248
|
return yield* new AutoRoutingUnavailableError({
|
|
196
|
-
profileId:
|
|
249
|
+
profileId: options.profileName,
|
|
197
250
|
message: "the compositional routing snapshot is not a V2 activation"
|
|
198
251
|
});
|
|
199
252
|
}
|
|
200
253
|
if (classifier.model !== undefined && classifier.model !== snapshot.classifierModel) {
|
|
201
254
|
return yield* new AutoRoutingUnavailableError({
|
|
202
|
-
profileId:
|
|
255
|
+
profileId: options.profileName,
|
|
203
256
|
message: `published routing activation requires classifier ${JSON.stringify(snapshot.classifierModel)}, but the running router is bound to ${JSON.stringify(classifier.model)}`
|
|
204
257
|
});
|
|
205
258
|
}
|
|
@@ -207,7 +260,7 @@ export function resolveCompositionalAutoRoutingModel(options) {
|
|
|
207
260
|
request: requestText,
|
|
208
261
|
dimensions: snapshot.dimensions
|
|
209
262
|
}).pipe(Effect.mapError((error) => new AutoRoutingUnavailableError({
|
|
210
|
-
profileId:
|
|
263
|
+
profileId: options.profileName,
|
|
211
264
|
message: error.message,
|
|
212
265
|
cause: error
|
|
213
266
|
})));
|
|
@@ -216,7 +269,7 @@ export function resolveCompositionalAutoRoutingModel(options) {
|
|
|
216
269
|
basisDigest: snapshot.basisDigest,
|
|
217
270
|
dimensions: snapshot.dimensions
|
|
218
271
|
}).pipe(Effect.mapError((error) => new AutoRoutingUnavailableError({
|
|
219
|
-
profileId:
|
|
272
|
+
profileId: options.profileName,
|
|
220
273
|
message: error.message,
|
|
221
274
|
cause: error
|
|
222
275
|
})));
|
|
@@ -234,7 +287,7 @@ export function resolveCompositionalAutoRoutingModel(options) {
|
|
|
234
287
|
maximumUnknownWeight: snapshot.maximumUnknownWeight,
|
|
235
288
|
...(snapshot.constraints === undefined ? {} : { constraints: snapshot.constraints })
|
|
236
289
|
}).pipe(Effect.mapError((cause) => new AutoRoutingUnavailableError({
|
|
237
|
-
profileId:
|
|
290
|
+
profileId: options.profileName,
|
|
238
291
|
message: cause.message,
|
|
239
292
|
cause
|
|
240
293
|
})));
|
|
@@ -284,7 +337,7 @@ function resolveAuthoritativeV3(options) {
|
|
|
284
337
|
options.onDecision(decision, classifierCallId);
|
|
285
338
|
return decision.selectedModel;
|
|
286
339
|
}).pipe(Effect.mapError((cause) => new AutoRoutingUnavailableError({
|
|
287
|
-
profileId:
|
|
340
|
+
profileId: options.profileName,
|
|
288
341
|
message: cause instanceof V3AuthoritativeRoutingFailure
|
|
289
342
|
? `authoritative compositional routing failed: ${cause.reason}`
|
|
290
343
|
: "authoritative compositional routing failed",
|
|
@@ -294,7 +347,18 @@ function resolveAuthoritativeV3(options) {
|
|
|
294
347
|
/** Apply the authoritative dimension-decomposition and evidence-matrix auto-router. */
|
|
295
348
|
export function resolveConfiguredAutoRoutingModel(options) {
|
|
296
349
|
const runtime = options.compositionalRouting;
|
|
297
|
-
|
|
350
|
+
let selector;
|
|
351
|
+
try {
|
|
352
|
+
selector = autoRoutingSelector(options.headers, options.model);
|
|
353
|
+
}
|
|
354
|
+
catch (cause) {
|
|
355
|
+
return Effect.fail(new AutoRoutingUnavailableError({
|
|
356
|
+
profileId: undefined,
|
|
357
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
358
|
+
cause
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
if (selector === undefined)
|
|
298
362
|
return Effect.succeed(options.model);
|
|
299
363
|
if (evalPolicyBypassRequested(options.headers))
|
|
300
364
|
return Effect.fail(new EvalAutoRoutingForbiddenError({
|
|
@@ -309,15 +373,15 @@ export function resolveConfiguredAutoRoutingModel(options) {
|
|
|
309
373
|
runtime.onObservation?.(observation);
|
|
310
374
|
options.onCompositionalObservation?.(observation);
|
|
311
375
|
};
|
|
312
|
-
const resolved = authoritativeRoutingActivation(runtime.policyReader).pipe(Effect.mapError((cause) => new AutoRoutingUnavailableError({
|
|
313
|
-
profileId:
|
|
376
|
+
const resolved = authoritativeRoutingActivation(runtime.policyReader, selector.profileName, selector.repositoryRoot).pipe(Effect.mapError((cause) => new AutoRoutingUnavailableError({
|
|
377
|
+
profileId: selector.profileName,
|
|
314
378
|
message: "failed to read the compositional routing deployment",
|
|
315
379
|
cause
|
|
316
380
|
})), Effect.flatMap((activation) => {
|
|
317
381
|
if (activation?.version === 3) {
|
|
318
382
|
if (options.requestBody === undefined || options.dialect === undefined)
|
|
319
383
|
return Effect.fail(new AutoRoutingUnavailableError({
|
|
320
|
-
profileId:
|
|
384
|
+
profileId: selector.profileName,
|
|
321
385
|
message: "authoritative compositional routing requires a request body and dialect"
|
|
322
386
|
}));
|
|
323
387
|
return resolveAuthoritativeV3({
|
|
@@ -327,6 +391,7 @@ export function resolveConfiguredAutoRoutingModel(options) {
|
|
|
327
391
|
requirements: options.requirements,
|
|
328
392
|
classifier: runtime.classifier,
|
|
329
393
|
availableModels: runtime.availableModels,
|
|
394
|
+
...(selector.profileName === undefined ? {} : { profileName: selector.profileName }),
|
|
330
395
|
onDecision: (decision, classifierCallId) => observe({
|
|
331
396
|
status: "decided",
|
|
332
397
|
decision,
|
|
@@ -346,6 +411,7 @@ export function resolveConfiguredAutoRoutingModel(options) {
|
|
|
346
411
|
objective: runtime.objective,
|
|
347
412
|
maximumUnknownWeight: runtime.maximumUnknownWeight,
|
|
348
413
|
...(runtime.constraints === undefined ? {} : { constraints: runtime.constraints }),
|
|
414
|
+
...(selector.profileName === undefined ? {} : { profileName: selector.profileName }),
|
|
349
415
|
onDecision: (decision, classifierCallId) => observe({
|
|
350
416
|
status: "decided",
|
|
351
417
|
decision,
|
package/dist/routing-api.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { DimensionDecompositionOperation, LanguageModelDimensionClassifierOptions, ObservedDecompositionResult } from "./routing/classifier.js";
|
|
2
2
|
export { CLASSIFIABLE_REQUEST_TEXT_LIMIT, ClassificationError, classifyRequestDimensions, extractClassifiableRequestText, makeFakeDimensionDecomposition, makeLanguageModelDimensionClassifier, parseDecompositionResult, validateDecompositionInput, validateDecompositionResult } from "./routing/classifier.js";
|
|
3
|
+
export { isAutoRoutingModel } from "./routing/eval-policy.js";
|
|
3
4
|
export { fakeDimensionRequestDecomposerLayer, languageModelDimensionRequestDecomposerLayer } from "./routing/dimension-request-decomposer.js";
|
|
4
5
|
export type { RequestDecomposerService } from "./services/request-decomposer/service.js";
|
|
5
6
|
export { RequestDecomposer } from "./services/request-decomposer/service.js";
|
package/dist/routing-api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { CLASSIFIABLE_REQUEST_TEXT_LIMIT, ClassificationError, classifyRequestDimensions, extractClassifiableRequestText, makeFakeDimensionDecomposition, makeLanguageModelDimensionClassifier, parseDecompositionResult, validateDecompositionInput, validateDecompositionResult } from "./routing/classifier.js";
|
|
2
|
+
export { isAutoRoutingModel } from "./routing/eval-policy.js";
|
|
2
3
|
export { fakeDimensionRequestDecomposerLayer, languageModelDimensionRequestDecomposerLayer } from "./routing/dimension-request-decomposer.js";
|
|
3
4
|
export { RequestDecomposer } from "./services/request-decomposer/service.js";
|
|
4
5
|
export { isSubscriptionProvider, modelPolicyAllowsModel, modelPolicyRuleMatches, NoModelAvailableError, RoutingBackend, UnknownModelError } from "./routing/router.js";
|
|
@@ -44,6 +44,12 @@ test("Claude selection accepts picker ids and unique native ids but rejects coll
|
|
|
44
44
|
clientModel: "anthropic.routekit.codex/gpt-5.5",
|
|
45
45
|
selection: { mode: "auto" }
|
|
46
46
|
});
|
|
47
|
+
assert.deepEqual(resolveClaudeModelSelection("anthropic.routekit.auto:quality", []), {
|
|
48
|
+
status: "resolved",
|
|
49
|
+
model: "auto:quality",
|
|
50
|
+
clientModel: "anthropic.routekit.auto:quality",
|
|
51
|
+
selection: { mode: "auto" }
|
|
52
|
+
});
|
|
47
53
|
assert.deepEqual(resolveClaudeModelSelection("gpt-5.5", ["openai/gpt-5.5", "fast"], [
|
|
48
54
|
{ publicId: "openai/gpt-5.5", nativeId: "gpt-5.5", provider: "openai", reasoning },
|
|
49
55
|
{ publicId: "fast", nativeId: "gpt-5.5", provider: "openai", reasoning }
|
package/dist/test/auth.test.js
CHANGED
|
@@ -47,6 +47,23 @@ test("parsePrincipalHeader accepts only well-formed JSON principals", () => {
|
|
|
47
47
|
label: "bob",
|
|
48
48
|
role: "admin"
|
|
49
49
|
});
|
|
50
|
+
assert.deepEqual(parsePrincipalHeader(JSON.stringify({
|
|
51
|
+
id: "launcher-token",
|
|
52
|
+
label: "launcher:quality",
|
|
53
|
+
role: "owner",
|
|
54
|
+
routing: {
|
|
55
|
+
repositoryRoot: "/workspace/repository",
|
|
56
|
+
profileName: "quality"
|
|
57
|
+
}
|
|
58
|
+
})), {
|
|
59
|
+
id: "launcher-token",
|
|
60
|
+
label: "launcher:quality",
|
|
61
|
+
role: "owner",
|
|
62
|
+
routing: {
|
|
63
|
+
repositoryRoot: "/workspace/repository",
|
|
64
|
+
profileName: "quality"
|
|
65
|
+
}
|
|
66
|
+
});
|
|
50
67
|
assert.deepEqual(parsePrincipalHeader(JSON.stringify({
|
|
51
68
|
id: "eval-token",
|
|
52
69
|
label: "eval-session",
|
|
@@ -67,6 +84,7 @@ test("parsePrincipalHeader accepts only well-formed JSON principals", () => {
|
|
|
67
84
|
}
|
|
68
85
|
});
|
|
69
86
|
assert.equal(parsePrincipalHeader('{"id":"eval-token","label":"eval-session","role":"eval"}'), undefined);
|
|
87
|
+
assert.equal(parsePrincipalHeader('{"id":"launcher","label":"launcher:quality","role":"owner","routing":{"repositoryRoot":"relative","profileName":"quality"}}'), undefined);
|
|
70
88
|
assert.equal(parsePrincipalHeader('{"id":"a","label":"bob","role":"root"}'), undefined);
|
|
71
89
|
assert.equal(parsePrincipalHeader("not-json"), undefined);
|
|
72
90
|
});
|
|
@@ -3,7 +3,9 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import { Effect } from "effect";
|
|
5
5
|
import { activationRevisionDigest, COMPOSITIONAL_ROUTING_VERSION } from "@velum-labs/routekit-eval-contracts";
|
|
6
|
+
import { ROUTING_PROFILE_HEADER } from "@velum-labs/routekit-eval-contracts";
|
|
6
7
|
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
8
|
+
import { ROUTEKIT_PRINCIPAL_HEADER } from "../http/auth.js";
|
|
7
9
|
import { CompositionalRoutingError, routeCompositionalRequest } from "../routing/compositional.js";
|
|
8
10
|
import { AutoRoutingUnavailableError, compositionalRoutingPolicyReaderFromActivation, resolveCompositionalAutoRoutingModel, resolveConfiguredAutoRoutingModel } from "../routing/eval-policy.js";
|
|
9
11
|
import { fakeDimensionRequestDecomposerLayer } from "../routing/dimension-request-decomposer.js";
|
|
@@ -291,6 +293,97 @@ test("configured auto routing uses only dimension decomposition and matrix scori
|
|
|
291
293
|
assert.equal(resolved, "openai/beta");
|
|
292
294
|
assert.deepEqual(observations, ["decided:openai/beta"]);
|
|
293
295
|
});
|
|
296
|
+
test("configured auto routing selects a named profile from model or header", async () => {
|
|
297
|
+
const selectedProfiles = [];
|
|
298
|
+
const policyReader = {
|
|
299
|
+
getActivation: (profileName) => {
|
|
300
|
+
selectedProfiles.push(profileName);
|
|
301
|
+
return Effect.succeed(snapshot());
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const compositionalRouting = {
|
|
305
|
+
policyReader,
|
|
306
|
+
classifier: fakeRequestDecomposer({
|
|
307
|
+
weights: decomposition().weights,
|
|
308
|
+
unknownWeight: 0
|
|
309
|
+
}),
|
|
310
|
+
availableModels,
|
|
311
|
+
objective: { kind: "highest-quality" },
|
|
312
|
+
maximumUnknownWeight: 0.25
|
|
313
|
+
};
|
|
314
|
+
assert.equal(await runRouteKitEffect(resolveConfiguredAutoRoutingModel({
|
|
315
|
+
headers: {},
|
|
316
|
+
model: "auto:quality",
|
|
317
|
+
requestText: "Implement a change",
|
|
318
|
+
requirements,
|
|
319
|
+
compositionalRouting
|
|
320
|
+
})), "openai/beta");
|
|
321
|
+
assert.equal(await runRouteKitEffect(resolveConfiguredAutoRoutingModel({
|
|
322
|
+
headers: { [ROUTING_PROFILE_HEADER]: "cost" },
|
|
323
|
+
model: "auto",
|
|
324
|
+
requestText: "Implement a change",
|
|
325
|
+
requirements,
|
|
326
|
+
compositionalRouting
|
|
327
|
+
})), "openai/beta");
|
|
328
|
+
assert.deepEqual(selectedProfiles, ["quality", "cost"]);
|
|
329
|
+
await assert.rejects(runRouteKitEffect(resolveConfiguredAutoRoutingModel({
|
|
330
|
+
headers: { [ROUTING_PROFILE_HEADER]: "cost" },
|
|
331
|
+
model: "auto:quality",
|
|
332
|
+
requestText: "Implement a change",
|
|
333
|
+
requirements,
|
|
334
|
+
compositionalRouting
|
|
335
|
+
})), /conflicts with header profile/);
|
|
336
|
+
});
|
|
337
|
+
test("configured auto routing uses trusted launcher context and rejects profile changes", async () => {
|
|
338
|
+
const reads = [];
|
|
339
|
+
const routingPrincipal = JSON.stringify({
|
|
340
|
+
id: "launcher-token",
|
|
341
|
+
label: "launcher:quality",
|
|
342
|
+
role: "owner",
|
|
343
|
+
routing: {
|
|
344
|
+
repositoryRoot: "/workspace/repository",
|
|
345
|
+
profileName: "quality"
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
const compositionalRouting = {
|
|
349
|
+
policyReader: {
|
|
350
|
+
getActivation: (profileName, repositoryRoot) => {
|
|
351
|
+
reads.push({ profileName, repositoryRoot });
|
|
352
|
+
return Effect.succeed(snapshot());
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
classifier: fakeRequestDecomposer({
|
|
356
|
+
weights: decomposition().weights,
|
|
357
|
+
unknownWeight: 0
|
|
358
|
+
}),
|
|
359
|
+
availableModels,
|
|
360
|
+
objective: { kind: "highest-quality" },
|
|
361
|
+
maximumUnknownWeight: 0.25
|
|
362
|
+
};
|
|
363
|
+
assert.equal(await runRouteKitEffect(resolveConfiguredAutoRoutingModel({
|
|
364
|
+
headers: { [ROUTEKIT_PRINCIPAL_HEADER]: routingPrincipal },
|
|
365
|
+
model: "auto",
|
|
366
|
+
requestText: "Implement a change",
|
|
367
|
+
requirements,
|
|
368
|
+
compositionalRouting
|
|
369
|
+
})), "openai/beta");
|
|
370
|
+
assert.deepEqual(reads, [
|
|
371
|
+
{
|
|
372
|
+
profileName: "quality",
|
|
373
|
+
repositoryRoot: "/workspace/repository"
|
|
374
|
+
}
|
|
375
|
+
]);
|
|
376
|
+
await assert.rejects(runRouteKitEffect(resolveConfiguredAutoRoutingModel({
|
|
377
|
+
headers: {
|
|
378
|
+
[ROUTEKIT_PRINCIPAL_HEADER]: routingPrincipal,
|
|
379
|
+
[ROUTING_PROFILE_HEADER]: "cost"
|
|
380
|
+
},
|
|
381
|
+
model: "auto",
|
|
382
|
+
requestText: "Implement a change",
|
|
383
|
+
requirements,
|
|
384
|
+
compositionalRouting
|
|
385
|
+
})), /conflicts with launcher profile/);
|
|
386
|
+
});
|
|
294
387
|
test("configured auto routing fails closed and observes missing evidence", async () => {
|
|
295
388
|
const observations = [];
|
|
296
389
|
await assert.rejects(runRouteKitEffect(resolveConfiguredAutoRoutingModel({
|
|
@@ -494,7 +587,6 @@ test("authoritative V3 incomplete callers fail closed and are observed", async (
|
|
|
494
587
|
onObservation: (observation) => observations.push(observation)
|
|
495
588
|
}
|
|
496
589
|
})), (error) => error instanceof AutoRoutingUnavailableError &&
|
|
497
|
-
error.message ===
|
|
498
|
-
"authoritative compositional routing requires a request body and dialect");
|
|
590
|
+
error.message === "authoritative compositional routing requires a request body and dialect");
|
|
499
591
|
assert.deepEqual(observations.map((observation) => observation.status), ["failed"]);
|
|
500
592
|
});
|
|
@@ -27,6 +27,7 @@ test("eval bypass requires a trusted eval-session principal", () => {
|
|
|
27
27
|
test("eval sessions reject auto and models outside their allowlist", () => {
|
|
28
28
|
const headers = evalHeaders(["openai/gpt-5.6-luna"]);
|
|
29
29
|
assert.match(evalAutoRouterRejection(headers, "auto") ?? "", /explicit provider\/model/);
|
|
30
|
+
assert.match(evalAutoRouterRejection(headers, "auto:quality") ?? "", /explicit provider\/model/);
|
|
30
31
|
assert.match(evalAutoRouterRejection(headers, "openai/gpt-5.6-terra") ?? "", /not authorized/);
|
|
31
32
|
assert.equal(evalAutoRouterRejection(headers, "openai/gpt-5.6-luna"), undefined);
|
|
32
33
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-gateway",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@aws-sdk/client-bedrock": "3.1095.0",
|
|
46
46
|
"@aws-sdk/client-bedrock-runtime": "3.1095.0",
|
|
47
47
|
"effect": "4.0.0-rc.108",
|
|
48
|
-
"@velum-labs/routekit-config-core": "1.
|
|
49
|
-
"@velum-labs/routekit-contracts": "1.
|
|
50
|
-
"@velum-labs/routekit-eval-contracts": "1.
|
|
51
|
-
"@velum-labs/routekit-eval-core": "1.
|
|
52
|
-
"@velum-labs/routekit-registry": "1.
|
|
53
|
-
"@velum-labs/routekit-runtime": "1.
|
|
48
|
+
"@velum-labs/routekit-config-core": "1.3.1",
|
|
49
|
+
"@velum-labs/routekit-contracts": "1.3.1",
|
|
50
|
+
"@velum-labs/routekit-eval-contracts": "1.3.1",
|
|
51
|
+
"@velum-labs/routekit-eval-core": "1.3.1",
|
|
52
|
+
"@velum-labs/routekit-registry": "1.3.1",
|
|
53
|
+
"@velum-labs/routekit-runtime": "1.3.1"
|
|
54
54
|
},
|
|
55
55
|
"keywords": [
|
|
56
56
|
"routekit",
|