proxitor 0.11.0 → 0.13.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,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";
@@ -16251,6 +16251,7 @@ const modelOverrideSchema = object({
16251
16251
  headers: record(string$1(), string$1()).optional(),
16252
16252
  cacheControl: triStateSchema.optional(),
16253
16253
  cacheControlTtl: ttlSchema.optional(),
16254
+ rewriteBlockTtl: triStateSchema.optional(),
16254
16255
  sessionId: triStateSchema.optional(),
16255
16256
  normalizeVolatileSystem: boolean().optional()
16256
16257
  }).strict();
@@ -16269,6 +16270,7 @@ const proxyConfigSchema = object({
16269
16270
  headers: record(string$1(), string$1()).optional(),
16270
16271
  cacheControl: triStateSchema.default("auto"),
16271
16272
  cacheControlTtl: ttlSchema.optional(),
16273
+ rewriteBlockTtl: triStateSchema.default("skip"),
16272
16274
  sessionId: triStateSchema.default("auto"),
16273
16275
  normalizeVolatileSystem: boolean().default(false),
16274
16276
  modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
@@ -16398,6 +16400,7 @@ function resolveModelConfig(config, modelName) {
16398
16400
  headers: config.headers ? { ...config.headers } : void 0,
16399
16401
  cacheControl: config.cacheControl,
16400
16402
  cacheControlTtl: config.cacheControlTtl,
16403
+ rewriteBlockTtl: config.rewriteBlockTtl,
16401
16404
  sessionId: config.sessionId,
16402
16405
  normalizeVolatileSystem: config.normalizeVolatileSystem
16403
16406
  };
@@ -16427,6 +16430,7 @@ function applyOverride(result, override) {
16427
16430
  };
16428
16431
  if (override.cacheControl !== void 0) result.cacheControl = override.cacheControl;
16429
16432
  if (override.cacheControlTtl !== void 0) result.cacheControlTtl = override.cacheControlTtl;
16433
+ if (override.rewriteBlockTtl !== void 0) result.rewriteBlockTtl = override.rewriteBlockTtl;
16430
16434
  if (override.sessionId !== void 0) result.sessionId = override.sessionId;
16431
16435
  if (override.normalizeVolatileSystem !== void 0) result.normalizeVolatileSystem = override.normalizeVolatileSystem;
16432
16436
  }
@@ -16491,18 +16495,29 @@ function findConfigFile(explicitPath) {
16491
16495
  if (found) return found;
16492
16496
  throw new MissingConfigError(getConfigSearchPaths());
16493
16497
  }
16494
- function readConfigFile(filePath) {
16498
+ function parseConfigFile(filePath) {
16495
16499
  const content = readFileSync(filePath, "utf-8");
16496
- let raw;
16497
16500
  try {
16498
- raw = filePath.endsWith(".json") ? JSON.parse(content) : (0, import_dist.parse)(content);
16501
+ return filePath.endsWith(".json") ? JSON.parse(content) : (0, import_dist.parse)(content);
16499
16502
  } catch (err) {
16500
16503
  throw new ConfigParseError(filePath, err instanceof Error ? err : void 0);
16501
16504
  }
16502
- const result = proxyConfigFileSchema.safeParse(raw);
16505
+ }
16506
+ function readConfigFile(filePath) {
16507
+ const result = proxyConfigFileSchema.safeParse(parseConfigFile(filePath));
16503
16508
  if (!result.success) throw new ConfigValidationError(filePath, result.error);
16504
16509
  return result.data;
16505
16510
  }
16511
+ /**
16512
+ * Validated like readConfigFile, but schema defaults are NOT applied, so absent
16513
+ * keys stay undefined — use when "absent" must differ from "explicit default".
16514
+ */
16515
+ function readConfigFileRaw(filePath) {
16516
+ const raw = parseConfigFile(filePath);
16517
+ const result = proxyConfigFileSchema.safeParse(raw);
16518
+ if (!result.success) throw new ConfigValidationError(filePath, result.error);
16519
+ return raw ?? {};
16520
+ }
16506
16521
  //#endregion
16507
16522
  //#region src/openrouter/cache.ts
16508
16523
  const CACHE_DIR = join(homedir(), ".cache", "proxitor");
@@ -17917,6 +17932,12 @@ const CACHE_HINTS = {
17917
17932
  always: "All models",
17918
17933
  skip: "Passthrough — leave client cache_control headers as-is"
17919
17934
  };
17935
+ /** Shared hint texts for the rewrite-block-ttl tri-state — used in cache-control command + override editor. */
17936
+ const REWRITE_HINTS = {
17937
+ auto: "Anthropic models only — normalize blocks to match root",
17938
+ always: "All models",
17939
+ skip: "Leave client block ttl as-is (may mismatch root)"
17940
+ };
17920
17941
  const NORMALIZE_HINTS = {
17921
17942
  on: "Rewrite cch → stable prefix cache",
17922
17943
  off: "Passthrough — rewrite nothing"
@@ -17938,7 +17959,7 @@ async function askNormalizeVolatileSystem(message, current, opts) {
17938
17959
  });
17939
17960
  return await select({
17940
17961
  message,
17941
- initialValue: current ?? false,
17962
+ initialValue: current ?? (opts?.removable ? "reset" : false),
17942
17963
  options
17943
17964
  });
17944
17965
  }
@@ -17967,7 +17988,7 @@ async function askTriState(message, current, hints, opts) {
17967
17988
  });
