proxitor 0.10.1 → 0.11.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/cli.mjs CHANGED
@@ -7,7 +7,7 @@ import tty, { ReadStream } from "node:tty";
7
7
  import { formatWithOptions, styleText } from "node:util";
8
8
  import * as l$1 from "node:readline";
9
9
  import l__default from "node:readline";
10
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
11
11
  import { dirname, join, resolve, sep } from "node:path";
12
12
  import { createServer } from "node:net";
13
13
  import { STATUS_CODES, createServer as createServer$1 } from "node:http";
@@ -19244,7 +19244,7 @@ async function runConfigMenu(client) {
19244
19244
  }
19245
19245
  //#endregion
19246
19246
  //#region src/version.ts
19247
- const version = "0.10.1";
19247
+ const version = "0.11.0";
19248
19248
  //#endregion
19249
19249
  //#region src/commands/doctor.ts
19250
19250
  const DEFAULT_TIMEOUT_MS = 3e3;
@@ -19482,6 +19482,142 @@ async function doctorCommand(opts = {}) {
19482
19482
  return exitCode;
19483
19483
  }
19484
19484
  //#endregion
19485
+ //#region src/config-source.ts
19486
+ /** Default watcher: fs.watchFile polling; returns a stop function. */
19487
+ const watchStat = (filename, pollIntervalMs, onChange) => {
19488
+ watchFile(filename, {
19489
+ interval: pollIntervalMs,
19490
+ persistent: false
19491
+ }, onChange);
19492
+ return () => unwatchFile(filename);
19493
+ };
19494
+ function fmt(value) {
19495
+ if (value === void 0) return "unset";
19496
+ if (value === true) return "on";
19497
+ if (value === false) return "off";
19498
+ return String(value);
19499
+ }
19500
+ const SCALAR_KEYS = [
19501
+ "cacheControl",
19502
+ "cacheControlTtl",
19503
+ "sessionId",
19504
+ "normalizeVolatileSystem",
19505
+ "authType",
19506
+ "verbose",
19507
+ "bodyLimit",
19508
+ "openrouterBaseUrl"
19509
+ ];
19510
+ function canonicalEntries(record) {
19511
+ if (!record) return "";
19512
+ return JSON.stringify(Object.keys(record).sort().map((key) => [key, record[key]]));
19513
+ }
19514
+ /** Diff of cache-relevant fields; '' if nothing changed. */
19515
+ function summarizeChanges(prev, next) {
19516
+ const parts = [];
19517
+ for (const key of SCALAR_KEYS) if (prev[key] !== next[key]) parts.push(`${key}: ${fmt(prev[key])}→${fmt(next[key])}`);
19518
+ if (JSON.stringify(buildProviderRouting(prev.provider)) !== JSON.stringify(buildProviderRouting(next.provider))) parts.push("provider routing");
19519
+ if (canonicalEntries(prev.modelOverrides) !== canonicalEntries(next.modelOverrides)) {
19520
+ const prevCount = prev.modelOverrides ? Object.keys(prev.modelOverrides).length : 0;
19521
+ const nextCount = next.modelOverrides ? Object.keys(next.modelOverrides).length : 0;
19522
+ parts.push(`modelOverrides: ${prevCount}→${nextCount}`);
19523
+ }
19524
+ if (canonicalEntries(prev.headers) !== canonicalEntries(next.headers)) parts.push("headers");
19525
+ return parts.join(", ");
19526
+ }
19527
+ function createConfigSource(options) {
19528
+ return new FileWatchingConfigSource(options);
19529
+ }
19530
+ var FileWatchingConfigSource = class {
19531
+ current;
19532
+ loadOptions;
19533
+ load;
19534
+ pollIntervalMs;
19535
+ watch;
19536
+ boundHost;
19537
+ boundPort;
19538
+ resolvedPath;
19539
+ stopWatch;
19540
+ loading = false;
19541
+ pending = false;
19542
+ watching = false;
19543
+ constructor(options) {
19544
+ this.current = options.initial;
19545
+ this.loadOptions = options.loadOptions;
19546
+ this.load = options.load ?? loadConfig;
19547
+ this.pollIntervalMs = options.pollIntervalMs ?? 1e3;
19548
+ this.watch = options.watch ?? watchStat;
19549
+ this.boundHost = options.initial.host;
19550
+ this.boundPort = options.initial.port;
19551
+ this.resolvedPath = options.loadOptions.noConfig ? null : tryFindConfigFile(options.loadOptions.configPath);
19552
+ }
19553
+ get() {
19554
+ return this.current;
19555
+ }
19556
+ async reload() {
19557
+ if (this.loading) {
19558
+ this.pending = true;
19559
+ return { ok: true };
19560
+ }
19561
+ this.loading = true;
19562
+ try {
19563
+ const next = await this.load(this.loadOptions);
19564
+ const restartNeeded = next.host !== this.boundHost || next.port !== this.boundPort;
19565
+ let diff = "";
19566
+ try {
19567
+ diff = summarizeChanges(this.current, next);
19568
+ } catch {
19569
+ diff = "";
19570
+ }
19571
+ this.current = next;
19572
+ if (restartNeeded) logger.warn("host/port changed — restart proxitor to apply (live reload does not re-bind the socket)");
19573
+ logger.info(`Config reloaded${diff ? ` — ${diff}` : " (no material changes)"}`);
19574
+ return { ok: true };
19575
+ } catch (error) {
19576
+ const msg = error instanceof Error ? error.message : String(error);
19577
+ logger.error(`Config reload failed — keeping previous config: ${msg}`);
19578
+ return {
19579
+ ok: false,
19580
+ error: msg
19581
+ };
19582
+ } finally {
19583
+ this.loading = false;
19584
+ if (this.pending) {
19585
+ this.pending = false;
19586
+ this.reload();
19587
+ }
19588
+ }
19589
+ }
19590
+ start() {
19591
+ if (this.watching) return;
19592
+ if (!this.resolvedPath) {
19593
+ logger.info("Live config reload disabled (no config file)");
19594
+ return;
19595
+ }
19596
+ this.watching = true;
19597
+ const path = this.resolvedPath;
19598
+ this.stopWatch = this.watch(path, this.pollIntervalMs, (curr, prev) => {
19599
+ try {
19600
+ this.onStat(path, curr, prev);
19601
+ } catch {}
19602
+ });
19603
+ }
19604
+ onStat(path, curr, prev) {
19605
+ if (curr.nlink === 0) {
19606
+ logger.warn(`config file disappeared — keeping current config (${path})`);
19607
+ return;
19608
+ }
19609
+ if (curr.mtimeMs === prev.mtimeMs) return;
19610
+ this.reload();
19611
+ }
19612
+ stop() {
19613
+ if (this.watching) {
19614
+ this.stopWatch?.();
19615
+ this.stopWatch = void 0;
19616
+ this.watching = false;
19617
+ }
19618
+ }
19619
+ };
19620
+ //#endregion
19485
19621
  //#region node_modules/.pnpm/@hono+node-server@2.0.4_hono@4.12.25/node_modules/@hono/node-server/dist/index.mjs
