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.
Files changed (90) hide show
  1. package/dist/accounting-store-schema.js +5 -6
  2. package/dist/accounting-store-schema.js.map +1 -1
  3. package/dist/accounting-store.d.ts +10 -0
  4. package/dist/accounting-store.js +60 -6
  5. package/dist/accounting-store.js.map +1 -1
  6. package/dist/backend/envelope-validator.d.ts +1 -1
  7. package/dist/backend/envelope-validator.js +89 -52
  8. package/dist/backend/envelope-validator.js.map +1 -1
  9. package/dist/backend/health-prober.js +3 -1
  10. package/dist/backend/health-prober.js.map +1 -1
  11. package/dist/backend.d.ts +48 -0
  12. package/dist/backend.js +831 -7
  13. package/dist/backend.js.map +1 -1
  14. package/dist/candidate-runner.d.ts +135 -50
  15. package/dist/candidate-runner.js +160 -55
  16. package/dist/candidate-runner.js.map +1 -1
  17. package/dist/cli.d.ts +24 -2
  18. package/dist/cli.js +122 -20
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config/routing-parser.d.ts +13 -8
  21. package/dist/config/routing-parser.js +369 -183
  22. package/dist/config/routing-parser.js.map +1 -1
  23. package/dist/config-types.d.ts +202 -0
  24. package/dist/config-types.js +2 -0
  25. package/dist/config-types.js.map +1 -1
  26. package/dist/config.d.ts +45 -1
  27. package/dist/config.js +87 -22
  28. package/dist/config.js.map +1 -1
  29. package/dist/dashboard-contract.d.ts +38 -0
  30. package/dist/dashboard-contract.js +19 -3
  31. package/dist/dashboard-contract.js.map +1 -1
  32. package/dist/dispatch-lane-stats.d.ts +33 -4
  33. package/dist/dispatch-lane-stats.js +65 -16
  34. package/dist/dispatch-lane-stats.js.map +1 -1
  35. package/dist/dispatch.d.ts +9 -0
  36. package/dist/dispatch.js +36 -4
  37. package/dist/dispatch.js.map +1 -1
  38. package/dist/key-checker.d.ts +21 -1
  39. package/dist/key-checker.js +38 -12
  40. package/dist/key-checker.js.map +1 -1
  41. package/dist/lane-affinity.d.ts +94 -1
  42. package/dist/lane-affinity.js +52 -4
  43. package/dist/lane-affinity.js.map +1 -1
  44. package/dist/mcp/lane-runner.d.ts +69 -16
  45. package/dist/mcp/lane-runner.js +30 -1
  46. package/dist/mcp/lane-runner.js.map +1 -1
  47. package/dist/mcp/server.d.ts +39 -4
  48. package/dist/mcp/server.js +70 -13
  49. package/dist/mcp/server.js.map +1 -1
  50. package/dist/openai-request.d.ts +21 -0
  51. package/dist/openai-request.js +1 -1
  52. package/dist/openai-request.js.map +1 -1
  53. package/dist/ping/cadence.js +9 -1
  54. package/dist/ping/cadence.js.map +1 -1
  55. package/dist/ping/probe-cache.d.ts +13 -0
  56. package/dist/ping/probe-cache.js +12 -0
  57. package/dist/ping/probe-cache.js.map +1 -1
  58. package/dist/responses-request.d.ts +20 -0
  59. package/dist/responses-request.js +7 -7
  60. package/dist/responses-request.js.map +1 -1
  61. package/dist/routes/admin.d.ts +8 -1
  62. package/dist/routes/admin.js +42 -6
  63. package/dist/routes/admin.js.map +1 -1
  64. package/dist/routes/messages.d.ts +1 -0
  65. package/dist/routes/messages.js +13 -8
  66. package/dist/routes/messages.js.map +1 -1
  67. package/dist/routes/openai-front.js +20 -7
  68. package/dist/routes/openai-front.js.map +1 -1
  69. package/dist/server.d.ts +9 -1
  70. package/dist/server.js +21 -3
  71. package/dist/server.js.map +1 -1
  72. package/dist/setup-claude.d.ts +2 -2
  73. package/dist/setup-claude.js +9 -7
  74. package/dist/setup-claude.js.map +1 -1
  75. package/dist/stream-pipeline.d.ts +116 -0
  76. package/dist/stream-pipeline.js +151 -0
  77. package/dist/stream-pipeline.js.map +1 -1
  78. package/dist/target-facts.d.ts +5 -4
  79. package/dist/target-facts.js +5 -0
  80. package/dist/target-facts.js.map +1 -1
  81. package/dist/telemetry.d.ts +15 -2
  82. package/dist/telemetry.js +4 -1
  83. package/dist/telemetry.js.map +1 -1
  84. package/dist/tier-data.d.ts +22 -0
  85. package/dist/tier-data.js +12 -2
  86. package/dist/tier-data.js.map +1 -1
  87. package/package.json +2 -1
  88. package/skills/llm-relay/SKILL.md +8 -3
  89. package/skills/llm-relay/references/dispatch-lanes.md +2 -1
  90. 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