17968
17989
  return await select({
17969
17990
  message,
17970
- initialValue: current ?? "auto",
17991
+ initialValue: current ?? (opts?.removable ? "reset" : "auto"),
17971
17992
  options
17972
17993
  });
17973
17994
  }
@@ -18003,7 +18024,7 @@ async function askCacheControlTtl(current, opts) {
18003
18024
  });
18004
18025
  return await select({
18005
18026
  message: "Cache TTL",
18006
- initialValue: current ?? "5m",
18027
+ initialValue: current ?? (opts?.removable ? "reset" : "5m"),
18007
18028
  options
18008
18029
  });
18009
18030
  }
@@ -18016,14 +18037,17 @@ function applyField(obj, key, field) {
18016
18037
  else obj[key] = field.value;
18017
18038
  }
18018
18039
  async function collectSessionTriState(currentSid) {
18019
- const sid = await askTriState("Session ID mode", currentSid ?? "auto", SESSION_HINTS, { removable: true });
18040
+ const sid = await askTriState("Session ID mode", currentSid, SESSION_HINTS, { removable: true });
18020
18041
  if (typeof sid === "symbol") return null;
18021
18042
  if (sid === "reset") return { sessionId: { remove: true } };
18022
18043
  return { sessionId: { value: sid } };
18023
18044
  }
18024
- /** TTL is independent of cache mode. Mode-cancel aborts; TTL-cancel keeps existing TTL. */
18025
- async function collectCacheTriState(currentCc, currentTtl, globalTtl) {
18026
- const cc = await askTriState("Cache control mode", currentCc ?? "auto", CACHE_HINTS, { removable: true });
18045
+ /**
18046
+ * Collects cacheControl mode, TTL, and rewriteBlockTtl.
18047
+ * Mode-cancel aborts; TTL-cancel keeps existing TTL; rewrite-cancel keeps existing rewrite.
18048
+ */
18049
+ async function collectCacheTriState(currentCc, currentTtl, globalTtl, currentRewrite) {
18050
+ const cc = await askTriState("Cache control mode", currentCc, CACHE_HINTS, { removable: true });
18027
18051
  if (typeof cc === "symbol") return null;
18028
18052
  let cacheControl;
18029
18053
  if (cc === "reset") cacheControl = { remove: true };
@@ -18032,14 +18056,19 @@ async function collectCacheTriState(currentCc, currentTtl, globalTtl) {
18032
18056
  removable: true,
18033
18057
  globalTtl
18034
18058
  });
18035
- if (typeof ttl === "symbol") return { cacheControl };
18036
- if (ttl === "reset") return {
18037
- cacheControl,
18038
- cacheControlTtl: { remove: true }
18039
- };
18059
+ let cacheControlTtl;
18060
+ if (typeof ttl === "symbol") cacheControlTtl = void 0;
18061
+ else if (ttl === "reset") cacheControlTtl = { remove: true };
18062
+ else cacheControlTtl = { value: ttl };
18063
+ const rewrite = await askTriState("Rewrite block TTLs", currentRewrite, REWRITE_HINTS, { removable: true });
18064
+ let rewriteBlockTtl;
18065
+ if (typeof rewrite === "symbol") rewriteBlockTtl = void 0;
18066
+ else if (rewrite === "reset") rewriteBlockTtl = { remove: true };
18067
+ else rewriteBlockTtl = { value: rewrite };
18040
18068
  return {
18041
18069
  cacheControl,
18042
- cacheControlTtl: { value: ttl }
18070
+ cacheControlTtl,
18071
+ rewriteBlockTtl
18043
18072
  };
18044
18073
  }
18045
18074
  async function collectNormalizeVolatileSystem(currentNvs) {
@@ -18318,30 +18347,137 @@ async function browseModelsCommand(client) {
18318
18347
  });
18319
18348
  }
18320
18349
  //#endregion
