proxitor 0.10.0 → 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
@@ -6,8 +6,8 @@ import * as tty$1 from "node:tty";
6
6
  import tty, { ReadStream } from "node:tty";
7
7
  import { formatWithOptions, styleText } from "node:util";
8
8
  import * as l$1 from "node:readline";
9
- import f from "node:readline";
10
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
9
+ import l__default from "node:readline";
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";
@@ -4603,7 +4603,7 @@ var V = class {
4603
4603
  this.state = "cancel", this.close();
4604
4604
  }, { once: true });
4605
4605
  }
4606
- this.rl = f.createInterface({
4606
+ this.rl = l__default.createInterface({
4607
4607
  input: this.input,
4608
4608
  tabSize: 2,
4609
4609
  prompt: "",
@@ -17220,7 +17220,7 @@ const r = Object.create(null), i = (e) => globalThis.process?.env || import.meta
17220
17220
  const e = i(true);
17221
17221
  return Object.keys(e);
17222
17222
  }
17223
- }), t = typeof process < "u" && process.env && process.env.NODE_ENV || "", f$1 = [
17223
+ }), t = typeof process < "u" && process.env && process.env.NODE_ENV || "", f = [
17224
17224
  ["APPVEYOR"],
17225
17225
  [
17226
17226
  "AWS_AMPLIFY",
@@ -17312,7 +17312,7 @@ const r = Object.create(null), i = (e) => globalThis.process?.env || import.meta
17312
17312
  ]
17313
17313
  ];
