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.
Files changed (97) hide show
  1. package/dist/backend.d.ts +43 -3
  2. package/dist/backend.js +0 -0
  3. package/dist/backend.js.map +1 -1
  4. package/dist/candidate-runner.d.ts +45 -9
  5. package/dist/candidate-runner.js +55 -22
  6. package/dist/candidate-runner.js.map +1 -1
  7. package/dist/catalog.d.ts +49 -0
  8. package/dist/catalog.js +18 -0
  9. package/dist/catalog.js.map +1 -1
  10. package/dist/circuit-breaker.d.ts +118 -1
  11. package/dist/circuit-breaker.js +96 -1
  12. package/dist/circuit-breaker.js.map +1 -1
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.js +23 -3
  15. package/dist/cli.js.map +1 -1
  16. package/dist/config/routing-parser.d.ts +1 -1
  17. package/dist/config/routing-parser.js +27 -1
  18. package/dist/config/routing-parser.js.map +1 -1
  19. package/dist/config-edit.js +70 -16
  20. package/dist/config-edit.js.map +1 -1
  21. package/dist/config-types.d.ts +62 -6
  22. package/dist/config-types.js +1 -1
  23. package/dist/config-types.js.map +1 -1
  24. package/dist/dashboard/.vite/manifest.json +2 -2
  25. package/dist/dashboard/assets/index-B2UQHoym.js +74 -0
  26. package/dist/dashboard/assets/index-DFEP3XPr.css +1 -0
  27. package/dist/dashboard/index.html +2 -2
  28. package/dist/dispatch-lane-stats.d.ts +59 -5
  29. package/dist/dispatch-lane-stats.js +96 -29
  30. package/dist/dispatch-lane-stats.js.map +1 -1
  31. package/dist/dispatch.d.ts +143 -1
  32. package/dist/dispatch.js +260 -98
  33. package/dist/dispatch.js.map +1 -1
  34. package/dist/lane-affinity.d.ts +8 -0
  35. package/dist/lane-affinity.js +10 -0
  36. package/dist/lane-affinity.js.map +1 -1
  37. package/dist/log.d.ts +16 -0
  38. package/dist/log.js +1 -0
  39. package/dist/log.js.map +1 -1
  40. package/dist/mcp/agy-quota-log.d.ts +37 -0
  41. package/dist/mcp/agy-quota-log.js +68 -0
  42. package/dist/mcp/agy-quota-log.js.map +1 -0
  43. package/dist/mcp/job-archive.d.ts +55 -0
  44. package/dist/mcp/job-archive.js +151 -0
  45. package/dist/mcp/job-archive.js.map +1 -0
  46. package/dist/mcp/lane-runner.d.ts +108 -5
  47. package/dist/mcp/lane-runner.js +93 -4
  48. package/dist/mcp/lane-runner.js.map +1 -1
  49. package/dist/mcp/readonly-boundary.d.ts +46 -0
  50. package/dist/mcp/readonly-boundary.js +119 -0
  51. package/dist/mcp/readonly-boundary.js.map +1 -1
  52. package/dist/mcp/server.d.ts +132 -8
  53. package/dist/mcp/server.js +267 -68
  54. package/dist/mcp/server.js.map +1 -1
  55. package/dist/openai-request.d.ts +8 -0
  56. package/dist/openai-request.js +33 -9
  57. package/dist/openai-request.js.map +1 -1
  58. package/dist/pacing.d.ts +125 -0
  59. package/dist/pacing.js +83 -0
  60. package/dist/pacing.js.map +1 -0
  61. package/dist/ping/cadence.d.ts +57 -0
  62. package/dist/ping/cadence.js +58 -0
  63. package/dist/ping/cadence.js.map +1 -1
  64. package/dist/refusal-interpretation.d.ts +24 -0
  65. package/dist/refusal-interpretation.js +10 -1
  66. package/dist/refusal-interpretation.js.map +1 -1
  67. package/dist/responses-request.d.ts +14 -1
  68. package/dist/responses-request.js +21 -4
  69. package/dist/responses-request.js.map +1 -1
  70. package/dist/routes/admin.d.ts +8 -0
  71. package/dist/routes/admin.js +122 -35
  72. package/dist/routes/admin.js.map +1 -1
  73. package/dist/routes/messages.d.ts +2 -0
  74. package/dist/routes/messages.js +7 -3
  75. package/dist/routes/messages.js.map +1 -1
  76. package/dist/routes/openai-front.d.ts +1 -0
  77. package/dist/routes/openai-front.js +7 -3
  78. package/dist/routes/openai-front.js.map +1 -1
  79. package/dist/server.d.ts +4 -0
  80. package/dist/server.js +28 -4
  81. package/dist/server.js.map +1 -1
  82. package/dist/setup-claude.d.ts +2 -2
  83. package/dist/setup-claude.js +3 -3
  84. package/dist/stream-commit.d.ts +65 -0
  85. package/dist/stream-commit.js +150 -8
  86. package/dist/stream-commit.js.map +1 -1
  87. package/dist/target-facts.js +3 -0
  88. package/dist/target-facts.js.map +1 -1
  89. package/dist/telemetry.d.ts +9 -1
  90. package/dist/telemetry.js +1 -1
  91. package/dist/telemetry.js.map +1 -1
  92. package/package.json +1 -1
  93. package/scripts/install-skill.mjs +2 -2
  94. package/skills/llm-relay/SKILL.md +5 -3
  95. package/skills/llm-relay/references/dispatch-lanes.md +8 -2
  96. package/dist/dashboard/assets/index-Bqtx14ES.css +0 -1
  97. package/dist/dashboard/assets/index-CetZdLCA.js +0 -74
