llm-relay 0.71.1 → 0.72.1
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/cli.d.ts +22 -0
- package/dist/cli.js +93 -22
- package/dist/cli.js.map +1 -1
- package/dist/delegate-gate/test-assertions.js +27 -6
- package/dist/delegate-gate/test-assertions.js.map +1 -1
- package/dist/dispatch-lane-stats.d.ts +133 -0
- package/dist/dispatch-lane-stats.js +274 -0
- package/dist/dispatch-lane-stats.js.map +1 -0
- package/dist/dispatch.d.ts +49 -1
- package/dist/dispatch.js +29 -0
- package/dist/dispatch.js.map +1 -1
- package/dist/mcp/lane-runner.d.ts +20 -1
- package/dist/mcp/lane-runner.js +11 -1
- package/dist/mcp/lane-runner.js.map +1 -1
- package/dist/mcp/server.d.ts +46 -1
- package/dist/mcp/server.js +60 -6
- package/dist/mcp/server.js.map +1 -1
- package/dist/routes/admin.d.ts +8 -0
- package/dist/routes/admin.js +73 -1
- package/dist/routes/admin.js.map +1 -1
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { splitSpec, type Config } from "./config.js";
|
|
|
3
3
|
import { type Candidate, type CandidateAvailability } from "./candidates.js";
|
|
4
4
|
import { type DispatchView } from "./dispatch.js";
|
|
5
5
|
import type { DispatchedQuotaReport } from "./mcp/lane-runner.js";
|
|
6
|
+
import type { DispatchedTelemetryReport } from "./dispatch-lane-stats.js";
|
|
6
7
|
import { ModelCatalog } from "./catalog.js";
|
|
7
8
|
import { type CommandEffect } from "./self-update.js";
|
|
8
9
|
import { type KeysCliDependencies } from "./keys-cli.js";
|
|
@@ -154,6 +155,14 @@ export declare function runTelemetry(deps?: TelemetryDeps): Promise<void>;
|
|
|
154
155
|
export type DispatchReportRequest = (cfg: Config, path: string, init: RequestInit) => Promise<unknown | null>;
|
|
155
156
|
/** Forward positive MCP lane quota evidence to the live relay's authenticated control route. */
|
|
156
157
|
export declare function reportMcpExhaustion(cfg: Config, report: DispatchedQuotaReport, request?: DispatchReportRequest): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Forward one MCP lane-execution report to the live relay's `POST /dispatch/telemetry`.
|
|
160
|
+
*
|
|
161
|
+
* ⚠ Asymmetry with `reportMcpExhaustion` is deliberate: it throws because a lost quota
|
|
162
|
+
* report changes routing; a lost telemetry row changes only a ledger, so this is
|
|
163
|
+
* best-effort — false when no proxy answered, and it NEVER throws.
|
|
164
|
+
*/
|
|
165
|
+
export declare function reportMcpTelemetry(cfg: Config, report: DispatchedTelemetryReport, request?: DispatchReportRequest): Promise<boolean>;
|
|
157
166
|
/** Render a normalized listener address as an HTTP URL, including required IPv6 brackets. */
|
|
158
167
|
export declare function proxyUrl(cfg: Pick<Config, "host" | "port">, path: string): string;
|
|
159
168
|
export declare const CLI_COMMAND_NAMES: ReadonlySet<string>;
|
|
@@ -227,6 +236,12 @@ export interface CostCommandDependencies {
|
|
|
227
236
|
readonly exit?: (code: number) => never;
|
|
228
237
|
/** Overrides the store directory; defaults to the production `~/.llm-relay/usage/`. */
|
|
229
238
|
readonly usageDir?: string;
|
|
239
|
+
/**
|
|
240
|
+
* Loads the relay config for the `--by model` lane-id footnote. Defaults to the live
|
|
241
|
+
* `loadOrExit`; tests inject a config holding the ladder under test. Read ONLY when
|
|
242
|
+
* `--by model` is asked — every other dimension must keep working config-less.
|
|
243
|
+
*/
|
|
244
|
+
readonly loadConfig?: () => Config;
|
|
230
245
|
}
|
|
231
246
|
/**
|
|
232
247
|
* `llm-relay cost` — the C1 roll-up (open-decisions-2026-08-16.md C1): what did this
|
|
@@ -287,6 +302,13 @@ export type WireDispatchView = Omit<DispatchView, "host"> & {
|
|
|
287
302
|
};
|
|
288
303
|
/** Normalize structured output from an older live proxy before exposing it to this host. */
|
|
289
304
|
export declare function normalizeDispatchCommands(view: WireDispatchView, platform?: NodeJS.Platform): DispatchView;
|
|
305
|
+
/**
|
|
306
|
+
* Substitute `{task}` placeholder into lane arguments and set `view.task`.
|
|
307
|
+
* Applied locally so dispatch view queries never need to send arbitrary task text in HTTP query strings.
|
|
308
|
+
*/
|
|
309
|
+
export declare function substituteTaskInView(view: DispatchView, task: string | undefined): DispatchView;
|
|
310
|
+
/** Safely reload configuration from disk without exiting if unreadable. */
|
|
311
|
+
export declare function loadConfigSafely(): Config | null;
|
|
290
312
|
/** Which shell's literal-quoting rules a rendered command line is written for. */
|
|
291
313
|
export type RenderShell = "sh" | "pwsh";
|
|
292
314
|
/** Human name for the shell a line was quoted for. Precise on purpose — see `quoteArg`. */
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { CREDENTIAL_LABEL_PATTERN, makeCredentialId } from "./credential-id.js";
|
|
|
14
14
|
import { providerCredentialSlots, slotAllowsModel } from "./credential-fleet.js";
|
|
15
15
|
import { loadLaneManifest, rosterIsStale, verifyModel } from "./lane-manifest.js";
|
|
16
16
|
import { probeLanes } from "./lane-probe.js";
|
|
17
|
-
import { buildDispatch, normalizeCliCommand, restoreExhaustedRows, specContextWindow, CONTEXT_TOKEN } from "./dispatch.js";
|
|
17
|
+
import { buildDispatch, allLadderRungs, normalizeCliCommand, restoreExhaustedRows, specContextWindow, CONTEXT_TOKEN, TASK_TOKEN, formatLaneStats } from "./dispatch.js";
|
|
18
18
|
import { McpDispatchServer } from "./mcp/server.js";
|
|
19
19
|
import { loadExhaustedRows } from "./dispatch-exhaustion-persistence.js";
|
|
20
20
|
import { detectHostRouting, parseHostRoutingState } from "./host-routing.js";
|
|
@@ -851,6 +851,19 @@ export async function reportMcpExhaustion(cfg, report, request = tryServer) {
|
|
|
851
851
|
if (live === null)
|
|
852
852
|
throw new Error("no proxy running — quota report not recorded");
|
|
853
853
|
}
|
|
854
|
+
export async function reportMcpTelemetry(cfg, report, request = tryServer) {
|
|
855
|
+
try {
|
|
856
|
+
const live = await request(cfg, "/dispatch/telemetry", {
|
|
857
|
+
method: "POST",
|
|
858
|
+
headers: { "content-type": "application/json" },
|
|
859
|
+
body: JSON.stringify(report),
|
|
860
|
+
});
|
|
861
|
+
return live !== null;
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
854
867
|
export function proxyUrl(cfg, path) {
|
|
855
868
|
const host = cfg.host.includes(":") ? `[${cfg.host}]` : cfg.host;
|
|
856
869
|
return `http://${host}:${cfg.port}${path}`;
|
|
@@ -1318,7 +1331,17 @@ export async function runCostCommand(dependencies = {}) {
|
|
|
1318
1331
|
write(JSON.stringify(report, null, 2) + "\n");
|
|
1319
1332
|
return;
|
|
1320
1333
|
}
|
|
1321
|
-
|
|
1334
|
+
let cliLaneIds;
|
|
1335
|
+
if (by === "model") {
|
|
1336
|
+
try {
|
|
1337
|
+
const cfg = (dependencies.loadConfig ?? loadOrExit)();
|
|
1338
|
+
cliLaneIds = new Set(allLadderRungs(cfg).filter((rung) => rung.kind === "cli").map((rung) => rung.id));
|
|
1339
|
+
}
|
|
1340
|
+
catch {
|
|
1341
|
+
cliLaneIds = undefined;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
renderCostReport(report, write, cliLaneIds === undefined ? {} : { cliLaneIds });
|
|
1322
1345
|
}
|
|
1323
1346
|
finally {
|
|
1324
1347
|
store.close();
|
|
@@ -1399,7 +1422,7 @@ function writeCoveredPeriod(from, to, generatedAt, write) {
|
|
|
1399
1422
|
write(`The window ends on a bucket boundary, so the most recent ${minutes} ` +
|
|
1400
1423
|
`${minutes === 1 ? "minute is" : "minutes are"} not counted.\n`);
|
|
1401
1424
|
}
|
|
1402
|
-
function renderCostReport(report, write) {
|
|
1425
|
+
function renderCostReport(report, write, opts = {}) {
|
|
1403
1426
|
if (report.coverage === "empty") {
|
|
1404
1427
|
write("No accounting data yet.\n\nThe relay records per-request usage under ~/.llm-relay/usage/ once it serves\ntraffic through configured providers. Run the proxy, send a request, then retry.\n");
|
|
1405
1428
|
return;
|
|
@@ -1416,6 +1439,17 @@ function renderCostReport(report, write) {
|
|
|
1416
1439
|
write("The lifetime window cannot prove the serve/repair split; use a day-bounded window (--window 1h|24h|7d|30d) for the repair share.\n");
|
|
1417
1440
|
}
|
|
1418
1441
|
write(`${formatTextTable(rows)}\n`);
|
|
1442
|
+
if (report.by === "client" && report.rows.some((row) => row.key === "mcp-dispatch")) {
|
|
1443
|
+
write("mcp-dispatch rows are the estimated dispatch envelope (task text + lane output), not the lane's provider consumption, which the relay cannot see; they are unpriced.\n");
|
|
1444
|
+
}
|
|
1445
|
+
if (report.by === "model" && opts.cliLaneIds !== undefined) {
|
|
1446
|
+
const laneKeys = [...new Set(report.rows
|
|
1447
|
+
.map((row) => row.key)
|
|
1448
|
+
.filter((key) => opts.cliLaneIds.has(key) || opts.cliLaneIds.has(key.slice(key.indexOf("/") + 1))))].sort();
|
|
1449
|
+
if (laneKeys.length > 0) {
|
|
1450
|
+
write(`Rows keyed by cli lane id (${laneKeys.join(", ")}) are the estimated dispatch envelope (task text + lane output), not the lane's provider consumption, which the relay cannot see; they are unpriced.\n`);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1419
1453
|
writeRepairShare(report, write);
|
|
1420
1454
|
writeAbandonedShare(report, write);
|
|
1421
1455
|
if (report.coverage === "partial") {
|
|
@@ -1457,6 +1491,42 @@ export function normalizeDispatchCommands(view, platform = process.platform) {
|
|
|
1457
1491
|
next: view.next ? normalizeLane(view.next) : null,
|
|
1458
1492
|
};
|
|
1459
1493
|
}
|
|
1494
|
+
export function substituteTaskInView(view, task) {
|
|
1495
|
+
if (!task)
|
|
1496
|
+
return view;
|
|
1497
|
+
const substituteArgs = (args) => args.map((a) => a.split(TASK_TOKEN).join(task));
|
|
1498
|
+
const substituteLane = (lane) => {
|
|
1499
|
+
if (!lane.invoke)
|
|
1500
|
+
return lane;
|
|
1501
|
+
return {
|
|
1502
|
+
...lane,
|
|
1503
|
+
invoke: {
|
|
1504
|
+
...lane.invoke,
|
|
1505
|
+
args: substituteArgs(lane.invoke.args),
|
|
1506
|
+
},
|
|
1507
|
+
};
|
|
1508
|
+
};
|
|
1509
|
+
return {
|
|
1510
|
+
...view,
|
|
1511
|
+
task,
|
|
1512
|
+
ladder: view.ladder.map(substituteLane),
|
|
1513
|
+
next: view.next ? substituteLane(view.next) : null,
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
export function loadConfigSafely() {
|
|
1517
|
+
try {
|
|
1518
|
+
const configPath = resolveConfigPath();
|
|
1519
|
+
const overrides = {
|
|
1520
|
+
routeDefault: argValue("--default", "-d"),
|
|
1521
|
+
mode: argValue("--mode", "-m"),
|
|
1522
|
+
listen: argValue("--listen", "-l"),
|
|
1523
|
+
};
|
|
1524
|
+
return loadConfig(configPath, overrides);
|
|
1525
|
+
}
|
|
1526
|
+
catch {
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1460
1530
|
export const SHELL_LABEL = {
|
|
1461
1531
|
sh: "sh/bash",
|
|
1462
1532
|
pwsh: "PowerShell 7+ (pwsh)",
|
|
@@ -1580,8 +1650,6 @@ ${lane} — ${entry.models.length} models, probed ${entry.probedAt} via \`${entr
|
|
|
1580
1650
|
export async function resolveDispatchView(opts) {
|
|
1581
1651
|
const cfg = opts.cfg ?? loadOrExit();
|
|
1582
1652
|
const qs = new URLSearchParams();
|
|
1583
|
-
if (opts.task)
|
|
1584
|
-
qs.set("task", opts.task);
|
|
1585
1653
|
if (opts.lane)
|
|
1586
1654
|
qs.set("lane", opts.lane);
|
|
1587
1655
|
if (opts.tier)
|
|
@@ -1599,16 +1667,20 @@ export async function resolveDispatchView(opts) {
|
|
|
1599
1667
|
const liveRaw = (await tryServer(cfg, `/dispatch?${qs}`));
|
|
1600
1668
|
const live = liveRaw !== null && liveRaw.host === "bypassed" ? liveRaw : null;
|
|
1601
1669
|
restoreExhaustedRows(cfg, loadExhaustedRows());
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1670
|
+
if (live !== null) {
|
|
1671
|
+
const view = normalizeDispatchCommands(live);
|
|
1672
|
+
return substituteTaskInView({ ...view, source: "daemon" }, opts.task);
|
|
1673
|
+
}
|
|
1674
|
+
const fallbackCfg = loadConfigSafely() ?? cfg;
|
|
1675
|
+
const fallbackView = normalizeDispatchCommands(buildDispatch(fallbackCfg, {
|
|
1676
|
+
...(opts.lane ? { lane: opts.lane } : {}),
|
|
1677
|
+
...(opts.tier ? { tier: opts.tier } : {}),
|
|
1678
|
+
...(opts.client ? { client: opts.client } : {}),
|
|
1679
|
+
host: "bypassed",
|
|
1680
|
+
publishedContextWindow: cachedContextWindow,
|
|
1681
|
+
manifest: loadLaneManifest(),
|
|
1682
|
+
}));
|
|
1683
|
+
return substituteTaskInView({ ...fallbackView, source: "local-fallback" }, opts.task);
|
|
1612
1684
|
}
|
|
1613
1685
|
export async function runMcp() {
|
|
1614
1686
|
const cfg = loadOrExit();
|
|
@@ -1619,6 +1691,7 @@ export async function runMcp() {
|
|
|
1619
1691
|
...(allowedRoots ? { allowedRoots } : {}),
|
|
1620
1692
|
version: currentVersion(),
|
|
1621
1693
|
reportExhaustion: (report) => reportMcpExhaustion(cfg, report),
|
|
1694
|
+
reportTelemetry: (report) => { void reportMcpTelemetry(cfg, report); },
|
|
1622
1695
|
write: (chunk) => process.stdout.write(chunk),
|
|
1623
1696
|
});
|
|
1624
1697
|
const stop = () => {
|
|
@@ -1630,9 +1703,7 @@ export async function runMcp() {
|
|
|
1630
1703
|
process.stdin.on("end", stop);
|
|
1631
1704
|
process.stdin.on("close", stop);
|
|
1632
1705
|
process.stdin.setEncoding("utf8");
|
|
1633
|
-
|
|
1634
|
-
await server.ingest(chunk);
|
|
1635
|
-
}
|
|
1706
|
+
await server.serve(process.stdin);
|
|
1636
1707
|
server.shutdown();
|
|
1637
1708
|
}
|
|
1638
1709
|
async function recordSpentDispatchLane(cfg, spent, options) {
|
|
@@ -1693,8 +1764,6 @@ export async function runDispatch(arg) {
|
|
|
1693
1764
|
await recordSpentDispatchLane(cfg, spent, { tier, client, outcome, retryAfterMs });
|
|
1694
1765
|
}
|
|
1695
1766
|
const qs = new URLSearchParams();
|
|
1696
|
-
if (task)
|
|
1697
|
-
qs.set("task", task);
|
|
1698
1767
|
if (lane)
|
|
1699
1768
|
qs.set("lane", lane);
|
|
1700
1769
|
if (after)
|
|
@@ -1725,9 +1794,8 @@ export async function runDispatch(arg) {
|
|
|
1725
1794
|
const staleProxy = hostStale || windowStale;
|
|
1726
1795
|
const live = staleProxy ? null : liveRaw;
|
|
1727
1796
|
restoreExhaustedRows(cfg, loadExhaustedRows());
|
|
1728
|
-
const
|
|
1797
|
+
const rawView = normalizeDispatchCommands(live ??
|
|
1729
1798
|
buildDispatch(cfg, {
|
|
1730
|
-
...(task ? { task } : {}),
|
|
1731
1799
|
...(lane ? { lane } : {}),
|
|
1732
1800
|
...(after ? { after } : {}),
|
|
1733
1801
|
...(tier ? { tier } : {}),
|
|
@@ -1737,6 +1805,7 @@ export async function runDispatch(arg) {
|
|
|
1737
1805
|
publishedContextWindow: cachedContextWindow,
|
|
1738
1806
|
manifest: loadLaneManifest(),
|
|
1739
1807
|
}));
|
|
1808
|
+
const view = substituteTaskInView(rawView, task);
|
|
1740
1809
|
if (hasFlag("--json")) {
|
|
1741
1810
|
process.stdout.write(JSON.stringify(view, null, 2) + "\n");
|
|
1742
1811
|
return;
|
|
@@ -1799,6 +1868,8 @@ export async function runDispatch(arg) {
|
|
|
1799
1868
|
}
|
|
1800
1869
|
if (l.note)
|
|
1801
1870
|
process.stdout.write(` note: ${l.note}\n`);
|
|
1871
|
+
if (l.stats)
|
|
1872
|
+
process.stdout.write(` ${formatLaneStats(l.stats)}\n`);
|
|
1802
1873
|
}
|
|
1803
1874
|
if (renderedCli) {
|
|
1804
1875
|
process.stdout.write(`\ncli lines are quoted for ${SHELL_LABEL[shell]} — the task is a single argument (--shell to change)\n`);
|