@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 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?.trim().toLowerCase() === "auto" &&
65
+ if (isAutoRoutingModel(decodedBody.model) &&
66
66
  decodedBody.previous_response_id != null) {
67
67
  context.transport.writeJson(400, {
68
68
  error: {
@@ -5,6 +5,10 @@ export type GatewayPrincipal = {
5
5
  id: string;
6
6
  label: string;
7
7
  role: "owner" | "admin" | "eval";
8
+ routing?: {
9
+ repositoryRoot: string;
10
+ profileName: string;
11
+ };
8
12
  evalSession?: {
9
13
  sessionId: string;
10
14
  allowedModels: readonly string[];
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 { COMPOSITIONAL_ROUTING_VERSION, EVAL_ATTRIBUTION_HEADER, EVAL_POLICY_BYPASS_HEADER, isForbiddenEvalModel } from "@velum-labs/routekit-eval-contracts";
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?.trim().toLowerCase() !== "auto") {
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: undefined,
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: undefined,
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: undefined,
191
- message: "no compositional routing snapshot is available"
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: undefined,
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: undefined,
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: undefined,
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: undefined,
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: undefined,
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: undefined,
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
- if (options.model?.trim().toLowerCase() !== "auto")
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: undefined,
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: undefined,
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,
@@ -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";
@@ -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 }
@@ -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.2.0",
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.2.0",
49
- "@velum-labs/routekit-contracts": "1.2.0",
50
- "@velum-labs/routekit-eval-contracts": "1.2.0",
51
- "@velum-labs/routekit-eval-core": "1.2.0",
52
- "@velum-labs/routekit-registry": "1.2.0",
53
- "@velum-labs/routekit-runtime": "1.2.0"
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",