18321
- //#region src/commands/config/edit.ts
18322
- function nvsHint(value) {
18323
- if (value === void 0) return "(inherit)";
18350
+ //#region src/commands/config/cache-control.ts
18351
+ /**
18352
+ * Map a ResolvedField into the `fields` object for `setGlobalConfigFields`:
18353
+ * {value}→assign value, {remove}→assign undefined (delete from disk),
18354
+ * undefined→omit the key (keep existing on disk).
18355
+ *
18356
+ * This is the file-write counterpart of `applyField`. The in-memory `applyField`
18357
+ * can `delete` an existing key, but a fresh `fields` object fed to
18358
+ * `setGlobalConfigFields` needs an explicit `undefined` value to trigger
18359
+ * removal — so keep-existing (undefined field) must be distinguished from
18360
+ * remove ({remove:true}) by omitting vs. assigning the key.
18361
+ */
18362
+ function assignField(fields, key, field) {
18363
+ if (field === void 0) return;
18364
+ fields[key] = "remove" in field ? void 0 : field.value;
18365
+ }
18366
+ function fieldLabel(field, fallback) {
18367
+ if (field === void 0) return fallback;
18368
+ if ("remove" in field) return "(default)";
18369
+ return String(field.value);
18370
+ }
18371
+ async function cacheControlCommand(opts) {
18372
+ const configPath = requireConfigPath(opts?.configPath);
18373
+ const cfg = readConfigFileRaw(configPath);
18374
+ const rawCc = cfg.cacheControl;
18375
+ const rawTtl = cfg.cacheControlTtl;
18376
+ const rawRewrite = cfg.rewriteBlockTtl;
18377
+ const effectiveCc = rawCc ?? DEFAULTS.cacheControl;
18378
+ const effectiveRewrite = rawRewrite ?? DEFAULTS.rewriteBlockTtl;
18379
+ log.info(`Current: cacheControl = ${rawCc === void 0 ? `(default -> ${effectiveCc})` : effectiveCc}`);
18380
+ if (rawTtl) log.info(`Current: cacheControlTtl = ${rawTtl}`);
18381
+ log.info(`Current: rewriteBlockTtl = ${rawRewrite === void 0 ? `(default -> ${effectiveRewrite})` : rawRewrite}`);
18382
+ const result = await collectCacheTriState(rawCc, rawTtl, void 0, rawRewrite);
18383
+ if (result === null) return;
18384
+ const fields = {};
18385
+ assignField(fields, "cacheControl", result.cacheControl);
18386
+ assignField(fields, "cacheControlTtl", result.cacheControlTtl);
18387
+ assignField(fields, "rewriteBlockTtl", result.rewriteBlockTtl);
18388
+ setGlobalConfigFields(configPath, fields);
18389
+ log.success(`cacheControl set to ${fieldLabel(result.cacheControl, effectiveCc)}, TTL = ${fieldLabel(result.cacheControlTtl, "(unchanged)")}, rewrite = ${fieldLabel(result.rewriteBlockTtl, effectiveRewrite)}`);
18390
+ }
18391
+ //#endregion
18392
+ //#region src/commands/config/caching-summary.ts
18393
+ /** Friendly TTL label (omit→strip, skip→passthrough). */
18394
+ function describeTtl(value) {
18395
+ if (value === void 0) return "(default)";
18396
+ if (value === "omit") return "strip";
18397
+ if (value === "skip") return "passthrough";
18398
+ return value;
18399
+ }
18400
+ /** Global NVS: distinguish absent (default-origin) from an explicit on/off. */
18401
+ function globalNvsLabel(value) {
18402
+ if (value === void 0) return `(default -> ${DEFAULTS.normalizeVolatileSystem ? "on" : "off"})`;
18324
18403
  return value ? "on" : "off";
18325
18404
  }
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)";
18405
+ /** Global tri-state: distinguish absent (default-origin) from an explicit mode. */
18406
+ function globalTriStateLabel(value, fallback) {
18407
+ return value === void 0 ? `(default -> ${fallback})` : value;
18338
18408
  }
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)";
18409
+ function nvsLabel(value, globalValue) {
18410
+ if (value === void 0) return `(inherit -> ${globalValue ? "on" : "off"})`;
18411
+ return value ? "on" : "off";
18412
+ }
18413
+ function perModelTtl(current, globalCfg) {
18414
+ if (current.cacheControlTtl !== void 0) return describeTtl(current.cacheControlTtl);
18415
+ if (globalCfg.cacheControlTtl !== void 0) return `(inherit -> ${describeTtl(globalCfg.cacheControlTtl)})`;
18416
+ return "(inherit)";
18417
+ }
18418
+ function formatGlobalCachingSummary(cfg) {
18419
+ const cc = globalTriStateLabel(cfg.cacheControl, DEFAULTS.cacheControl);
18420
+ const rewrite = globalTriStateLabel(cfg.rewriteBlockTtl, DEFAULTS.rewriteBlockTtl);
18421
+ const sid = globalTriStateLabel(cfg.sessionId, DEFAULTS.sessionId);
18422
+ const nvs = globalNvsLabel(cfg.normalizeVolatileSystem);
18423
+ return [
18424
+ "Three settings shape the request so cache survives.",
18425
+ "",
18426
+ ` cacheControl = ${cc} (activate cache)`,
18427
+ ` cacheControlTtl = ${describeTtl(cfg.cacheControlTtl)}`,
18428
+ ` rewriteBlockTtl = ${rewrite} (normalize block ttl)`,
18429
+ ` sessionId = ${sid} (pin provider)`,
18430
+ ` normalizeVolatileSystem = ${nvs} (stable prefix)`,
18431
+ "",
18432
+ "Anthropic -> levers 1+2 - non-Anthropic (qwen/glm) -> all 3"
18433
+ ].join("\n");
18434
+ }
18435
+ function formatPerModelCachingSummary(modelKey, current, globalCfg) {
18436
+ const gCc = globalCfg.cacheControl ?? DEFAULTS.cacheControl;
18437
+ const gRewrite = globalCfg.rewriteBlockTtl ?? DEFAULTS.rewriteBlockTtl;
18438
+ const gSid = globalCfg.sessionId ?? DEFAULTS.sessionId;
18439
+ const gNvs = globalCfg.normalizeVolatileSystem ?? DEFAULTS.normalizeVolatileSystem;
18440
+ const cc = current.cacheControl ?? `(inherit -> ${gCc})`;
18441
+ const ttl = perModelTtl(current, globalCfg);
18442
+ const rewrite = current.rewriteBlockTtl ?? `(inherit -> ${gRewrite})`;
18443
+ const sid = current.sessionId ?? `(inherit -> ${gSid})`;
18444
+ const nvs = nvsLabel(current.normalizeVolatileSystem, gNvs);
18445
+ return [
18446
+ `Caching for "${modelKey}"`,
18447
+ "",
18448
+ ` cacheControl = ${cc}`,
18449
+ ` cacheControlTtl = ${ttl}`,
18450
+ ` rewriteBlockTtl = ${rewrite}`,
18451
+ ` sessionId = ${sid}`,
18452
+ ` normalizeVolatileSystem = ${nvs}`
18453
+ ].join("\n");
18454
+ }
18455
+ //#endregion
18456
+ //#region src/commands/config/equality.ts
18457
+ /** Deep equality for overrides — gates no-op writes. */
18458
+ function overridesEqual(a, b) {
18459
+ return isDeepStrictEqual(a, b);
18460
+ }
18461
+ //#endregion
18462
+ //#region src/commands/config/normalize-system.ts
18463
+ async function normalizeVolatileSystemCommand(opts) {
18464
+ const configPath = requireConfigPath(opts?.configPath);
18465
+ const raw = readConfigFileRaw(configPath).normalizeVolatileSystem;
18466
+ const effective = raw ?? DEFAULTS.normalizeVolatileSystem;
18467
+ log.info(`Current: normalizeVolatileSystem = ${raw === void 0 ? `(default -> ${effective ? "on" : "off"})` : effective}`);
18468
+ 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, {
18469
+ removable: true,
18470
+ resetHint: `remove (default: ${DEFAULTS.normalizeVolatileSystem})`
18471
+ });
18472
+ if (typeof choice === "symbol") return;
18473
+ const fields = {};
18474
+ fields.normalizeVolatileSystem = choice === "reset" ? void 0 : choice;
18475
+ setGlobalConfigFields(configPath, fields);
18476
+ const label = choice === "reset" ? `(default: ${DEFAULTS.normalizeVolatileSystem})` : choice;
18477
+ log.success(`normalizeVolatileSystem set to ${label}`);
18344
18478
  }
