llm-relay 0.77.0 → 0.78.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/accounting-store-schema.js +5 -6
- package/dist/accounting-store-schema.js.map +1 -1
- package/dist/accounting-store.d.ts +10 -0
- package/dist/accounting-store.js +60 -6
- package/dist/accounting-store.js.map +1 -1
- package/dist/backend/envelope-validator.d.ts +1 -1
- package/dist/backend/envelope-validator.js +89 -52
- package/dist/backend/envelope-validator.js.map +1 -1
- package/dist/backend/health-prober.js +3 -1
- package/dist/backend/health-prober.js.map +1 -1
- package/dist/backend.d.ts +48 -0
- package/dist/backend.js +831 -7
- package/dist/backend.js.map +1 -1
- package/dist/candidate-runner.d.ts +135 -50
- package/dist/candidate-runner.js +160 -55
- package/dist/candidate-runner.js.map +1 -1
- package/dist/cli.d.ts +24 -2
- package/dist/cli.js +122 -20
- package/dist/cli.js.map +1 -1
- package/dist/config/routing-parser.d.ts +13 -8
- package/dist/config/routing-parser.js +369 -183
- package/dist/config/routing-parser.js.map +1 -1
- package/dist/config-types.d.ts +202 -0
- package/dist/config-types.js +2 -0
- package/dist/config-types.js.map +1 -1
- package/dist/config.d.ts +45 -1
- package/dist/config.js +87 -22
- package/dist/config.js.map +1 -1
- package/dist/dashboard-contract.d.ts +38 -0
- package/dist/dashboard-contract.js +19 -3
- package/dist/dashboard-contract.js.map +1 -1
- package/dist/dispatch-lane-stats.d.ts +33 -4
- package/dist/dispatch-lane-stats.js +65 -16
- package/dist/dispatch-lane-stats.js.map +1 -1
- package/dist/dispatch.d.ts +9 -0
- package/dist/dispatch.js +36 -4
- package/dist/dispatch.js.map +1 -1
- package/dist/key-checker.d.ts +21 -1
- package/dist/key-checker.js +38 -12
- package/dist/key-checker.js.map +1 -1
- package/dist/lane-affinity.d.ts +94 -1
- package/dist/lane-affinity.js +52 -4
- package/dist/lane-affinity.js.map +1 -1
- package/dist/mcp/lane-runner.d.ts +69 -16
- package/dist/mcp/lane-runner.js +30 -1
- package/dist/mcp/lane-runner.js.map +1 -1
- package/dist/mcp/server.d.ts +39 -4
- package/dist/mcp/server.js +70 -13
- package/dist/mcp/server.js.map +1 -1
- package/dist/openai-request.d.ts +21 -0
- package/dist/openai-request.js +1 -1
- package/dist/openai-request.js.map +1 -1
- package/dist/ping/cadence.js +9 -1
- package/dist/ping/cadence.js.map +1 -1
- package/dist/ping/probe-cache.d.ts +13 -0
- package/dist/ping/probe-cache.js +12 -0
- package/dist/ping/probe-cache.js.map +1 -1
- package/dist/responses-request.d.ts +20 -0
- package/dist/responses-request.js +7 -7
- package/dist/responses-request.js.map +1 -1
- package/dist/routes/admin.d.ts +8 -1
- package/dist/routes/admin.js +42 -6
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/messages.d.ts +1 -0
- package/dist/routes/messages.js +13 -8
- package/dist/routes/messages.js.map +1 -1
- package/dist/routes/openai-front.js +20 -7
- package/dist/routes/openai-front.js.map +1 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.js +21 -3
- package/dist/server.js.map +1 -1
- package/dist/setup-claude.d.ts +2 -2
- package/dist/setup-claude.js +9 -7
- package/dist/setup-claude.js.map +1 -1
- package/dist/stream-pipeline.d.ts +116 -0
- package/dist/stream-pipeline.js +151 -0
- package/dist/stream-pipeline.js.map +1 -1
- package/dist/target-facts.d.ts +5 -4
- package/dist/target-facts.js +5 -0
- package/dist/target-facts.js.map +1 -1
- package/dist/telemetry.d.ts +15 -2
- package/dist/telemetry.js +4 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tier-data.d.ts +22 -0
- package/dist/tier-data.js +12 -2
- package/dist/tier-data.js.map +1 -1
- package/package.json +2 -1
- package/skills/llm-relay/SKILL.md +8 -3
- package/skills/llm-relay/references/dispatch-lanes.md +2 -1
- package/skills/llm-relay/references/operations.md +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EFFORT_LEVELS, } from "../config-types.js";
|
|
1
|
+
import { DEFAULT_MCP_MAX_WAIT_MS, EFFORT_LEVELS, } from "../config-types.js";
|
|
2
2
|
import { AUTO_MODEL, POOL_PREFIX, splitSpec } from "../spec.js";
|
|
3
3
|
const EFFORT_LEVEL_SET = new Set(EFFORT_LEVELS);
|
|
4
4
|
function parseQuotaEnforcement(raw) {
|
|
@@ -92,122 +92,91 @@ function parseHedge(raw) {
|
|
|
92
92
|
}
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
throw new Error(
|
|
95
|
+
function parseProbation(raw) {
|
|
96
|
+
if (raw === undefined || raw === null)
|
|
97
|
+
return {};
|
|
98
|
+
if (typeof raw === "boolean")
|
|
99
|
+
return { enabled: raw };
|
|
100
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
101
|
+
throw new Error("config.routing.probation must be an object or a boolean");
|
|
102
102
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
throw new Error(`config.routing.default array must contain at least one valid spec string`);
|
|
103
|
+
const value = raw;
|
|
104
|
+
const known = new Set(["enabled", "minSamples"]);
|
|
105
|
+
for (const key of Object.keys(value)) {
|
|
106
|
+
if (!known.has(key)) {
|
|
107
|
+
throw new Error(`config.routing.probation has an unknown key "${key}"`);
|
|
109
108
|
}
|
|
110
109
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
const out = {};
|
|
111
|
+
if (value.enabled !== undefined) {
|
|
112
|
+
if (typeof value.enabled !== "boolean")
|
|
113
|
+
throw new Error("config.routing.probation.enabled must be a boolean");
|
|
114
|
+
out.enabled = value.enabled;
|
|
116
115
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const arr = v.filter((s) => typeof s === "string" && s.length > 0);
|
|
122
|
-
if (arr.length > 0)
|
|
123
|
-
tiers[k] = arr;
|
|
124
|
-
}
|
|
125
|
-
else if (typeof v === "string" && v.length > 0) {
|
|
126
|
-
tiers[k] = v;
|
|
127
|
-
}
|
|
116
|
+
if (value.minSamples !== undefined) {
|
|
117
|
+
const n = value.minSamples;
|
|
118
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
|
|
119
|
+
throw new Error("config.routing.probation.minSamples must be a positive integer");
|
|
128
120
|
}
|
|
121
|
+
out.minSamples = n;
|
|
129
122
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
if (policy.exclude !== undefined &&
|
|
147
|
-
(!Array.isArray(policy.exclude) ||
|
|
148
|
-
policy.exclude.some((s) => typeof s !== "string" ||
|
|
149
|
-
!/^\S+\/\S+$/.test(s) ||
|
|
150
|
-
s.startsWith(`${POOL_PREFIX}/`)))) {
|
|
151
|
-
throw new Error(`config.routing.pools.${k}.exclude must be an array of "provider/model" specs`);
|
|
152
|
-
}
|
|
153
|
-
if (policy.effort !== undefined && !EFFORT_LEVEL_SET.has(policy.effort)) {
|
|
154
|
-
throw new Error(`config.routing.pools.${k}.effort must be low, medium, high, or xhigh`);
|
|
155
|
-
}
|
|
156
|
-
const exclude = [...(policy.exclude ?? [])];
|
|
157
|
-
const excluded = new Set(exclude);
|
|
158
|
-
declared = policy.preferred.filter((spec) => !excluded.has(spec));
|
|
159
|
-
poolPolicies[k] = {
|
|
160
|
-
preferred: declared,
|
|
161
|
-
include: "free",
|
|
162
|
-
...(exclude.length > 0 ? { exclude } : {}),
|
|
163
|
-
...(policy.effort ? { effort: policy.effort } : {}),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
throw new Error(`config.routing.pools.${k} must be an array of specs or {"preferred":[...],"include":"free"}`);
|
|
168
|
-
}
|
|
169
|
-
const arr = declared.filter((s) => {
|
|
170
|
-
const { provider } = splitSpec(s);
|
|
171
|
-
if (!disabledProviders.has(provider))
|
|
172
|
-
return true;
|
|
173
|
-
warnings.push(`routing.pools.${k}: dropped "${s}" — provider "${provider}" is disabled`);
|
|
174
|
-
return false;
|
|
175
|
-
});
|
|
176
|
-
if (arr.length === 0 && !poolPolicies[k]) {
|
|
177
|
-
throw new Error(`config.routing.pools.${k} must contain at least one valid spec string`);
|
|
178
|
-
}
|
|
179
|
-
const nested = arr.find((s) => s.startsWith(`${POOL_PREFIX}/`));
|
|
180
|
-
if (nested) {
|
|
181
|
-
throw new Error(`config.routing.pools.${k} member "${nested}" — a pool cannot reference another pool`);
|
|
182
|
-
}
|
|
183
|
-
pools[k] = arr;
|
|
184
|
-
if (poolPolicies[k])
|
|
185
|
-
poolPolicies[k] = { ...poolPolicies[k], preferred: arr, include: "free" };
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function parseCrawl(raw) {
|
|
126
|
+
if (raw === undefined || raw === null)
|
|
127
|
+
return {};
|
|
128
|
+
if (typeof raw === "boolean")
|
|
129
|
+
return { enabled: raw };
|
|
130
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
131
|
+
throw new Error("config.routing.crawl must be an object or a boolean");
|
|
132
|
+
}
|
|
133
|
+
const value = raw;
|
|
134
|
+
const known = new Set(["enabled", "msPerToken", "windowMs", "minTokens"]);
|
|
135
|
+
for (const key of Object.keys(value)) {
|
|
136
|
+
if (!known.has(key)) {
|
|
137
|
+
throw new Error(`config.routing.crawl has an unknown key "${key}"`);
|
|
186
138
|
}
|
|
187
139
|
}
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
140
|
+
const out = {};
|
|
141
|
+
if (value.enabled !== undefined) {
|
|
142
|
+
if (typeof value.enabled !== "boolean")
|
|
143
|
+
throw new Error("config.routing.crawl.enabled must be a boolean");
|
|
144
|
+
out.enabled = value.enabled;
|
|
145
|
+
}
|
|
146
|
+
for (const key of ["msPerToken", "windowMs", "minTokens"]) {
|
|
147
|
+
const n = value[key];
|
|
148
|
+
if (n === undefined)
|
|
149
|
+
continue;
|
|
150
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) {
|
|
151
|
+
throw new Error(`config.routing.crawl.${key} must be a positive finite number`);
|
|
193
152
|
}
|
|
153
|
+
out[key] = n;
|
|
194
154
|
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
export function parseRouting(raw, providers, overrideDefault, warnings = [], disabledProviders = new Set()) {
|
|
158
|
+
const r = (typeof raw === "object" && raw !== null ? raw : {});
|
|
159
|
+
assertNoReservedProviderNames(providers);
|
|
160
|
+
const dflt = parseDefault(r.default, overrideDefault);
|
|
161
|
+
const tiers = parseTiers(r.tiers);
|
|
162
|
+
const { pools, poolPolicies } = parsePools(r.pools, disabledProviders, warnings);
|
|
163
|
+
const subagents = parseSubagents(r.subagents);
|
|
195
164
|
const benchmarkSort = typeof r.benchmarkSort === "boolean" ? r.benchmarkSort : true;
|
|
196
165
|
const offload = parseOffload(r.offload);
|
|
197
166
|
const routing = { default: dflt, tiers, benchmarkSort, offload };
|
|
198
|
-
const
|
|
199
|
-
if (sticky)
|
|
200
|
-
routing.sticky = sticky;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
routing.
|
|
205
|
-
routing.
|
|
206
|
-
routing.
|
|
207
|
-
routing.
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
routing.mcp =
|
|
167
|
+
const optionalBlocks = parseOptionalBlocks(r);
|
|
168
|
+
if (optionalBlocks.sticky)
|
|
169
|
+
routing.sticky = optionalBlocks.sticky;
|
|
170
|
+
if (optionalBlocks.quota)
|
|
171
|
+
routing.quota = optionalBlocks.quota;
|
|
172
|
+
routing.latency = optionalBlocks.latency;
|
|
173
|
+
routing.hedge = optionalBlocks.hedge;
|
|
174
|
+
routing.probation = optionalBlocks.probation;
|
|
175
|
+
routing.crawl = optionalBlocks.crawl;
|
|
176
|
+
routing.laneProbe = optionalBlocks.laneProbe;
|
|
177
|
+
routing.dispatchWalk = optionalBlocks.dispatchWalk;
|
|
178
|
+
if (optionalBlocks.mcp)
|
|
179
|
+
routing.mcp = optionalBlocks.mcp;
|
|
211
180
|
if (Object.keys(pools).length > 0)
|
|
212
181
|
routing.pools = pools;
|
|
213
182
|
if (Object.keys(poolPolicies).length > 0)
|
|
@@ -217,82 +186,14 @@ export function parseRouting(raw, providers, overrideDefault, warnings = [], dis
|
|
|
217
186
|
const ladder = parseLadder(r.ladder, "config.routing.ladder");
|
|
218
187
|
if (ladder.length > 0)
|
|
219
188
|
routing.ladder = ladder;
|
|
220
|
-
|
|
221
|
-
throw new Error(`config.routing.ladders must be an object of named ladder arrays`);
|
|
222
|
-
}
|
|
223
|
-
const ladders = {};
|
|
224
|
-
for (const [tier, rawLadder] of Object.entries((r.ladders ?? {}))) {
|
|
225
|
-
const parsed = parseLadder(rawLadder, `config.routing.ladders.${tier}`);
|
|
226
|
-
if (parsed.length === 0)
|
|
227
|
-
throw new Error(`config.routing.ladders.${tier} must contain at least one rung`);
|
|
228
|
-
ladders[tier] = parsed;
|
|
229
|
-
}
|
|
189
|
+
const ladders = parseLadders(r.ladders);
|
|
230
190
|
if (Object.keys(ladders).length > 0)
|
|
231
191
|
routing.ladders = ladders;
|
|
232
192
|
const cliLane = parseCliLane(r.cliLane, "config.routing.cliLane");
|
|
233
193
|
if (cliLane)
|
|
234
194
|
routing.cliLane = cliLane;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (kept === null)
|
|
238
|
-
delete tiers[tier];
|
|
239
|
-
else
|
|
240
|
-
tiers[tier] = kept;
|
|
241
|
-
}
|
|
242
|
-
for (const [tier, spec] of Object.entries(subagents)) {
|
|
243
|
-
const kept = dropDisabledSpecs(spec, disabledProviders, warnings, `routing.subagents.${tier}`);
|
|
244
|
-
if (kept === null)
|
|
245
|
-
delete subagents[tier];
|
|
246
|
-
else
|
|
247
|
-
subagents[tier] = kept;
|
|
248
|
-
}
|
|
249
|
-
if (Array.isArray(routing.default)) {
|
|
250
|
-
const kept = dropDisabledSpecs(routing.default, disabledProviders, warnings, "routing.default");
|
|
251
|
-
if (kept !== null)
|
|
252
|
-
routing.default = kept;
|
|
253
|
-
}
|
|
254
|
-
routing.ladder = ladder.filter((rung) => {
|
|
255
|
-
if (rung.kind !== "relay" || !rung.spec)
|
|
256
|
-
return true;
|
|
257
|
-
return dropDisabledSpecs(rung.spec, disabledProviders, warnings, `routing.ladder[${rung.id}].spec`) !== null;
|
|
258
|
-
});
|
|
259
|
-
if (routing.ladder.length === 0)
|
|
260
|
-
delete routing.ladder;
|
|
261
|
-
for (const [tier, tierLadder] of Object.entries(routing.ladders ?? {})) {
|
|
262
|
-
const kept = tierLadder.filter((rung) => {
|
|
263
|
-
if (rung.kind !== "relay" || !rung.spec)
|
|
264
|
-
return true;
|
|
265
|
-
return dropDisabledSpecs(rung.spec, disabledProviders, warnings, `routing.ladders.${tier}[${rung.id}].spec`) !== null;
|
|
266
|
-
});
|
|
267
|
-
if (kept.length === 0)
|
|
268
|
-
delete routing.ladders[tier];
|
|
269
|
-
else
|
|
270
|
-
routing.ladders[tier] = kept;
|
|
271
|
-
}
|
|
272
|
-
if (routing.ladders && Object.keys(routing.ladders).length === 0)
|
|
273
|
-
delete routing.ladders;
|
|
274
|
-
assertSpecResolvable(routing.default, providers, pools, "routing.default", disabledProviders);
|
|
275
|
-
for (const [tier, spec] of Object.entries(tiers)) {
|
|
276
|
-
assertSpecResolvable(spec, providers, pools, `routing.tiers.${tier}`);
|
|
277
|
-
}
|
|
278
|
-
for (const [pool, specs] of Object.entries(pools)) {
|
|
279
|
-
assertSpecResolvable(specs, providers, {}, `routing.pools.${pool}`);
|
|
280
|
-
}
|
|
281
|
-
for (const [tier, spec] of Object.entries(subagents)) {
|
|
282
|
-
assertSpecResolvable(spec, providers, pools, `routing.subagents.${tier}`);
|
|
283
|
-
}
|
|
284
|
-
for (const rung of routing.ladder ?? []) {
|
|
285
|
-
if (rung.kind === "relay" && rung.spec) {
|
|
286
|
-
assertSpecResolvable(rung.spec, providers, pools, `routing.ladder[${rung.id}].spec`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
for (const [tier, tierLadder] of Object.entries(routing.ladders ?? {})) {
|
|
290
|
-
for (const rung of tierLadder) {
|
|
291
|
-
if (rung.kind === "relay" && rung.spec) {
|
|
292
|
-
assertSpecResolvable(rung.spec, providers, pools, `routing.ladders.${tier}[${rung.id}].spec`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
195
|
+
degradeDisabledRouting(routing, tiers, subagents, ladder, disabledProviders, warnings);
|
|
196
|
+
assertRoutingResolvable(routing, tiers, pools, subagents, providers, disabledProviders);
|
|
296
197
|
return routing;
|
|
297
198
|
}
|
|
298
199
|
function parseMcpSettings(raw) {
|
|
@@ -303,16 +204,27 @@ function parseMcpSettings(raw) {
|
|
|
303
204
|
}
|
|
304
205
|
const o = raw;
|
|
305
206
|
for (const key of Object.keys(o)) {
|
|
306
|
-
if (key !== "allowedRoots") {
|
|
307
|
-
throw new Error(`config.routing.mcp.${key} is not a recognized key (allowedRoots)`);
|
|
207
|
+
if (key !== "allowedRoots" && key !== "maxWaitMs") {
|
|
208
|
+
throw new Error(`config.routing.mcp.${key} is not a recognized key (allowedRoots, maxWaitMs)`);
|
|
308
209
|
}
|
|
309
210
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
211
|
+
const out = {};
|
|
212
|
+
if (o.allowedRoots !== undefined) {
|
|
213
|
+
if (!Array.isArray(o.allowedRoots) || o.allowedRoots.some((r) => typeof r !== "string" || r.length === 0)) {
|
|
214
|
+
throw new Error(`config.routing.mcp.allowedRoots must be an array of non-empty strings`);
|
|
215
|
+
}
|
|
216
|
+
out.allowedRoots = [...o.allowedRoots];
|
|
217
|
+
}
|
|
218
|
+
if (o.maxWaitMs === undefined) {
|
|
219
|
+
out.maxWaitMs = DEFAULT_MCP_MAX_WAIT_MS;
|
|
220
|
+
}
|
|
221
|
+
else if (typeof o.maxWaitMs !== "number" || !Number.isInteger(o.maxWaitMs) || o.maxWaitMs <= 0) {
|
|
222
|
+
throw new Error(`config.routing.mcp.maxWaitMs must be a positive integer (milliseconds)`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
out.maxWaitMs = o.maxWaitMs;
|
|
314
226
|
}
|
|
315
|
-
return
|
|
227
|
+
return out;
|
|
316
228
|
}
|
|
317
229
|
export const DEFAULT_LANE_PROBE = {
|
|
318
230
|
enabled: true,
|
|
@@ -350,6 +262,11 @@ function parseLaneProbe(raw) {
|
|
|
350
262
|
catalogIntervalMs: interval("catalogIntervalMs", o.catalogIntervalMs, DEFAULT_LANE_PROBE.catalogIntervalMs),
|
|
351
263
|
};
|
|
352
264
|
}
|
|
265
|
+
export const DEFAULT_DISPATCH_WALK_OUTLIER = {
|
|
266
|
+
recentCount: 5,
|
|
267
|
+
historyQuantile: 0.8,
|
|
268
|
+
outlierFactor: 7.6,
|
|
269
|
+
};
|
|
353
270
|
export const DEFAULT_DISPATCH_WALK = {
|
|
354
271
|
enabled: true,
|
|
355
272
|
attemptMs: 90_000,
|
|
@@ -358,6 +275,7 @@ export const DEFAULT_DISPATCH_WALK = {
|
|
|
358
275
|
maxLanes: 4,
|
|
359
276
|
pinMs: 15 * 60 * 1000,
|
|
360
277
|
demoteMs: 15 * 60 * 1000,
|
|
278
|
+
outlier: { ...DEFAULT_DISPATCH_WALK_OUTLIER },
|
|
361
279
|
};
|
|
362
280
|
function parseDispatchWalk(raw) {
|
|
363
281
|
if (raw === undefined || raw === null)
|
|
@@ -368,7 +286,7 @@ function parseDispatchWalk(raw) {
|
|
|
368
286
|
throw new Error(`config.routing.dispatchWalk must be a boolean or an object`);
|
|
369
287
|
}
|
|
370
288
|
const o = raw;
|
|
371
|
-
const keys = ["enabled", "attemptMs", "attemptQuantile", "attemptMinSamples", "maxLanes", "pinMs", "demoteMs"];
|
|
289
|
+
const keys = ["enabled", "attemptMs", "attemptQuantile", "attemptMinSamples", "maxLanes", "pinMs", "demoteMs", "outlier"];
|
|
372
290
|
for (const key of Object.keys(o)) {
|
|
373
291
|
if (!keys.includes(key)) {
|
|
374
292
|
throw new Error(`config.routing.dispatchWalk.${key} is not a recognized key (${keys.join(", ")})`);
|
|
@@ -401,7 +319,45 @@ function parseDispatchWalk(raw) {
|
|
|
401
319
|
maxLanes: bounded("maxLanes", o.maxLanes, DEFAULT_DISPATCH_WALK.maxLanes, 1, 20),
|
|
402
320
|
pinMs: bounded("pinMs", o.pinMs, DEFAULT_DISPATCH_WALK.pinMs, 0, 6 * 60 * 60 * 1000),
|
|
403
321
|
demoteMs: bounded("demoteMs", o.demoteMs, DEFAULT_DISPATCH_WALK.demoteMs, 0, 6 * 60 * 60 * 1000),
|
|
322
|
+
outlier: parseDispatchWalkOutlier(o.outlier),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function parseDispatchWalkOutlier(raw) {
|
|
326
|
+
const parsed = raw === false ? false : parseDispatchWalkOutlierObject(raw);
|
|
327
|
+
return parsed;
|
|
328
|
+
}
|
|
329
|
+
function parseDispatchWalkOutlierObject(raw) {
|
|
330
|
+
if (raw === undefined)
|
|
331
|
+
return { ...DEFAULT_DISPATCH_WALK_OUTLIER };
|
|
332
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
333
|
+
throw new Error(`config.routing.dispatchWalk.outlier must be false or an object`);
|
|
334
|
+
}
|
|
335
|
+
const o = raw;
|
|
336
|
+
const keys = ["recentCount", "historyQuantile", "outlierFactor"];
|
|
337
|
+
for (const key of Object.keys(o)) {
|
|
338
|
+
if (!keys.includes(key)) {
|
|
339
|
+
throw new Error(`config.routing.dispatchWalk.outlier.${key} is not a recognized key (${keys.join(", ")})`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const d = DEFAULT_DISPATCH_WALK_OUTLIER;
|
|
343
|
+
const recentCount = o.recentCount;
|
|
344
|
+
if (recentCount !== undefined && (typeof recentCount !== "number" || !Number.isInteger(recentCount) || recentCount < 1 || recentCount > 100)) {
|
|
345
|
+
throw new Error(`config.routing.dispatchWalk.outlier.recentCount must be an integer between 1 and 100`);
|
|
346
|
+
}
|
|
347
|
+
const historyQuantile = o.historyQuantile;
|
|
348
|
+
if (historyQuantile !== undefined && (typeof historyQuantile !== "number" || !Number.isFinite(historyQuantile) || historyQuantile <= 0 || historyQuantile >= 1)) {
|
|
349
|
+
throw new Error(`config.routing.dispatchWalk.outlier.historyQuantile must be a number greater than 0 and less than 1`);
|
|
350
|
+
}
|
|
351
|
+
const outlierFactor = o.outlierFactor;
|
|
352
|
+
if (outlierFactor !== undefined && (typeof outlierFactor !== "number" || !Number.isFinite(outlierFactor) || outlierFactor <= 1 || outlierFactor > 100)) {
|
|
353
|
+
throw new Error(`config.routing.dispatchWalk.outlier.outlierFactor must be a number greater than 1 and at most 100`);
|
|
354
|
+
}
|
|
355
|
+
const parsed = {
|
|
356
|
+
recentCount: recentCount ?? d.recentCount,
|
|
357
|
+
historyQuantile: historyQuantile ?? d.historyQuantile,
|
|
358
|
+
outlierFactor: outlierFactor ?? d.outlierFactor,
|
|
404
359
|
};
|
|
360
|
+
return parsed;
|
|
405
361
|
}
|
|
406
362
|
function parseSticky(raw) {
|
|
407
363
|
if (raw === undefined || raw === null)
|
|
@@ -438,6 +394,154 @@ function parseSticky(raw) {
|
|
|
438
394
|
}
|
|
439
395
|
return { enabled: sticky.enabled, ttlMs, maxSessions };
|
|
440
396
|
}
|
|
397
|
+
function assertNoReservedProviderNames(providers) {
|
|
398
|
+
if (providers[POOL_PREFIX]) {
|
|
399
|
+
throw new Error(`config.providers."${POOL_PREFIX}" is reserved — it would shadow "pool/<name>" routing`);
|
|
400
|
+
}
|
|
401
|
+
if (providers[AUTO_MODEL]) {
|
|
402
|
+
throw new Error(`config.providers."${AUTO_MODEL}" is reserved — it would shadow "${AUTO_MODEL}" routing`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function parseDefault(dfltRaw, overrideDefault) {
|
|
406
|
+
const raw = overrideDefault !== undefined ? overrideDefault : dfltRaw;
|
|
407
|
+
let result;
|
|
408
|
+
if (Array.isArray(raw)) {
|
|
409
|
+
const filtered = raw.filter((s) => typeof s === "string" && s.length > 0);
|
|
410
|
+
if (filtered.length === 0) {
|
|
411
|
+
throw new Error(`config.routing.default array must contain at least one valid spec string`);
|
|
412
|
+
}
|
|
413
|
+
result = filtered;
|
|
414
|
+
}
|
|
415
|
+
else if (typeof raw === "string" && raw.length > 0) {
|
|
416
|
+
result = raw;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
throw new Error(`config.routing.default ("provider/model") is required`);
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
function parseTiers(raw) {
|
|
424
|
+
const tiers = {};
|
|
425
|
+
if (typeof raw === "object" && raw !== null) {
|
|
426
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
427
|
+
if (Array.isArray(v)) {
|
|
428
|
+
const arr = v.filter((s) => typeof s === "string" && s.length > 0);
|
|
429
|
+
if (arr.length > 0)
|
|
430
|
+
tiers[k] = arr;
|
|
431
|
+
}
|
|
432
|
+
else if (typeof v === "string" && v.length > 0) {
|
|
433
|
+
tiers[k] = v;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return tiers;
|
|
438
|
+
}
|
|
439
|
+
function parsePoolPolicy(k, policy) {
|
|
440
|
+
if (!Array.isArray(policy.preferred) || policy.preferred.some((s) => typeof s !== "string" || s.length === 0)) {
|
|
441
|
+
throw new Error(`config.routing.pools.${k}.preferred must be an array of non-empty "provider/model" specs`);
|
|
442
|
+
}
|
|
443
|
+
if (policy.include !== "free") {
|
|
444
|
+
throw new Error(`config.routing.pools.${k}.include must be "free"`);
|
|
445
|
+
}
|
|
446
|
+
if (policy.exclude !== undefined &&
|
|
447
|
+
(!Array.isArray(policy.exclude) ||
|
|
448
|
+
policy.exclude.some((s) => typeof s !== "string" ||
|
|
449
|
+
!/^\S+\/\S+$/.test(s) ||
|
|
450
|
+
s.startsWith(`${POOL_PREFIX}/`)))) {
|
|
451
|
+
throw new Error(`config.routing.pools.${k}.exclude must be an array of "provider/model" specs`);
|
|
452
|
+
}
|
|
453
|
+
if (policy.effort !== undefined && !EFFORT_LEVEL_SET.has(policy.effort)) {
|
|
454
|
+
throw new Error(`config.routing.pools.${k}.effort must be low, medium, high, or xhigh`);
|
|
455
|
+
}
|
|
456
|
+
const exclude = [...(policy.exclude ?? [])];
|
|
457
|
+
const excluded = new Set(exclude);
|
|
458
|
+
const declared = policy.preferred.filter((spec) => !excluded.has(spec));
|
|
459
|
+
const poolPolicy = {
|
|
460
|
+
preferred: declared,
|
|
461
|
+
include: "free",
|
|
462
|
+
...(exclude.length > 0 ? { exclude } : {}),
|
|
463
|
+
...(policy.effort ? { effort: policy.effort } : {}),
|
|
464
|
+
};
|
|
465
|
+
return { declared, poolPolicy };
|
|
466
|
+
}
|
|
467
|
+
function parsePoolEntry(k, v, disabledProviders, warnings) {
|
|
468
|
+
let declared;
|
|
469
|
+
let poolPolicy;
|
|
470
|
+
if (Array.isArray(v)) {
|
|
471
|
+
declared = v.filter((s) => typeof s === "string" && s.length > 0);
|
|
472
|
+
}
|
|
473
|
+
else if (typeof v === "object" && v !== null) {
|
|
474
|
+
const parsed = parsePoolPolicy(k, v);
|
|
475
|
+
declared = parsed.declared;
|
|
476
|
+
poolPolicy = parsed.poolPolicy;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
throw new Error(`config.routing.pools.${k} must be an array of specs or {"preferred":[...],"include":"free"}`);
|
|
480
|
+
}
|
|
481
|
+
const arr = declared.filter((s) => {
|
|
482
|
+
const { provider } = splitSpec(s);
|
|
483
|
+
if (!disabledProviders.has(provider))
|
|
484
|
+
return true;
|
|
485
|
+
warnings.push(`routing.pools.${k}: dropped "${s}" — provider "${provider}" is disabled`);
|
|
486
|
+
return false;
|
|
487
|
+
});
|
|
488
|
+
if (arr.length === 0 && !poolPolicy) {
|
|
489
|
+
throw new Error(`config.routing.pools.${k} must contain at least one valid spec string`);
|
|
490
|
+
}
|
|
491
|
+
const nested = arr.find((s) => s.startsWith(`${POOL_PREFIX}/`));
|
|
492
|
+
if (nested) {
|
|
493
|
+
throw new Error(`config.routing.pools.${k} member "${nested}" — a pool cannot reference another pool`);
|
|
494
|
+
}
|
|
495
|
+
if (poolPolicy) {
|
|
496
|
+
poolPolicy = { ...poolPolicy, preferred: arr, include: "free" };
|
|
497
|
+
}
|
|
498
|
+
return { members: arr, poolPolicy };
|
|
499
|
+
}
|
|
500
|
+
function parsePools(raw, disabledProviders, warnings) {
|
|
501
|
+
const pools = {};
|
|
502
|
+
const poolPolicies = {};
|
|
503
|
+
if (typeof raw === "object" && raw !== null) {
|
|
504
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
505
|
+
const { members, poolPolicy } = parsePoolEntry(k, v, disabledProviders, warnings);
|
|
506
|
+
pools[k] = members;
|
|
507
|
+
if (poolPolicy)
|
|
508
|
+
poolPolicies[k] = poolPolicy;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return { pools, poolPolicies };
|
|
512
|
+
}
|
|
513
|
+
function parseOptionalBlocks(r) {
|
|
514
|
+
const sticky = parseSticky(r.sticky);
|
|
515
|
+
const quota = parseQuotaEnforcement(r.quota);
|
|
516
|
+
const latency = parseLatencyDemotion(r.latency);
|
|
517
|
+
const hedge = parseHedge(r.hedge);
|
|
518
|
+
const probation = parseProbation(r.probation);
|
|
519
|
+
const crawl = parseCrawl(r.crawl);
|
|
520
|
+
const laneProbe = parseLaneProbe(r.laneProbe);
|
|
521
|
+
const dispatchWalk = parseDispatchWalk(r.dispatchWalk);
|
|
522
|
+
const mcp = parseMcpSettings(r.mcp);
|
|
523
|
+
return {
|
|
524
|
+
...(sticky ? { sticky } : {}),
|
|
525
|
+
...(quota ? { quota } : {}),
|
|
526
|
+
latency,
|
|
527
|
+
hedge,
|
|
528
|
+
probation,
|
|
529
|
+
crawl,
|
|
530
|
+
laneProbe,
|
|
531
|
+
dispatchWalk,
|
|
532
|
+
...(mcp ? { mcp } : {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function parseSubagents(raw) {
|
|
536
|
+
const subagents = {};
|
|
537
|
+
if (typeof raw === "object" && raw !== null) {
|
|
538
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
539
|
+
if (typeof v === "string" && v.length > 0)
|
|
540
|
+
subagents[k] = v;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return subagents;
|
|
544
|
+
}
|
|
441
545
|
export function parseOffload(raw) {
|
|
442
546
|
let out = false;
|
|
443
547
|
if (typeof raw === "boolean") {
|
|
@@ -554,6 +658,14 @@ function parseCliLane(raw, root) {
|
|
|
554
658
|
}
|
|
555
659
|
return lane;
|
|
556
660
|
}
|
|
661
|
+
function applyCliMaxConcurrent(rung, raw, where, id) {
|
|
662
|
+
if (raw === undefined)
|
|
663
|
+
return;
|
|
664
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0) {
|
|
665
|
+
throw new Error(`${where}.maxConcurrent must be a positive integer (rung "${id}")`);
|
|
666
|
+
}
|
|
667
|
+
rung.maxConcurrent = raw;
|
|
668
|
+
}
|
|
557
669
|
function parseLadder(raw, root) {
|
|
558
670
|
if (raw === undefined || raw === null)
|
|
559
671
|
return [];
|
|
@@ -596,6 +708,7 @@ function parseLadder(raw, root) {
|
|
|
596
708
|
const env = parseSpawnEnv(e.env, where);
|
|
597
709
|
if (env)
|
|
598
710
|
rung.env = env;
|
|
711
|
+
applyCliMaxConcurrent(rung, e.maxConcurrent, where, id);
|
|
599
712
|
}
|
|
600
713
|
else {
|
|
601
714
|
if (typeof e.spec !== "string" || e.spec.length === 0) {
|
|
@@ -607,6 +720,56 @@ function parseLadder(raw, root) {
|
|
|
607
720
|
}
|
|
608
721
|
return out;
|
|
609
722
|
}
|
|
723
|
+
function parseLadders(raw) {
|
|
724
|
+
if (raw !== undefined && (typeof raw !== "object" || raw === null || Array.isArray(raw))) {
|
|
725
|
+
throw new Error(`config.routing.ladders must be an object of named ladder arrays`);
|
|
726
|
+
}
|
|
727
|
+
const ladders = {};
|
|
728
|
+
for (const [tier, rawLadder] of Object.entries((raw ?? {}))) {
|
|
729
|
+
const parsed = parseLadder(rawLadder, `config.routing.ladders.${tier}`);
|
|
730
|
+
if (parsed.length === 0)
|
|
731
|
+
throw new Error(`config.routing.ladders.${tier} must contain at least one rung`);
|
|
732
|
+
ladders[tier] = parsed;
|
|
733
|
+
}
|
|
734
|
+
return ladders;
|
|
735
|
+
}
|
|
736
|
+
function dropDisabledRungs(rungs, disabledProviders, warnings, prefix) {
|
|
737
|
+
return rungs.filter((rung) => {
|
|
738
|
+
if (rung.kind !== "relay" || !rung.spec)
|
|
739
|
+
return true;
|
|
740
|
+
return dropDisabledSpecs(rung.spec, disabledProviders, warnings, `${prefix}[${rung.id}].spec`) !== null;
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function pruneDisabledSpecEntries(map, disabledProviders, warnings, prefix) {
|
|
744
|
+
for (const [tier, spec] of Object.entries(map)) {
|
|
745
|
+
const kept = dropDisabledSpecs(spec, disabledProviders, warnings, `${prefix}.${tier}`);
|
|
746
|
+
if (kept === null)
|
|
747
|
+
delete map[tier];
|
|
748
|
+
else
|
|
749
|
+
map[tier] = kept;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function degradeDisabledRouting(routing, tiers, subagents, ladder, disabledProviders, warnings) {
|
|
753
|
+
pruneDisabledSpecEntries(tiers, disabledProviders, warnings, "routing.tiers");
|
|
754
|
+
pruneDisabledSpecEntries(subagents, disabledProviders, warnings, "routing.subagents");
|
|
755
|
+
if (Array.isArray(routing.default)) {
|
|
756
|
+
const kept = dropDisabledSpecs(routing.default, disabledProviders, warnings, "routing.default");
|
|
757
|
+
if (kept !== null)
|
|
758
|
+
routing.default = kept;
|
|
759
|
+
}
|
|
760
|
+
routing.ladder = dropDisabledRungs(ladder, disabledProviders, warnings, "routing.ladder");
|
|
761
|
+
if (routing.ladder.length === 0)
|
|
762
|
+
delete routing.ladder;
|
|
763
|
+
for (const [tier, tierLadder] of Object.entries(routing.ladders ?? {})) {
|
|
764
|
+
const kept = dropDisabledRungs(tierLadder, disabledProviders, warnings, `routing.ladders.${tier}`);
|
|
765
|
+
if (kept.length === 0)
|
|
766
|
+
delete routing.ladders[tier];
|
|
767
|
+
else
|
|
768
|
+
routing.ladders[tier] = kept;
|
|
769
|
+
}
|
|
770
|
+
if (routing.ladders && Object.keys(routing.ladders).length === 0)
|
|
771
|
+
delete routing.ladders;
|
|
772
|
+
}
|
|
610
773
|
function assertSpecResolvable(spec, providers, pools, where, disabled = new Set()) {
|
|
611
774
|
const specs = Array.isArray(spec) ? spec : [spec];
|
|
612
775
|
for (const s of specs) {
|
|
@@ -629,4 +792,27 @@ function assertSpecResolvable(spec, providers, pools, where, disabled = new Set(
|
|
|
629
792
|
throw new Error(`config.${where} "${s}" needs a model id for openai provider "${provider}"`);
|
|
630
793
|
}
|
|
631
794
|
}
|
|
795
|
+
function assertRungsResolvable(rungs, providers, pools, prefix) {
|
|
796
|
+
for (const rung of rungs) {
|
|
797
|
+
if (rung.kind === "relay" && rung.spec) {
|
|
798
|
+
assertSpecResolvable(rung.spec, providers, pools, `${prefix}[${rung.id}].spec`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function assertRoutingResolvable(routing, tiers, pools, subagents, providers, disabledProviders) {
|
|
803
|
+
assertSpecResolvable(routing.default, providers, pools, "routing.default", disabledProviders);
|
|
804
|
+
for (const [tier, spec] of Object.entries(tiers)) {
|
|
805
|
+
assertSpecResolvable(spec, providers, pools, `routing.tiers.${tier}`);
|
|
806
|
+
}
|
|
807
|
+
for (const [pool, specs] of Object.entries(pools)) {
|
|
808
|
+
assertSpecResolvable(specs, providers, {}, `routing.pools.${pool}`);
|
|
809
|
+
}
|
|
810
|
+
for (const [tier, spec] of Object.entries(subagents)) {
|
|
811
|
+
assertSpecResolvable(spec, providers, pools, `routing.subagents.${tier}`);
|
|
812
|
+
}
|
|
813
|
+
assertRungsResolvable(routing.ladder ?? [], providers, pools, "routing.ladder");
|
|
814
|
+
for (const [tier, tierLadder] of Object.entries(routing.ladders ?? {})) {
|
|
815
|
+
assertRungsResolvable(tierLadder, providers, pools, `routing.ladders.${tier}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
632
818
|
//# sourceMappingURL=routing-parser.js.map
|