proxitor 0.10.1 → 0.12.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
@@ -4,10 +4,10 @@ import process$1, { stdin, stdout } from "node:process";
4
4
  import os, { homedir } from "node:os";
5
5
  import * as tty$1 from "node:tty";
6
6
  import tty, { ReadStream } from "node:tty";
7
- import { formatWithOptions, styleText } from "node:util";
7
+ import { formatWithOptions, isDeepStrictEqual, 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";
@@ -16491,18 +16491,29 @@ function findConfigFile(explicitPath) {
16491
16491
  if (found) return found;
16492
16492
  throw new MissingConfigError(getConfigSearchPaths());
16493
16493
  }
16494
- function readConfigFile(filePath) {
16494
+ function parseConfigFile(filePath) {
16495
16495
  const content = readFileSync(filePath, "utf-8");
16496
- let raw;
16497
16496
  try {
16498
- raw = filePath.endsWith(".json") ? JSON.parse(content) : (0, import_dist.parse)(content);
16497
+ return filePath.endsWith(".json") ? JSON.parse(content) : (0, import_dist.parse)(content);
16499
16498
  } catch (err) {
16500
16499
  throw new ConfigParseError(filePath, err instanceof Error ? err : void 0);
16501
16500
  }
16502
- const result = proxyConfigFileSchema.safeParse(raw);
16501
+ }
16502
+ function readConfigFile(filePath) {
16503
+ const result = proxyConfigFileSchema.safeParse(parseConfigFile(filePath));
16503
16504
  if (!result.success) throw new ConfigValidationError(filePath, result.error);
16504
16505
  return result.data;
16505
16506
  }
16507
+ /**
16508
+ * Validated like readConfigFile, but schema defaults are NOT applied, so absent
16509
+ * keys stay undefined — use when "absent" must differ from "explicit default".
16510
+ */
16511
+ function readConfigFileRaw(filePath) {
16512
+ const raw = parseConfigFile(filePath);
16513
+ const result = proxyConfigFileSchema.safeParse(raw);
16514
+ if (!result.success) throw new ConfigValidationError(filePath, result.error);
16515
+ return raw ?? {};
16516
+ }
16506
16517
  //#endregion
16507
16518
  //#region src/openrouter/cache.ts
16508
16519
  const CACHE_DIR = join(homedir(), ".cache", "proxitor");
@@ -17938,7 +17949,7 @@ async function askNormalizeVolatileSystem(message, current, opts) {
17938
17949
  });
17939
17950
  return await select({
17940
17951
  message,
17941
- initialValue: current ?? false,
17952
+ initialValue: current ?? (opts?.removable ? "reset" : false),
17942
17953
  options
17943
17954
  });
17944
17955
  }
@@ -17967,7 +17978,7 @@ async function askTriState(message, current, hints, opts) {
17967
17978
  });
17968
17979
  return await select({
17969
17980
  message,
17970
- initialValue: current ?? "auto",
17981
+ initialValue: current ?? (opts?.removable ? "reset" : "auto"),
17971
17982
  options
17972
17983
  });
17973
17984
  }
@@ -18003,7 +18014,7 @@ async function askCacheControlTtl(current, opts) {
18003
18014
  });
18004
18015
  return await select({
18005
18016
  message: "Cache TTL",
18006
- initialValue: current ?? "5m",
18017
+ initialValue: current ?? (opts?.removable ? "reset" : "5m"),
18007
18018
  options
18008
18019
  });
18009
18020
  }
@@ -18016,14 +18027,14 @@ function applyField(obj, key, field) {
18016
18027
  else obj[key] = field.value;
18017
18028
  }
18018
18029
  async function collectSessionTriState(currentSid) {
18019
- const sid = await askTriState("Session ID mode", currentSid ?? "auto", SESSION_HINTS, { removable: true });
18030
+ const sid = await askTriState("Session ID mode", currentSid, SESSION_HINTS, { removable: true });
18020
18031
  if (typeof sid === "symbol") return null;
18021
18032
  if (sid === "reset") return { sessionId: { remove: true } };
18022
18033
  return { sessionId: { value: sid } };
18023
18034
  }
18024
18035
  /** TTL is independent of cache mode. Mode-cancel aborts; TTL-cancel keeps existing TTL. */
18025
18036
  async function collectCacheTriState(currentCc, currentTtl, globalTtl) {
18026
- const cc = await askTriState("Cache control mode", currentCc ?? "auto", CACHE_HINTS, { removable: true });
18037
+ const cc = await askTriState("Cache control mode", currentCc, CACHE_HINTS, { removable: true });
18027
18038
  if (typeof cc === "symbol") return null;
18028
18039
  let cacheControl;
18029
18040
  if (cc === "reset") cacheControl = { remove: true };
@@ -18318,30 +18329,116 @@ async function browseModelsCommand(client) {
18318
18329
  });
18319
18330
  }
18320
18331
  //#endregion