18479
+ //#endregion
18480
+ //#region src/commands/config/override-levers.ts
18345
18481
  function readGlobalTtl(configPath) {
18346
18482
  if (!configPath) return void 0;
18347
18483
  try {
@@ -18350,32 +18486,6 @@ function readGlobalTtl(configPath) {
18350
18486
  return;
18351
18487
  }
18352
18488
  }
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
18489
  async function editSessionId(current) {
18380
18490
  const result = await collectSessionTriState(current.sessionId);
18381
18491
  if (result === null) return current;
@@ -18383,17 +18493,16 @@ async function editSessionId(current) {
18383
18493
  applyField(next, "sessionId", result.sessionId);
18384
18494
  return next;
18385
18495
  }
18386
- /** @internal */
18387
18496
  async function editCacheControl(current, configPath) {
18388
18497
  const globalTtl = readGlobalTtl(configPath);
18389
- const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl, globalTtl);
18498
+ const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl, globalTtl, current.rewriteBlockTtl);
18390
18499
  if (result === null) return current;
18391
18500
  const next = { ...current };
18392
18501
  applyField(next, "cacheControl", result.cacheControl);
18393
18502
  applyField(next, "cacheControlTtl", result.cacheControlTtl);
18503
+ applyField(next, "rewriteBlockTtl", result.rewriteBlockTtl);
18394
18504
  return next;
18395
18505
  }