@@ -2,6 +2,7 @@ import type { Config, LadderRung } from "./config-types.js";
2
2
  import type { HostRoutingState } from "./host-routing.js";
3
3
  import type { ContextWindowSource, ResolvedContextWindow } from "./metadata.js";
4
4
  import { type LaneManifest } from "./lane-manifest.js";
5
+ import { type DispatchMode } from "./dispatch-lane-stats.js";
5
6
  /**
6
7
  * The dispatch ladder: which LANE a host agent should hand a delegated task to, in what order,
7
8
  * and what to do when one is spent.
@@ -40,6 +41,15 @@ export declare const MAX_EXHAUSTED_MS: number;
40
41
  */
41
42
  export type DispatchOutcome = "rate_limited" | "quota_exhausted";
42
43
  export declare const OUTCOME_DEFAULT_MS: Record<DispatchOutcome, number>;
44
+ /** The ceiling a RAISED budget stops at — the same one-hour ceiling `attemptMs` is bounded by. */
45
+ export declare const MAX_ATTEMPT_BUDGET_MS = 3600000;
46
+ /**
47
+ * Own failures in a row after which a lane cannot be relied on to answer. `runWalk` will not stop
48
+ * an earlier lane that is still working in order to reach lanes that are all past this streak.
49
+ */
50
+ export declare const LANE_UNRELIABLE_STREAK = 3;
51
+ /** Own failures in a row after which a lane is ordered behind the lanes that have none. */
52
+ export declare const FAILING_LANE_STREAK = 5;
43
53
  export type LaneState = "ready" | "exhausted" | "disabled" | "not-servable";
44
54
  /**
45
55
  * Advisory per-lane execution stats for one ladder rung: how often this config took the lane
@@ -90,6 +100,7 @@ export declare function formatAttemptBudget(budget: {
90
100
  basis: "history" | "floor" | "clamped";
91
101
  samples: number;
92
102
  quantileMs?: number;
103
+ raisedBy?: number;
93
104
  }): string;
94
105
  export interface DispatchLane {
95
106
  id: string;
@@ -232,6 +243,16 @@ export interface DispatchLane {
232
243
  * came to read and `ms` is not it. Never a guess: absent unless a quantile was computed.
233
244
  */