18321
- //#region src/commands/config/edit.ts
18322
- function nvsHint(value) {
18323
- if (value === void 0) return "(inherit)";
18332
+ //#region src/commands/config/cache-control.ts
18333
+ async function cacheControlCommand(opts) {
18334
+ const configPath = requireConfigPath(opts?.configPath);
18335
+ const cfg = readConfigFileRaw(configPath);
18336
+ const rawCc = cfg.cacheControl;
18337
+ const rawTtl = cfg.cacheControlTtl;
18338
+ const effectiveCc = rawCc ?? DEFAULTS.cacheControl;
18339
+ log.info(`Current: cacheControl = ${rawCc === void 0 ? `(default -> ${effectiveCc})` : effectiveCc}`);
18340
+ if (rawTtl) log.info(`Current: cacheControlTtl = ${rawTtl}`);
18341
+ const cc = await askTriState("Cache control mode", rawCc, CACHE_HINTS, { removable: true });
18342
+ if (typeof cc === "symbol") return;
18343
+ const fields = {};
18344
+ fields.cacheControl = cc === "reset" ? void 0 : cc;
18345
+ const ttlResult = await askCacheControlTtl(rawTtl, { removable: true });
18346
+ if (typeof ttlResult === "symbol") {
18347
+ setGlobalConfigFields(configPath, fields);
18348
+ log.success(`cacheControl set to ${cc === "reset" ? "(default)" : cc}`);
18349
+ return;
18350
+ }
18351
+ fields.cacheControlTtl = ttlResult === "reset" ? void 0 : ttlResult;
18352
+ setGlobalConfigFields(configPath, fields);
18353
+ const ccLabel = cc === "reset" ? "(default)" : cc;
18354
+ const ttlLabel = ttlResult === "reset" ? "(default)" : ttlResult;
18355
+ log.success(`cacheControl set to ${ccLabel}, TTL = ${ttlLabel}`);
18356
+ }
18357
+ //#endregion
18358
+ //#region src/commands/config/caching-summary.ts
18359
+ /** Friendly TTL label (omit→strip, skip→passthrough). */
18360
+ function describeTtl(value) {
18361
+ if (value === void 0) return "(default)";
18362
+ if (value === "omit") return "strip";
18363
+ if (value === "skip") return "passthrough";
18364
+ return value;
18365
+ }
18366
+ /** Global NVS: distinguish absent (default-origin) from an explicit on/off. */
18367
+ function globalNvsLabel(value) {
18368
+ if (value === void 0) return `(default -> ${DEFAULTS.normalizeVolatileSystem ? "on" : "off"})`;
18324
18369
  return value ? "on" : "off";
18325
18370
  }
18326
- function formatOverrideHint(override) {
18327
- if (!override) return "(empty)";
18328
- const parts = [];
18329
- if (override.provider) {
18330
- const keys = Object.keys(override.provider);
18331
- parts.push(`provider: ${keys.join(", ")}`);
18332
- }
18333
- if (override.sessionId) parts.push(`session: ${override.sessionId}`);
18334
- if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
18335
- if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${nvsHint(override.normalizeVolatileSystem)}`);
18336
- if (override.headers) parts.push(`${Object.keys(override.headers).length} header(s)`);
18337
- return parts.join(", ") || "(empty)";
18371
+ /** Global tri-state: distinguish absent (default-origin) from an explicit mode. */
18372
+ function globalTriStateLabel(value, fallback) {
18373
+ return value === void 0 ? `(default -> ${fallback})` : value;
18374
+ }
18375
+ function nvsLabel(value, globalValue) {
18376
+ if (value === void 0) return `(inherit -> ${globalValue ? "on" : "off"})`;
18377
+ return value ? "on" : "off";
18338
18378
  }
18339
- function formatCacheHint(cc, ttl) {
18340
- let ttlLabel = ttl ?? "";
18341
- if (ttl === "omit") ttlLabel = "ttl strip";
18342
- else if (ttl === "skip") ttlLabel = "ttl passthrough";
18343
- return [cc, ttlLabel].filter(Boolean).join(", ") || "(inherit)";
18379
+ function perModelTtl(current, globalCfg) {
18380
+ if (current.cacheControlTtl !== void 0) return describeTtl(current.cacheControlTtl);
18381
+ if (globalCfg.cacheControlTtl !== void 0) return `(inherit -> ${describeTtl(globalCfg.cacheControlTtl)})`;
18382
+ return "(inherit)";
18344
18383
  }
18384
+ function formatGlobalCachingSummary(cfg) {
18385
+ const cc = globalTriStateLabel(cfg.cacheControl, DEFAULTS.cacheControl);
18386
+ const sid = globalTriStateLabel(cfg.sessionId, DEFAULTS.sessionId);
18387
+ const nvs = globalNvsLabel(cfg.normalizeVolatileSystem);
18388
+ return [
18389
+ "Three settings shape the request so cache survives.",
18390
+ "",
18391
+ ` cacheControl = ${cc} (activate cache)`,
18392
+ ` cacheControlTtl = ${describeTtl(cfg.cacheControlTtl)}`,
18393
+ ` sessionId = ${sid} (pin provider)`,
18394
+ ` normalizeVolatileSystem = ${nvs} (stable prefix)`,
18395
+ "",
18396
+ "Anthropic -> levers 1+2 - non-Anthropic (qwen/glm) -> all 3"
18397
+ ].join("\n");
18398
+ }
18399
+ function formatPerModelCachingSummary(modelKey, current, globalCfg) {
18400
+ const gCc = globalCfg.cacheControl ?? DEFAULTS.cacheControl;
18401
+ const gSid = globalCfg.sessionId ?? DEFAULTS.sessionId;
18402
+ const gNvs = globalCfg.normalizeVolatileSystem ?? DEFAULTS.normalizeVolatileSystem;
18403
+ const cc = current.cacheControl ?? `(inherit -> ${gCc})`;
18404
+ const ttl = perModelTtl(current, globalCfg);
18405
+ const sid = current.sessionId ?? `(inherit -> ${gSid})`;
18406
+ const nvs = nvsLabel(current.normalizeVolatileSystem, gNvs);
18407
+ return [
18408
+ `Caching for "${modelKey}"`,
18409
+ "",
18410
+ ` cacheControl = ${cc}`,
18411
+ ` cacheControlTtl = ${ttl}`,
18412
+ ` sessionId = ${sid}`,
18413
+ ` normalizeVolatileSystem = ${nvs}`
18414
+ ].join("\n");
18415
+ }
18416
+ //#endregion
18417
+ //#region src/commands/config/equality.ts
18418
+ /** Deep equality for overrides — gates no-op writes. */
18419
+ function overridesEqual(a, b) {
18420
+ return isDeepStrictEqual(a, b);
18421
+ }
18422
+ //#endregion
18423
+ //#region src/commands/config/normalize-system.ts
18424
+ async function normalizeVolatileSystemCommand(opts) {
18425
+ const configPath = requireConfigPath(opts?.configPath);
18426
+ const raw = readConfigFileRaw(configPath).normalizeVolatileSystem;
18427
+ const effective = raw ?? DEFAULTS.normalizeVolatileSystem;
18428
+ log.info(`Current: normalizeVolatileSystem = ${raw === void 0 ? `(default -> ${effective ? "on" : "off"})` : effective}`);
18429
+ 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.).", raw, {
18430
+ removable: true,
18431
+ resetHint: `remove (default: ${DEFAULTS.normalizeVolatileSystem})`
18432
+ });
18433
+ if (typeof choice === "symbol") return;
18434
+ const fields = {};
18435
+ fields.normalizeVolatileSystem = choice === "reset" ? void 0 : choice;
18436
+ setGlobalConfigFields(configPath, fields);
18437
+ const label = choice === "reset" ? `(default: ${DEFAULTS.normalizeVolatileSystem})` : choice;
18438
+ log.success(`normalizeVolatileSystem set to ${label}`);
18439
+ }
18440
+ //#endregion
18441
+ //#region src/commands/config/override-levers.ts
18345
18442
  function readGlobalTtl(configPath) {
18346
18443
  if (!configPath) return void 0;
18347
18444
  try {
@@ -18350,32 +18447,6 @@ function readGlobalTtl(configPath) {
18350
18447
  return;
18351
18448
  }
18352
18449
  }
18353
- function showCurrentConfig(modelKey, current) {
18354
- log.info(`Current config for "${modelKey}":`);
18355
- if (current.provider) for (const [field, value] of Object.entries(current.provider)) log.info(` provider.${field}: ${JSON.stringify(value)}`);
18356
- if (current.sessionId) log.info(` sessionId: ${current.sessionId}`);
18357
- if (current.cacheControl) log.info(` cacheControl: ${current.cacheControl}`);
18358
- if (current.cacheControlTtl) log.info(` cacheControlTtl: ${current.cacheControlTtl}`);
18359
- if (current.normalizeVolatileSystem !== void 0) log.info(` normalizeVolatileSystem: ${current.normalizeVolatileSystem}`);
18360
- if (current.headers) for (const [name, value] of Object.entries(current.headers)) log.info(` headers.${name}: ${value}`);
18361
- }
18362
- async function editProvider(modelKey, current, client) {
18363
- const isPattern = modelKey.includes("*");
18364
- const mode = await selectRoutingMode("Routing mode");
18365
- if (isCancel(mode)) return current;
18366
- if (mode === "skip") {
18367
- const { provider: _, ...rest } = current;
18368
- return rest;
18369
- }
18370
- const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern);
18371
- if (!providerOptions) return current;
18372
- const result = await selectProvidersByMode(mode, providerOptions);
18373
- if (!result) return current;
18374
- return {
18375
- ...current,
18376
- provider: result.provider
18377
- };
18378
- }
18379
18450
  async function editSessionId(current) {
18380
18451
  const result = await collectSessionTriState(current.sessionId);
18381
18452
  if (result === null) return current;
@@ -18383,7 +18454,6 @@ async function editSessionId(current) {
18383
18454
  applyField(next, "sessionId", result.sessionId);
18384
18455
  return next;
18385
18456
  }
18386
- /** @internal */
18387
18457
  async function editCacheControl(current, configPath) {
18388
18458
  const globalTtl = readGlobalTtl(configPath);
18389
18459
  const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl, globalTtl);
@@ -18393,7 +18463,6 @@ async function editCacheControl(current, configPath) {
18393
18463
  applyField(next, "cacheControlTtl", result.cacheControlTtl);
18394
18464
  return next;
18395
18465
  }
18396
- /** @internal */
18397
18466
  async function editNormalizeVolatileSystem(current) {
18398
18467
  const result = await collectNormalizeVolatileSystem(current.normalizeVolatileSystem);
18399
18468
  if (result === null) return current;
@@ -18401,15 +18470,134 @@ async function editNormalizeVolatileSystem(current) {
18401
18470
  applyField(next, "normalizeVolatileSystem", result.normalizeVolatileSystem);
18402
18471
  return next;
18403
18472
  }
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;
18473
+ //#endregion
18474
+ //#region src/commands/config/session-routing.ts
18475
+ async function sessionRoutingCommand(opts) {
18476
+ const configPath = requireConfigPath(opts?.configPath);
18477
+ const raw = readConfigFileRaw(configPath).sessionId;
18478
+ const effective = raw ?? DEFAULTS.sessionId;
18479
+ log.info(`Current: sessionId = ${raw === void 0 ? `(default -> ${effective})` : effective}`);
18480
+ const result = await askTriState("Session routing mode", raw, SESSION_HINTS, { removable: true });
18481
+ if (typeof result === "symbol") return;
18482
+ if (result === "reset") {
18483
+ setGlobalConfigField(configPath, "sessionId", void 0);
18484
+ log.success("sessionId reset to default (auto)");
18485
+ return;
18486
+ }
18487
+ setGlobalConfigField(configPath, "sessionId", result);
18488
+ log.success(`sessionId set to ${result}`);
18489
+ }
18490
+ //#endregion
18491
+ //#region src/commands/config/caching-menu.ts
18492
+ /** Cache levers: option list + dispatch in one table. */
18493
+ const CACHING_LEVERS = [
18494
+ {
18495
+ value: "cacheControl",
18496
+ label: "Activate caching — mode + TTL (cacheControl)",
18497
+ global: (configPath) => cacheControlCommand({ configPath }),
18498
+ perModel: (current, configPath) => editCacheControl(current, configPath)
18499
+ },
18500
+ {
18501
+ value: "sessionId",
18502
+ label: "Pin provider (sessionId)",
18503
+ global: (configPath) => sessionRoutingCommand({ configPath }),
18504
+ perModel: (current) => editSessionId(current)
18505
+ },
18506
+ {
18507
+ value: "normalizeVolatileSystem",
18508
+ label: "Stabilize prefix (normalizeVolatileSystem)",
18509
+ global: (configPath) => normalizeVolatileSystemCommand({ configPath }),
18510
+ perModel: (current) => editNormalizeVolatileSystem(current)
18511
+ }
18512
+ ];
18513
+ async function runCachingLeverMenu(opts) {
18514
+ for (;;) {
18515
+ note(opts.renderNote(), opts.noteTitle);
18516
+ const choice = await select({
18517
+ message: "Tune which lever?",
18518
+ options: [...CACHING_LEVERS.map((lever) => ({
18519
+ value: lever.value,
18520
+ label: lever.label
18521
+ })), {
18522
+ value: "back",
18523
+ label: opts.backLabel
18524
+ }]
18525
+ });
18526
+ if (isCancel(choice) || choice === "back") return;
18527
+ const chosen = CACHING_LEVERS.find((l) => l.value === choice);
18528
+ if (chosen) await opts.onLever(chosen);
18411
18529
  }
18412
18530
  }
18531
+ async function globalCachingMenu(opts) {
18532
+ const configPath = requireConfigPath(opts?.configPath);
18533
+ await runCachingLeverMenu({
18534
+ noteTitle: "Prompt caching",
18535
+ backLabel: "← Back",
18536
+ renderNote: () => formatGlobalCachingSummary(readConfigFileRaw(configPath)),
18537
+ onLever: (lever) => lever.global(configPath)
18538
+ });
18539
+ }
18540
+ /** Self-persists each lever; returns the latest override. */
18541
+ async function perModelCachingMenu(opts) {
18542
+ let current = opts.current;
18543
+ await runCachingLeverMenu({
18544
+ noteTitle: `Caching for "${opts.modelKey}"`,
18545
+ backLabel: "← Back to override edit",
18546
+ renderNote: () => formatPerModelCachingSummary(opts.modelKey, current, readConfigFile(opts.configPath)),
18547
+ onLever: async (lever) => {
18548
+ const next = await lever.perModel(current, opts.configPath);
18549
+ if (!overridesEqual(next, current)) {
18550
+ setModelOverride(opts.configPath, opts.modelKey, next);
18551
+ current = next;
18552
+ }
18553
+ }
18554
+ });
18555
+ return current;
18556
+ }
18557
+ async function cachingCommand(opts) {
18558
+ intro("Proxitor · Caching");
18559
+ await globalCachingMenu(opts);
18560
+ outro("Bye!");
18561
+ }
18562
+ //#endregion
18563
+ //#region src/commands/config/edit.ts
18564
+ function nvsHint(value) {
18565
+ if (value === void 0) return "(inherit)";
18566
+ return value ? "on" : "off";
18567
+ }
18568
+ function formatOverrideHint(override) {
18569
+ if (!override) return "(empty)";
18570
+ const parts = [];
18571
+ if (override.provider) {
18572
+ const keys = Object.keys(override.provider);
18573
+ parts.push(`provider: ${keys.join(", ")}`);
18574
+ }
18575
+ if (override.sessionId) parts.push(`session: ${override.sessionId}`);
18576
+ if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
18577
+ if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${nvsHint(override.normalizeVolatileSystem)}`);
18578
+ if (override.headers) parts.push(`${Object.keys(override.headers).length} header(s)`);
18579
+ return parts.join(", ") || "(empty)";
18580
+ }
18581
+ function formatCachingHint(current) {
18582
+ return `cc ${current.cacheControl ?? "inherit"} · ttl ${current.cacheControlTtl ? describeTtl(current.cacheControlTtl) : "inherit"} · sid ${current.sessionId ?? "inherit"} · nvs ${nvsHint(current.normalizeVolatileSystem)}`;
18583
+ }
18584
+ async function editProvider(modelKey, current, client) {
18585
+ const isPattern = modelKey.includes("*");
18586
+ const mode = await selectRoutingMode("Routing mode");
18587
+ if (isCancel(mode)) return current;
18588
+ if (mode === "skip") {
18589
+ const { provider: _, ...rest } = current;
18590
+ return rest;
18591
+ }
18592
+ const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern);
18593
+ if (!providerOptions) return current;
18594
+ const result = await selectProvidersByMode(mode, providerOptions);
18595
+ if (!result) return current;
18596
+ return {
18597
+ ...current,
18598
+ provider: result.provider
18599
+ };
18600
+ }
18413
18601
  /** Run the interactive "Edit model override" flow. */