18396
- /** @internal */
18397
18506
  async function editNormalizeVolatileSystem(current) {
18398
18507
  const result = await collectNormalizeVolatileSystem(current.normalizeVolatileSystem);
18399
18508
  if (result === null) return current;
@@ -18401,15 +18510,134 @@ async function editNormalizeVolatileSystem(current) {
18401
18510
  applyField(next, "normalizeVolatileSystem", result.normalizeVolatileSystem);
18402
18511
  return next;
18403
18512
  }
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;
18513
+ //#endregion
18514
+ //#region src/commands/config/session-routing.ts
18515
+ async function sessionRoutingCommand(opts) {
18516
+ const configPath = requireConfigPath(opts?.configPath);
18517
+ const raw = readConfigFileRaw(configPath).sessionId;
18518
+ const effective = raw ?? DEFAULTS.sessionId;
18519
+ log.info(`Current: sessionId = ${raw === void 0 ? `(default -> ${effective})` : effective}`);
18520
+ const result = await askTriState("Session routing mode", raw, SESSION_HINTS, { removable: true });
18521
+ if (typeof result === "symbol") return;
18522
+ if (result === "reset") {
18523
+ setGlobalConfigField(configPath, "sessionId", void 0);
18524
+ log.success("sessionId reset to default (auto)");
18525
+ return;
18526
+ }
18527
+ setGlobalConfigField(configPath, "sessionId", result);
18528
+ log.success(`sessionId set to ${result}`);
18529
+ }
18530
+ //#endregion
18531
+ //#region src/commands/config/caching-menu.ts
18532
+ /** Cache levers: option list + dispatch in one table. */
18533
+ const CACHING_LEVERS = [
18534
+ {
18535
+ value: "cacheControl",
18536
+ label: "Activate caching — mode + TTL (cacheControl)",
18537
+ global: (configPath) => cacheControlCommand({ configPath }),
18538
+ perModel: (current, configPath) => editCacheControl(current, configPath)
18539
+ },
18540
+ {
18541
+ value: "sessionId",
18542
+ label: "Pin provider (sessionId)",
18543
+ global: (configPath) => sessionRoutingCommand({ configPath }),
18544
+ perModel: (current) => editSessionId(current)
18545
+ },
18546
+ {
18547
+ value: "normalizeVolatileSystem",
18548
+ label: "Stabilize prefix (normalizeVolatileSystem)",
18549
+ global: (configPath) => normalizeVolatileSystemCommand({ configPath }),
18550
+ perModel: (current) => editNormalizeVolatileSystem(current)
18551
+ }
18552
+ ];
18553
+ async function runCachingLeverMenu(opts) {
18554
+ for (;;) {
18555
+ note(opts.renderNote(), opts.noteTitle);
18556
+ const choice = await select({
18557
+ message: "Tune which lever?",
18558
+ options: [...CACHING_LEVERS.map((lever) => ({
18559
+ value: lever.value,
18560
+ label: lever.label
18561
+ })), {
18562
+ value: "back",
18563
+ label: opts.backLabel
18564
+ }]
18565
+ });
18566
+ if (isCancel(choice) || choice === "back") return;
18567
+ const chosen = CACHING_LEVERS.find((l) => l.value === choice);
18568
+ if (chosen) await opts.onLever(chosen);
18411
18569
  }
18412
18570
  }
18571
+ async function globalCachingMenu(opts) {
18572
+ const configPath = requireConfigPath(opts?.configPath);
18573
+ await runCachingLeverMenu({
18574
+ noteTitle: "Prompt caching",
18575
+ backLabel: "← Back",
18576
+ renderNote: () => formatGlobalCachingSummary(readConfigFileRaw(configPath)),
18577
+ onLever: (lever) => lever.global(configPath)
18578
+ });
18579
+ }
18580
+ /** Self-persists each lever; returns the latest override. */
18581
+ async function perModelCachingMenu(opts) {
18582
+ let current = opts.current;
18583
+ await runCachingLeverMenu({
18584
+ noteTitle: `Caching for "${opts.modelKey}"`,
18585
+ backLabel: "← Back to override edit",
18586
+ renderNote: () => formatPerModelCachingSummary(opts.modelKey, current, readConfigFile(opts.configPath)),
18587
+ onLever: async (lever) => {
18588
+ const next = await lever.perModel(current, opts.configPath);
18589
+ if (!overridesEqual(next, current)) {
18590
+ setModelOverride(opts.configPath, opts.modelKey, next);
18591
+ current = next;
18592
+ }
18593
+ }
18594
+ });
18595
+ return current;
18596
+ }
18597
+ async function cachingCommand(opts) {
18598
+ intro("Proxitor · Caching");
18599
+ await globalCachingMenu(opts);
18600
+ outro("Bye!");
18601
+ }
18602
+ //#endregion
18603
+ //#region src/commands/config/edit.ts
18604
+ function nvsHint(value) {
18605
+ if (value === void 0) return "(inherit)";
18606
+ return value ? "on" : "off";
18607
+ }
18608
+ function formatOverrideHint(override) {
18609
+ if (!override) return "(empty)";
18610
+ const parts = [];
18611
+ if (override.provider) {
18612
+ const keys = Object.keys(override.provider);
18613
+ parts.push(`provider: ${keys.join(", ")}`);
18614
+ }
18615
+ if (override.sessionId) parts.push(`session: ${override.sessionId}`);
18616
+ if (override.cacheControl) parts.push(`cache: ${override.cacheControl}`);
18617
+ if (override.normalizeVolatileSystem !== void 0) parts.push(`normalize: ${nvsHint(override.normalizeVolatileSystem)}`);
18618
+ if (override.headers) parts.push(`${Object.keys(override.headers).length} header(s)`);
18619
+ return parts.join(", ") || "(empty)";
18620
+ }
18621
+ function formatCachingHint(current) {
18622
+ return `cc ${current.cacheControl ?? "inherit"} · ttl ${current.cacheControlTtl ? describeTtl(current.cacheControlTtl) : "inherit"} · sid ${current.sessionId ?? "inherit"} · nvs ${nvsHint(current.normalizeVolatileSystem)}`;
18623
+ }
18624
+ async function editProvider(modelKey, current, client) {
18625
+ const isPattern = modelKey.includes("*");
18626
+ const mode = await selectRoutingMode("Routing mode");
18627
+ if (isCancel(mode)) return current;
18628
+ if (mode === "skip") {
18629
+ const { provider: _, ...rest } = current;
18630
+ return rest;
18631
+ }
18632
+ const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern);
18633
+ if (!providerOptions) return current;
18634
+ const result = await selectProvidersByMode(mode, providerOptions);
18635
+ if (!result) return current;
18636
+ return {
18637
+ ...current,
18638
+ provider: result.provider
18639
+ };
18640
+ }
18413
18641
  /** Run the interactive "Edit model override" flow. */