- export function parseRouting(raw, providers, overrideDefault, warnings = [], disabledProviders = new Set()) {
96
- const r = (typeof raw === "object" && raw !== null ? raw : {});
97
- if (providers[POOL_PREFIX]) {
98
- throw new Error(`config.providers."${POOL_PREFIX}" is reserved — it would shadow "pool/<name>" routing`);
99
- }
100
- if (providers[AUTO_MODEL]) {
101
- throw new Error(`config.providers."${AUTO_MODEL}" is reserved — it would shadow "${AUTO_MODEL}" routing`);
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 dfltRaw = overrideDefault !== undefined ? overrideDefault : r.default;
104
- let dflt;
105
- if (Array.isArray(dfltRaw)) {
106
- dflt = dfltRaw.filter((s) => typeof s === "string" && s.length > 0);
107
- if (dflt.length === 0) {
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
- else if (typeof dfltRaw === "string" && dfltRaw.length > 0) {
112
- dflt = dfltRaw;
113
- }
114
- else {
115
- throw new Error(`config.routing.default ("provider/model") is required`);
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
- const tiers = {};
118
- if (typeof r.tiers === "object" && r.tiers !== null) {
119
- for (const [k, v] of Object.entries(r.tiers)) {
120
- if (Array.isArray(v)) {
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
- const pools = {};
131
- const poolPolicies = {};
132
- if (typeof r.pools === "object" && r.pools !== null) {
133
- for (const [k, v] of Object.entries(r.pools)) {
134
- let declared;
135
- if (Array.isArray(v)) {
136
- declared = v.filter((s) => typeof s === "string" && s.length > 0);
137
- }
138
- else if (typeof v === "object" && v !== null) {
139
- const policy = v;
140
- if (!Array.isArray(policy.preferred) || policy.preferred.some((s) => typeof s !== "string" || s.length === 0)) {
141
- throw new Error(`config.routing.pools.${k}.preferred must be an array of non-empty "provider/model" specs`);
142
- }
143
- if (policy.include !== "free") {
144
- throw new Error(`config.routing.pools.${k}.include must be "free"`);
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 subagents = {};
189
- if (typeof r.subagents === "object" && r.subagents !== null) {
190
- for (const [k, v] of Object.entries(r.subagents)) {
191
- if (typeof v === "string" && v.length > 0)
192
- subagents[k] = v;
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 sticky = parseSticky(r.sticky);
199
- if (sticky)
200
- routing.sticky = sticky;
201
- const quota = parseQuotaEnforcement(r.quota);
202
- if (quota)
203
- routing.quota = quota;
204
- routing.latency = parseLatencyDemotion(r.latency);
205
- routing.hedge = parseHedge(r.hedge);
206
- routing.laneProbe = parseLaneProbe(r.laneProbe);
207
- routing.dispatchWalk = parseDispatchWalk(r.dispatchWalk);
208
- const mcpSettings = parseMcpSettings(r.mcp);
209
- if (mcpSettings)
210
- routing.mcp = mcpSettings;
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
- if (r.ladders !== undefined && (typeof r.ladders !== "object" || r.ladders === null || Array.isArray(r.ladders))) {
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
- for (const [tier, spec] of Object.entries(tiers)) {
236
- const kept = dropDisabledSpecs(spec, disabledProviders, warnings, `routing.tiers.${tier}`);
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
- if (o.allowedRoots === undefined)
311
- return {};
312
- if (!Array.isArray(o.allowedRoots) || o.allowedRoots.some((r) => typeof r !== "string" || r.length === 0)) {
313
- throw new Error(`config.routing.mcp.allowedRoots must be an array of non-empty strings`);
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 { allowedRoots: [...o.allowedRoots] };
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