18414
18602
  async function editOverrideCommand(client, configPath) {
18415
18603
  intro("Edit Model Override");
@@ -18432,7 +18620,6 @@ async function editOverrideCommand(client, configPath) {
18432
18620
  if (isCancel(selected)) return;
18433
18621
  const modelKey = selected;
18434
18622
  let current = overrides[modelKey] ?? {};
18435
- showCurrentConfig(modelKey, current);
18436
18623
  for (;;) {
18437
18624
  const field = await select({
18438
18625
  message: "Edit which field?",
@@ -18443,19 +18630,9 @@ async function editOverrideCommand(client, configPath) {
18443
18630
  hint: formatOverrideHint({ provider: current.provider })
18444
18631
  },
18445
18632
  {
18446
- value: "sessionId",
18447
- label: "Session ID",
18448
- hint: current.sessionId ?? "(inherit)"
18449
- },
18450
- {
18451
- value: "cacheControl",
18452
- label: "Cache control",
18453
- hint: formatCacheHint(current.cacheControl, current.cacheControlTtl)
18454
- },
18455
- {
18456
- value: "normalizeVolatileSystem",
18457
- label: "Normalize volatile system",
18458
- hint: nvsHint(current.normalizeVolatileSystem)
18633
+ value: "caching",
18634
+ label: "💾 Caching",
18635
+ hint: formatCachingHint(current)
18459
18636
  },
18460
18637
  {
18461
18638
  value: "done",
@@ -18464,15 +18641,19 @@ async function editOverrideCommand(client, configPath) {
18464
18641
  ]
18465
18642
  });
18466
18643
  if (isCancel(field) || field === "done") break;
18467
- current = await applyFieldEdit(field, modelKey, current, client, resolvedConfigPath);
18468
- }
18469
- const save = await confirm({ message: "Save changes?" });
18470
- if (isCancel(save) || !save) {
18471
- outro("Cancelled");
18472
- return;
18644
+ if (field === "caching") {
18645
+ current = await perModelCachingMenu({
18646
+ modelKey,
18647
+ current,
18648
+ configPath: resolvedConfigPath
18649
+ });
18650
+ continue;
18651
+ }
18652
+ const before = current;
18653
+ current = await editProvider(modelKey, current, client);
18654
+ if (!overridesEqual(current, before)) setModelOverride(resolvedConfigPath, modelKey, current);
18473
18655
  }
18474
- setModelOverride(resolvedConfigPath, modelKey, current);
18475
- outro("✓ Override updated");
18656
+ outro("✓ Done");
18476
18657
  }
18477
18658
  //#endregion
18478
18659
  //#region src/commands/config/list.ts
@@ -18998,31 +19179,6 @@ async function runWizard(opts = {}) {
18998
19179
  }
18999
19180
  }
19000
19181
  //#endregion
19001
- //#region src/commands/config/cache-control.ts
19002
- async function cacheControlCommand(opts) {
19003
- const configPath = requireConfigPath(opts?.configPath);
19004
- const cfg = readConfigFile(configPath);
19005
- const currentCc = cfg.cacheControl ?? DEFAULTS.cacheControl;
19006
- const currentTtl = cfg.cacheControlTtl;
19007
- log.info(`Current: cacheControl = ${currentCc}`);
19008
- if (currentTtl) log.info(`Current: cacheControlTtl = ${currentTtl}`);
19009
- const cc = await askTriState("Cache control mode", currentCc, CACHE_HINTS, { removable: true });
19010
- if (typeof cc === "symbol") return;
19011
- const fields = {};
19012
- fields.cacheControl = cc === "reset" ? void 0 : cc;
19013
- const ttlResult = await askCacheControlTtl(currentTtl, { removable: true });
19014
- if (typeof ttlResult === "symbol") {
19015
- setGlobalConfigFields(configPath, fields);
19016
- log.success(`cacheControl set to ${cc === "reset" ? "(default)" : cc}`);
19017
- return;
19018
- }
19019
- fields.cacheControlTtl = ttlResult === "reset" ? void 0 : ttlResult;
19020
- setGlobalConfigFields(configPath, fields);
19021
- const ccLabel = cc === "reset" ? "(default)" : cc;
19022
- const ttlLabel = ttlResult === "reset" ? "(default)" : ttlResult;
19023
- log.success(`cacheControl set to ${ccLabel}, TTL = ${ttlLabel}`);
19024
- }
19025
- //#endregion
19026
19182
  //#region src/commands/config/connection.ts
19027
19183
  const FIELD_MAP = {
19028
19184
  apiKey: {
@@ -19096,39 +19252,6 @@ async function connectionMenuCommand(opts) {
19096
19252
  log.success(`${entry.label} updated`);
19097
19253
  }
19098
19254
  //#endregion
19099
- //#region src/commands/config/normalize-system.ts
19100
- async function normalizeVolatileSystemCommand(opts) {
19101
- const configPath = requireConfigPath(opts?.configPath);
19102
- const current = readConfigFile(configPath).normalizeVolatileSystem ?? DEFAULTS.normalizeVolatileSystem;
19103
- log.info(`Current: normalizeVolatileSystem = ${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})`
19107
- });
19108
- if (typeof choice === "symbol") return;
19109
- const fields = {};
19110
- fields.normalizeVolatileSystem = choice === "reset" ? void 0 : choice;
19111
- setGlobalConfigFields(configPath, fields);
19112
- const label = choice === "reset" ? `(default: ${DEFAULTS.normalizeVolatileSystem})` : choice;
19113
- log.success(`normalizeVolatileSystem set to ${label}`);
19114
- }
19115
- //#endregion
19116
- //#region src/commands/config/session-routing.ts
19117
- async function sessionRoutingCommand(opts) {
19118
- const configPath = requireConfigPath(opts?.configPath);
19119
- const current = readConfigFile(configPath).sessionId ?? DEFAULTS.sessionId;
19120
- log.info(`Current: sessionId = ${current}`);
19121
- const result = await askTriState("Session routing mode", current, SESSION_HINTS, { removable: true });
19122
- if (typeof result === "symbol") return;
19123
- if (result === "reset") {
19124
- setGlobalConfigField(configPath, "sessionId", void 0);
19125
- log.success("sessionId reset to default (auto)");
19126
- return;
19127
- }
19128
- setGlobalConfigField(configPath, "sessionId", result);
19129
- log.success(`sessionId set to ${result}`);
19130
- }
19131
- //#endregion
19132
19255
  //#region src/commands/config.ts
19133
19256
  async function runConfigMenu(client) {
19134
19257
  intro("Proxitor Config Manager");
@@ -19150,16 +19273,8 @@ async function runConfigMenu(client) {
19150
19273
  label: "🔑 API key & connection"
19151
19274
  },
19152
19275
  {
19153
- value: "session",
19154
- label: "🔗 Session routing"
19155
- },
19156
- {
19157
- value: "cache",
19158
- label: "💾 Cache control"
19159
- },
19160
- {
19161
- value: "normalize",
19162
- label: "🧹 Normalize volatile system"
19276
+ value: "caching",
19277
+ label: "💾 Caching"
19163
19278
  },
19164
19279
  {
19165
19280
  value: "_sep2",
@@ -19212,14 +19327,8 @@ async function runConfigMenu(client) {
19212
19327
  case "connection":
19213
19328
  await connectionMenuCommand();
19214
19329
  break;
19215
- case "session":
19216
- await sessionRoutingCommand();
19217
- break;
19218
- case "cache":
19219
- await cacheControlCommand();
19220
- break;
19221
- case "normalize":
19222
- await normalizeVolatileSystemCommand();
19330
+ case "caching":
19331
+ await globalCachingMenu();
19223
19332
  break;
19224
19333
  case "add":
19225
19334
  await addOverrideCommand({ client });
@@ -19244,7 +19353,7 @@ async function runConfigMenu(client) {
19244
19353
  }
19245
19354
  //#endregion
19246
19355
  //#region src/version.ts
19247
- const version = "0.10.1";
19356
+ const version = "0.12.0";
19248
19357
  //#endregion
19249
19358
  //#region src/commands/doctor.ts
19250
19359
  const DEFAULT_TIMEOUT_MS = 3e3;
@@ -19482,6 +19591,167 @@ async function doctorCommand(opts = {}) {
19482
19591
  return exitCode;
19483
19592
  }
19484
19593
  //#endregion
19594
+ //#region src/config-source.ts
19595
+ /** Default watcher: fs.watchFile polling; returns a stop function. */
19596
+ const watchStat = (filename, pollIntervalMs, onChange) => {
19597
+ watchFile(filename, {
19598
+ interval: pollIntervalMs,
19599
+ persistent: false
19600
+ }, onChange);
19601
+ return () => unwatchFile(filename);
19602
+ };
19603
+ function fmt(value) {
19604
+ if (value === void 0) return "unset";
19605
+ if (value === true) return "on";
19606
+ if (value === false) return "off";
19607
+ return String(value);
19608
+ }
19609
+ /** Cache-lever keys shared by global config + per-model overrides. */
19610
+ const CACHE_LEVER_KEYS = [
19611
+ "cacheControl",
19612
+ "cacheControlTtl",
19613
+ "sessionId",
19614
+ "normalizeVolatileSystem"
19615
+ ];
19616
+ const SCALAR_KEYS = [
19617
+ ...CACHE_LEVER_KEYS,
19618
+ "authType",
19619
+ "verbose",
19620
+ "bodyLimit",
19621
+ "openrouterBaseUrl"
19622
+ ];
19623
+ function canonicalEntries(record) {
19624
+ if (!record) return "";
19625
+ return JSON.stringify(Object.keys(record).sort().map((key) => [key, record[key]]));
19626
+ }
19627
+ /** Field-level diff for a single model override; '' if nothing changed. */
19628
+ function overrideFieldDiff(prev, next) {
19629
+ const parts = [];
19630
+ for (const key of CACHE_LEVER_KEYS) if (prev?.[key] !== next?.[key]) parts.push(`${key}: ${fmt(prev?.[key])}→${fmt(next?.[key])}`);
19631
+ if (JSON.stringify(buildProviderRouting(prev?.provider)) !== JSON.stringify(buildProviderRouting(next?.provider))) parts.push("provider routing");
19632
+ if (canonicalEntries(prev?.headers) !== canonicalEntries(next?.headers)) parts.push("headers");
19633
+ return parts.join(", ");
19634
+ }
19635
+ /** Per-override diff: +added, -removed, `key (fields)` for modified; '' if unchanged. */
19636
+ function summarizeModelOverridesDiff(prev, next) {
19637
+ const prevKeys = new Set(Object.keys(prev ?? {}));
19638
+ const nextKeys = new Set(Object.keys(next ?? {}));
19639
+ const parts = [];
19640
+ for (const key of [...new Set([...prevKeys, ...nextKeys])].sort()) {
19641
+ const inPrev = prevKeys.has(key);
19642
+ const inNext = nextKeys.has(key);
19643
+ if (inPrev && inNext) {
19644
+ const fields = overrideFieldDiff(prev?.[key], next?.[key]);
19645
+ if (fields) parts.push(`${key} (${fields})`);
19646
+ } else if (inNext) parts.push(`+${key}`);
19647
+ else parts.push(`-${key}`);
19648
+ }
19649
+ return parts.join(", ");
19650
+ }
19651
+ /** Diff of cache-relevant fields; '' if nothing changed. */
19652
+ function summarizeChanges(prev, next) {
19653
+ const parts = [];
19654
+ for (const key of SCALAR_KEYS) if (prev[key] !== next[key]) parts.push(`${key}: ${fmt(prev[key])}→${fmt(next[key])}`);
19655
+ if (JSON.stringify(buildProviderRouting(prev.provider)) !== JSON.stringify(buildProviderRouting(next.provider))) parts.push("provider routing");
19656
+ const overridesDiff = summarizeModelOverridesDiff(prev.modelOverrides, next.modelOverrides);
19657
+ if (overridesDiff) parts.push(`modelOverrides: ${overridesDiff}`);
19658
+ if (canonicalEntries(prev.headers) !== canonicalEntries(next.headers)) parts.push("headers");
19659
+ return parts.join(", ");
19660
+ }
19661
+ function createConfigSource(options) {
19662
+ return new FileWatchingConfigSource(options);
19663
+ }
19664
+ var FileWatchingConfigSource = class {
19665
+ current;
19666
+ loadOptions;
19667
+ load;
19668
+ pollIntervalMs;
19669
+ watch;
19670
+ boundHost;
19671
+ boundPort;
19672
+ resolvedPath;
19673
+ stopWatch;
19674
+ loading = false;
19675
+ pending = false;
19676
+ watching = false;
19677
+ constructor(options) {
19678
+ this.current = options.initial;
19679
+ this.loadOptions = options.loadOptions;
19680
+ this.load = options.load ?? loadConfig;
19681
+ this.pollIntervalMs = options.pollIntervalMs ?? 1e3;
19682
+ this.watch = options.watch ?? watchStat;
19683
+ this.boundHost = options.initial.host;
19684
+ this.boundPort = options.initial.port;
19685
+ this.resolvedPath = options.loadOptions.noConfig ? null : tryFindConfigFile(options.loadOptions.configPath);
19686
+ }
19687
+ get() {
19688
+ return this.current;
19689
+ }
19690
+ async reload() {
19691
+ if (this.loading) {
19692
+ this.pending = true;
19693
+ return { ok: true };
19694
+ }
19695
+ this.loading = true;
19696
+ try {
19697
+ const next = await this.load(this.loadOptions);
19698
+ const restartNeeded = next.host !== this.boundHost || next.port !== this.boundPort;
19699
+ let diff = "";
19700
+ try {
19701
+ diff = summarizeChanges(this.current, next);
19702
+ } catch {
19703
+ diff = "";
19704
+ }
19705
+ this.current = next;
19706
+ if (restartNeeded) logger.warn("host/port changed — restart proxitor to apply (live reload does not re-bind the socket)");
19707
+ logger.info(`Config reloaded${diff ? ` — ${diff}` : " (no material changes)"}`);
19708
+ return { ok: true };
19709
+ } catch (error) {
19710
+ const msg = error instanceof Error ? error.message : String(error);
19711
+ logger.error(`Config reload failed — keeping previous config: ${msg}`);
19712
+ return {
19713
+ ok: false,
19714
+ error: msg
19715
+ };
19716
+ } finally {
19717
+ this.loading = false;
19718
+ if (this.pending) {
19719
+ this.pending = false;
19720
+ this.reload();
19721
+ }
19722
+ }
19723
+ }
19724
+ start() {
19725
+ if (this.watching) return;
19726
+ if (!this.resolvedPath) {
19727
+ logger.info("Live config reload disabled (no config file)");
19728
+ return;
19729
+ }
19730
+ this.watching = true;
19731
+ const path = this.resolvedPath;
19732
+ this.stopWatch = this.watch(path, this.pollIntervalMs, (curr, prev) => {
19733
+ try {
19734
+ this.onStat(path, curr, prev);
19735
+ } catch {}
19736
+ });
19737
+ }
19738
+ onStat(path, curr, prev) {
19739
+ if (curr.nlink === 0) {
19740
+ logger.warn(`config file disappeared — keeping current config (${path})`);
19741
+ return;
19742
+ }
19743
+ if (curr.mtimeMs === prev.mtimeMs) return;
19744
+ this.reload();
19745
+ }
19746
+ stop() {
19747
+ if (this.watching) {
19748
+ this.stopWatch?.();
19749
+ this.stopWatch = void 0;
19750
+ this.watching = false;
19751
+ }
19752
+ }
19753
+ };
19754
+ //#endregion
19485
19755
  //#region node_modules/.pnpm/@hono+node-server@2.0.4_hono@4.12.25/node_modules/@hono/node-server/dist/index.mjs
19486
19756
  var RequestError = class extends Error {
19487
19757
  constructor(message, options) {
@@ -22876,20 +23146,23 @@ const injectChain = [
22876
23146
  normalizeVolatileSystemMiddleware,
22877
23147
  injectSessionId
22878
23148
  ];
22879
- function createProxyServer(config, onReady) {
23149
+ function createProxyServer(source, onReady) {
22880
23150
  const app = new Hono();
22881
- const globalRouting = buildProviderRouting(config.provider);
22882
- const modelOverrideKeys = Object.keys(config.modelOverrides ?? []);
22883
23151
  app.use("*", async (c, next) => {
22884
- c.set("config", config);
23152
+ c.set("config", source.get());
22885
23153
  await next();
22886
23154
  });
22887
- app.get("/health", (c) => c.json({
22888
- ok: true,
22889
- upstream: config.openrouterBaseUrl,
22890
- provider: globalRouting ?? "not configured",
22891
- modelOverrides: modelOverrideKeys
22892
- }));
23155
+ app.get("/health", (c) => {
23156
+ const config = source.get();
23157
+ const globalRouting = buildProviderRouting(config.provider);
23158
+ const modelOverrideKeys = Object.keys(config.modelOverrides ?? []);
23159
+ return c.json({
23160
+ ok: true,
23161
+ upstream: config.openrouterBaseUrl,
23162
+ provider: globalRouting ?? "not configured",
23163
+ modelOverrides: modelOverrideKeys
23164
+ });
23165
+ });
22893
23166
  for (const path of INJECT_PATHS) app.post(path, setupRequest, readBody, ...injectChain, buildUpstreamReq, forwardRequest);
22894
23167
  app.all("*", setupRequest, readBody, resolveConfig, buildUpstreamReq, forwardRequest);
22895
23168
  app.onError((err, c) => {
@@ -22900,20 +23173,22 @@ function createProxyServer(config, onReady) {
22900
23173
  type: "proxy_internal_error"
22901
23174
  } }, { status: 500 });
22902
23175
  });
23176
+ const initial = source.get();
22903
23177
  return serve({
22904
23178
  fetch: app.fetch,
22905
- port: config.port,
22906
- hostname: config.host
23179
+ port: initial.port,
23180
+ hostname: initial.host
22907
23181
  }, onReady);
22908
23182
  }
22909
23183
  const SHUTDOWN_TIMEOUT_MS = 1e4;
22910
- function startProxyServer(config, onReady) {
22911
- const server = createProxyServer(config, onReady);
23184
+ function startProxyServer(source, onReady) {
23185
+ const server = createProxyServer(source, onReady);
22912
23186
  let shuttingDown = false;
22913
23187
  function shutdown(signal) {
22914
23188
  if (shuttingDown) return;
22915
23189
  shuttingDown = true;
22916
23190
  logger.info(`${signal} received — draining active connections…`);
23191
+ source.stop();
22917
23192
  const timer = setTimeout(() => {
22918
23193
  logger.warn("Forcing shutdown — drain timeout exceeded");
22919
23194
  process.exit(1);
@@ -23031,17 +23306,24 @@ const startCommand = (0, import_cjs.command)({
23031
23306
  },
23032
23307
  handler: async ({ configPath, port, host, noConfig, openrouterKey, verbose }) => {
23033
23308
  try {
23034
- const cfg = await loadConfig({
23309
+ const loadOptions = {
23035
23310
  configPath,
23036
23311
  noConfig,
23037
23312
  port,
23038
23313
  host,
23039
23314
  openrouterKey,
23040
23315
  verbose
23316
+ };
23317
+ const cfg = await loadConfig(loadOptions);
23318
+ const source = createConfigSource({
23319
+ loadOptions,
23320
+ initial: cfg
23041
23321
  });
23042
- startProxyServer(cfg, () => {
23322
+ source.start();
23323
+ startProxyServer(source, () => {
23043
23324
  logger.ready(`Proxitor proxy listening on ${cfg.host}:${cfg.port}`);
23044
23325
  logger.info("Routing requests to OpenRouter");
23326
+ if (source.resolvedPath) logger.info(`Watching ${source.resolvedPath} for changes (live reload)`);
23045
23327
  });
23046
23328
  } catch (error) {
23047
23329
  logger.error("Failed to start proxy:", error);
@@ -23082,6 +23364,14 @@ const configCli = (0, import_cjs.subcommands)({
23082
23364
  });
23083
23365
  }
23084
23366
  }),
23367
+ cache: (0, import_cjs.command)({
23368
+ name: "cache",
23369
+ description: "Tune prompt-caching settings (interactive)",
23370
+ args: { ...configArgs },
23371
+ handler: async (args) => {
23372
+ await cachingCommand({ configPath: args.configPath });
23373
+ }
23374
+ }),
23085
23375
  edit: (0, import_cjs.command)({
23086
23376
  name: "edit",
23087
23377
  description: "Edit an existing model override (interactive)",