18414
18642
  async function editOverrideCommand(client, configPath) {
18415
18643
  intro("Edit Model Override");
@@ -18432,7 +18660,6 @@ async function editOverrideCommand(client, configPath) {
18432
18660
  if (isCancel(selected)) return;
18433
18661
  const modelKey = selected;
18434
18662
  let current = overrides[modelKey] ?? {};
18435
- showCurrentConfig(modelKey, current);
18436
18663
  for (;;) {
18437
18664
  const field = await select({
18438
18665
  message: "Edit which field?",
@@ -18443,19 +18670,9 @@ async function editOverrideCommand(client, configPath) {
18443
18670
  hint: formatOverrideHint({ provider: current.provider })
18444
18671
  },
18445
18672
  {
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)
18673
+ value: "caching",
18674
+ label: "💾 Caching",
18675
+ hint: formatCachingHint(current)
18459
18676
  },
18460
18677
  {
18461
18678
  value: "done",
@@ -18464,15 +18681,19 @@ async function editOverrideCommand(client, configPath) {
18464
18681
  ]
18465
18682
  });
18466
18683
  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;
18684
+ if (field === "caching") {
18685
+ current = await perModelCachingMenu({
18686
+ modelKey,
18687
+ current,
18688
+ configPath: resolvedConfigPath
18689
+ });
18690
+ continue;
18691
+ }
18692
+ const before = current;
18693
+ current = await editProvider(modelKey, current, client);
18694
+ if (!overridesEqual(current, before)) setModelOverride(resolvedConfigPath, modelKey, current);
18473
18695
  }
18474
- setModelOverride(resolvedConfigPath, modelKey, current);
18475
- outro("✓ Override updated");
18696
+ outro("✓ Done");
18476
18697
  }
18477
18698
  //#endregion
18478
18699
  //#region src/commands/config/list.ts
@@ -18998,31 +19219,6 @@ async function runWizard(opts = {}) {
18998
19219
  }
18999
19220
  }
19000
19221
  //#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
19222
  //#region src/commands/config/connection.ts