234
245
  quantileMs?: number;
246
+ /**
247
+ * How many times the budget was DOUBLED because the walk abandoned this lane since its last
248
+ * success (`LaneStats.abandonedSinceSuccess`). Present only when above zero.
249
+ *
250
+ * ⚠ It exists because an abandoned run leaves no duration sample, so a window fed only by runs
251
+ * that finished inside the budget can never show that the lane needed longer — the floor lock
252
+ * measured on 2026-09-10 (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §3b). Each abandonment
253
+ * doubles the next budget, capped at `MAX_ATTEMPT_BUDGET_MS`; one success resets the count.
254
+ */
255
+ raisedBy?: number;
235
256
  };
236
257
  /**
237
258
  * This lane recently failed to answer inside the budget a dispatch walk gave it, so ready lanes
@@ -253,6 +274,44 @@ export interface DispatchLane {
253
274
  until: string;
254
275
  reason: string;
255
276
  };
277
+ /**
278
+ * The lane's usual time to ANSWER in the requested mode: median and 80th percentile of the
279
+ * durations in the window its budget was read from — since 2026-09-10 only a COMPLETED run adds
280
+ * one, and `restoreLaneStatsRows` empties an older window that provably holds anything else.
281
+ * Present only when that window holds a duration. A poll renders it (`describeJob` in `mcp/server.ts`), so a caller can tell a slow
282
+ * lane from a stuck one — 23 of the 182 unanswered dispatches in the 2026-09-10 transcript sweep
283
+ * ended with the caller simply no longer polling.
284
+ */
285
+ timeToAnswer?: {
286
+ medianMs: number;
287
+ p80Ms: number;
288
+ samples: number;
289
+ mode: DispatchMode | null;
290
+ };
291
+ /**
292
+ * Own failures (`failed` or `timed_out`, never a walk abandonment) in a row, read from the same
293
+ * window as the budget. Present only when above zero.
294
+ *
295
+ * ⚠ Evidence the walk reads: a later lane on a streak of `LANE_UNRELIABLE_STREAK` or more cannot
296
+ * be relied on to answer, so the walk does not stop an earlier lane that is still working in
297
+ * order to reach it (`runWalk`). Measured 2026-09-10: the walk stopped `free-pool` at 90 s to try
298
+ * `opencode-muse-spark` (0 of 12), `agy-claude-opus` (0 of 34) and `anthropic` (0 of 21).
299
+ */
300
+ recentFailures?: number;
301
+ /**
302
+ * Set at `FAILING_LANE_STREAK` own failures in a row: the lane is ordered behind every lane that
303
+ * has none, the same band as a walk demotion, until it answers again. Demoted, never dropped.
304
+ */
305
+ failing?: {
306
+ streak: number;
307
+ reason: string;
308
+ };
309
+ /**
310
+ * Not a ladder rung: built for ONE dispatch from a caller-named `model`. Never reported to
311
+ * `/dispatch/telemetry`, which knows only ladder lane ids; its HTTP traffic is metered by the
312
+ * relay's own pipeline anyway.
313
+ */
314
+ adHoc?: true;
256
315
  }
