@danypops/jittor 0.13.0 → 0.15.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/package.json +4 -3
- package/src/adapters/artificial-analysis-direct-source.ts +42 -11
- package/src/adapters/lmarena-hf-source.ts +37 -15
- package/src/adapters/metric-benchmark-store.ts +29 -18
- package/src/adapters/openrouter-benchmark-source.ts +75 -19
- package/src/adapters/openrouter-design-arena-source.ts +36 -19
- package/src/adapters/sqlite-metric-store.ts +48 -25
- package/src/adapters/sqlite-session-identity-store.ts +13 -7
- package/src/cli-commands/benchmarks.ts +87 -22
- package/src/cli-commands/compaction.ts +7 -2
- package/src/cli-commands/context.ts +10 -2
- package/src/cli-commands/metrics.ts +128 -31
- package/src/cli-commands/op.ts +6 -1
- package/src/cli-commands/route-args.ts +5 -1
- package/src/cli-commands/router.ts +61 -18
- package/src/cli-commands/service-daemon.ts +28 -12
- package/src/cli-commands/session.ts +15 -4
- package/src/cli-commands/support.ts +1 -1
- package/src/cli.ts +30 -23
- package/src/client.ts +1 -1
- package/src/constants.ts +6 -2
- package/src/daemon.ts +44 -24
- package/src/domain/benchmark.ts +72 -40
- package/src/domain/codex-recovery.ts +34 -25
- package/src/domain/context-hub.ts +44 -25
- package/src/domain/context-telemetry.ts +106 -35
- package/src/domain/metric.ts +11 -11
- package/src/domain/model-observation.ts +139 -55
- package/src/domain/model-ranking-service.ts +13 -3
- package/src/domain/model-ranking.ts +126 -60
- package/src/domain/task-cost.ts +52 -11
- package/src/domain/task-focus.ts +11 -8
- package/src/domain/usage.ts +2 -2
- package/src/index.ts +69 -69
- package/src/log.ts +7 -2
- package/src/operations/benchmark-operations.ts +1 -1
- package/src/operations/context-operations.ts +15 -6
- package/src/operations/metrics-operations.ts +63 -28
- package/src/operations/model-ranking-operations.ts +9 -2
- package/src/operations/router-operations.ts +8 -4
- package/src/operations/session-identity-operations.ts +1 -1
- package/src/operations/session-scope.ts +5 -3
- package/src/policy.ts +22 -17
- package/src/ports/benchmark-controller.ts +1 -5
- package/src/ports/metric-store.ts +1 -1
- package/src/providers/anthropic-contracts.ts +13 -3
- package/src/providers/codex-contracts.ts +60 -52
- package/src/providers/codex.ts +16 -19
- package/src/providers/google-vertex-budget-contracts.ts +24 -14
- package/src/providers/google-vertex-budget.ts +15 -13
- package/src/providers/google-vertex-contracts.ts +36 -24
- package/src/providers/openrouter-contracts.ts +49 -51
- package/src/providers/openrouter.ts +21 -15
- package/src/providers/telemetry-sources.ts +13 -10
- package/src/router.ts +92 -43
- package/src/service.ts +93 -32
- package/src/session-identity-service.ts +10 -2
- package/src/state.ts +4 -10
- package/src/vehicle-registration.ts +154 -0
package/src/router.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { ROUTER_MAX_SESSION_SCOPES, ROUTER_SESSION_ID_MAX_CHARACTERS } from "./constants.ts";
|
|
2
|
+
import { type BudgetWindow, evaluateRoutingPolicy, type PolicyConfig, type PolicyDecision, type Route } from "./policy.ts";
|
|
2
3
|
import type { MetricStore } from "./ports/metric-store.ts";
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
RouteOverride,
|
|
6
|
+
RouterController,
|
|
7
|
+
RouterStatus,
|
|
8
|
+
TelemetryPollResult,
|
|
9
|
+
TelemetrySourceStatus,
|
|
10
|
+
} from "./ports/router-controller.ts";
|
|
4
11
|
import type { TelemetrySource } from "./ports/telemetry-source.ts";
|
|
5
|
-
import {
|
|
6
|
-
evaluateRoutingPolicy,
|
|
7
|
-
type BudgetWindow,
|
|
8
|
-
type PolicyConfig,
|
|
9
|
-
type PolicyDecision,
|
|
10
|
-
type Route,
|
|
11
|
-
} from "./policy.ts";
|
|
12
12
|
|
|
13
13
|
export interface JittorRouterOptions {
|
|
14
14
|
metrics: MetricStore;
|
|
@@ -57,7 +57,9 @@ export class JittorRouter implements RouterController {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
poll(): Promise<TelemetryPollResult> {
|
|
60
|
-
this.inFlightPoll ??= this.runPoll().finally(() => {
|
|
60
|
+
this.inFlightPoll ??= this.runPoll().finally(() => {
|
|
61
|
+
this.inFlightPoll = null;
|
|
62
|
+
});
|
|
61
63
|
return this.inFlightPoll;
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -79,33 +81,66 @@ export class JittorRouter implements RouterController {
|
|
|
79
81
|
const now = this.clock();
|
|
80
82
|
const state = this.sessionState(sessionId);
|
|
81
83
|
this.expireOverride(state);
|
|
82
|
-
if (state.paused)
|
|
84
|
+
if (state.paused)
|
|
85
|
+
return this.remember(state, {
|
|
86
|
+
action: "halt",
|
|
87
|
+
pressure: Number.POSITIVE_INFINITY,
|
|
88
|
+
reason: "Jittor is paused",
|
|
89
|
+
decidedAt: now,
|
|
90
|
+
trace: ["manual pause"],
|
|
91
|
+
});
|
|
83
92
|
if (state.override) {
|
|
84
93
|
const route = state.override.route;
|
|
85
|
-
const action =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
const action =
|
|
95
|
+
route.provider !== state.currentRoute.provider
|
|
96
|
+
? "switch-provider"
|
|
97
|
+
: route.model !== state.currentRoute.model
|
|
98
|
+
? "switch-model"
|
|
99
|
+
: route.thinking !== state.currentRoute.thinking
|
|
100
|
+
? "lower-thinking"
|
|
101
|
+
: "continue";
|
|
102
|
+
return this.remember(state, {
|
|
103
|
+
action,
|
|
104
|
+
route,
|
|
105
|
+
pressure: 0,
|
|
106
|
+
reason: "manual route override",
|
|
107
|
+
decidedAt: now,
|
|
108
|
+
trace: ["manual override"],
|
|
109
|
+
});
|
|
91
110
|
}
|
|
92
|
-
if (!this.isReady(state))
|
|
111
|
+
if (!this.isReady(state))
|
|
112
|
+
return this.remember(state, {
|
|
113
|
+
action: "halt",
|
|
114
|
+
pressure: Number.POSITIVE_INFINITY,
|
|
115
|
+
reason: "required telemetry is not ready",
|
|
116
|
+
decidedAt: now,
|
|
117
|
+
trace: ["fail closed"],
|
|
118
|
+
});
|
|
93
119
|
const activeSources = this.options.sources.filter((source) => source.provider === state.currentRoute.provider);
|
|
94
120
|
const activeSourceIds = new Set(activeSources.map((source) => source.id));
|
|
95
121
|
const requiredSourceIds = new Set(activeSources.filter((source) => source.required).map((source) => source.id));
|
|
96
122
|
const activeWindows = [...this.windows.entries()].filter(([sourceId]) => activeSourceIds.has(sourceId));
|
|
97
123
|
const requiredWindows = activeWindows.filter(([sourceId]) => requiredSourceIds.has(sourceId)).flatMap(([, windows]) => windows);
|
|
98
124
|
if (requiredSourceIds.size === 0 && activeWindows.every(([, windows]) => windows.length === 0)) {
|
|
99
|
-
return this.rememberPolicy(state, {
|
|
125
|
+
return this.rememberPolicy(state, {
|
|
126
|
+
action: "continue",
|
|
127
|
+
pressure: 0,
|
|
128
|
+
reason: "provider has no enforceable budget window; monitor-only",
|
|
129
|
+
decidedAt: now,
|
|
130
|
+
trace: ["monitor-only"],
|
|
131
|
+
});
|
|
100
132
|
}
|
|
101
|
-
return this.rememberPolicy(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
return this.rememberPolicy(
|
|
134
|
+
state,
|
|
135
|
+
evaluateRoutingPolicy({
|
|
136
|
+
now,
|
|
137
|
+
windows: requiredSourceIds.size > 0 && requiredWindows.length === 0 ? [] : activeWindows.flatMap(([, windows]) => windows),
|
|
138
|
+
currentRoute: state.currentRoute,
|
|
139
|
+
routes: state.availableRoutes,
|
|
140
|
+
config: this.options.policy,
|
|
141
|
+
previousDecision: state.previousPolicyDecision ?? undefined,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
109
144
|
}
|
|
110
145
|
|
|
111
146
|
pause(sessionId?: string): RouterStatus {
|
|
@@ -120,7 +155,8 @@ export class JittorRouter implements RouterController {
|
|
|
120
155
|
|
|
121
156
|
setOverride(override: RouteOverride | undefined, sessionId?: string): RouterStatus {
|
|
122
157
|
const state = this.sessionState(sessionId);
|
|
123
|
-
if (!override || !state.availableRoutes.some((route) => sameRoute(route, override.route)))
|
|
158
|
+
if (!override || !state.availableRoutes.some((route) => sameRoute(route, override.route)))
|
|
159
|
+
throw new Error("override route is not available in Pi");
|
|
124
160
|
if (override.expiresAt !== null && override.expiresAt <= this.clock()) throw new Error("override expiry must be in the future");
|
|
125
161
|
state.override = structuredClone(override);
|
|
126
162
|
return this.status(sessionId);
|
|
@@ -139,10 +175,18 @@ export class JittorRouter implements RouterController {
|
|
|
139
175
|
|
|
140
176
|
setAvailableRoutes(routes: Route[], sessionId?: string): RouterStatus {
|
|
141
177
|
if (!Array.isArray(routes)) throw new Error("available routes must be an array");
|
|
142
|
-
const valid = routes.filter(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
178
|
+
const valid = routes.filter(
|
|
179
|
+
(route) =>
|
|
180
|
+
typeof route?.provider === "string" &&
|
|
181
|
+
route.provider.length > 0 &&
|
|
182
|
+
typeof route.model === "string" &&
|
|
183
|
+
route.model.length > 0 &&
|
|
184
|
+
typeof route.thinking === "string" &&
|
|
185
|
+
route.thinking.length > 0,
|
|
186
|
+
);
|
|
187
|
+
this.sessionState(sessionId).availableRoutes = valid
|
|
188
|
+
.filter((route, index) => valid.findIndex((candidate) => sameRoute(candidate, route)) === index)
|
|
189
|
+
.map((route) => structuredClone(route));
|
|
146
190
|
return this.status(sessionId);
|
|
147
191
|
}
|
|
148
192
|
|
|
@@ -155,22 +199,27 @@ export class JittorRouter implements RouterController {
|
|
|
155
199
|
.filter((route): route is Route => route !== undefined);
|
|
156
200
|
const current = ranked.find((route) => sameRoute(route, state.currentRoute));
|
|
157
201
|
if (!current) throw new Error("model ranking does not contain the current available route");
|
|
158
|
-
state.availableRoutes = [
|
|
202
|
+
state.availableRoutes = [
|
|
203
|
+
structuredClone(current),
|
|
204
|
+
...ranked.filter((route) => !sameRoute(route, current)).map((route) => structuredClone(route)),
|
|
205
|
+
];
|
|
159
206
|
return this.status(sessionId);
|
|
160
207
|
}
|
|
161
208
|
|
|
162
209
|
private async runPoll(): Promise<TelemetryPollResult> {
|
|
163
|
-
const statuses = await Promise.all(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
210
|
+
const statuses = await Promise.all(
|
|
211
|
+
this.options.sources.map(async (source): Promise<TelemetrySourceStatus> => {
|
|
212
|
+
try {
|
|
213
|
+
const batch = await source.poll();
|
|
214
|
+
for (const observation of batch.metrics) this.options.metrics.record(observation);
|
|
215
|
+
this.windows.set(source.id, batch.windows);
|
|
216
|
+
return { id: source.id, provider: source.provider, ok: true, metrics: batch.metrics.length, observedAt: batch.observedAt };
|
|
217
|
+
} catch {
|
|
218
|
+
this.windows.delete(source.id);
|
|
219
|
+
return { id: source.id, provider: source.provider, ok: false, metrics: 0, observedAt: this.clock(), error: "poll failed" };
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
174
223
|
this.sourceStatuses = statuses;
|
|
175
224
|
return { sources: structuredClone(statuses), observedAt: this.clock() };
|
|
176
225
|
}
|
package/src/service.ts
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
|
+
import { VehicleRegistry } from "@danypops/vehicle-server";
|
|
2
|
+
import { createVehicleHttpApp } from "@danypops/vehicle-server/http";
|
|
1
3
|
import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/vehicle-server/rpc-http";
|
|
2
4
|
import { SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES } from "./constants.ts";
|
|
3
|
-
import {
|
|
4
|
-
import { VERSION } from "./version.ts";
|
|
5
|
-
import type { MetricObservation, MetricQuery, StoredMetricObservation } from "./domain/metric.ts";
|
|
5
|
+
import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "./domain/benchmark.ts";
|
|
6
6
|
import type { CompactionDurationEstimate, ContextAssessment } from "./domain/context-telemetry.ts";
|
|
7
|
+
import type { MetricObservation, MetricQuery, StoredMetricObservation } from "./domain/metric.ts";
|
|
8
|
+
import type { ModelRankingResult } from "./domain/model-ranking.ts";
|
|
9
|
+
import type { ModelRanker, ModelRecommendationInput } from "./domain/model-ranking-service.ts";
|
|
7
10
|
import type { TaskCostSummary } from "./domain/task-cost.ts";
|
|
8
11
|
import type { UsageAggregateRow } from "./domain/usage.ts";
|
|
9
|
-
import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "./domain/benchmark.ts";
|
|
10
|
-
import type { ModelRanker, ModelRecommendationInput } from "./domain/model-ranking-service.ts";
|
|
11
|
-
import type { ModelRankingResult } from "./domain/model-ranking.ts";
|
|
12
|
-
import type { BenchmarkController } from "./ports/benchmark-controller.ts";
|
|
13
|
-
import type { MetricStore } from "./ports/metric-store.ts";
|
|
14
|
-
import type { RouteOverride, RouterController, RouterStatus, TelemetryPollResult } from "./ports/router-controller.ts";
|
|
15
|
-
import type { PolicyDecision, Route } from "./policy.ts";
|
|
16
|
-
import { metricsOperations } from "./operations/metrics-operations.ts";
|
|
17
12
|
import { benchmarkOperations } from "./operations/benchmark-operations.ts";
|
|
18
13
|
import { contextOperations } from "./operations/context-operations.ts";
|
|
19
|
-
import {
|
|
14
|
+
import { metricsOperations } from "./operations/metrics-operations.ts";
|
|
20
15
|
import { modelRankingOperations } from "./operations/model-ranking-operations.ts";
|
|
16
|
+
import { routerOperations } from "./operations/router-operations.ts";
|
|
21
17
|
import { sessionIdentityOperations } from "./operations/session-identity-operations.ts";
|
|
22
18
|
import { routerMutationAuthorizer } from "./operations/session-scope.ts";
|
|
23
19
|
import type { OperationHandlerMap } from "./operations/types.ts";
|
|
20
|
+
import type { PolicyDecision, Route } from "./policy.ts";
|
|
21
|
+
import type { BenchmarkController } from "./ports/benchmark-controller.ts";
|
|
22
|
+
import type { MetricStore } from "./ports/metric-store.ts";
|
|
23
|
+
import type { RouteOverride, RouterController, RouterStatus, TelemetryPollResult } from "./ports/router-controller.ts";
|
|
24
|
+
import { InvalidSessionSecretError, type RegisterSessionIdentityResult, type SessionIdentity } from "./session-identity-service.ts";
|
|
25
|
+
import { registerJittorVehicleOperations } from "./vehicle-registration.ts";
|
|
26
|
+
import { VERSION } from "./version.ts";
|
|
24
27
|
|
|
25
28
|
export const EXPECTED_OPERATION_NAMES = [
|
|
26
29
|
"metrics.record",
|
|
@@ -50,8 +53,11 @@ export const EXPECTED_OPERATION_NAMES = [
|
|
|
50
53
|
"router.available_routes",
|
|
51
54
|
] as const;
|
|
52
55
|
|
|
53
|
-
export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
|
|
54
|
-
interface RouterScopeInput {
|
|
56
|
+
export type OperationName = (typeof EXPECTED_OPERATION_NAMES)[number];
|
|
57
|
+
interface RouterScopeInput {
|
|
58
|
+
session_id?: string;
|
|
59
|
+
session_secret?: string;
|
|
60
|
+
}
|
|
55
61
|
export interface OperationInputs {
|
|
56
62
|
"session.register": { session_id: string };
|
|
57
63
|
"session.release": { session_id: string; session_secret?: string };
|
|
@@ -111,31 +117,73 @@ export class UnknownOperationError extends Error {}
|
|
|
111
117
|
export { InvalidSessionSecretError };
|
|
112
118
|
|
|
113
119
|
class UnavailableModelRanker implements ModelRanker {
|
|
114
|
-
rank(): ModelRankingResult {
|
|
120
|
+
rank(): ModelRankingResult {
|
|
121
|
+
throw new Error("model ranking is not configured");
|
|
122
|
+
}
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
class UnavailableBenchmarkController implements BenchmarkController {
|
|
118
|
-
async refresh(): Promise<BenchmarkRefreshResult> {
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
async refresh(): Promise<BenchmarkRefreshResult> {
|
|
127
|
+
return this.status();
|
|
128
|
+
}
|
|
129
|
+
status(): BenchmarkRefreshResult {
|
|
130
|
+
return { observedAt: Date.now(), sources: [] };
|
|
131
|
+
}
|
|
132
|
+
query(): BenchmarkQueryResult {
|
|
133
|
+
throw new Error("benchmark evidence is not configured");
|
|
134
|
+
}
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
class UnavailableRouter implements RouterController {
|
|
124
|
-
private readonly unavailable: RouterStatus = {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
private readonly unavailable: RouterStatus = {
|
|
139
|
+
ready: false,
|
|
140
|
+
paused: false,
|
|
141
|
+
sources: [],
|
|
142
|
+
lastDecision: null,
|
|
143
|
+
override: null,
|
|
144
|
+
currentRoute: null,
|
|
145
|
+
availableRoutes: [],
|
|
146
|
+
};
|
|
147
|
+
async poll(): Promise<TelemetryPollResult> {
|
|
148
|
+
return { sources: [], observedAt: Date.now() };
|
|
149
|
+
}
|
|
150
|
+
status(): RouterStatus {
|
|
151
|
+
return structuredClone(this.unavailable);
|
|
152
|
+
}
|
|
153
|
+
decide(): PolicyDecision {
|
|
154
|
+
return {
|
|
155
|
+
action: "halt",
|
|
156
|
+
pressure: Number.POSITIVE_INFINITY,
|
|
157
|
+
reason: "router is not configured",
|
|
158
|
+
decidedAt: Date.now(),
|
|
159
|
+
trace: ["fail closed"],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
pause(): RouterStatus {
|
|
163
|
+
return this.status();
|
|
164
|
+
}
|
|
165
|
+
resume(): RouterStatus {
|
|
166
|
+
return this.status();
|
|
167
|
+
}
|
|
168
|
+
setOverride(): RouterStatus {
|
|
169
|
+
return this.status();
|
|
170
|
+
}
|
|
171
|
+
clearOverride(): RouterStatus {
|
|
172
|
+
return this.status();
|
|
173
|
+
}
|
|
174
|
+
setCurrentRoute(): RouterStatus {
|
|
175
|
+
return this.status();
|
|
176
|
+
}
|
|
177
|
+
setAvailableRoutes(): RouterStatus {
|
|
178
|
+
return this.status();
|
|
179
|
+
}
|
|
134
180
|
}
|
|
135
181
|
|
|
136
182
|
export class JittorService {
|
|
137
183
|
private readonly router: RouterController;
|
|
138
184
|
private readonly operations: OperationHandlerMap;
|
|
185
|
+
/** Every jittor operation, also projected onto the real Vehicle protocol -- see vehicle-registration.ts. Served alongside (not replacing) the /api/v1/ops route below. */
|
|
186
|
+
readonly vehicleRegistry: VehicleRegistry;
|
|
139
187
|
|
|
140
188
|
constructor(
|
|
141
189
|
private readonly metrics: MetricStore,
|
|
@@ -157,6 +205,12 @@ export class JittorService {
|
|
|
157
205
|
...modelRankingOperations(modelRanker, router, authorize),
|
|
158
206
|
...sessionIdentityOperations(sessionIdentity),
|
|
159
207
|
};
|
|
208
|
+
this.vehicleRegistry = new VehicleRegistry({
|
|
209
|
+
name: "jittor",
|
|
210
|
+
version: "1.0.0",
|
|
211
|
+
description: "Just-in-Time Token Optimizing Router for Pi -- metrics, benchmark evidence, model ranking, and router policy.",
|
|
212
|
+
});
|
|
213
|
+
registerJittorVehicleOperations(this.vehicleRegistry, this.operations);
|
|
160
214
|
}
|
|
161
215
|
|
|
162
216
|
operationNames(): OperationName[] {
|
|
@@ -204,10 +258,16 @@ function json(value: unknown, status = 200): Response {
|
|
|
204
258
|
|
|
205
259
|
export function createApp(options: JittorAppOptions): { fetch(request: Request): Promise<Response> } {
|
|
206
260
|
const maxBodyBytes = options.maxBodyBytes ?? SERVICE_MAX_BODY_BYTES;
|
|
261
|
+
// A real second transport for every jittor operation (see vehicle-registration.ts) --
|
|
262
|
+
// composed here rather than replacing /api/v1/ops, matching every other Vehicle-migrated
|
|
263
|
+
// daemon in this ecosystem ("served alongside", not "instead of"). Routed before the
|
|
264
|
+
// top-level bearer check below since createVehicleHttpApp performs its own.
|
|
265
|
+
const vehicleApp = createVehicleHttpApp({ registry: options.service.vehicleRegistry, token: options.token });
|
|
207
266
|
return {
|
|
208
267
|
async fetch(request: Request): Promise<Response> {
|
|
209
|
-
if (!requireBearerToken(request, options.token)) return errorResponse("unauthorized", 401);
|
|
210
268
|
const url = new URL(request.url);
|
|
269
|
+
if (url.pathname.startsWith("/vehicle/")) return vehicleApp.fetch(request);
|
|
270
|
+
if (!requireBearerToken(request, options.token)) return errorResponse("unauthorized", 401);
|
|
211
271
|
if (request.method === "GET" && url.pathname === "/health") return healthResponse(VERSION);
|
|
212
272
|
if (request.method === "GET" && url.pathname === "/ready") return readyResponse(options.service.ready());
|
|
213
273
|
if (request.method === "GET" && url.pathname === "/api/v1/ops") return json({ operations: options.service.operationNames() });
|
|
@@ -219,9 +279,10 @@ export function createApp(options: JittorAppOptions): { fetch(request: Request):
|
|
|
219
279
|
try {
|
|
220
280
|
const body = JSON.parse(text) as { op?: unknown; input?: unknown };
|
|
221
281
|
if (typeof body.op !== "string") throw new Error("op is required");
|
|
222
|
-
const input =
|
|
223
|
-
|
|
224
|
-
|
|
282
|
+
const input =
|
|
283
|
+
typeof body.input === "object" && body.input !== null && !Array.isArray(body.input)
|
|
284
|
+
? (body.input as Record<string, unknown>)
|
|
285
|
+
: {};
|
|
225
286
|
return json({ result: await options.service.execute(body.op, input) });
|
|
226
287
|
} catch (error) {
|
|
227
288
|
if (error instanceof UnknownOperationError) return json({ error: error.message }, 404);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isSessionRegistered,
|
|
3
|
+
registerSessionIdentity,
|
|
4
|
+
releaseSessionIdentity,
|
|
5
|
+
verifySessionSecret,
|
|
6
|
+
} from "@danypops/vehicle-server/session-identity";
|
|
2
7
|
import type { SessionIdentityStore } from "./ports/session-identity-store.ts";
|
|
3
8
|
|
|
4
9
|
export interface RegisterSessionIdentityResult {
|
|
@@ -50,6 +55,9 @@ export class SessionIdentity {
|
|
|
50
55
|
assertAuthorized(sessionId: string | undefined, secret: string | undefined): void {
|
|
51
56
|
if (sessionId === undefined) return;
|
|
52
57
|
if (!this.isRegistered(sessionId)) return;
|
|
53
|
-
if (!this.verify(sessionId, secret))
|
|
58
|
+
if (!this.verify(sessionId, secret))
|
|
59
|
+
throw new InvalidSessionSecretError(
|
|
60
|
+
`session "${sessionId}" is registered; a valid session_secret is required to mutate its router state`,
|
|
61
|
+
);
|
|
54
62
|
}
|
|
55
63
|
}
|
package/src/state.ts
CHANGED
|
@@ -6,22 +6,16 @@
|
|
|
6
6
|
* client.ts, cli.ts, and their tests) is untouched by this migration.
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
|
+
type DaemonHandle,
|
|
10
|
+
type DaemonPaths,
|
|
9
11
|
ensureAuthToken as ensureVehicleAuthToken,
|
|
12
|
+
type PathEnvironment,
|
|
10
13
|
readDaemonHandle as readVehicleHandle,
|
|
11
14
|
removeDaemonHandle as removeVehicleHandle,
|
|
12
15
|
resolveDaemonPaths,
|
|
13
16
|
writeDaemonHandle as writeVehicleHandle,
|
|
14
|
-
type DaemonHandle,
|
|
15
|
-
type DaemonPaths,
|
|
16
|
-
type PathEnvironment,
|
|
17
17
|
} from "@danypops/vehicle-server/paths";
|
|
18
|
-
import {
|
|
19
|
-
DATABASE_FILENAME,
|
|
20
|
-
HANDLE_FILENAME,
|
|
21
|
-
JITTOR_STATE_DIRECTORY,
|
|
22
|
-
SYSTEMD_UNIT_NAME,
|
|
23
|
-
TOKEN_FILENAME,
|
|
24
|
-
} from "./constants.ts";
|
|
18
|
+
import { DATABASE_FILENAME, HANDLE_FILENAME, JITTOR_STATE_DIRECTORY, SYSTEMD_UNIT_NAME, TOKEN_FILENAME } from "./constants.ts";
|
|
25
19
|
|
|
26
20
|
export type JittorPaths = DaemonPaths;
|
|
27
21
|
export type { DaemonHandle, PathEnvironment };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jittor's 25-operation surface projected onto the real Vehicle protocol.
|
|
3
|
+
* Every operation delegates to the exact same handler function service.ts's
|
|
4
|
+
* hand-rolled dispatch already calls (one implementation, two projections --
|
|
5
|
+
* the same shape every other Vehicle-migrated daemon in this ecosystem
|
|
6
|
+
* uses) -- no behavior change, only a second real transport served
|
|
7
|
+
* alongside (not replacing) the existing /api/v1/ops route.
|
|
8
|
+
*
|
|
9
|
+
* Every jittor operation already validates its own loosely-typed
|
|
10
|
+
* Record<string, unknown> input internally (see operations/*.ts) -- there
|
|
11
|
+
* is no separate Vehicle-side schema to duplicate that logic, so both input
|
|
12
|
+
* and output use passthroughVehicleSchema and let the real handler's own
|
|
13
|
+
* validation (already covered by service.test.ts) be the single source of
|
|
14
|
+
* truth for what's accepted.
|
|
15
|
+
*
|
|
16
|
+
* jittor's operations are never exposed as Pi tools (confirmed: pi-jittor
|
|
17
|
+
* has zero pi.registerTool() call sites -- its whole surface is consumed
|
|
18
|
+
* internally by the extension's own footer/context-hub/router capabilities),
|
|
19
|
+
* so effect/permission classification here is about standardized taxonomy
|
|
20
|
+
* and future eligibility (Vehicle Jobs, Approval Gate, Safety), not gating
|
|
21
|
+
* an agent-facing tool call the way it does for a Vehicle whose operations
|
|
22
|
+
* ARE projected as tools.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { VehicleEffect, VehicleIdempotency } from "@danypops/vehicle-core";
|
|
26
|
+
import { bindVehicleOperation, defineVehicleOperation, passthroughVehicleSchema, VehicleError } from "@danypops/vehicle-core";
|
|
27
|
+
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
28
|
+
import type { OperationHandlerMap } from "./operations/types.ts";
|
|
29
|
+
import type { OperationName } from "./service.ts";
|
|
30
|
+
import { InvalidSessionSecretError } from "./session-identity-service.ts";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Preserves /api/v1/ops's own error-to-status behavior (see createApp() in
|
|
34
|
+
* service.ts): InvalidSessionSecretError -> 403, everything else a handler
|
|
35
|
+
* throws -> 400. Without this, VehicleRegistry's own invoke() wraps any
|
|
36
|
+
* handler-thrown error that isn't already a VehicleError into
|
|
37
|
+
* category:"internal" (HTTP 500), losing both the specific status and the
|
|
38
|
+
* error's own message (a VehicleFailure never serializes an error's cause
|
|
39
|
+
* over the wire) -- the same regression class found and fixed in
|
|
40
|
+
* web-spider's own migration (see its vehicle-error-parity.ts).
|
|
41
|
+
*/
|
|
42
|
+
async function withJittorErrorParity<T>(run: () => T | Promise<T>): Promise<T> {
|
|
43
|
+
try {
|
|
44
|
+
return await run();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof VehicleError) throw error;
|
|
47
|
+
if (error instanceof InvalidSessionSecretError) {
|
|
48
|
+
throw new VehicleError("invalid-session-secret", error.message, { category: "authorization", cause: error });
|
|
49
|
+
}
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
throw new VehicleError("operation-rejected", message, { category: "validation", cause: error });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const OWNER = "jittor";
|
|
56
|
+
const LIMITS = { defaultTimeoutMs: 10_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 1_048_576 };
|
|
57
|
+
|
|
58
|
+
interface OperationMeta {
|
|
59
|
+
readonly description: string;
|
|
60
|
+
readonly effect: VehicleEffect;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const READ: VehicleIdempotency = { mode: "safe" };
|
|
64
|
+
const WRITE: VehicleIdempotency = { mode: "unsafe" };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* One entry per EXPECTED_OPERATION_NAMES member -- effect classification
|
|
68
|
+
* rationale:
|
|
69
|
+
* - metrics.record/record_batch: local-write (jittor's own metric store).
|
|
70
|
+
* - metrics.query/distinct_scopes/usage_series/cost_by_task: read (pure
|
|
71
|
+
* projections of already-recorded local data).
|
|
72
|
+
* - metrics.prune: destructive (irreversible deletion; the handler itself
|
|
73
|
+
* already refuses a too-recent cutoff without force:true).
|
|
74
|
+
* - benchmark.refresh: external-write (refreshes from external model
|
|
75
|
+
* benchmark/leaderboard sources) -- a genuine future Vehicle Jobs
|
|
76
|
+
* candidate given its existing status/query split, deferred here (see
|
|
77
|
+
* this task's closing note) same as web-spider's fetch/crawl.
|
|
78
|
+
* - benchmark.status/query: read (local cache of the last refresh).
|
|
79
|
+
* - session.register/release: local-write (jittor's own session-identity
|
|
80
|
+
* registry).
|
|
81
|
+
* - models.rank: local-write -- primarily a read/scoring operation, but an
|
|
82
|
+
* automatic selection applies it as a real router mutation as a side
|
|
83
|
+
* effect (see model-ranking-operations.ts), so classified by its most
|
|
84
|
+
* consequential possible outcome, not its common case.
|
|
85
|
+
* - context.assess/compaction.estimate: read.
|
|
86
|
+
* - service.checkpoint: local-write (flushes the metric store's WAL).
|
|
87
|
+
* - telemetry.poll: read (reads external telemetry provider status; does
|
|
88
|
+
* not itself expose a way to write anything back to those providers).
|
|
89
|
+
* - router.status/decide: read (a decision query, not a mutation).
|
|
90
|
+
* - router.pause/resume/override/clear_override/current_route/
|
|
91
|
+
* available_routes: local-write (every one is a real router.set (or
|
|
92
|
+
* pause/resume) call, despite router.current_route's read-sounding name
|
|
93
|
+
* -- confirmed directly against router-operations.ts).
|
|
94
|
+
*/
|
|
95
|
+
const OPERATION_META: Record<OperationName, OperationMeta> = {
|
|
96
|
+
"metrics.record": { description: "Records one metric observation.", effect: "local-write" },
|
|
97
|
+
"metrics.record_batch": { description: "Records a bounded batch of metric observations as one atomic unit.", effect: "local-write" },
|
|
98
|
+
"metrics.query": { description: "Queries recorded metric observations.", effect: "read" },
|
|
99
|
+
"metrics.distinct_scopes": { description: "Lists distinct scopes recorded for a source within a bounded time window.", effect: "read" },
|
|
100
|
+
"metrics.usage_series": { description: "Server-side bucketed usage aggregation for a source's distinct scopes.", effect: "read" },
|
|
101
|
+
"metrics.cost_by_task": { description: "Sums cost/token metrics by focused task within a bounded time window.", effect: "read" },
|
|
102
|
+
"metrics.prune": { description: "Deletes metric observations older than a cutoff. Irreversible.", effect: "destructive" },
|
|
103
|
+
"benchmark.refresh": { description: "Refreshes model benchmark evidence from external sources.", effect: "external-write" },
|
|
104
|
+
"benchmark.status": { description: "Reports the last benchmark refresh's own status.", effect: "read" },
|
|
105
|
+
"benchmark.query": { description: "Queries cached benchmark evidence.", effect: "read" },
|
|
106
|
+
"session.register": { description: "Registers a Pi session identity, issuing a session secret.", effect: "local-write" },
|
|
107
|
+
"session.release": { description: "Releases a registered Pi session identity.", effect: "local-write" },
|
|
108
|
+
"models.rank": {
|
|
109
|
+
description: "Scores model candidates; an automatic selection also applies it as a router mutation.",
|
|
110
|
+
effect: "local-write",
|
|
111
|
+
},
|
|
112
|
+
"context.assess": { description: "Assesses context-injection/compaction health within a bounded time window.", effect: "read" },
|
|
113
|
+
"compaction.estimate": { description: "Estimates compaction duration from recorded samples.", effect: "read" },
|
|
114
|
+
"service.checkpoint": { description: "Flushes the metric store's write-ahead log.", effect: "local-write" },
|
|
115
|
+
"telemetry.poll": { description: "Polls external telemetry provider status.", effect: "read" },
|
|
116
|
+
"router.status": { description: "Reports the router's current status.", effect: "read" },
|
|
117
|
+
"router.decide": { description: "Computes the router's current policy decision without applying it.", effect: "read" },
|
|
118
|
+
"router.pause": { description: "Pauses automatic routing.", effect: "local-write" },
|
|
119
|
+
"router.resume": { description: "Resumes automatic routing.", effect: "local-write" },
|
|
120
|
+
"router.override": { description: "Sets a time-bounded manual route override.", effect: "local-write" },
|
|
121
|
+
"router.clear_override": { description: "Clears a manual route override.", effect: "local-write" },
|
|
122
|
+
"router.current_route": { description: "Sets the router's currently-active route.", effect: "local-write" },
|
|
123
|
+
"router.available_routes": { description: "Sets the router's currently-available routes.", effect: "local-write" },
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** Read effects need only jittor:read; every other effect needs both (writes commonly also read first). */
|
|
127
|
+
function permissionsFor(effect: VehicleEffect): readonly string[] {
|
|
128
|
+
return effect === "read" ? ["jittor:read"] : ["jittor:read", "jittor:write"];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function registerJittorVehicleOperations(registry: VehicleRegistry, operations: OperationHandlerMap): void {
|
|
132
|
+
for (const [name, meta] of Object.entries(OPERATION_META) as Array<[OperationName, OperationMeta]>) {
|
|
133
|
+
const handler = operations[name];
|
|
134
|
+
if (!handler) throw new Error(`jittor Vehicle registration: no handler configured for operation "${name}"`);
|
|
135
|
+
const operation = defineVehicleOperation({
|
|
136
|
+
name,
|
|
137
|
+
version: 1,
|
|
138
|
+
description: meta.description,
|
|
139
|
+
input: passthroughVehicleSchema,
|
|
140
|
+
output: passthroughVehicleSchema,
|
|
141
|
+
permissions: permissionsFor(meta.effect),
|
|
142
|
+
effect: meta.effect,
|
|
143
|
+
idempotency: meta.effect === "read" ? READ : WRITE,
|
|
144
|
+
limits: LIMITS,
|
|
145
|
+
});
|
|
146
|
+
registry.register(
|
|
147
|
+
OWNER,
|
|
148
|
+
bindVehicleOperation(
|
|
149
|
+
operation,
|
|
150
|
+
() => async (context) => withJittorErrorParity(() => handler(context.input as Record<string, unknown>)),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|