19027
19223
  const FIELD_MAP = {
19028
19224
  apiKey: {
@@ -19096,39 +19292,6 @@ async function connectionMenuCommand(opts) {
19096
19292
  log.success(`${entry.label} updated`);
19097
19293
  }
19098
19294
  //#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
19295
  //#region src/commands/config.ts
19133
19296
  async function runConfigMenu(client) {
19134
19297
  intro("Proxitor Config Manager");
@@ -19150,16 +19313,8 @@ async function runConfigMenu(client) {
19150
19313
  label: "🔑 API key & connection"
19151
19314
  },
19152
19315
  {
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"
19316
+ value: "caching",
19317
+ label: "💾 Caching"
19163
19318
  },
19164
19319
  {
19165
19320
  value: "_sep2",
@@ -19212,14 +19367,8 @@ async function runConfigMenu(client) {
19212
19367
  case "connection":
19213
19368
  await connectionMenuCommand();
19214
19369
  break;
19215
- case "session":
19216
- await sessionRoutingCommand();
19217
- break;
19218
- case "cache":
19219
- await cacheControlCommand();
19220
- break;
19221
- case "normalize":
19222
- await normalizeVolatileSystemCommand();
19370
+ case "caching":
19371
+ await globalCachingMenu();
19223
19372
  break;
19224
19373
  case "add":
19225
19374
  await addOverrideCommand({ client });
@@ -19244,7 +19393,7 @@ async function runConfigMenu(client) {
19244
19393
  }
19245
19394
  //#endregion
19246
19395
  //#region src/version.ts
19247
- const version = "0.11.0";
19396
+ const version = "0.13.0";
19248
19397
  //#endregion
19249
19398
  //#region src/commands/doctor.ts
19250
19399
  const DEFAULT_TIMEOUT_MS = 3e3;
@@ -19497,11 +19646,15 @@ function fmt(value) {
19497
19646
  if (value === false) return "off";
19498
19647
  return String(value);
19499
19648
  }
19500
- const SCALAR_KEYS = [
19649
+ /** Cache-lever keys shared by global config + per-model overrides. */
19650
+ const CACHE_LEVER_KEYS = [
19501
19651
  "cacheControl",
19502
19652
  "cacheControlTtl",
19503
19653
  "sessionId",
19504
- "normalizeVolatileSystem",
19654
+ "normalizeVolatileSystem"
19655
+ ];
19656
+ const SCALAR_KEYS = [
19657
+ ...CACHE_LEVER_KEYS,
19505
19658
  "authType",
19506
19659
  "verbose",
19507
19660
  "bodyLimit",
@@ -19511,16 +19664,37 @@ function canonicalEntries(record) {
19511
19664
  if (!record) return "";
19512
19665
  return JSON.stringify(Object.keys(record).sort().map((key) => [key, record[key]]));
19513
19666
  }
19667
+ /** Field-level diff for a single model override; '' if nothing changed. */
19668
+ function overrideFieldDiff(prev, next) {
19669
+ const parts = [];
19670
+ for (const key of CACHE_LEVER_KEYS) if (prev?.[key] !== next?.[key]) parts.push(`${key}: ${fmt(prev?.[key])}→${fmt(next?.[key])}`);
19671
+ if (JSON.stringify(buildProviderRouting(prev?.provider)) !== JSON.stringify(buildProviderRouting(next?.provider))) parts.push("provider routing");
19672
+ if (canonicalEntries(prev?.headers) !== canonicalEntries(next?.headers)) parts.push("headers");
19673
+ return parts.join(", ");
19674
+ }
19675
+ /** Per-override diff: +added, -removed, `key (fields)` for modified; '' if unchanged. */
19676
+ function summarizeModelOverridesDiff(prev, next) {
19677
+ const prevKeys = new Set(Object.keys(prev ?? {}));
19678
+ const nextKeys = new Set(Object.keys(next ?? {}));
19679
+ const parts = [];
19680
+ for (const key of [...new Set([...prevKeys, ...nextKeys])].sort()) {
19681
+ const inPrev = prevKeys.has(key);
19682
+ const inNext = nextKeys.has(key);
19683
+ if (inPrev && inNext) {
19684
+ const fields = overrideFieldDiff(prev?.[key], next?.[key]);
19685
+ if (fields) parts.push(`${key} (${fields})`);
19686
+ } else if (inNext) parts.push(`+${key}`);
19687
+ else parts.push(`-${key}`);
19688
+ }
19689
+ return parts.join(", ");
19690
+ }
19514
19691
  /** Diff of cache-relevant fields; '' if nothing changed. */
19515
19692
  function summarizeChanges(prev, next) {
19516
19693
  const parts = [];
19517
19694
  for (const key of SCALAR_KEYS) if (prev[key] !== next[key]) parts.push(`${key}: ${fmt(prev[key])}→${fmt(next[key])}`);
19518
19695
  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
- }
19696
+ const overridesDiff = summarizeModelOverridesDiff(prev.modelOverrides, next.modelOverrides);
19697
+ if (overridesDiff) parts.push(`modelOverrides: ${overridesDiff}`);
19524
19698
  if (canonicalEntries(prev.headers) !== canonicalEntries(next.headers)) parts.push("headers");
19525
19699
  return parts.join(", ");
19526
19700
  }
@@ -22771,6 +22945,56 @@ function buildCacheControl(existing, ttl, isAnthropic) {
22771
22945
  }
22772
22946
  return base;
22773
22947
  }
22948
+ /**
22949
+ * Whether to normalize block-level cache_control TTLs. Mirrors the cacheControl
22950
+ * tri-state: `auto` rewrites only when injection is active on an Anthropic-native
22951
+ * endpoint; `always` rewrites everywhere; `skip` never.
22952
+ */
22953
+ function shouldRewriteBlockTtl(mode, cacheControlMode, modelName, path) {
22954
+ if (mode === "skip") return false;
22955
+ if (mode === "always") return true;
22956
+ return isAnthropicEndpoint(modelName, path) && shouldInjectCacheControl(cacheControlMode, modelName, path);
22957
+ }
22958
+ /** Shallow, order-independent comparison of two cache_control objects. */
22959
+ function sameCacheControl(a, b) {
22960
+ const ka = Object.keys(a);
22961
+ const kb = Object.keys(b);
22962
+ if (ka.length !== kb.length) return false;
22963
+ return ka.every((k) => a[k] === b[k]);
22964
+ }
22965
+ /**
22966
+ * Normalize the TTL on every existing block-level cache_control breakpoint
22967
+ * (system, tools, messages[].content) to match the configured TTL. Reuses
22968
+ * buildCacheControl so block and root stay consistent. Adds no new breakpoints.
22969
+ * Returns whether any block was changed.
22970
+ */
22971
+ function rewriteBlockTtls(body, ttl, isAnthropic) {
22972
+ let mutated = false;
22973
+ const rewriteNode = (obj) => {
22974
+ const cc = obj.cache_control;
22975
+ if (cc !== null && typeof cc === "object" && !Array.isArray(cc)) {
22976
+ const next = buildCacheControl(cc, ttl, isAnthropic);
22977
+ if (!sameCacheControl(cc, next)) {
22978
+ obj.cache_control = next;
22979
+ mutated = true;
22980
+ }
22981
+ }
22982
+ };
22983
+ const visitBlocks = (arr) => {
22984
+ if (!Array.isArray(arr)) return;
22985
+ for (const item of arr) {
22986
+ if (item === null || typeof item !== "object" || Array.isArray(item)) continue;
22987
+ const obj = item;
22988
+ rewriteNode(obj);
22989
+ visitBlocks(obj.content);
22990
+ }
22991
+ };
22992
+ visitBlocks(body.system);
22993
+ visitBlocks(body.tools);
22994
+ const messages = body.messages;
22995
+ for (const m of messages ?? []) visitBlocks(m?.content);
22996
+ return mutated;
22997
+ }
22774
22998
  //#endregion
22775
22999
  //#region src/proxy/middleware/inject-cache-control.ts
22776
23000
  const injectCacheControl = createMiddleware(async (c, next) => {
@@ -22780,11 +23004,16 @@ const injectCacheControl = createMiddleware(async (c, next) => {
22780
23004
  await next();
22781
23005
  return;
22782
23006
  }
23007
+ const isAnthropic = isAnthropicEndpoint(c.var.modelName, c.req.path);
23008
+ let mutated = false;
22783
23009
  if (shouldInjectCacheControl(resolved.cacheControl, c.var.modelName, c.req.path)) {
22784
- const isAnthropic = isAnthropicEndpoint(c.var.modelName, c.req.path);
22785
23010
  parsedBody.cache_control = buildCacheControl(parsedBody.cache_control, resolved.cacheControlTtl, isAnthropic);
22786
- c.set("bodyMutated", true);
23011
+ mutated = true;
22787
23012
  }
23013
+ if (shouldRewriteBlockTtl(resolved.rewriteBlockTtl, resolved.cacheControl, c.var.modelName, c.req.path)) {
23014
+ if (rewriteBlockTtls(parsedBody, resolved.cacheControlTtl, isAnthropic)) mutated = true;
23015
+ }
23016
+ if (mutated) c.set("bodyMutated", true);
22788
23017
  await next();
22789
23018
  });
22790
23019
  //#endregion
@@ -23230,6 +23459,14 @@ const configCli = (0, import_cjs.subcommands)({
23230
23459
  });
23231
23460
  }
23232
23461
  }),
23462
+ cache: (0, import_cjs.command)({
23463
+ name: "cache",
23464
+ description: "Tune prompt-caching settings (interactive)",
23465
+ args: { ...configArgs },
23466
+ handler: async (args) => {
23467
+ await cachingCommand({ configPath: args.configPath });
23468
+ }
23469
+ }),
23233
23470
  edit: (0, import_cjs.command)({
23234
23471
  name: "edit",
23235
23472
  description: "Edit an existing model override (interactive)",