proxitor 0.11.0 → 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/README.md CHANGED
@@ -111,6 +111,16 @@ While proxitor runs, it logs cache usage from upstream so you can see whether ca
111
111
 
112
112
  Quick health poke: `curl http://localhost:8828/health`.
113
113
 
114
+ ### Tuning the cache
115
+
116
+ If the cache hit looks low, three levers fix it — tune them from `proxitor config` → **💾 Caching** (or `proxitor config cache`):
117
+
118
+ - **`cacheControl`** — inject `cache_control` to activate caching (Anthropic-native).
119
+ - **`sessionId`** — inject `session_id` so the provider pins from the first request.
120
+ - **`normalizeVolatileSystem`** — strip Claude Code's volatile `cch` hash so the prefix cache warms on non-Anthropic providers (qwen/glm/…).
121
+
122
+ See the [configuration reference](./docs/configuration.md#prompt-caching) for the full detail.
123
+
114
124
  ## Commands
115
125
 
116
126
  | Command | Description |
package/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ 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
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
@@ -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;
18338
18374
  }
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)";
18375
+ function nvsLabel(value, globalValue) {
18376
+ if (value === void 0) return `(inherit -> ${globalValue ? "on" : "off"})`;
18377
+ return value ? "on" : "off";
18378
+ }
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)";
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");
18344
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,14 +18470,133 @@ 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);
18529
+ }
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;
18411
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
+ };
18412
18600
  }
18413
18601
  /** Run the interactive "Edit model override" flow. */
18414
18602
  async function editOverrideCommand(client, configPath) {
@@ -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.11.0";
19356
+ const version = "0.12.0";
19248
19357
  //#endregion
19249
19358
  //#region src/commands/doctor.ts
19250
19359
  const DEFAULT_TIMEOUT_MS = 3e3;
@@ -19497,11 +19606,15 @@ function fmt(value) {
19497
19606
  if (value === false) return "off";
19498
19607
  return String(value);
19499
19608
  }
19500
- const SCALAR_KEYS = [
19609
+ /** Cache-lever keys shared by global config + per-model overrides. */
19610
+ const CACHE_LEVER_KEYS = [
19501
19611
  "cacheControl",
19502
19612
  "cacheControlTtl",
19503
19613
  "sessionId",
19504
- "normalizeVolatileSystem",
19614
+ "normalizeVolatileSystem"
19615
+ ];
19616
+ const SCALAR_KEYS = [
19617
+ ...CACHE_LEVER_KEYS,
19505
19618
  "authType",
19506
19619
  "verbose",
19507
19620
  "bodyLimit",
@@ -19511,16 +19624,37 @@ function canonicalEntries(record) {
19511
19624
  if (!record) return "";
19512
19625
  return JSON.stringify(Object.keys(record).sort().map((key) => [key, record[key]]));
19513
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
+ }
19514
19651
  /** Diff of cache-relevant fields; '' if nothing changed. */
19515
19652
  function summarizeChanges(prev, next) {
19516
19653
  const parts = [];
19517
19654
  for (const key of SCALAR_KEYS) if (prev[key] !== next[key]) parts.push(`${key}: ${fmt(prev[key])}→${fmt(next[key])}`);
19518
19655
  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
- }
19656
+ const overridesDiff = summarizeModelOverridesDiff(prev.modelOverrides, next.modelOverrides);
19657
+ if (overridesDiff) parts.push(`modelOverrides: ${overridesDiff}`);
19524
19658
  if (canonicalEntries(prev.headers) !== canonicalEntries(next.headers)) parts.push("headers");
19525
19659
  return parts.join(", ");
19526
19660
  }
@@ -23230,6 +23364,14 @@ const configCli = (0, import_cjs.subcommands)({
23230
23364
  });
23231
23365
  }
23232
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
+ }),
23233
23375
  edit: (0, import_cjs.command)({
23234
23376
  name: "edit",
23235
23377
  description: "Edit an existing model override (interactive)",