llm-relay 0.67.0 → 0.68.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 +14 -0
- package/dist/cli.js +118 -5
- package/dist/cli.js.map +1 -1
- package/dist/hedge-trigger.js +3 -2
- package/dist/hedge-trigger.js.map +1 -1
- package/dist/mcp/lane-runner.d.ts +19 -1
- package/dist/mcp/lane-runner.js +78 -2
- package/dist/mcp/lane-runner.js.map +1 -1
- package/dist/mcp/server.d.ts +3 -1
- package/dist/mcp/server.js +25 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/llm-relay/SKILL.md +8 -0
package/dist/cli.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
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
|
+
import type { DispatchedQuotaReport } from "./mcp/lane-runner.js";
|
|
5
6
|
import { ModelCatalog } from "./catalog.js";
|
|
6
7
|
import { type CommandEffect } from "./self-update.js";
|
|
7
8
|
import { type KeysCliDependencies } from "./keys-cli.js";
|
|
@@ -130,6 +131,17 @@ export declare function runKeysEnable(credential?: string | undefined, deps?: Ke
|
|
|
130
131
|
export declare function runKeysExport(deps?: KeysCliDependencies): Promise<void>;
|
|
131
132
|
export declare function runKeysImport(importPath?: string | undefined, deps?: KeysCliDependencies): Promise<void>;
|
|
132
133
|
export declare function runKeysUnlock(deps?: KeysCliDependencies): Promise<void>;
|
|
134
|
+
export interface TelemetryDeps {
|
|
135
|
+
readonly loadConfig?: () => Config;
|
|
136
|
+
readonly request?: (cfg: Config) => Promise<unknown | null>;
|
|
137
|
+
readonly localReport?: (cfg: Config) => unknown;
|
|
138
|
+
readonly write?: (text: string) => void;
|
|
139
|
+
}
|
|
140
|
+
/** Print telemetry from the live relay when available, falling back to the local snapshot. */
|
|
141
|
+
export declare function runTelemetry(deps?: TelemetryDeps): Promise<void>;
|
|
142
|
+
export type DispatchReportRequest = (cfg: Config, path: string, init: RequestInit) => Promise<unknown | null>;
|
|
143
|
+
/** Forward positive MCP lane quota evidence to the live relay's authenticated control route. */
|
|
144
|
+
export declare function reportMcpExhaustion(cfg: Config, report: DispatchedQuotaReport, request?: DispatchReportRequest): Promise<void>;
|
|
133
145
|
/** Render a normalized listener address as an HTTP URL, including required IPv6 brackets. */
|
|
134
146
|
export declare function proxyUrl(cfg: Pick<Config, "host" | "port">, path: string): string;
|
|
135
147
|
export declare const CLI_COMMAND_NAMES: ReadonlySet<string>;
|
|
@@ -159,6 +171,8 @@ export declare const CLI_COMMAND_NAMES: ReadonlySet<string>;
|
|
|
159
171
|
export declare const ARITY_EXEMPT: ReadonlySet<string>;
|
|
160
172
|
/** The commands this guard bounds — exported for the drift test, not for dispatch. */
|
|
161
173
|
export declare const ARITY_GUARDED_COMMANDS: readonly string[];
|
|
174
|
+
/** Return a bounded, value-free diagnostic for an option not accepted by this command/action. */
|
|
175
|
+
export declare function commandOptionError(argv: readonly string[]): string | null;
|
|
162
176
|
export declare function commandArityError(positionals: readonly string[]): string | null;
|
|
163
177
|
/** Clear process-local routing cooldowns through the protected live control plane only. */
|
|
164
178
|
export declare function runCooldowns(_action: string | undefined, _spec: string | undefined): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -176,6 +176,7 @@ ${formatTextTable([
|
|
|
176
176
|
["llm-relay keys | keys check | check-keys", "Check every configured credential slot."],
|
|
177
177
|
["llm-relay keys <action> ...", "action: add|list|rotate|revoke|remove|disable|enable|export|import|unlock."],
|
|
178
178
|
["llm-relay pools [--probe]", "List members; --probe tests each deployment once."],
|
|
179
|
+
["llm-relay pools list | pools <name> | pools show <name> [--json]", "List pool names or one pool's effective members."],
|
|
179
180
|
["llm-relay pools <action> <name> [<spec>...]", "action: set|add|remove|delete."],
|
|
180
181
|
["llm-relay routing <action> ...", "action: show|get|default|tier|subagent|sort|benchmark|set|unset|answered."],
|
|
181
182
|
["llm-relay route <action> ...", "Alias for routing."],
|
|
@@ -191,7 +192,7 @@ ${formatTextTable([
|
|
|
191
192
|
["llm-relay candidates [-p <name>]", "Compare deployment x credential-slot targets."],
|
|
192
193
|
["llm-relay cost [--window <w>] [--by <d>] [--include-repair]", "Summarise spend from the local accounting ledger."],
|
|
193
194
|
["llm-relay eligibility", "What backends said about themselves; refusals awaiting interpretation."],
|
|
194
|
-
["llm-relay eligibility <propose|accept> <n> [--sig <digest>] --class <kind> --scope <scope>", "Scopes: attempt | group | deployment | credential | provider | model. --sig pins the item across queue reorderings."],
|
|
195
|
+
["llm-relay eligibility <propose|accept|reject> <n> [--sig <digest>] --class <kind> --scope <scope>", "Scopes: attempt | group | deployment | credential | provider | model. --sig pins the item across queue reorderings."],
|
|
195
196
|
["llm-relay eligibility ... --scope group --members <id,id,...> [--all-credentials]", "Groups default to the current credential slot; the flag explicitly widens them."],
|
|
196
197
|
["llm-relay eligibility ... --cost-class free|paid|unknown", "Cover only deployments of that cost class, resolved live from catalog prices."],
|
|
197
198
|
["eligibility scope breadth", "credential = current credential slot; provider = all credentials for that provider."],
|
|
@@ -800,6 +801,26 @@ async function tryServer(cfg, path, init) {
|
|
|
800
801
|
return null;
|
|
801
802
|
}
|
|
802
803
|
}
|
|
804
|
+
export async function runTelemetry(deps = {}) {
|
|
805
|
+
const cfg = (deps.loadConfig ?? loadOrExit)();
|
|
806
|
+
const live = await (deps.request ?? ((config) => tryServer(config, "/telemetry")))(cfg);
|
|
807
|
+
const report = live ?? (deps.localReport ? deps.localReport(cfg) : getTelemetryReport(cfg, globalCircuitBreaker));
|
|
808
|
+
(deps.write ?? ((text) => process.stdout.write(text)))(`${JSON.stringify(report, null, 2)}\n`);
|
|
809
|
+
}
|
|
810
|
+
export async function reportMcpExhaustion(cfg, report, request = tryServer) {
|
|
811
|
+
const live = await request(cfg, "/dispatch", {
|
|
812
|
+
method: "POST",
|
|
813
|
+
headers: { "content-type": "application/json" },
|
|
814
|
+
body: JSON.stringify({
|
|
815
|
+
exhausted: report.laneId,
|
|
816
|
+
...(report.tier ? { tier: report.tier } : {}),
|
|
817
|
+
outcome: report.outcome,
|
|
818
|
+
...(report.retryAfterMs !== undefined ? { retryAfterMs: report.retryAfterMs } : {}),
|
|
819
|
+
}),
|
|
820
|
+
});
|
|
821
|
+
if (live === null)
|
|
822
|
+
throw new Error("no proxy running — quota report not recorded");
|
|
823
|
+
}
|
|
803
824
|
export function proxyUrl(cfg, path) {
|
|
804
825
|
const host = cfg.host.includes(":") ? `[${cfg.host}]` : cfg.host;
|
|
805
826
|
return `http://${host}:${cfg.port}${path}`;
|
|
@@ -846,6 +867,69 @@ const COMMAND_ARITY = {
|
|
|
846
867
|
};
|
|
847
868
|
export const ARITY_EXEMPT = new Set(["keys", "cooldowns", "help", "version"]);
|
|
848
869
|
export const ARITY_GUARDED_COMMANDS = Object.keys(COMMAND_ARITY);
|
|
870
|
+
const CLI_OPTIONS = {
|
|
871
|
+
proxy: ["--config", "--default", "--mode", "--listen", "--ping"],
|
|
872
|
+
onboard: ["--config", "--import", "--force"],
|
|
873
|
+
setup: [],
|
|
874
|
+
"check-keys": ["--config"],
|
|
875
|
+
models: ["--config", "--provider", "--refresh"],
|
|
876
|
+
ping: ["--config", "--provider", "--ping"],
|
|
877
|
+
dashboard: ["--config"],
|
|
878
|
+
telemetry: ["--config"],
|
|
879
|
+
offload: ["--config", "--scope"],
|
|
880
|
+
lanes: ["--config", "--probe"],
|
|
881
|
+
dispatch: ["--config", "--task", "--exhausted", "--outcome", "--retry-after-ms", "--after", "--lane", "--tier", "--client", "--host", "--shell", "--next-command", "--json"],
|
|
882
|
+
eligibility: ["--config", "--sig", "--class", "--rationale", "--scope", "--members", "--all-credentials", "--reset-field", "--reset-ms", "--cost-class"],
|
|
883
|
+
candidates: ["--config", "--provider"],
|
|
884
|
+
cost: ["--window", "--by", "--include-repair", "--json"],
|
|
885
|
+
pools: ["--config", "--probe", "--json"],
|
|
886
|
+
routing: ["--config", "--clear"],
|
|
887
|
+
route: ["--config", "--clear"],
|
|
888
|
+
config: ["--config"],
|
|
889
|
+
"delegate-gate": ["--repo", "--fix"],
|
|
890
|
+
mcp: ["--config"],
|
|
891
|
+
};
|
|
892
|
+
const ACTION_OPTIONS = {
|
|
893
|
+
offload: { on: ["--config", "--scope"], off: ["--config", "--scope"], enable: ["--config", "--scope"], disable: ["--config", "--scope"], status: ["--config"] },
|
|
894
|
+
pools: {
|
|
895
|
+
list: ["--config", "--json", "--probe"], show: ["--config", "--json"],
|
|
896
|
+
set: ["--config", "--include", "--free", "--effort"], add: ["--config", "--include", "--free", "--effort"],
|
|
897
|
+
remove: ["--config", "--include", "--free", "--effort"], delete: ["--config"], rm: ["--config"],
|
|
898
|
+
},
|
|
899
|
+
eligibility: {
|
|
900
|
+
status: ["--config"], reject: ["--config", "--sig"],
|
|
901
|
+
propose: ["--config", "--sig", "--class", "--rationale", "--scope", "--members", "--all-credentials", "--reset-field", "--reset-ms", "--cost-class"],
|
|
902
|
+
accept: ["--config", "--sig", "--class", "--scope", "--members", "--all-credentials", "--reset-field", "--reset-ms", "--cost-class"],
|
|
903
|
+
},
|
|
904
|
+
routing: {
|
|
905
|
+
tier: ["--config", "--clear"], subagent: ["--config", "--clear"],
|
|
906
|
+
show: ["--config"], get: ["--config"], default: ["--config"], sort: ["--config"],
|
|
907
|
+
benchmark: ["--config"], set: ["--config"], unset: ["--config"], answered: ["--config"],
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
export function commandOptionError(argv) {
|
|
911
|
+
const hasHelp = argv.slice(2).some((a) => a === "--help" || a === "-h" || a === "--version" || a === "-v");
|
|
912
|
+
if (hasHelp)
|
|
913
|
+
return null;
|
|
914
|
+
const positionals = getPositionalArgs([...argv]);
|
|
915
|
+
const command = positionals[0] ?? "proxy";
|
|
916
|
+
if (command === "keys" || command === "cooldowns" || command === "help" || command === "version")
|
|
917
|
+
return null;
|
|
918
|
+
const base = CLI_OPTIONS[command];
|
|
919
|
+
if (!base)
|
|
920
|
+
return null;
|
|
921
|
+
const action = positionals[1];
|
|
922
|
+
const actionOptions = ACTION_OPTIONS[command];
|
|
923
|
+
const allowed = actionOptions
|
|
924
|
+
? actionOptions[action ?? ""] ?? (action === undefined ? base : ["--config", "--json"])
|
|
925
|
+
: base;
|
|
926
|
+
const aliases = expandFlagAliases([...allowed]);
|
|
927
|
+
for (const flag of parseCliArgs([...argv]).flags) {
|
|
928
|
+
if (!aliases.has(flag))
|
|
929
|
+
return `llm-relay ${command}: unsupported option`;
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
849
933
|
export function commandArityError(positionals) {
|
|
850
934
|
const name = positionals[0];
|
|
851
935
|
if (name === undefined)
|
|
@@ -1273,6 +1357,18 @@ function writeAbandonedShare(report, write) {
|
|
|
1273
1357
|
`${formatTextTable(rows)}\n` +
|
|
1274
1358
|
`The totals above are what the answers you received cost. Add this table for what the requests cost.\n`);
|
|
1275
1359
|
}
|
|
1360
|
+
function writeCoveredPeriod(from, to, generatedAt, write) {
|
|
1361
|
+
write(from === null ? `Covering everything up to ${to} (UTC).\n` : `Covering ${from} → ${to} (UTC).\n`);
|
|
1362
|
+
const end = Date.parse(to);
|
|
1363
|
+
const now = Date.parse(generatedAt);
|
|
1364
|
+
if (!Number.isFinite(end) || !Number.isFinite(now))
|
|
1365
|
+
return;
|
|
1366
|
+
const minutes = Math.floor((now - end) / 60_000);
|
|
1367
|
+
if (minutes < 1)
|
|
1368
|
+
return;
|
|
1369
|
+
write(`The window ends on a bucket boundary, so the most recent ${minutes} ` +
|
|
1370
|
+
`${minutes === 1 ? "minute is" : "minutes are"} not counted.\n`);
|
|
1371
|
+
}
|
|
1276
1372
|
function renderCostReport(report, write) {
|
|
1277
1373
|
if (report.coverage === "empty") {
|
|
1278
1374
|
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");
|
|
@@ -1285,6 +1381,7 @@ function renderCostReport(report, write) {
|
|
|
1285
1381
|
const rows = costTableRows(report);
|
|
1286
1382
|
const repairDeclinedByWindow = report.includeRepair && report.repair === null && report.window === "lifetime";
|
|
1287
1383
|
write(`llm-relay cost — ${report.window}${report.includeRepair && !repairDeclinedByWindow ? ", repair included" : ""}\n`);
|
|
1384
|
+
writeCoveredPeriod(report.from, report.to, report.generatedAt, write);
|
|
1288
1385
|
if (repairDeclinedByWindow) {
|
|
1289
1386
|
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");
|
|
1290
1387
|
}
|
|
@@ -1491,6 +1588,7 @@ export async function runMcp() {
|
|
|
1491
1588
|
buildView: (o) => resolveDispatchView({ ...o, cfg }),
|
|
1492
1589
|
...(allowedRoots ? { allowedRoots } : {}),
|
|
1493
1590
|
version: currentVersion(),
|
|
1591
|
+
reportExhaustion: (report) => reportMcpExhaustion(cfg, report),
|
|
1494
1592
|
write: (chunk) => process.stdout.write(chunk),
|
|
1495
1593
|
});
|
|
1496
1594
|
const stop = () => {
|
|
@@ -2654,6 +2752,12 @@ export function main() {
|
|
|
2654
2752
|
process.stdout.write(`${currentVersion()}\n`);
|
|
2655
2753
|
process.exit(0);
|
|
2656
2754
|
}
|
|
2755
|
+
const optionError = commandOptionError(process.argv);
|
|
2756
|
+
if (optionError !== null) {
|
|
2757
|
+
process.stderr.write(`${optionError}\n`);
|
|
2758
|
+
process.exit(1);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2657
2761
|
const arityError = commandArityError(positionals);
|
|
2658
2762
|
if (arityError !== null) {
|
|
2659
2763
|
process.stderr.write(`${arityError}\n`);
|
|
@@ -2698,9 +2802,10 @@ export function main() {
|
|
|
2698
2802
|
return;
|
|
2699
2803
|
}
|
|
2700
2804
|
if (arg2 === "telemetry") {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2805
|
+
void runTelemetry().catch((e) => {
|
|
2806
|
+
process.stderr.write(`llm-relay telemetry: ${e.message}\n`);
|
|
2807
|
+
process.exit(1);
|
|
2808
|
+
});
|
|
2704
2809
|
return;
|
|
2705
2810
|
}
|
|
2706
2811
|
if (arg2 === "check-keys") {
|
|
@@ -2917,14 +3022,22 @@ export function classifyCommand(argv) {
|
|
|
2917
3022
|
case "dashboard":
|
|
2918
3023
|
return "read-only";
|
|
2919
3024
|
case "delegate-gate":
|
|
2920
|
-
return "read-only";
|
|
3025
|
+
return argv.some((a) => a === "--fix" || a === "-fix") ? "mutating" : "read-only";
|
|
2921
3026
|
case "mcp":
|
|
2922
3027
|
return "read-only";
|
|
3028
|
+
case "dispatch":
|
|
3029
|
+
return argv.some((a) => a === "--exhausted" || a === "-x") ? "mutating" : "read-only";
|
|
2923
3030
|
default:
|
|
2924
3031
|
return "read-only";
|
|
2925
3032
|
}
|
|
2926
3033
|
}
|
|
2927
3034
|
export async function run() {
|
|
3035
|
+
const optionError = commandOptionError(process.argv);
|
|
3036
|
+
if (optionError !== null) {
|
|
3037
|
+
process.stderr.write(`${optionError}\n`);
|
|
3038
|
+
process.exit(1);
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
2928
3041
|
const command = rawCliCommand(process.argv);
|
|
2929
3042
|
if (command === "cooldowns")
|
|
2930
3043
|
parseCooldownClearArgs(process.argv);
|