17314
17314
  function b() {
17315
- if (globalThis.process?.env) for (const e of f$1) {
17315
+ if (globalThis.process?.env) for (const e of f) {
17316
17316
  const s = e[1] || e[0];
17317
17317
  if (globalThis.process?.env[s]) return {
17318
17318
  name: e[0].toLowerCase(),
@@ -17917,6 +17917,31 @@ const CACHE_HINTS = {
17917
17917
  always: "All models",
17918
17918
  skip: "Passthrough — leave client cache_control headers as-is"
17919
17919
  };
17920
+ const NORMALIZE_HINTS = {
17921
+ on: "Rewrite cch → stable prefix cache",
17922
+ off: "Passthrough — rewrite nothing"
17923
+ };
17924
+ async function askNormalizeVolatileSystem(message, current, opts) {
17925
+ const options = [{
17926
+ value: true,
17927
+ label: "On",
17928
+ hint: NORMALIZE_HINTS.on
17929
+ }, {
17930
+ value: false,
17931
+ label: "Off",
17932
+ hint: NORMALIZE_HINTS.off
17933
+ }];
17934
+ if (opts?.removable) options.push({
17935
+ value: "reset",
17936
+ label: "Reset / inherit",
17937
+ hint: opts.resetHint ?? "Remove override"
17938
+ });
17939
+ return await select({
17940
+ message,
17941
+ initialValue: current ?? false,
17942
+ options
17943
+ });
17944
+ }
17920
17945
  async function askTriState(message, current, hints, opts) {
17921
17946
  const options = [
17922
17947
  {
@@ -18017,6 +18042,12 @@ async function collectCacheTriState(currentCc, currentTtl, globalTtl) {
18017
18042
  cacheControlTtl: { value: ttl }
18018
18043
  };
18019
18044
  }
18045
+ async function collectNormalizeVolatileSystem(currentNvs) {
18046
+ const nvs = await askNormalizeVolatileSystem("Normalize volatile system (cch hash)", currentNvs, { removable: true });
18047
+ if (typeof nvs === "symbol") return null;
18048
+ if (nvs === "reset") return { normalizeVolatileSystem: { remove: true } };
18049
+ return { normalizeVolatileSystem: { value: nvs } };
18050
+ }
18020
18051
  //#endregion
18021
18052
  //#region src/commands/config/add.ts
18022
18053
  const CUSTOM_PATTERN_VALUE = "__proxitor_custom_pattern__";
@@ -18119,13 +18150,24 @@ async function configureProviderAndSave(configPath, client, modelKey, isPattern)
18119
18150
  const providerResult = await selectProvidersByMode(mode, providerOptions);
18120
18151
  if (!providerResult) return;
18121
18152
  override = providerResult;
18122
- override = await collectSessionAndCache(override);
18153
+ override = await collectOptionalOverrides(override);
18123
18154
  if (!await confirmAndSave(configPath, modelKey, override, client)) return;
18124
18155
  outro("✓ Model override saved");
18125
18156
  }
18126
- async function collectSessionAndCache(override) {
18157
+ async function collectOptionalOverrides(override) {
18127
18158
  override = await collectSession(override);
18128
18159
  override = await collectCache(override);
18160
+ override = await collectNormalize(override);
18161
+ return override;
18162
+ }
18163
+ async function collectNormalize(override) {
18164
+ const want = await confirm({
18165
+ message: "Configure normalizeVolatileSystem for this model?",
18166
+ initialValue: false
18167
+ });
18168
+ if (isCancel(want) || !want) return override;
18169
+ const result = await collectNormalizeVolatileSystem(override.normalizeVolatileSystem);
18170
+ if (result && !("remove" in result.normalizeVolatileSystem)) override.normalizeVolatileSystem = result.normalizeVolatileSystem.value;
18129
18171
  return override;
18130
18172
  }
18131
18173
  async function collectSession(override) {
@@ -18201,6 +18243,7 @@ function formatOverrideYaml(override) {
18201
18243
  if (override.sessionId) parts.push(`sessionId: ${override.sessionId}`);
18202
18244
  if (override.cacheControl) parts.push(`cacheControl: ${override.cacheControl}`);
18203
18245
  if (override.cacheControlTtl) parts.push(`cacheControlTtl: ${override.cacheControlTtl}`);
18246
+ if (override.normalizeVolatileSystem !== void 0) parts.push(`normalizeVolatileSystem: ${override.normalizeVolatileSystem}`);
18204
18247
  return parts.join("\n ") || "(empty)";
18205
18248
  }
18206
18249
  //#endregion
@@ -18276,6 +18319,10 @@ async function browseModelsCommand(client) {
18276
18319
  }
18277
18320
  //#endregion
18278
18321
  //#region src/commands/config/edit.ts
18322
+ function nvsHint(value) {
18323
+ if (value === void 0) return "(inherit)";
18324
+ return value ? "on" : "off";
18325
+ }
18279
18326
  function formatOverrideHint(override) {
18280
18327
  if (!override) return "(empty)";
18281
18328
  const parts = [];
@@ -18285,6 +18332,7 @@ function formatOverrideHint(override) {
18285
18332
  }
18286
18333
  if (override.sessionId) parts.push(`session: ${override.sessionId}`);
18287
18334
  if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
18335
+ if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${nvsHint(override.normalizeVolatileSystem)}`);
18288
18336
  if (override.headers) parts.push(`${Object.keys(override.headers).length} header(s)`);
18289
18337
  return parts.join(", ") || "(empty)";
18290
18338
  }
@@ -18308,6 +18356,7 @@ function showCurrentConfig(modelKey, current) {
18308
18356
  if (current.sessionId) log.info(` sessionId: ${current.sessionId}`);
18309
18357
  if (current.cacheControl) log.info(` cacheControl: ${current.cacheControl}`);
18310
18358
  if (current.cacheControlTtl) log.info(` cacheControlTtl: ${current.cacheControlTtl}`);
18359
+ if (current.normalizeVolatileSystem !== void 0) log.info(` normalizeVolatileSystem: ${current.normalizeVolatileSystem}`);
18311
18360
  if (current.headers) for (const [name, value] of Object.entries(current.headers)) log.info(` headers.${name}: ${value}`);
18312
18361
  }
18313
18362
  async function editProvider(modelKey, current, client) {
@@ -18344,6 +18393,23 @@ async function editCacheControl(current, configPath) {
18344
18393
  applyField(next, "cacheControlTtl", result.cacheControlTtl);
18345
18394
  return next;
18346
18395
  }
18396
+ /** @internal */
18397
+ async function editNormalizeVolatileSystem(current) {
18398
+ const result = await collectNormalizeVolatileSystem(current.normalizeVolatileSystem);
18399
+ if (result === null) return current;
18400
+ const next = { ...current };
18401
+ applyField(next, "normalizeVolatileSystem", result.normalizeVolatileSystem);
18402
+ return next;
18403
+ }
18404
+ async function applyFieldEdit(field, modelKey, current, client, configPath) {
18405
+ switch (field) {
18406
+ case "provider": return editProvider(modelKey, current, client);
18407
+ case "sessionId": return editSessionId(current);
18408
+ case "cacheControl": return editCacheControl(current, configPath);
18409
+ case "normalizeVolatileSystem": return editNormalizeVolatileSystem(current);
18410
+ default: return current;
18411
+ }
18412
+ }
18347
18413
  /** Run the interactive "Edit model override" flow. */
18348
18414
  async function editOverrideCommand(client, configPath) {
18349
18415
  intro("Edit Model Override");
@@ -18386,6 +18452,11 @@ async function editOverrideCommand(client, configPath) {
18386
18452
  label: "Cache control",
18387
18453
  hint: formatCacheHint(current.cacheControl, current.cacheControlTtl)
18388
18454
  },
18455
+ {
18456
+ value: "normalizeVolatileSystem",
18457
+ label: "Normalize volatile system",
18458
+ hint: nvsHint(current.normalizeVolatileSystem)
18459
+ },
18389
18460
  {
18390
18461
  value: "done",
18391
18462
  label: "✓ Done"
@@ -18393,17 +18464,7 @@ async function editOverrideCommand(client, configPath) {
18393
18464
  ]
18394
18465
  });
18395
18466
  if (isCancel(field) || field === "done") break;
18396
- switch (field) {
18397
- case "provider":
18398
- current = await editProvider(modelKey, current, client);
18399
- break;
18400
- case "sessionId":
18401
- current = await editSessionId(current);
18402
- break;
18403
- case "cacheControl":
18404
- current = await editCacheControl(current, resolvedConfigPath);
18405
- break;
18406
- }
18467
+ current = await applyFieldEdit(field, modelKey, current, client, resolvedConfigPath);
18407
18468
  }
18408
18469
  const save = await confirm({ message: "Save changes?" });
18409
18470
  if (isCancel(save) || !save) {
@@ -19040,26 +19101,9 @@ async function normalizeVolatileSystemCommand(opts) {
19040
19101
  const configPath = requireConfigPath(opts?.configPath);
19041
19102
  const current = readConfigFile(configPath).normalizeVolatileSystem ?? DEFAULTS.normalizeVolatileSystem;
19042
19103
  log.info(`Current: normalizeVolatileSystem = ${current}`);
19043
- const choice = await select({
19044
- message: "Normalize Claude Code's volatile cch hash in the system prompt? Stabilizes the prefix cache for non-Anthropic providers (qwen/glm/etc.).",
19045
- options: [
19046
- {
19047
- value: true,
19048
- label: "On",
19049
- hint: "rewrite cch → stable prefix"
19050
- },
19051
- {
19052
- value: false,
19053
- label: "Off",
19054
- hint: "passthrough"
19055
- },
19056
- {
19057
- value: "reset",
19058
- label: "Reset",
19059
- hint: `remove (default: ${DEFAULTS.normalizeVolatileSystem})`
19060
- }
19061
- ],
19062
- initialValue: current
19104
+ const choice = await askNormalizeVolatileSystem("Normalize Claude Code's volatile cch hash in the system prompt? Stabilizes the prefix cache for non-Anthropic providers (qwen/glm/etc.).", current, {
19105
+ removable: true,
19106
+ resetHint: `remove (default: ${DEFAULTS.normalizeVolatileSystem})`
19063
19107
  });
19064
19108
  if (typeof choice === "symbol") return;
19065
19109
  const fields = {};
@@ -19200,7 +19244,7 @@ async function runConfigMenu(client) {
19200
19244
  }
19201
19245
  //#endregion
19202
19246
  //#region src/version.ts
19203
- const version = "0.10.0";
19247
+ const version = "0.11.0";
19204
19248
  //#endregion
19205
19249
  //#region src/commands/doctor.ts
19206
19250
  const DEFAULT_TIMEOUT_MS = 3e3;
@@ -19438,6 +19482,142 @@ async function doctorCommand(opts = {}) {
19438
19482
  return exitCode;
19439
19483
  }
19440
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
19441
19621
  //#region node_modules/.pnpm/@hono+node-server@2.0.4_hono@4.12.25/node_modules/@hono/node-server/dist/index.mjs
19442
19622
  var RequestError = class extends Error {
19443
19623
  constructor(message, options) {
@@ -22832,20 +23012,23 @@ const injectChain = [
22832
23012
  normalizeVolatileSystemMiddleware,
22833
23013
  injectSessionId
22834
23014
  ];
22835
- function createProxyServer(config, onReady) {
23015
+ function createProxyServer(source, onReady) {
22836
23016
  const app = new Hono();
22837
- const globalRouting = buildProviderRouting(config.provider);
22838
- const modelOverrideKeys = Object.keys(config.modelOverrides ?? []);
22839
23017
  app.use("*", async (c, next) => {
22840
- c.set("config", config);
23018
+ c.set("config", source.get());
22841
23019
  await next();
22842
23020
  });
22843
- app.get("/health", (c) => c.json({
22844
- ok: true,
22845
- upstream: config.openrouterBaseUrl,
22846
- provider: globalRouting ?? "not configured",
22847
- modelOverrides: modelOverrideKeys
22848
- }));
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
+ });
22849
23032
  for (const path of INJECT_PATHS) app.post(path, setupRequest, readBody, ...injectChain, buildUpstreamReq, forwardRequest);
22850
23033
  app.all("*", setupRequest, readBody, resolveConfig, buildUpstreamReq, forwardRequest);
22851
23034
  app.onError((err, c) => {
@@ -22856,20 +23039,22 @@ function createProxyServer(config, onReady) {
22856
23039
  type: "proxy_internal_error"
22857
23040
  } }, { status: 500 });
22858
23041
  });
23042
+ const initial = source.get();
22859
23043
  return serve({
22860
23044
  fetch: app.fetch,
22861
- port: config.port,
22862
- hostname: config.host
23045
+ port: initial.port,
23046
+ hostname: initial.host
22863
23047
  }, onReady);
22864
23048
  }
22865
23049
  const SHUTDOWN_TIMEOUT_MS = 1e4;
22866
- function startProxyServer(config, onReady) {
22867
- const server = createProxyServer(config, onReady);
23050
+ function startProxyServer(source, onReady) {
23051
+ const server = createProxyServer(source, onReady);
22868
23052
  let shuttingDown = false;
22869
23053
  function shutdown(signal) {
22870
23054
  if (shuttingDown) return;
22871
23055
  shuttingDown = true;
22872
23056
  logger.info(`${signal} received — draining active connections…`);
23057
+ source.stop();
22873
23058
  const timer = setTimeout(() => {
22874
23059
  logger.warn("Forcing shutdown — drain timeout exceeded");
22875
23060
  process.exit(1);
@@ -22987,17 +23172,24 @@ const startCommand = (0, import_cjs.command)({
22987
23172
  },
22988
23173
  handler: async ({ configPath, port, host, noConfig, openrouterKey, verbose }) => {
22989
23174
  try {
22990
- const cfg = await loadConfig({
23175
+ const loadOptions = {
22991
23176
  configPath,
22992
23177
  noConfig,
22993
23178
  port,
22994
23179
  host,
22995
23180
  openrouterKey,
22996
23181
  verbose
23182
+ };
23183
+ const cfg = await loadConfig(loadOptions);
23184
+ const source = createConfigSource({
23185
+ loadOptions,
23186
+ initial: cfg
22997
23187
  });
22998
- startProxyServer(cfg, () => {
23188
+ source.start();
23189
+ startProxyServer(source, () => {
22999
23190
  logger.ready(`Proxitor proxy listening on ${cfg.host}:${cfg.port}`);
23000
23191
  logger.info("Routing requests to OpenRouter");
23192
+ if (source.resolvedPath) logger.info(`Watching ${source.resolvedPath} for changes (live reload)`);
23001
23193
  });
23002
23194
  } catch (error) {
23003
23195
  logger.error("Failed to start proxy:", error);