@velum-labs/routekit-daemon 1.0.25 → 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/daemon-generations.d.ts +1 -1
- package/dist/daemon-generations.js +1 -1
- package/dist/daemon-lifecycle.d.ts +1 -1
- package/dist/effect-api.d.ts +2 -2
- package/dist/effect-api.js +1 -1
- package/dist/eval-routing-policy.d.ts +1 -4
- package/dist/eval-routing-policy.js +3 -6
- package/dist/eval-routing-service.d.ts +1 -1
- package/dist/eval-routing-service.js +120 -48
- package/dist/services/active-gateway/service.d.ts +1 -1
- package/dist/{gateway-generation.d.ts → services/gateway-generation/service.d.ts} +5 -5
- package/dist/{gateway-generation.js → services/gateway-generation/service.js} +32 -23
- package/dist/test/application-services.test.js +2 -0
- package/dist/test/eval-routing-application-service.test.js +66 -6
- package/dist/test/eval-routing-policy.test.js +5 -5
- package/package.json +10 -10
|
@@ -5,7 +5,7 @@ import { type RouteKitPlatform } from "@velum-labs/routekit-runtime/effect";
|
|
|
5
5
|
import { Effect } from "effect";
|
|
6
6
|
import type { CliproxySidecar } from "./cliproxy-sidecar.js";
|
|
7
7
|
import type { RevisionState } from "./daemon-state.js";
|
|
8
|
-
import { type RunningGatewayGeneration } from "./gateway-generation.js";
|
|
8
|
+
import { type RunningGatewayGeneration } from "./services/gateway-generation/service.js";
|
|
9
9
|
export type DaemonGenerationStage = "prepare" | "validate" | "persist" | "commit" | "retire";
|
|
10
10
|
export type DaemonGenerationMutation = {
|
|
11
11
|
write: boolean;
|
|
@@ -4,7 +4,7 @@ import { RouteKitFailure, toRouteKitFailure } from "@velum-labs/routekit-runtime
|
|
|
4
4
|
import { writeFileAtomic } from "@velum-labs/routekit-runtime/filesystem";
|
|
5
5
|
import { Effect } from "effect";
|
|
6
6
|
import { writeDaemonRevisions } from "./daemon-state.js";
|
|
7
|
-
import { startGatewayGenerationEffect } from "./gateway-generation.js";
|
|
7
|
+
import { startGatewayGenerationEffect } from "./services/gateway-generation/service.js";
|
|
8
8
|
const tryPromise = (run) => Effect.tryPromise({
|
|
9
9
|
try: async () => await run(),
|
|
10
10
|
catch: toRouteKitFailure
|
|
@@ -5,7 +5,7 @@ import type { RunningControlServer } from "@velum-labs/routekit-runtime/control"
|
|
|
5
5
|
import { type RouteKitPlatform } from "@velum-labs/routekit-runtime/effect";
|
|
6
6
|
import { Effect, type ManagedRuntime } from "effect";
|
|
7
7
|
import type { DaemonRuntimeState } from "./daemon-runtime-state.js";
|
|
8
|
-
import type { RunningGatewayGeneration } from "./gateway-generation.js";
|
|
8
|
+
import type { RunningGatewayGeneration } from "./services/gateway-generation/service.js";
|
|
9
9
|
import type { DaemonTelemetry, GatewayTelemetryAggregator } from "./telemetry.js";
|
|
10
10
|
type Supervisor = "systemd" | "launchd" | "detached" | "unknown";
|
|
11
11
|
export type DaemonLifecycleOptions = {
|
package/dist/effect-api.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export { DaemonState } from "./daemon-state-context.js";
|
|
|
18
18
|
export type { DataPlaneValue } from "./data-plane-context.js";
|
|
19
19
|
export { DataPlane } from "./data-plane-context.js";
|
|
20
20
|
export { EvalSessions } from "./services/eval-session/service.js";
|
|
21
|
-
export type { GatewayGenerationOptions, GatewayGenerationRedeemResetOptions, GatewayGenerationRedeemResetResponse, RunningGatewayGeneration } from "./gateway-generation.js";
|
|
22
|
-
export { startGatewayGenerationEffect } from "./gateway-generation.js";
|
|
21
|
+
export type { GatewayGenerationOptions, GatewayGenerationRedeemResetOptions, GatewayGenerationRedeemResetResponse, RunningGatewayGeneration } from "./services/gateway-generation/service.js";
|
|
22
|
+
export { startGatewayGenerationEffect } from "./services/gateway-generation/service.js";
|
|
23
23
|
export type { DaemonGenerationHooks } from "./services/generations/service.js";
|
|
24
24
|
export { Generations } from "./services/generations/service.js";
|
|
25
25
|
export type { LeaderboardValue } from "./leaderboard-context.js";
|
package/dist/effect-api.js
CHANGED
|
@@ -10,7 +10,7 @@ export { DaemonPolicy } from "./daemon-policy-context.js";
|
|
|
10
10
|
export { DaemonState } from "./daemon-state-context.js";
|
|
11
11
|
export { DataPlane } from "./data-plane-context.js";
|
|
12
12
|
export { EvalSessions } from "./services/eval-session/service.js";
|
|
13
|
-
export { startGatewayGenerationEffect } from "./gateway-generation.js";
|
|
13
|
+
export { startGatewayGenerationEffect } from "./services/gateway-generation/service.js";
|
|
14
14
|
export { Generations } from "./services/generations/service.js";
|
|
15
15
|
export { Leaderboard } from "./leaderboard-context.js";
|
|
16
16
|
export { Sidecar } from "./sidecar-context.js";
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { type CompositionalRoutingPolicyReader } from "@velum-labs/routekit-gateway";
|
|
2
2
|
/** Daemon-owned location for the compact policy artifact consumed online. */
|
|
3
3
|
export declare function evalRoutingSnapshotDirectory(routekitHome: string): string;
|
|
4
|
-
/**
|
|
5
|
-
* Read the compositional snapshot without restarting router generations. A
|
|
6
|
-
* corrupt current file falls back to the last known-good publication.
|
|
7
|
-
*/
|
|
4
|
+
/** Read the authoritative routing activation without restarting router generations. */
|
|
8
5
|
export declare function makeCompositionalRoutingPolicyReader(routekitHome: string): CompositionalRoutingPolicyReader;
|
|
@@ -6,18 +6,15 @@ import { Effect } from "effect";
|
|
|
6
6
|
export function evalRoutingSnapshotDirectory(routekitHome) {
|
|
7
7
|
return join(routekitHome, "eval");
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Read the compositional snapshot without restarting router generations. A
|
|
11
|
-
* corrupt current file falls back to the last known-good publication.
|
|
12
|
-
*/
|
|
9
|
+
/** Read the authoritative routing activation without restarting router generations. */
|
|
13
10
|
export function makeCompositionalRoutingPolicyReader(routekitHome) {
|
|
14
11
|
const snapshots = makeRoutingActivationStore(evalRoutingSnapshotDirectory(routekitHome));
|
|
15
12
|
return {
|
|
16
|
-
|
|
13
|
+
getActivation: () => snapshots.readDeployment().pipe(Effect.map((deployment) => deployment.authoritative?.activation), Effect.catch((currentCause) => snapshots
|
|
17
14
|
.readPrevious()
|
|
18
15
|
.pipe(Effect.flatMap((previous) => previous === undefined ? Effect.fail(currentCause) : Effect.succeed(previous)))), Effect.mapError((cause) => new RoutingPolicyReadError({
|
|
19
16
|
profileId: "*",
|
|
20
|
-
message: "failed to read the
|
|
17
|
+
message: "failed to read the authoritative routing activation",
|
|
21
18
|
cause
|
|
22
19
|
})))
|
|
23
20
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EffectRouteKitControlHandlers } from "@velum-labs/routekit-control/effect";
|
|
2
|
-
type EvalRoutingHandlers = Pick<EffectRouteKitControlHandlers, "evalRouting.status" | "evalRouting.activate">;
|
|
2
|
+
type EvalRoutingHandlers = Pick<EffectRouteKitControlHandlers, "evalRouting.status" | "evalRouting.activate" | "evalRouting.installAuthoritative" | "evalRouting.rollbackAuthoritative">;
|
|
3
3
|
/** Owns target-local compositional routing status and atomic activation. */
|
|
4
4
|
export declare class EvalRoutingApplicationService {
|
|
5
5
|
handlers(): EvalRoutingHandlers;
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { DEFAULT_CLASSIFIER_MODEL } from "@velum-labs/routekit-config";
|
|
2
|
-
import { assertPublishedRoutingActivation, PublishedRoutingActivation } from "@velum-labs/routekit-eval-contracts";
|
|
3
|
-
import { makeRoutingActivationStore, RoutingActivationConflictError } from "@velum-labs/routekit-eval-store/effect";
|
|
2
|
+
import { AnyPublishedRoutingActivation, assertPublishedRoutingActivation, assertPublishedRoutingActivationV3, PublishedRoutingActivation } from "@velum-labs/routekit-eval-contracts";
|
|
3
|
+
import { makeRoutingActivationStore, RoutingActivationConflictError, RoutingDeploymentConflictError } from "@velum-labs/routekit-eval-store/effect";
|
|
4
4
|
import { ControlError } from "@velum-labs/routekit-runtime/control";
|
|
5
5
|
import { Effect, Schema } from "effect";
|
|
6
|
-
import { ActiveGateway } from "./services/active-gateway/service.js";
|
|
7
6
|
import { DaemonEnv } from "./daemon-env-context.js";
|
|
8
7
|
import { DaemonState } from "./daemon-state-context.js";
|
|
9
8
|
import { evalRoutingSnapshotDirectory } from "./eval-routing-policy.js";
|
|
9
|
+
import { ActiveGateway } from "./services/active-gateway/service.js";
|
|
10
10
|
function detailOf(cause) {
|
|
11
11
|
return cause instanceof Error ? cause.message : String(cause);
|
|
12
12
|
}
|
|
13
|
-
function
|
|
13
|
+
function identity(revision) {
|
|
14
|
+
return revision === null
|
|
15
|
+
? null
|
|
16
|
+
: {
|
|
17
|
+
version: revision.activation.version,
|
|
18
|
+
activationRevisionDigest: revision.activationRevisionDigest,
|
|
19
|
+
basisDigest: revision.activation.basisDigest,
|
|
20
|
+
evidenceDigest: revision.activation.evidenceDigest
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function decodeLegacyActivation(value) {
|
|
14
24
|
return Schema.decodeUnknownEffect(PublishedRoutingActivation)(value).pipe(Effect.flatMap((activation) => Effect.try({
|
|
15
25
|
try: () => {
|
|
16
26
|
assertPublishedRoutingActivation(activation);
|
|
@@ -28,61 +38,123 @@ function decodeActivation(value) {
|
|
|
28
38
|
message: `routing activation is invalid: ${detailOf(cause)}`
|
|
29
39
|
})));
|
|
30
40
|
}
|
|
41
|
+
function decodeAnyActivation(value) {
|
|
42
|
+
return Schema.decodeUnknownEffect(AnyPublishedRoutingActivation)(value).pipe(Effect.flatMap((activation) => Effect.try({
|
|
43
|
+
try: () => {
|
|
44
|
+
if (activation.version === 2)
|
|
45
|
+
assertPublishedRoutingActivation(activation);
|
|
46
|
+
else
|
|
47
|
+
assertPublishedRoutingActivationV3(activation);
|
|
48
|
+
return activation;
|
|
49
|
+
},
|
|
50
|
+
catch: (cause) => new ControlError({
|
|
51
|
+
code: "bad_request",
|
|
52
|
+
message: `routing activation is invalid: ${detailOf(cause)}`
|
|
53
|
+
})
|
|
54
|
+
})), Effect.mapError((cause) => cause instanceof ControlError
|
|
55
|
+
? cause
|
|
56
|
+
: new ControlError({
|
|
57
|
+
code: "bad_request",
|
|
58
|
+
message: `routing activation is invalid: ${detailOf(cause)}`
|
|
59
|
+
})));
|
|
60
|
+
}
|
|
61
|
+
const mapStoreError = (operation) => (cause) => cause instanceof RoutingActivationConflictError ||
|
|
62
|
+
cause instanceof RoutingDeploymentConflictError
|
|
63
|
+
? new ControlError({ code: "conflict", message: cause.message })
|
|
64
|
+
: new ControlError({
|
|
65
|
+
code: "internal",
|
|
66
|
+
message: `failed to ${operation}: ${detailOf(cause)}`
|
|
67
|
+
});
|
|
68
|
+
function validateRuntimeCompatibility(activation) {
|
|
69
|
+
return Effect.gen(function* () {
|
|
70
|
+
const state = yield* DaemonState;
|
|
71
|
+
const gateway = yield* ActiveGateway;
|
|
72
|
+
const router = gateway.router();
|
|
73
|
+
if (router === undefined)
|
|
74
|
+
return yield* Effect.fail(new ControlError({
|
|
75
|
+
code: "unavailable",
|
|
76
|
+
message: "routing activation requires a running data gateway"
|
|
77
|
+
}));
|
|
78
|
+
const classifierModel = activation.version === 2 ? activation.classifierModel : activation.classifier.model;
|
|
79
|
+
if (activation.version === 2) {
|
|
80
|
+
const configuredClassifier = state.config.classifierModel ?? DEFAULT_CLASSIFIER_MODEL;
|
|
81
|
+
if (classifierModel !== configuredClassifier)
|
|
82
|
+
return yield* Effect.fail(new ControlError({
|
|
83
|
+
code: "unavailable",
|
|
84
|
+
message: `routing activation classifier ${JSON.stringify(classifierModel)} does not match the running classifier ${JSON.stringify(configuredClassifier)}`
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (state.config.defaultModel !== undefined &&
|
|
89
|
+
state.config.defaultModel !== activation.defaultModel)
|
|
90
|
+
return yield* Effect.fail(new ControlError({
|
|
91
|
+
code: "unavailable",
|
|
92
|
+
message: "the running default model does not match the V3 activation-pinned default"
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
const served = new Set(router.modelCatalog().map((model) => model.id));
|
|
96
|
+
const required = [
|
|
97
|
+
classifierModel,
|
|
98
|
+
...(activation.version === 3 ? [activation.defaultModel] : []),
|
|
99
|
+
...activation.candidateModels
|
|
100
|
+
];
|
|
101
|
+
const unavailable = required.filter((model, index, models) => !served.has(model) && models.indexOf(model) === index);
|
|
102
|
+
if (unavailable.length > 0)
|
|
103
|
+
return yield* Effect.fail(new ControlError({
|
|
104
|
+
code: "unavailable",
|
|
105
|
+
message: `routing activation references models not served by this target: ${unavailable.map((model) => JSON.stringify(model)).join(", ")}`
|
|
106
|
+
}));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
31
109
|
/** Owns target-local compositional routing status and atomic activation. */
|
|
32
110
|
export class EvalRoutingApplicationService {
|
|
33
111
|
handlers() {
|
|
34
112
|
return {
|
|
35
113
|
"evalRouting.status": () => Effect.gen(function* () {
|
|
36
114
|
const env = yield* DaemonEnv;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
115
|
+
const deployment = yield* makeRoutingActivationStore(evalRoutingSnapshotDirectory(env.home))
|
|
116
|
+
.readDeployment()
|
|
117
|
+
.pipe(Effect.mapError(mapStoreError("read routing deployment")));
|
|
118
|
+
return {
|
|
119
|
+
stateRevision: deployment.stateRevision,
|
|
120
|
+
authoritative: identity(deployment.authoritative),
|
|
121
|
+
previousAuthoritative: identity(deployment.previousAuthoritative)
|
|
122
|
+
};
|
|
43
123
|
}),
|
|
44
124
|
"evalRouting.activate": (params) => Effect.gen(function* () {
|
|
45
125
|
const env = yield* DaemonEnv;
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const router = gateway.router();
|
|
50
|
-
if (router === undefined) {
|
|
51
|
-
return yield* Effect.fail(new ControlError({
|
|
52
|
-
code: "unavailable",
|
|
53
|
-
message: "routing activation requires a running data gateway"
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
|
-
const configuredClassifier = state.config.classifierModel ?? DEFAULT_CLASSIFIER_MODEL;
|
|
57
|
-
if (publication.classifierModel !== configuredClassifier) {
|
|
58
|
-
return yield* Effect.fail(new ControlError({
|
|
59
|
-
code: "unavailable",
|
|
60
|
-
message: `routing activation classifier ${JSON.stringify(publication.classifierModel)} does not match the running classifier ${JSON.stringify(configuredClassifier)}`
|
|
61
|
-
}));
|
|
62
|
-
}
|
|
63
|
-
const served = new Set(router.modelCatalog().map((model) => model.id));
|
|
64
|
-
const unavailableModels = [
|
|
65
|
-
publication.classifierModel,
|
|
66
|
-
...publication.candidateModels
|
|
67
|
-
].filter((model, index, models) => !served.has(model) && models.indexOf(model) === index);
|
|
68
|
-
if (unavailableModels.length > 0) {
|
|
69
|
-
return yield* Effect.fail(new ControlError({
|
|
70
|
-
code: "unavailable",
|
|
71
|
-
message: `routing activation references models not served by this target: ${unavailableModels
|
|
72
|
-
.map((model) => JSON.stringify(model))
|
|
73
|
-
.join(", ")}`
|
|
74
|
-
}));
|
|
75
|
-
}
|
|
76
|
-
const store = makeRoutingActivationStore(evalRoutingSnapshotDirectory(env.home));
|
|
77
|
-
const activation = yield* store
|
|
126
|
+
const publication = yield* decodeLegacyActivation(params.activation);
|
|
127
|
+
yield* validateRuntimeCompatibility(params.activation);
|
|
128
|
+
const activation = yield* makeRoutingActivationStore(evalRoutingSnapshotDirectory(env.home))
|
|
78
129
|
.publishIfCurrent(publication, params.expectedEvidenceDigest ?? undefined)
|
|
79
|
-
.pipe(Effect.mapError((
|
|
80
|
-
? new ControlError({ code: "conflict", message: cause.message })
|
|
81
|
-
: new ControlError({
|
|
82
|
-
code: "internal",
|
|
83
|
-
message: `failed to activate routing policy: ${detailOf(cause)}`
|
|
84
|
-
})));
|
|
130
|
+
.pipe(Effect.mapError(mapStoreError("activate routing policy")));
|
|
85
131
|
return { activated: true, activation };
|
|
132
|
+
}),
|
|
133
|
+
"evalRouting.installAuthoritative": (params) => Effect.gen(function* () {
|
|
134
|
+
const env = yield* DaemonEnv;
|
|
135
|
+
const activation = yield* decodeAnyActivation(params.activation);
|
|
136
|
+
yield* validateRuntimeCompatibility(activation);
|
|
137
|
+
const deployment = yield* makeRoutingActivationStore(evalRoutingSnapshotDirectory(env.home))
|
|
138
|
+
.installAuthoritative(activation, params.expectedAuthoritativeRevisionDigest, params.expectedPreviousAuthoritativeRevisionDigest)
|
|
139
|
+
.pipe(Effect.mapError(mapStoreError("install authoritative routing policy")));
|
|
140
|
+
return {
|
|
141
|
+
installed: true,
|
|
142
|
+
stateRevision: deployment.stateRevision,
|
|
143
|
+
authoritative: identity(deployment.authoritative),
|
|
144
|
+
previousAuthoritative: identity(deployment.previousAuthoritative)
|
|
145
|
+
};
|
|
146
|
+
}),
|
|
147
|
+
"evalRouting.rollbackAuthoritative": (params) => Effect.gen(function* () {
|
|
148
|
+
const env = yield* DaemonEnv;
|
|
149
|
+
const deployment = yield* makeRoutingActivationStore(evalRoutingSnapshotDirectory(env.home))
|
|
150
|
+
.rollbackAuthoritative(params.expectedAuthoritativeRevisionDigest, params.expectedPreviousAuthoritativeRevisionDigest)
|
|
151
|
+
.pipe(Effect.mapError(mapStoreError("roll back authoritative routing policy")));
|
|
152
|
+
return {
|
|
153
|
+
rolledBack: true,
|
|
154
|
+
stateRevision: deployment.stateRevision,
|
|
155
|
+
authoritative: identity(deployment.authoritative),
|
|
156
|
+
previousAuthoritative: identity(deployment.previousAuthoritative)
|
|
157
|
+
};
|
|
86
158
|
})
|
|
87
159
|
};
|
|
88
160
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SwitchingGatewayProxy } from "@velum-labs/routekit-gateway";
|
|
2
2
|
import type { RunningControlServer } from "@velum-labs/routekit-runtime/control";
|
|
3
3
|
import { Context, Layer, Ref } from "effect";
|
|
4
|
-
import type { RunningGatewayGeneration } from "
|
|
4
|
+
import type { RunningGatewayGeneration } from "../gateway-generation/service.js";
|
|
5
5
|
export type ActiveGatewayState = Readonly<{
|
|
6
6
|
router?: RunningGatewayGeneration;
|
|
7
7
|
proxy?: SwitchingGatewayProxy;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { RedeemResetCreditResult, ResetCreditSnapshot, SubscriptionAccountSetSnapshot, SubscriptionUsageResponse } from "@velum-labs/routekit-accounts";
|
|
2
2
|
import type { AccountActivityService, AccountAuthService } from "@velum-labs/routekit-accounts/effect";
|
|
3
3
|
import { type ProviderId, type RouterConfig } from "@velum-labs/routekit-config";
|
|
4
|
-
import type { CatalogModelInfo, CompositionalRoutingObservation, CompositionalRoutingPolicyReader, Gateway, ProvenanceSink, ProviderSource
|
|
5
|
-
import { RoutingBackend } from "@velum-labs/routekit-gateway";
|
|
4
|
+
import type { CatalogModelInfo, CompositionalRoutingObservation, CompositionalRoutingPolicyReader, Gateway, ProvenanceSink, ProviderSource } from "@velum-labs/routekit-gateway";
|
|
5
|
+
import { RequestDecomposer, RoutingBackend } from "@velum-labs/routekit-gateway";
|
|
6
6
|
import { type RouteKitPlatform } from "@velum-labs/routekit-runtime/effect";
|
|
7
|
-
import { Effect } from "effect";
|
|
7
|
+
import { Effect, Layer } from "effect";
|
|
8
8
|
export type GatewayGenerationOptions = {
|
|
9
9
|
config: RouterConfig;
|
|
10
10
|
host?: string;
|
|
@@ -15,8 +15,8 @@ export type GatewayGenerationOptions = {
|
|
|
15
15
|
provenance?: ProvenanceSink;
|
|
16
16
|
/** Published model-by-dimension evidence used by automatic routing. */
|
|
17
17
|
compositionalPolicyReader?: CompositionalRoutingPolicyReader;
|
|
18
|
-
/** Override the default
|
|
19
|
-
|
|
18
|
+
/** Override the default semantic classifier Layer. */
|
|
19
|
+
requestDecomposerLayer?: Layer.Layer<RequestDecomposer>;
|
|
20
20
|
/** Receives sanitized automatic-routing decisions and failures. */
|
|
21
21
|
onCompositionalRoutingObservation?(observation: CompositionalRoutingObservation): void;
|
|
22
22
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLIPROXY_API_KEY_ENV, cliproxyApiKey, closeSubscriptionAccountSets, collectSubscriptionUsage, defaultSubscriptionAccountDirectory, defaultSubscriptionCredentialPath, openSubscriptionAccountSets, SubscriptionAccountBackend, snapshotsToUsage, subscriptionRelaysFromAccountSets } from "@velum-labs/routekit-accounts";
|
|
2
2
|
import { DEFAULT_CLASSIFIER_MODEL, resolveCompositionalRoutingConfig } from "@velum-labs/routekit-config";
|
|
3
|
-
import { AnthropicBackend,
|
|
3
|
+
import { AnthropicBackend, CodexResponsesBackend, invokeObservedModelCall, lunaDirectRequestDecomposerLayer, RequestDecomposer, RoutingBackend, routingModelAvailability } from "@velum-labs/routekit-gateway";
|
|
4
4
|
import { startGatewayEffect } from "@velum-labs/routekit-gateway/effect";
|
|
5
5
|
import { RouteKitFailure, toRouteKitFailure } from "@velum-labs/routekit-runtime/effect";
|
|
6
6
|
import { assertAuthenticatedBind } from "@velum-labs/routekit-runtime/network";
|
|
@@ -130,29 +130,38 @@ const acquireGatewayGeneration = Effect.fn("GatewayGeneration.acquire")(function
|
|
|
130
130
|
yield* addOwnedFinalizer(scope, platform, closeErrors, backend.close());
|
|
131
131
|
const configuredClassifierModel = options.config.classifierModel;
|
|
132
132
|
const classifierModel = configuredClassifierModel ?? DEFAULT_CLASSIFIER_MODEL;
|
|
133
|
-
const classifierComplete = (endpointId) => (body) =>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
133
|
+
const classifierComplete = (endpointId) => (body) => {
|
|
134
|
+
const requestedModel = typeof body === "object" &&
|
|
135
|
+
body !== null &&
|
|
136
|
+
!Array.isArray(body) &&
|
|
137
|
+
typeof body.model === "string"
|
|
138
|
+
? body.model.trim()
|
|
139
|
+
: "";
|
|
140
|
+
if (requestedModel.length === 0) {
|
|
141
|
+
return Effect.fail(new RouteKitFailure({
|
|
142
|
+
message: "classifier completion requires an explicit provider/model id"
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
return invokeObservedModelCall(options.provenance, {
|
|
146
|
+
dialect: "openai-chat",
|
|
147
|
+
body,
|
|
148
|
+
defaultModel: requestedModel,
|
|
149
|
+
requestedModel,
|
|
150
|
+
endpointId,
|
|
151
|
+
invoke: (callId, signal, onAttribution) => backend.chat(body, signal, {
|
|
152
|
+
modelCallId: callId,
|
|
153
|
+
responseMode: "buffered",
|
|
154
|
+
onAttribution
|
|
154
155
|
})
|
|
155
|
-
|
|
156
|
+
}).pipe(Effect.provide(platform));
|
|
157
|
+
};
|
|
158
|
+
const compositionalConfig = resolveCompositionalRoutingConfig(options.config);
|
|
159
|
+
const requestDecomposer = yield* RequestDecomposer.pipe(Effect.provide(options.requestDecomposerLayer ??
|
|
160
|
+
lunaDirectRequestDecomposerLayer({
|
|
161
|
+
model: classifierModel,
|
|
162
|
+
modelAvailable: (model) => backend.ports.models.serves(model),
|
|
163
|
+
complete: (body) => classifierComplete("dimension-request-classifier")(body)
|
|
164
|
+
})));
|
|
156
165
|
const compositionalRouting = {
|
|
157
166
|
policyReader: options.compositionalPolicyReader,
|
|
158
167
|
classifier: requestDecomposer,
|
|
@@ -97,6 +97,8 @@ test("application services expose concrete bounded handler groups", () => {
|
|
|
97
97
|
]);
|
|
98
98
|
assert.deepEqual(Object.keys(new EvalRoutingApplicationService().handlers()).sort(), [
|
|
99
99
|
"evalRouting.activate",
|
|
100
|
+
"evalRouting.installAuthoritative",
|
|
101
|
+
"evalRouting.rollbackAuthoritative",
|
|
100
102
|
"evalRouting.status"
|
|
101
103
|
]);
|
|
102
104
|
assert.deepEqual(Object.keys(new TelemetryApplicationService().handlers()).sort(), [
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
+
import { parseRouterConfig } from "@velum-labs/routekit-config";
|
|
6
7
|
import { ControlError } from "@velum-labs/routekit-runtime/control";
|
|
7
8
|
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
8
9
|
import { Effect, Layer, Ref } from "effect";
|
|
@@ -72,9 +73,9 @@ test("routing activation handlers expose status and enforce compare-and-swap", a
|
|
|
72
73
|
revisions: { daemon: 1, config: 1, accounts: 1 }
|
|
73
74
|
});
|
|
74
75
|
const activeGateway = Layer.succeed(ActiveGateway, (() => {
|
|
75
|
-
const router =
|
|
76
|
+
const router = {
|
|
76
77
|
modelCatalog: () => ["openai/classifier", "openai/model-a"].map((id) => ({ id }))
|
|
77
|
-
}
|
|
78
|
+
};
|
|
78
79
|
const state = Ref.makeUnsafe({ router, dataUrl: "http://127.0.0.1:8080" });
|
|
79
80
|
return ActiveGateway.of({
|
|
80
81
|
state,
|
|
@@ -89,7 +90,9 @@ test("routing activation handlers expose status and enforce compare-and-swap", a
|
|
|
89
90
|
const run = (effect) => runRouteKitEffect(effect.pipe(Effect.provide(services)));
|
|
90
91
|
try {
|
|
91
92
|
assert.deepEqual(await run(handlers["evalRouting.status"]({}, undefined)), {
|
|
92
|
-
|
|
93
|
+
stateRevision: 0,
|
|
94
|
+
authoritative: null,
|
|
95
|
+
previousAuthoritative: null
|
|
93
96
|
});
|
|
94
97
|
const first = await run(handlers["evalRouting.activate"]({
|
|
95
98
|
expectedEvidenceDigest: null,
|
|
@@ -113,8 +116,65 @@ test("routing activation handlers expose status and enforce compare-and-swap", a
|
|
|
113
116
|
activation: activation("evidence-second")
|
|
114
117
|
}, undefined));
|
|
115
118
|
assert.equal(second.activation.evidenceDigest, "evidence-second");
|
|
116
|
-
assert.equal((await run(handlers["evalRouting.status"]({}, undefined)))
|
|
117
|
-
|
|
119
|
+
assert.equal((await run(handlers["evalRouting.status"]({}, undefined))).authoritative
|
|
120
|
+
?.evidenceDigest, "evidence-second");
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
rmSync(home, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
test("V3 authority installs when its pinned models match the running gateway", async () => {
|
|
127
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-routing-v3-authority-"));
|
|
128
|
+
const activationV3 = JSON.parse(readFileSync(new URL("../../../../test/fixtures/routing-v3/examples/published-routing-activation-v3.example.json", import.meta.url), "utf8"));
|
|
129
|
+
const daemonEnv = Layer.succeed(DaemonEnv, DaemonEnv.of({
|
|
130
|
+
home,
|
|
131
|
+
configPath: join(home, "router.yaml"),
|
|
132
|
+
env: {},
|
|
133
|
+
packageVersion: "0.0.0-test",
|
|
134
|
+
generation: 1,
|
|
135
|
+
startedAt: "2026-08-23T00:00:00.000Z",
|
|
136
|
+
hosted: undefined
|
|
137
|
+
}));
|
|
138
|
+
const runtimeState = new DaemonRuntimeState({
|
|
139
|
+
config: parseRouterConfig({
|
|
140
|
+
providers: { openai: {}, anthropic: {} },
|
|
141
|
+
defaultModel: activationV3.defaultModel
|
|
142
|
+
}),
|
|
143
|
+
document: "",
|
|
144
|
+
revisions: { daemon: 1, config: 1, accounts: 1 }
|
|
145
|
+
});
|
|
146
|
+
const requiredModels = [
|
|
147
|
+
activationV3.classifier.model,
|
|
148
|
+
activationV3.defaultModel,
|
|
149
|
+
...activationV3.candidateModels
|
|
150
|
+
];
|
|
151
|
+
const activeGateway = Layer.succeed(ActiveGateway, (() => {
|
|
152
|
+
const router = {
|
|
153
|
+
modelCatalog: () => [...new Set(requiredModels)].map((id) => ({
|
|
154
|
+
id
|
|
155
|
+
}))
|
|
156
|
+
};
|
|
157
|
+
const state = Ref.makeUnsafe({ router, dataUrl: "http://127.0.0.1:8080" });
|
|
158
|
+
return ActiveGateway.of({
|
|
159
|
+
state,
|
|
160
|
+
router: () => router,
|
|
161
|
+
proxy: () => undefined,
|
|
162
|
+
dataUrl: () => "http://127.0.0.1:8080",
|
|
163
|
+
control: () => undefined
|
|
164
|
+
});
|
|
165
|
+
})());
|
|
166
|
+
const services = Layer.mergeAll(daemonEnv, DaemonState.layer(runtimeState), activeGateway);
|
|
167
|
+
const handler = new EvalRoutingApplicationService().handlers()["evalRouting.installAuthoritative"];
|
|
168
|
+
const run = (effect) => runRouteKitEffect(effect.pipe(Effect.provide(services)));
|
|
169
|
+
const params = {
|
|
170
|
+
expectedAuthoritativeRevisionDigest: null,
|
|
171
|
+
expectedPreviousAuthoritativeRevisionDigest: null,
|
|
172
|
+
activation: activationV3
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
const installed = await run(handler(params, undefined));
|
|
176
|
+
assert.equal(installed.installed, true);
|
|
177
|
+
assert.equal(installed.authoritative.version, 3);
|
|
118
178
|
}
|
|
119
179
|
finally {
|
|
120
180
|
rmSync(home, { recursive: true, force: true });
|
|
@@ -6,7 +6,7 @@ import test from "node:test";
|
|
|
6
6
|
import { makeRoutingActivationStore } from "@velum-labs/routekit-eval-store/effect";
|
|
7
7
|
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
8
8
|
import { evalRoutingSnapshotDirectory, makeCompositionalRoutingPolicyReader } from "../eval-routing-policy.js";
|
|
9
|
-
test("daemon compositional reader
|
|
9
|
+
test("daemon compositional reader projects authority and falls back to previous V2", async () => {
|
|
10
10
|
const home = mkdtempSync(join(tmpdir(), "routekit-daemon-compositional-policy-"));
|
|
11
11
|
const dimensions = [
|
|
12
12
|
"code-change",
|
|
@@ -41,13 +41,13 @@ test("daemon compositional reader observes publications and falls back to previo
|
|
|
41
41
|
});
|
|
42
42
|
try {
|
|
43
43
|
const reader = makeCompositionalRoutingPolicyReader(home);
|
|
44
|
-
assert.equal(await runRouteKitEffect(reader.
|
|
44
|
+
assert.equal(await runRouteKitEffect(reader.getActivation()), undefined);
|
|
45
45
|
const store = makeRoutingActivationStore(evalRoutingSnapshotDirectory(home));
|
|
46
46
|
await runRouteKitEffect(store.publish(publication("first")));
|
|
47
47
|
await runRouteKitEffect(store.publish(publication("second")));
|
|
48
|
-
assert.equal((await runRouteKitEffect(reader.
|
|
49
|
-
writeFileSync(join(evalRoutingSnapshotDirectory(home), "
|
|
50
|
-
assert.equal((await runRouteKitEffect(reader.
|
|
48
|
+
assert.equal((await runRouteKitEffect(reader.getActivation()))?.evidenceDigest, "second");
|
|
49
|
+
writeFileSync(join(evalRoutingSnapshotDirectory(home), "routing-deployment.v1.json"), '{"version":2}');
|
|
50
|
+
assert.equal((await runRouteKitEffect(reader.getActivation()))?.evidenceDigest, "first");
|
|
51
51
|
}
|
|
52
52
|
finally {
|
|
53
53
|
rmSync(home, { recursive: true, force: true });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-daemon",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"description": "Singleton RouteKit control daemon and stable model gateway.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -44,15 +44,15 @@
|
|
|
44
44
|
"effect": "4.0.0-rc.108",
|
|
45
45
|
"posthog-node": "5.46.1",
|
|
46
46
|
"yaml": "2.9.0",
|
|
47
|
-
"@velum-labs/routekit-accounts": "1.0
|
|
48
|
-
"@velum-labs/routekit-config": "1.0
|
|
49
|
-
"@velum-labs/routekit-control": "1.0
|
|
50
|
-
"@velum-labs/routekit-eval-contracts": "1.0
|
|
51
|
-
"@velum-labs/routekit-eval-store": "1.0
|
|
52
|
-
"@velum-labs/routekit-gateway": "1.0
|
|
53
|
-
"@velum-labs/routekit-registry": "1.0
|
|
54
|
-
"@velum-labs/routekit-runtime": "1.0
|
|
55
|
-
"@velum-labs/routekit-telemetry-core": "1.0
|
|
47
|
+
"@velum-labs/routekit-accounts": "1.1.0",
|
|
48
|
+
"@velum-labs/routekit-config": "1.1.0",
|
|
49
|
+
"@velum-labs/routekit-control": "1.1.0",
|
|
50
|
+
"@velum-labs/routekit-eval-contracts": "1.1.0",
|
|
51
|
+
"@velum-labs/routekit-eval-store": "1.1.0",
|
|
52
|
+
"@velum-labs/routekit-gateway": "1.1.0",
|
|
53
|
+
"@velum-labs/routekit-registry": "1.1.0",
|
|
54
|
+
"@velum-labs/routekit-runtime": "1.1.0",
|
|
55
|
+
"@velum-labs/routekit-telemetry-core": "1.1.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsc -b",
|