llm-relay 0.68.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 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)
@@ -1504,6 +1588,7 @@ export async function runMcp() {
1504
1588
  buildView: (o) => resolveDispatchView({ ...o, cfg }),
1505
1589
  ...(allowedRoots ? { allowedRoots } : {}),
1506
1590
  version: currentVersion(),
1591
+ reportExhaustion: (report) => reportMcpExhaustion(cfg, report),
1507
1592
  write: (chunk) => process.stdout.write(chunk),
1508
1593
  });
1509
1594
  const stop = () => {
@@ -2667,6 +2752,12 @@ export function main() {
2667
2752
  process.stdout.write(`${currentVersion()}\n`);
2668
2753
  process.exit(0);
2669
2754
  }
2755
+ const optionError = commandOptionError(process.argv);
2756
+ if (optionError !== null) {
2757
+ process.stderr.write(`${optionError}\n`);
2758
+ process.exit(1);
2759
+ return;
2760
+ }
2670
2761
  const arityError = commandArityError(positionals);
2671
2762
  if (arityError !== null) {
2672
2763
  process.stderr.write(`${arityError}\n`);
@@ -2711,9 +2802,10 @@ export function main() {
2711
2802
  return;
2712
2803
  }
2713
2804
  if (arg2 === "telemetry") {
2714
- const cfg = loadOrExit();
2715
- const report = getTelemetryReport(cfg, globalCircuitBreaker);
2716
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
2805
+ void runTelemetry().catch((e) => {
2806
+ process.stderr.write(`llm-relay telemetry: ${e.message}\n`);
2807
+ process.exit(1);
2808
+ });
2717
2809
  return;
2718
2810
  }
2719
2811
  if (arg2 === "check-keys") {
@@ -2930,14 +3022,22 @@ export function classifyCommand(argv) {
2930
3022
  case "dashboard":
2931
3023
  return "read-only";
2932
3024
  case "delegate-gate":
2933
- return "read-only";
3025
+ return argv.some((a) => a === "--fix" || a === "-fix") ? "mutating" : "read-only";
2934
3026
  case "mcp":
2935
3027
  return "read-only";
3028
+ case "dispatch":
3029
+ return argv.some((a) => a === "--exhausted" || a === "-x") ? "mutating" : "read-only";
2936
3030
  default:
2937
3031
  return "read-only";
2938
3032
  }
2939
3033
  }
2940
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
+ }
2941
3041
  const command = rawCliCommand(process.argv);
2942
3042
  if (command === "cooldowns")
2943
3043
  parseCooldownClearArgs(process.argv);