257
316
  export interface DispatchView {
258
317
  /** Selected tier-specific ladder, or null when using the legacy single ladder. */
@@ -328,6 +387,27 @@ export interface DispatchOptions {
328
387
  * existed. Never guess a number here: see `specContextWindow`.
329
388
  */
330
389
  publishedContextWindow?: (spec: string) => ResolvedContextWindow | null;
390
+ /**
391
+ * WHO is asking, when that changes what can run. `"mcp"` is the `llm-relay mcp` server: it runs
392
+ * lanes itself and has no `Agent` tool, so a pass-through relay rung — one that forwards the
393
+ * caller's own Anthropic credential — can never run there. It comes back `unreachable`, never as a
394
+ * lane the walk tries and fails in 0 s (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §4).
395
+ * Absent ⇒ exactly the behaviour before this existed.
396
+ */
397
+ requester?: "mcp";
398
+ /**
399
+ * The mode the caller will run lanes in, so each lane's budget comes from runs of the SAME mode
400
+ * (`attemptBudget`). Absent ⇒ the mode-less legacy window, as before this existed.
401
+ */
402
+ mode?: DispatchMode;
403
+ /**
404
+ * A routing spec (`deepseek/deepseek-flash`, `pool/high`, …) to run as its OWN one-lane view
405
+ * instead of the ladder. Validated against the configured providers and pools: an unknown spec
406
+ * yields no lane and a reason. `dispatch` could not name a model before 2026-09-10, so agents that
407
+ * had to use DeepSeek wrote their own HTTP calls to the relay
408
+ * (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §7).
409
+ */
410
+ model?: string;
331
411
  }
332
412
  /**
333
413
  * Report a rung spent (quota gone, rate-limited, CLI missing). Rungs sharing a `quota` bucket
@@ -339,6 +419,25 @@ export interface DispatchOptions {
339
419
  export declare function markExhausted(cfg: Config, id: string, ttlMs?: number, tier?: string): boolean;
340
420
  /** Clear one rung's cooldown, or every cooldown for this config when no id is given. */
341
421
  export declare function clearExhausted(cfg: Config, id?: string, tier?: string): void;
422
+ /**
423
+ * What a tier-scoped ladder lookup found: the tier the ladder resolved to (`null` for the legacy
424
+ * single ladder), the rung when the id names one ON THAT LADDER, and — when `routing.ladders`
425
+ * exists but holds no ladder under the requested name — that name, so a caller can say so.
426
+ */
427
+ export interface LadderRungLookup {
428
+ tier: string | null;
429
+ rung: LadderRung | undefined;
430
+ missingTier: string | undefined;
431
+ }
432
+ /**
433
+ * The tier-scoped rung lookup — the ONE resolution `markExhausted`, `clearExhausted` and the
434
+ * operator pin in `routes/admin.ts` share. It answers with the SAME `(tier, rung)` pair
435
+ * `buildDispatch` annotates a view with (`selectLadder` on both sides), which is what makes a
436
+ * pin recorded through it land where the next `GET /dispatch` reads. ⚠ Distinct from
437
+ * `findLadderRung`, which is tier-AGNOSTIC (a telemetry report names a lane, not a tier); a
438
+ * memory or a cooldown written here is keyed by tier, so the lookup must be too.
439
+ */
440
+ export declare function lookupLadderRung(cfg: Config, laneId: unknown, tier?: string): LadderRungLookup;
342
441
  /**
343
442
  * One exported/persisted cooldown row: the raw map key (`rung:<id>` / `quota:<name>`) and its
344
443
  * absolute expiry in epoch ms. The KEY travels, not the rung, because a bucket outlives any one
@@ -408,6 +507,18 @@ export declare const CONTEXT_TOKEN = "{contextWindow}";
408
507
  export declare function specContextWindow(spec: string, cfg: Config, published: (spec: string) => ResolvedContextWindow | null): (ResolvedContextWindow & {
409
508
  unknownMembers: number;
410
509
  }) | null;
510
+ /**
511
+ * Does `spec` reach only the caller's own pass-through vendor (`reachableWithoutRelay`)? Exported
512
+ * for the MCP server, which filters such a lane out of a walk itself when its view came from a
513
+ * daemon older than `requester=mcp` — a version-skew guard, because the daemon started at logon
514
+ * routinely runs older code than a freshly spawned MCP process.
515
+ */
516
+ export declare function isPassThroughSpec(spec: string, cfg: Config): boolean;
517
+ /**
518
+ * Why the MCP server cannot run a pass-through rung — the ONE wording, used for the `unreachable`
519
+ * verdict here and by the MCP server's own fallback text, so the two never disagree.
520
+ */
521
+ export declare function mcpPassThroughReason(spec: string): string;
411
522
  /**
412
523
  * Resolve command names whose Windows shell semantics differ from their POSIX spelling.
413
524
  *
@@ -418,6 +529,28 @@ export declare function specContextWindow(spec: string, cfg: Config, published:
418
529
  * deliberately narrow: explicit paths and every other configured command remain authoritative.
419
530
  */
420
531
  export declare function normalizeCliCommand(command: string, platform?: NodeJS.Platform): string;
532
+ /**
533
+ * Where the windowless-console launcher lives, if the operator has installed one.
534
+ *
535
+ * Not shipped by this package or by any of its installers (verified: no `lane-launch.ps1` exists
536
+ * anywhere under this repository) — it is a hand-maintained artifact documented in CLAUDE.md's AGY
537
+ * lane notes and `docs/agy-popup-fix-2026-09-07.md`. So absence is the ORDINARY case on a fresh
538
+ * machine and on every non-Windows host, not a misconfiguration to warn about. Resolved through the
539
+ * same config-kind XDG base every other operator-authored artifact under this directory uses
540
+ * (`state-paths.ts`), so an XDG override on the config side moves this alongside `config.json`
541
+ * itself — matching where every hand-authored `cli` ladder rung already points its own `-File`
542
+ * argument. (`state-paths.ts` stays the one module naming the XDG variables directly, per
543
+ * `test/state-paths.test.ts` — this reaches them only through `relayStatePath`.)
544
+ *
545
+ * Deliberately the ONE impure seam in this module (the `buildDispatch`/`platform` precedent): every
546
+ * function below it stays pure over a caller-resolved `launcherPath: string | null`. ⚠ Guarded like
547
+ * `winenv.ts`/`os-keyring.ts`/`lane-runner.ts`'s default spawner: under vitest, a caller that omits
548
+ * `exists` gets `null` unconditionally rather than the real filesystem — a suite must never depend
549
+ * on whether THIS machine happens to have the launcher installed (it does), which is exactly what
550
+ * broke `routes/admin.ts` and `cli.ts`'s own call sites (neither injects a seam) before this guard
551
+ * existed. A test that wants the real check injects `exists` itself, same as every seam above it.
552
+ */
553
+ export declare function resolveLaneLauncherPath(platform?: NodeJS.Platform, exists?: (path: string) => boolean): string | null;
421
554
  /**
422
555
  * Every rung across EVERY ladder the config declares — each tiered ladder, then the legacy
423
556
  * single ladder. The ONE ladder walk: `POST /dispatch/telemetry` (unknown-lane 400) and the
@@ -433,7 +566,16 @@ export declare function allLadderRungs(cfg: Config): LadderRung[];
433
566
  * shared lookup both branches mean — one walk, not two.
434
567
  */
435
568
  export declare function findLadderRung(cfg: Config, laneId: string): LadderRung | undefined;
436
- export declare function buildDispatch(cfg: Config, rawOpts?: DispatchOptions, platform?: NodeJS.Platform): DispatchView;
569
+ /**
570
+ * Render a caller-supplied id for a `reason` string. The reason is reflected back verbatim in the
571
+ * JSON response AND printed to a terminal by `llm-relay dispatch`, so echoing raw caller input let
572
+ * a `?lane=` carrying ESC sequences rewrite the operator's terminal, and an arbitrarily long one
573
+ * bloat a response about a lane that does not exist. Control characters go, and the echo is capped
574
+ * — enough to recognise your own typo, not a channel. Exported for `routes/admin.ts`, whose
575
+ * `POST /dispatch` 400s echo a caller-supplied lane id or tier under the same rule.
576
+ */
577
+ export declare function describeId(id: string): string;
578
+ export declare function buildDispatch(cfg: Config, rawOpts?: DispatchOptions, platform?: NodeJS.Platform, launcherPath?: string | null): DispatchView;
437
579
  export declare const AUTO_TIERS: readonly ["low", "medium", "high", "xhigh"];
438
580
  export type AutoTier = (typeof AUTO_TIERS)[number];
439
581
  export declare function normalizeAutoTier(tier?: string | null): string;