19486
19622
  var RequestError = class extends Error {
19487
19623
  constructor(message, options) {
@@ -22876,20 +23012,23 @@ const injectChain = [
22876
23012
  normalizeVolatileSystemMiddleware,
22877
23013
  injectSessionId
22878
23014
  ];
22879
- function createProxyServer(config, onReady) {
23015
+ function createProxyServer(source, onReady) {
22880
23016
  const app = new Hono();
22881
- const globalRouting = buildProviderRouting(config.provider);
22882
- const modelOverrideKeys = Object.keys(config.modelOverrides ?? []);
22883
23017
  app.use("*", async (c, next) => {
22884
- c.set("config", config);
23018
+ c.set("config", source.get());
22885
23019
  await next();
22886
23020
  });
22887
- app.get("/health", (c) => c.json({
22888
- ok: true,
22889
- upstream: config.openrouterBaseUrl,
22890
- provider: globalRouting ?? "not configured",
22891
- modelOverrides: modelOverrideKeys
22892
- }));
23021
+ app.get("/health", (c) => {
23022
+ const config = source.get();
23023
+ const globalRouting = buildProviderRouting(config.provider);
23024
+ const modelOverrideKeys = Object.keys(config.modelOverrides ?? []);
23025
+ return c.json({
23026
+ ok: true,
23027
+ upstream: config.openrouterBaseUrl,
23028
+ provider: globalRouting ?? "not configured",
23029
+ modelOverrides: modelOverrideKeys
23030
+ });
23031
+ });
22893
23032
  for (const path of INJECT_PATHS) app.post(path, setupRequest, readBody, ...injectChain, buildUpstreamReq, forwardRequest);
22894
23033
  app.all("*", setupRequest, readBody, resolveConfig, buildUpstreamReq, forwardRequest);
22895
23034
  app.onError((err, c) => {
@@ -22900,20 +23039,22 @@ function createProxyServer(config, onReady) {
22900
23039
  type: "proxy_internal_error"
22901
23040
  } }, { status: 500 });
22902
23041
  });
23042
+ const initial = source.get();
22903
23043
  return serve({
22904
23044
  fetch: app.fetch,
22905
- port: config.port,
22906
- hostname: config.host
23045
+ port: initial.port,
23046
+ hostname: initial.host
22907
23047
  }, onReady);
22908
23048
  }
22909
23049
  const SHUTDOWN_TIMEOUT_MS = 1e4;
22910
- function startProxyServer(config, onReady) {
22911
- const server = createProxyServer(config, onReady);
23050
+ function startProxyServer(source, onReady) {
23051
+ const server = createProxyServer(source, onReady);
22912
23052
  let shuttingDown = false;
22913
23053
  function shutdown(signal) {
22914
23054
  if (shuttingDown) return;
22915
23055
  shuttingDown = true;
22916
23056
  logger.info(`${signal} received — draining active connections…`);
23057
+ source.stop();
22917
23058
  const timer = setTimeout(() => {
22918
23059
  logger.warn("Forcing shutdown — drain timeout exceeded");
22919
23060
  process.exit(1);
@@ -23031,17 +23172,24 @@ const startCommand = (0, import_cjs.command)({
23031
23172
  },
23032
23173
  handler: async ({ configPath, port, host, noConfig, openrouterKey, verbose }) => {
23033
23174
  try {
23034
- const cfg = await loadConfig({
23175
+ const loadOptions = {
23035
23176
  configPath,
23036
23177
  noConfig,
23037
23178
  port,
23038
23179
  host,
23039
23180
  openrouterKey,
23040
23181
  verbose
23182
+ };
23183
+ const cfg = await loadConfig(loadOptions);
23184
+ const source = createConfigSource({
23185
+ loadOptions,
23186
+ initial: cfg
23041
23187
  });
23042
- startProxyServer(cfg, () => {
23188
+ source.start();
23189
+ startProxyServer(source, () => {
23043
23190
  logger.ready(`Proxitor proxy listening on ${cfg.host}:${cfg.port}`);
23044
23191
  logger.info("Routing requests to OpenRouter");
23192
+ if (source.resolvedPath) logger.info(`Watching ${source.resolvedPath} for changes (live reload)`);
23045
23193
  });
23046
23194
  } catch (error) {
23047
23195
  logger.error("Failed to start proxy:", error);