llm-relay 0.80.0 → 0.82.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/backend.d.ts +43 -3
- package/dist/backend.js +0 -0
- package/dist/backend.js.map +1 -1
- package/dist/candidate-runner.d.ts +45 -9
- package/dist/candidate-runner.js +55 -22
- package/dist/candidate-runner.js.map +1 -1
- package/dist/catalog.d.ts +49 -0
- package/dist/catalog.js +18 -0
- package/dist/catalog.js.map +1 -1
- package/dist/circuit-breaker.d.ts +118 -1
- package/dist/circuit-breaker.js +96 -1
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +23 -3
- package/dist/cli.js.map +1 -1
- package/dist/config/routing-parser.d.ts +1 -1
- package/dist/config/routing-parser.js +27 -1
- package/dist/config/routing-parser.js.map +1 -1
- package/dist/config-edit.js +70 -16
- package/dist/config-edit.js.map +1 -1
- package/dist/config-types.d.ts +62 -6
- package/dist/config-types.js +1 -1
- package/dist/config-types.js.map +1 -1
- package/dist/dashboard/.vite/manifest.json +2 -2
- package/dist/dashboard/assets/index-B2UQHoym.js +74 -0
- package/dist/dashboard/assets/index-DFEP3XPr.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dispatch-lane-stats.d.ts +59 -5
- package/dist/dispatch-lane-stats.js +96 -29
- package/dist/dispatch-lane-stats.js.map +1 -1
- package/dist/dispatch.d.ts +143 -1
- package/dist/dispatch.js +260 -98
- package/dist/dispatch.js.map +1 -1
- package/dist/lane-affinity.d.ts +8 -0
- package/dist/lane-affinity.js +10 -0
- package/dist/lane-affinity.js.map +1 -1
- package/dist/log.d.ts +16 -0
- package/dist/log.js +1 -0
- package/dist/log.js.map +1 -1
- package/dist/mcp/agy-quota-log.d.ts +37 -0
- package/dist/mcp/agy-quota-log.js +68 -0
- package/dist/mcp/agy-quota-log.js.map +1 -0
- package/dist/mcp/job-archive.d.ts +55 -0
- package/dist/mcp/job-archive.js +151 -0
- package/dist/mcp/job-archive.js.map +1 -0
- package/dist/mcp/lane-runner.d.ts +108 -5
- package/dist/mcp/lane-runner.js +93 -4
- package/dist/mcp/lane-runner.js.map +1 -1
- package/dist/mcp/readonly-boundary.d.ts +46 -0
- package/dist/mcp/readonly-boundary.js +119 -0
- package/dist/mcp/readonly-boundary.js.map +1 -1
- package/dist/mcp/server.d.ts +132 -8
- package/dist/mcp/server.js +267 -68
- package/dist/mcp/server.js.map +1 -1
- package/dist/openai-request.d.ts +8 -0
- package/dist/openai-request.js +33 -9
- package/dist/openai-request.js.map +1 -1
- package/dist/pacing.d.ts +125 -0
- package/dist/pacing.js +83 -0
- package/dist/pacing.js.map +1 -0
- package/dist/ping/cadence.d.ts +57 -0
- package/dist/ping/cadence.js +58 -0
- package/dist/ping/cadence.js.map +1 -1
- package/dist/refusal-interpretation.d.ts +24 -0
- package/dist/refusal-interpretation.js +10 -1
- package/dist/refusal-interpretation.js.map +1 -1
- package/dist/responses-request.d.ts +14 -1
- package/dist/responses-request.js +21 -4
- package/dist/responses-request.js.map +1 -1
- package/dist/routes/admin.d.ts +8 -0
- package/dist/routes/admin.js +122 -35
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/messages.d.ts +2 -0
- package/dist/routes/messages.js +7 -3
- package/dist/routes/messages.js.map +1 -1
- package/dist/routes/openai-front.d.ts +1 -0
- package/dist/routes/openai-front.js +7 -3
- package/dist/routes/openai-front.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +28 -4
- package/dist/server.js.map +1 -1
- package/dist/setup-claude.d.ts +2 -2
- package/dist/setup-claude.js +3 -3
- package/dist/stream-commit.d.ts +65 -0
- package/dist/stream-commit.js +150 -8
- package/dist/stream-commit.js.map +1 -1
- package/dist/target-facts.js +3 -0
- package/dist/target-facts.js.map +1 -1
- package/dist/telemetry.d.ts +9 -1
- package/dist/telemetry.js +1 -1
- package/dist/telemetry.js.map +1 -1
- package/package.json +1 -1
- package/scripts/install-skill.mjs +2 -2
- package/skills/llm-relay/SKILL.md +5 -3
- package/skills/llm-relay/references/dispatch-lanes.md +8 -2
- package/dist/dashboard/assets/index-Bqtx14ES.css +0 -1
- package/dist/dashboard/assets/index-CetZdLCA.js +0 -74
package/dist/dispatch.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { AUTO_MODEL, expandPoolSpecs, offloadRule, splitSpec, POOL_PREFIX } from "./config.js";
|
|
2
3
|
import { unsupportedArgValues, verifyModel } from "./lane-manifest.js";
|
|
3
|
-
import { allLaneStats, laneStatsFor, medianWallClockMs, p95WallClockMs, quantileWallClockMs } from "./dispatch-lane-stats.js";
|
|
4
|
+
import { allLaneStats, DISPATCH_MODES, laneStatsFor, medianWallClockMs, p95WallClockMs, quantileWallClockMs } from "./dispatch-lane-stats.js";
|
|
4
5
|
import { laneDemotion, lanePin } from "./lane-affinity.js";
|
|
6
|
+
import { relayStatePath } from "./state-paths.js";
|
|
5
7
|
export const DEFAULT_EXHAUSTED_MS = 15 * 60 * 1000;
|
|
6
8
|
export const MAX_EXHAUSTED_MS = 30 * 24 * 60 * 60 * 1000;
|
|
7
9
|
export const OUTCOME_DEFAULT_MS = {
|
|
@@ -9,6 +11,9 @@ export const OUTCOME_DEFAULT_MS = {
|
|
|
9
11
|
quota_exhausted: 60 * 60 * 1000,
|
|
10
12
|
};
|
|
11
13
|
const MAX_ECHOED_ID = 120;
|
|
14
|
+
export const MAX_ATTEMPT_BUDGET_MS = 3_600_000;
|
|
15
|
+
export const LANE_UNRELIABLE_STREAK = 3;
|
|
16
|
+
export const FAILING_LANE_STREAK = 5;
|
|
12
17
|
export function formatLaneStats(stats) {
|
|
13
18
|
const seconds = (ms) => (ms === null ? "n/a" : `${Math.round(ms / 100) / 10}s`);
|
|
14
19
|
return (`stats: ${stats.calls} calls, ${stats.successes} ok, ${stats.failures} failed, ` +
|
|
@@ -17,15 +22,19 @@ export function formatLaneStats(stats) {
|
|
|
17
22
|
export function formatAttemptBudget(budget) {
|
|
18
23
|
const render = (ms) => `${Math.round(ms / 100) / 10}s`;
|
|
19
24
|
const seconds = render(budget.ms);
|
|
25
|
+
const raised = budget.raisedBy
|
|
26
|
+
? `; raised after ${budget.raisedBy} abandoned run${budget.raisedBy === 1 ? "" : "s"}, ` +
|
|
27
|
+
`×2 each, at most ${render(MAX_ATTEMPT_BUDGET_MS)}`
|
|
28
|
+
: "";
|
|
20
29
|
switch (budget.basis) {
|
|
21
30
|
case "history":
|
|
22
|
-
return `budget: ${seconds} (from ${budget.samples} recorded runs)`;
|
|
31
|
+
return `budget: ${seconds} (from ${budget.samples} recorded runs${raised})`;
|
|
23
32
|
case "clamped":
|
|
24
33
|
return budget.quantileMs === undefined
|
|
25
|
-
? `budget: ${seconds} (flat floor — this lane's ${budget.samples} runs are faster)`
|
|
26
|
-
: `budget: ${seconds} (flat floor — this lane's ${budget.samples} runs put it at ${render(budget.quantileMs)})`;
|
|
34
|
+
? `budget: ${seconds} (flat floor — this lane's ${budget.samples} runs are faster${raised})`
|
|
35
|
+
: `budget: ${seconds} (flat floor — this lane's ${budget.samples} runs put it at ${render(budget.quantileMs)}${raised})`;
|
|
27
36
|
case "floor":
|
|
28
|
-
return `budget: ${seconds} (flat — too few recorded runs, ${budget.samples})`;
|
|
37
|
+
return `budget: ${seconds} (flat — too few recorded runs, ${budget.samples}${raised})`;
|
|
29
38
|
default: {
|
|
30
39
|
const _never = budget.basis;
|
|
31
40
|
return _never;
|
|
@@ -64,9 +73,7 @@ function clampExhaustedDeadline(untilMs, now) {
|
|
|
64
73
|
return Math.min(Math.max(now, untilMs), now + MAX_EXHAUSTED_MS);
|
|
65
74
|
}
|
|
66
75
|
export function markExhausted(cfg, id, ttlMs = DEFAULT_EXHAUSTED_MS, tier) {
|
|
67
|
-
|
|
68
|
-
return false;
|
|
69
|
-
const rung = selectLadder(cfg, tier).rungs.find((r) => r.id === id);
|
|
76
|
+
const { rung } = lookupLadderRung(cfg, id, tier);
|
|
70
77
|
if (!rung)
|
|
71
78
|
return false;
|
|
72
79
|
markExhaustedKey(cfg, cooldownKey(rung), Date.now() + normalizeTtl(ttlMs));
|
|
@@ -81,12 +88,17 @@ export function clearExhausted(cfg, id, tier) {
|
|
|
81
88
|
}
|
|
82
89
|
return;
|
|
83
90
|
}
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
const rung = selectLadder(cfg, tier).rungs.find((r) => r.id === id);
|
|
91
|
+
const { rung } = lookupLadderRung(cfg, id, tier);
|
|
87
92
|
if (rung)
|
|
88
93
|
clearExhaustedKey(cfg, cooldownKey(rung));
|
|
89
94
|
}
|
|
95
|
+
export function lookupLadderRung(cfg, laneId, tier) {
|
|
96
|
+
const selected = selectLadder(cfg, tier);
|
|
97
|
+
const rung = typeof laneId === "string" && laneId.length > 0
|
|
98
|
+
? selected.rungs.find((r) => r.id === laneId)
|
|
99
|
+
: undefined;
|
|
100
|
+
return { tier: selected.tier, rung, missingTier: selected.missing };
|
|
101
|
+
}
|
|
90
102
|
const exhaustionListeners = new WeakMap();
|
|
91
103
|
export function onExhaustionChanged(cfg, listener) {
|
|
92
104
|
let set = exhaustionListeners.get(cfg);
|
|
@@ -192,6 +204,13 @@ function reachableWithoutRelay(spec, cfg) {
|
|
|
192
204
|
provider.credentialMode !== "contained");
|
|
193
205
|
});
|
|
194
206
|
}
|
|
207
|
+
export function isPassThroughSpec(spec, cfg) {
|
|
208
|
+
return reachableWithoutRelay(spec, cfg);
|
|
209
|
+
}
|
|
210
|
+
export function mcpPassThroughReason(spec) {
|
|
211
|
+
return (`the MCP server cannot run "${describeId(spec)}": it forwards the caller's own Anthropic credential, ` +
|
|
212
|
+
"which only the calling host holds, so the host must reach it with its own subagent");
|
|
213
|
+
}
|
|
195
214
|
function transposeToCli(spec, lane, task, platform, contextWindow) {
|
|
196
215
|
const fillShared = (value) => {
|
|
197
216
|
const withSpec = value.split(SPEC_TOKEN).join(spec);
|
|
@@ -223,6 +242,36 @@ function transposeToCli(spec, lane, task, platform, contextWindow) {
|
|
|
223
242
|
export function normalizeCliCommand(command, platform = process.platform) {
|
|
224
243
|
return platform === "win32" && /^agy$/i.test(command) ? `${command}.exe` : command;
|
|
225
244
|
}
|
|
245
|
+
export function resolveLaneLauncherPath(platform = process.platform, exists) {
|
|
246
|
+
if (platform !== "win32")
|
|
247
|
+
return null;
|
|
248
|
+
if (exists === undefined && process.env["VITEST"])
|
|
249
|
+
return null;
|
|
250
|
+
const path = relayStatePath("config", ["bin", "lane-launch.ps1"]);
|
|
251
|
+
return (exists ?? existsSync)(path) ? path : null;
|
|
252
|
+
}
|
|
253
|
+
function isAlreadyLaneLaunched(command, args) {
|
|
254
|
+
if (!/^(pwsh|powershell)(\.exe)?$/i.test(command))
|
|
255
|
+
return false;
|
|
256
|
+
const fileIdx = args.findIndex((a) => a === "-File" || a === "/File");
|
|
257
|
+
if (fileIdx === -1)
|
|
258
|
+
return false;
|
|
259
|
+
const target = args[fileIdx + 1];
|
|
260
|
+
if (!target)
|
|
261
|
+
return false;
|
|
262
|
+
const basename = target.split(/[\\/]/).pop() ?? "";
|
|
263
|
+
return /^lane-launch\.ps1$/i.test(basename);
|
|
264
|
+
}
|
|
265
|
+
function wrapForWindowsConsoleSafety(invoke, launcherPath, platform) {
|
|
266
|
+
if (platform !== "win32" || launcherPath === null)
|
|
267
|
+
return invoke;
|
|
268
|
+
if (isAlreadyLaneLaunched(invoke.command, invoke.args))
|
|
269
|
+
return invoke;
|
|
270
|
+
return {
|
|
271
|
+
command: "pwsh",
|
|
272
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", launcherPath, invoke.command, ...invoke.args],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
226
275
|
function normalizeOptions(opts) {
|
|
227
276
|
const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
|
|
228
277
|
const out = {};
|
|
@@ -253,6 +302,14 @@ function normalizeOptions(opts) {
|
|
|
253
302
|
if (manifest && typeof manifest === "object" && manifest.version === 1 && typeof manifest.lanes === "object" && manifest.lanes !== null) {
|
|
254
303
|
out.manifest = manifest;
|
|
255
304
|
}
|
|
305
|
+
if (opts.requester === "mcp")
|
|
306
|
+
out.requester = "mcp";
|
|
307
|
+
const mode = str(opts.mode);
|
|
308
|
+
if (mode !== undefined && DISPATCH_MODES.includes(mode))
|
|
309
|
+
out.mode = mode;
|
|
310
|
+
const model = str(opts.model);
|
|
311
|
+
if (model !== undefined && model.length <= 200)
|
|
312
|
+
out.model = model;
|
|
256
313
|
return out;
|
|
257
314
|
}
|
|
258
315
|
function inferredTier(cfg) {
|
|
@@ -294,7 +351,7 @@ function stripControlCharacters(value) {
|
|
|
294
351
|
}
|
|
295
352
|
return out;
|
|
296
353
|
}
|
|
297
|
-
function describeId(id) {
|
|
354
|
+
export function describeId(id) {
|
|
298
355
|
const clean = stripControlCharacters(id);
|
|
299
356
|
return clean.length > MAX_ECHOED_ID ? `${clean.slice(0, MAX_ECHOED_ID)}\u2026` : clean;
|
|
300
357
|
}
|
|
@@ -309,7 +366,7 @@ function unreachableReason(host, who) {
|
|
|
309
366
|
? `${who} is not running inside a Claude Code session, so it has no subagent mechanism to reach`
|
|
310
367
|
: `${who} does not route its traffic through this relay, so a subagent cannot reach`;
|
|
311
368
|
}
|
|
312
|
-
function toLane(rung, position, cfg, opts, now, client, platform, host, entrypoint) {
|
|
369
|
+
function toLane(rung, position, cfg, opts, now, client, platform, host, entrypoint, launcherPath) {
|
|
313
370
|
const until = cooldownUntil(cfg, rung, now);
|
|
314
371
|
const state = !rung.enabled ? "disabled" : until !== null ? "exhausted" : "ready";
|
|
315
372
|
const lane = { id: rung.id, kind: rung.kind, position, state };
|
|
@@ -339,7 +396,11 @@ function toLane(rung, position, cfg, opts, now, client, platform, host, entrypoi
|
|
|
339
396
|
delete lane.invoke;
|
|
340
397
|
}
|
|
341
398
|
}
|
|
342
|
-
if (rung.kind === "relay" && rung.spec) {
|
|
399
|
+
if (rung.kind === "relay" && rung.spec && opts.requester === "mcp" && reachableWithoutRelay(rung.spec, cfg)) {
|
|
400
|
+
lane.spec = rung.spec;
|
|
401
|
+
lane.unreachable = mcpPassThroughReason(rung.spec);
|
|
402
|
+
}
|
|
403
|
+
else if (rung.kind === "relay" && rung.spec) {
|
|
343
404
|
lane.spec = rung.spec;
|
|
344
405
|
if (canAddressAsSubagent(host)) {
|
|
345
406
|
lane.requiresDirective = !offloadRule(cfg, client).enabled;
|
|
@@ -366,6 +427,10 @@ function toLane(rung, position, cfg, opts, now, client, platform, host, entrypoi
|
|
|
366
427
|
}
|
|
367
428
|
}
|
|
368
429
|
}
|
|
430
|
+
if (lane.invoke) {
|
|
431
|
+
const wrapped = wrapForWindowsConsoleSafety(lane.invoke, launcherPath, platform);
|
|
432
|
+
lane.invoke = { ...lane.invoke, command: wrapped.command, args: wrapped.args };
|
|
433
|
+
}
|
|
369
434
|
return lane;
|
|
370
435
|
}
|
|
371
436
|
function rungModel(rung) {
|
|
@@ -440,14 +505,102 @@ function stripArgs(args, drop) {
|
|
|
440
505
|
}
|
|
441
506
|
return out;
|
|
442
507
|
}
|
|
443
|
-
export function buildDispatch(cfg, rawOpts = {}, platform = process.platform) {
|
|
508
|
+
export function buildDispatch(cfg, rawOpts = {}, platform = process.platform, launcherPath = null) {
|
|
444
509
|
const opts = normalizeOptions(rawOpts ?? {});
|
|
445
510
|
const client = opts.client ?? "default";
|
|
446
511
|
const host = opts.host ?? "unknown";
|
|
447
512
|
const now = Date.now();
|
|
448
513
|
const selected = selectLadder(cfg, opts.tier);
|
|
449
|
-
const
|
|
450
|
-
const ladder = rungs.map((r, i) =>
|
|
514
|
+
const makeLane = (rung, position) => toLane(rung, position, cfg, opts, now, client, platform, opts.host, opts.entrypoint, launcherPath);
|
|
515
|
+
const ladder = selected.rungs.map((r, i) => makeLane(r, i + 1));
|
|
516
|
+
annotateLaneStats(cfg, ladder);
|
|
517
|
+
annotateLaneHistory(cfg, ladder, selected.tier, opts.mode ?? null);
|
|
518
|
+
annotateAffinity(cfg, ladder, selected.tier, now);
|
|
519
|
+
const offload = offloadRule(cfg, client).enabled;
|
|
520
|
+
const base = { tier: selected.tier, offload, client, host, ladder };
|
|
521
|
+
if (opts.model !== undefined)
|
|
522
|
+
return modelView(cfg, base, opts.model, opts.lane, makeLane);
|
|
523
|
+
if (selected.missing) {
|
|
524
|
+
return {
|
|
525
|
+
...base,
|
|
526
|
+
order: [],
|
|
527
|
+
next: null,
|
|
528
|
+
reason: `no dispatch tier "${describeId(selected.missing)}" configured (have: ${Object.keys(cfg.routing.ladders ?? {}).join(", ")})`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (ladder.length === 0) {
|
|
532
|
+
return {
|
|
533
|
+
...base,
|
|
534
|
+
order: [],
|
|
535
|
+
next: null,
|
|
536
|
+
reason: "no routing.ladder configured — dispatch order is the host's to choose",
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (opts.lane !== undefined)
|
|
540
|
+
return forcedLaneView(base, opts.lane);
|
|
541
|
+
return rankedView(base, opts.after);
|
|
542
|
+
}
|
|
543
|
+
function laneWindows(cfg, laneId, tier, mode) {
|
|
544
|
+
const keys = [];
|
|
545
|
+
const add = (t, m) => {
|
|
546
|
+
if (!keys.some(([kt, km]) => kt === t && km === m))
|
|
547
|
+
keys.push([t, m]);
|
|
548
|
+
};
|
|
549
|
+
add(tier, mode);
|
|
550
|
+
add(tier, null);
|
|
551
|
+
add(null, mode);
|
|
552
|
+
add(null, null);
|
|
553
|
+
const out = [];
|
|
554
|
+
for (const [t, m] of keys) {
|
|
555
|
+
const w = laneStatsFor(cfg, laneId, t, m);
|
|
556
|
+
if (w)
|
|
557
|
+
out.push(w);
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
function laneHistoryFacts(cfg, laneId, tier, mode) {
|
|
562
|
+
const windows = laneWindows(cfg, laneId, tier, mode);
|
|
563
|
+
const walk = cfg.routing.dispatchWalk;
|
|
564
|
+
const minSamples = walk?.attemptMinSamples ?? 5;
|
|
565
|
+
const sampled = windows.find((w) => w.wallClockMs.length >= minSamples);
|
|
566
|
+
const hinted = sampled ?? windows.find((w) => w.wallClockMs.length > 0);
|
|
567
|
+
const timeToAnswer = hinted === undefined
|
|
568
|
+
? undefined
|
|
569
|
+
: {
|
|
570
|
+
medianMs: Math.round(medianWallClockMs(hinted.wallClockMs) ?? 0),
|
|
571
|
+
p80Ms: Math.round(quantileWallClockMs(hinted.wallClockMs, 0.8) ?? 0),
|
|
572
|
+
samples: hinted.wallClockMs.length,
|
|
573
|
+
mode: hinted.mode,
|
|
574
|
+
};
|
|
575
|
+
const newest = windows[0];
|
|
576
|
+
const recentFailures = newest?.consecutiveFailures ?? 0;
|
|
577
|
+
if (!walk || !walk.enabled)
|
|
578
|
+
return { budget: undefined, timeToAnswer, recentFailures };
|
|
579
|
+
const floorMs = mode === "agent" ? Math.max(walk.attemptMs, walk.agentAttemptMs) : walk.attemptMs;
|
|
580
|
+
const base = budgetFromSamples(sampled?.wallClockMs ?? newest?.wallClockMs ?? [], { ...walk, attemptMs: floorMs });
|
|
581
|
+
const raisedBy = newest?.abandonedSinceSuccess ?? 0;
|
|
582
|
+
if (base === undefined || raisedBy <= 0)
|
|
583
|
+
return { budget: base, timeToAnswer, recentFailures };
|
|
584
|
+
const ms = Math.min(MAX_ATTEMPT_BUDGET_MS, base.ms * 2 ** Math.min(raisedBy, 16));
|
|
585
|
+
return { budget: { ...base, ms, raisedBy }, timeToAnswer, recentFailures };
|
|
586
|
+
}
|
|
587
|
+
function budgetFromSamples(samples, walk) {
|
|
588
|
+
if (samples.length < walk.attemptMinSamples) {
|
|
589
|
+
return { ms: walk.attemptMs, basis: "floor", samples: samples.length };
|
|
590
|
+
}
|
|
591
|
+
const quantile = quantileWallClockMs(samples, walk.attemptQuantile);
|
|
592
|
+
if (quantile === null)
|
|
593
|
+
return { ms: walk.attemptMs, basis: "floor", samples: samples.length };
|
|
594
|
+
const own = Math.round(quantile);
|
|
595
|
+
if (own < walk.attemptMs) {
|
|
596
|
+
return { ms: walk.attemptMs, basis: "clamped", samples: samples.length, quantileMs: own };
|
|
597
|
+
}
|
|
598
|
+
return { ms: own, basis: "history", samples: samples.length };
|
|
599
|
+
}
|
|
600
|
+
function isSelectable(lane) {
|
|
601
|
+
return lane.state === "ready" && lane.unreachable === undefined && lane.notServable === undefined;
|
|
602
|
+
}
|
|
603
|
+
function annotateLaneStats(cfg, ladder) {
|
|
451
604
|
const statsByLane = new Map();
|
|
452
605
|
for (const row of allLaneStats(cfg)) {
|
|
453
606
|
const agg = statsByLane.get(row.laneId);
|
|
@@ -483,108 +636,112 @@ export function buildDispatch(cfg, rawOpts = {}, platform = process.platform) {
|
|
|
483
636
|
lastAt: row.lastAt,
|
|
484
637
|
};
|
|
485
638
|
}
|
|
639
|
+
}
|
|
640
|
+
function annotateLaneHistory(cfg, ladder, tier, mode) {
|
|
641
|
+
const walkOn = cfg.routing.dispatchWalk?.enabled === true;
|
|
486
642
|
for (const lane of ladder) {
|
|
487
|
-
const
|
|
488
|
-
if (budget !== undefined)
|
|
489
|
-
lane.attemptBudget = budget;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
};
|
|
643
|
+
const facts = laneHistoryFacts(cfg, lane.id, tier, mode);
|
|
644
|
+
if (facts.budget !== undefined)
|
|
645
|
+
lane.attemptBudget = facts.budget;
|
|
646
|
+
if (facts.timeToAnswer !== undefined)
|
|
647
|
+
lane.timeToAnswer = facts.timeToAnswer;
|
|
648
|
+
if (facts.recentFailures > 0)
|
|
649
|
+
lane.recentFailures = facts.recentFailures;
|
|
650
|
+
if (walkOn && facts.recentFailures >= FAILING_LANE_STREAK && isSelectable(lane)) {
|
|
651
|
+
lane.failing = {
|
|
652
|
+
streak: facts.recentFailures,
|
|
653
|
+
reason: `${facts.recentFailures} own failures in a row; ordered behind the other lanes until it answers again`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
501
656
|
}
|
|
502
|
-
|
|
657
|
+
}
|
|
658
|
+
function forcedLaneView(base, laneId) {
|
|
659
|
+
const forced = base.ladder.find((l) => l.id === laneId);
|
|
660
|
+
if (!forced) {
|
|
503
661
|
return {
|
|
504
662
|
...base,
|
|
505
663
|
order: [],
|
|
506
664
|
next: null,
|
|
507
|
-
reason:
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
if (opts.lane !== undefined) {
|
|
511
|
-
const forced = ladder.find((l) => l.id === opts.lane);
|
|
512
|
-
if (!forced) {
|
|
513
|
-
return {
|
|
514
|
-
...base,
|
|
515
|
-
order: [],
|
|
516
|
-
next: null,
|
|
517
|
-
reason: `no lane "${describeId(opts.lane)}" in the ladder (have: ${ladder.map((l) => l.id).join(", ")})`,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
return {
|
|
521
|
-
...base,
|
|
522
|
-
order: [forced.id],
|
|
523
|
-
next: forced,
|
|
524
|
-
reason: forced.unreachable !== undefined
|
|
525
|
-
? `lane "${forced.id}" selected by host override (${forced.unreachable})`
|
|
526
|
-
: forced.state === "ready"
|
|
527
|
-
? `lane "${forced.id}" selected by host override`
|
|
528
|
-
: `lane "${forced.id}" selected by host override (currently ${forced.state})`,
|
|
665
|
+
reason: `no lane "${describeId(laneId)}" in the ladder (have: ${base.ladder.map((l) => l.id).join(", ")})`,
|
|
529
666
|
};
|
|
530
667
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
668
|
+
return {
|
|
669
|
+
...base,
|
|
670
|
+
order: [forced.id],
|
|
671
|
+
next: forced,
|
|
672
|
+
reason: forced.unreachable !== undefined
|
|
673
|
+
? `lane "${forced.id}" selected by host override (${forced.unreachable})`
|
|
674
|
+
: forced.state === "ready"
|
|
675
|
+
? `lane "${forced.id}" selected by host override`
|
|
676
|
+
: `lane "${forced.id}" selected by host override (currently ${forced.state})`,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function rankedView(base, after) {
|
|
680
|
+
let pool = base.ladder;
|
|
681
|
+
if (after !== undefined) {
|
|
682
|
+
const idx = base.ladder.findIndex((l) => l.id === after);
|
|
534
683
|
if (idx < 0) {
|
|
535
684
|
return {
|
|
536
685
|
...base,
|
|
537
686
|
order: [],
|
|
538
687
|
next: null,
|
|
539
|
-
reason: `no lane "${describeId(
|
|
688
|
+
reason: `no lane "${describeId(after)}" in the ladder (have: ${base.ladder.map((l) => l.id).join(", ")})`,
|
|
540
689
|
};
|
|
541
690
|
}
|
|
542
|
-
pool = ladder.slice(idx + 1);
|
|
691
|
+
pool = base.ladder.slice(idx + 1);
|
|
543
692
|
}
|
|
544
|
-
const usable = pool.filter(
|
|
693
|
+
const usable = pool.filter(isSelectable);
|
|
545
694
|
const ranked = rankSelectable(usable);
|
|
546
695
|
const next = ranked[0] ?? null;
|
|
547
696
|
if (!next) {
|
|
548
697
|
const blocked = pool.filter((l) => l.unreachable !== undefined).length;
|
|
549
|
-
const why =
|
|
550
|
-
? `no ready lane after "${describeId(
|
|
698
|
+
const why = after !== undefined
|
|
699
|
+
? `no ready lane after "${describeId(after)}" — the ladder is exhausted`
|
|
551
700
|
: "every lane is exhausted or disabled";
|
|
701
|
+
return { ...base, order: [], next: null, reason: blocked > 0 ? `${why} (${blocked} unreachable from this host)` : why };
|
|
702
|
+
}
|
|
703
|
+
return { ...base, order: ranked.map((l) => l.id), next, reason: selectionReason(next, after, usable) };
|
|
704
|
+
}
|
|
705
|
+
function unknownModelReason(cfg, spec) {
|
|
706
|
+
if (/\s/.test(spec) || stripControlCharacters(spec) !== spec) {
|
|
707
|
+
return `model "${describeId(spec)}" is not a routing spec: it holds whitespace or control characters`;
|
|
708
|
+
}
|
|
709
|
+
if (spec === AUTO_MODEL)
|
|
710
|
+
return null;
|
|
711
|
+
const { provider, model } = splitSpec(spec);
|
|
712
|
+
if (provider === POOL_PREFIX) {
|
|
713
|
+
const pools = Object.keys(cfg.routing.pools ?? {});
|
|
714
|
+
if (model !== undefined && pools.includes(model))
|
|
715
|
+
return null;
|
|
716
|
+
return `no pool "${describeId(spec)}" configured (have: ${pools.map((p) => `${POOL_PREFIX}/${p}`).join(", ")})`;
|
|
717
|
+
}
|
|
718
|
+
if (Object.keys(cfg.providers).includes(provider))
|
|
719
|
+
return null;
|
|
720
|
+
return `model "${describeId(spec)}" names no configured provider or pool (providers: ${Object.keys(cfg.providers).join(", ")})`;
|
|
721
|
+
}
|
|
722
|
+
function modelView(cfg, base, spec, laneOverride, makeLane) {
|
|
723
|
+
if (laneOverride !== undefined) {
|
|
552
724
|
return {
|
|
553
725
|
...base,
|
|
554
726
|
order: [],
|
|
555
727
|
next: null,
|
|
556
|
-
reason:
|
|
728
|
+
reason: `pass lane or model, not both: lane "${describeId(laneOverride)}" names a ladder rung and ` +
|
|
729
|
+
`model "${describeId(spec)}" names a routing spec`,
|
|
557
730
|
};
|
|
558
731
|
}
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
const legacy = laneStatsFor(cfg, laneId, null)?.wallClockMs ?? [];
|
|
574
|
-
return budgetFromSamples(legacy, walk);
|
|
575
|
-
}
|
|
576
|
-
function budgetFromSamples(samples, walk) {
|
|
577
|
-
if (samples.length < walk.attemptMinSamples) {
|
|
578
|
-
return { ms: walk.attemptMs, basis: "floor", samples: samples.length };
|
|
579
|
-
}
|
|
580
|
-
const quantile = quantileWallClockMs(samples, walk.attemptQuantile);
|
|
581
|
-
if (quantile === null)
|
|
582
|
-
return { ms: walk.attemptMs, basis: "floor", samples: samples.length };
|
|
583
|
-
const own = Math.round(quantile);
|
|
584
|
-
if (own < walk.attemptMs) {
|
|
585
|
-
return { ms: walk.attemptMs, basis: "clamped", samples: samples.length, quantileMs: own };
|
|
586
|
-
}
|
|
587
|
-
return { ms: own, basis: "history", samples: samples.length };
|
|
732
|
+
const unknown = unknownModelReason(cfg, spec);
|
|
733
|
+
if (unknown !== null)
|
|
734
|
+
return { ...base, order: [], next: null, reason: unknown };
|
|
735
|
+
const lane = makeLane({ id: `model:${spec}`, kind: "relay", enabled: true, spec }, base.ladder.length + 1);
|
|
736
|
+
lane.adHoc = true;
|
|
737
|
+
const named = `model "${spec}" named by the caller`;
|
|
738
|
+
return {
|
|
739
|
+
...base,
|
|
740
|
+
ladder: [...base.ladder, lane],
|
|
741
|
+
order: [lane.id],
|
|
742
|
+
next: lane,
|
|
743
|
+
reason: lane.unreachable === undefined ? named : `${named} (${lane.unreachable})`,
|
|
744
|
+
};
|
|
588
745
|
}
|
|
589
746
|
function annotateAffinity(cfg, ladder, tier, now) {
|
|
590
747
|
const walk = cfg.routing.dispatchWalk;
|
|
@@ -606,7 +763,7 @@ function annotateAffinity(cfg, ladder, tier, now) {
|
|
|
606
763
|
}
|
|
607
764
|
}
|
|
608
765
|
function rankSelectable(usable) {
|
|
609
|
-
const rank = (lane) => (lane.pinned ? 0 : lane.demoted ? 2 : 1);
|
|
766
|
+
const rank = (lane) => (lane.pinned ? 0 : lane.demoted || lane.failing ? 2 : 1);
|
|
610
767
|
return [...usable].sort((a, b) => rank(a) - rank(b));
|
|
611
768
|
}
|
|
612
769
|
function selectionReason(next, after, usable) {
|
|
@@ -614,17 +771,22 @@ function selectionReason(next, after, usable) {
|
|
|
614
771
|
return `lane "${next.id}" is pinned (${next.pinned.reason})`;
|
|
615
772
|
if (after !== undefined)
|
|
616
773
|
return `first ready lane after "${describeId(after)}"`;
|
|
617
|
-
if (next.demoted)
|
|
618
|
-
return `every ready lane is demoted; "${next.id}" is first among them in the ladder`;
|
|
774
|
+
if (next.demoted || next.failing) {
|
|
775
|
+
return `every ready lane is demoted or failing; "${next.id}" is first among them in the ladder`;
|
|
776
|
+
}
|
|
619
777
|
if (next.position === 1)
|
|
620
778
|
return "first lane in the ladder";
|
|
621
|
-
const
|
|
622
|
-
const
|
|
779
|
+
const ahead = usable.filter((l) => l.position < next.position);
|
|
780
|
+
const aheadFailing = ahead.filter((l) => l.failing !== undefined).length;
|
|
781
|
+
const aheadDemoted = ahead.length - aheadFailing;
|
|
782
|
+
const aheadUnavailable = next.position - 1 - ahead.length;
|
|
623
783
|
const parts = [];
|
|
624
784
|
if (aheadUnavailable > 0)
|
|
625
785
|
parts.push(`${aheadUnavailable} ahead of it unavailable`);
|
|
626
786
|
if (aheadDemoted > 0)
|
|
627
787
|
parts.push(`${aheadDemoted} ahead of it ready but demoted`);
|
|
788
|
+
if (aheadFailing > 0)
|
|
789
|
+
parts.push(`${aheadFailing} ahead of it ready but failing`);
|
|
628
790
|
return parts.length === 0 ? "first lane in the ladder" : `first undemoted lane (${parts.join(", ")})`;
|
|
629
791
|
}
|
|
630
792
|
export const AUTO_TIERS = ["low", "medium", "high", "xhigh"];
|