proxitor 0.12.0 → 0.14.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
@@ -117,7 +117,8 @@ If the cache hit looks low, three levers fix it — tune them from `proxitor con
117
117
 
118
118
  - **`cacheControl`** — inject `cache_control` to activate caching (Anthropic-native).
119
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/…).
120
+ - **`normalizeVolatileSystem`** — strip Claude Code's volatile `cch`/`cc_version` hashes so the prefix cache warms on non-Anthropic providers (qwen/glm/…).
121
+ - **`rewriteBlockTtl`** — normalize the TTL on Claude Code's block `cache_control` breakpoints to match your `cacheControlTtl`. Enable it (`auto`/`always`) if Anthropic rejects requests where the root `ttl` is `1h` but the block breakpoints stay at `5m`.
121
122
 
122
123
  See the [configuration reference](./docs/configuration.md#prompt-caching) for the full detail.
123
124
 
package/dist/cli.mjs CHANGED
@@ -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
  }
@@ -17928,6 +17932,12 @@ const CACHE_HINTS = {
17928
17932
  always: "All models",
17929
17933
  skip: "Passthrough — leave client cache_control headers as-is"
17930
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
+ };
17931
17941
  const NORMALIZE_HINTS = {
17932
17942
  on: "Rewrite cch → stable prefix cache",
17933
17943
  off: "Passthrough — rewrite nothing"
@@ -18032,8 +18042,11 @@ async function collectSessionTriState(currentSid) {
18032
18042
  if (sid === "reset") return { sessionId: { remove: true } };
18033
18043
  return { sessionId: { value: sid } };
18034
18044
  }
18035
- /** TTL is independent of cache mode. Mode-cancel aborts; TTL-cancel keeps existing TTL. */
18036
- async function collectCacheTriState(currentCc, currentTtl, globalTtl) {
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) {
18037
18050
  const cc = await askTriState("Cache control mode", currentCc, CACHE_HINTS, { removable: true });
18038
18051
  if (typeof cc === "symbol") return null;
18039
18052
  let cacheControl;
@@ -18043,18 +18056,23 @@ async function collectCacheTriState(currentCc, currentTtl, globalTtl) {
18043
18056
  removable: true,
18044
18057
  globalTtl
18045
18058
  });
18046
- if (typeof ttl === "symbol") return { cacheControl };
18047
- if (ttl === "reset") return {
18048
- cacheControl,
18049
- cacheControlTtl: { remove: true }
18050
- };
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 };
18051
18068
  return {
18052
18069
  cacheControl,
18053
- cacheControlTtl: { value: ttl }
18070
+ cacheControlTtl,
18071
+ rewriteBlockTtl
18054
18072
  };
18055
18073
  }
18056
18074
  async function collectNormalizeVolatileSystem(currentNvs) {
18057
- const nvs = await askNormalizeVolatileSystem("Normalize volatile system (cch hash)", currentNvs, { removable: true });
18075
+ const nvs = await askNormalizeVolatileSystem("Normalize volatile system (cch/cc_version hashes)", currentNvs, { removable: true });
18058
18076
  if (typeof nvs === "symbol") return null;
18059
18077
  if (nvs === "reset") return { normalizeVolatileSystem: { remove: true } };
18060
18078
  return { normalizeVolatileSystem: { value: nvs } };
@@ -18330,29 +18348,45 @@ async function browseModelsCommand(client) {
18330
18348
  }
18331
18349
  //#endregion
18332
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
+ }
18333
18371
  async function cacheControlCommand(opts) {
18334
18372
  const configPath = requireConfigPath(opts?.configPath);
18335
18373
  const cfg = readConfigFileRaw(configPath);
18336
18374
  const rawCc = cfg.cacheControl;
18337
18375
  const rawTtl = cfg.cacheControlTtl;
18376
+ const rawRewrite = cfg.rewriteBlockTtl;
18338
18377
  const effectiveCc = rawCc ?? DEFAULTS.cacheControl;
18378
+ const effectiveRewrite = rawRewrite ?? DEFAULTS.rewriteBlockTtl;
18339
18379
  log.info(`Current: cacheControl = ${rawCc === void 0 ? `(default -> ${effectiveCc})` : effectiveCc}`);
18340
18380
  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;
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;
18343
18384
  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;
18385
+ assignField(fields, "cacheControl", result.cacheControl);
18386
+ assignField(fields, "cacheControlTtl", result.cacheControlTtl);
18387
+ assignField(fields, "rewriteBlockTtl", result.rewriteBlockTtl);
18352
18388
  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}`);
18389
+ log.success(`cacheControl set to ${fieldLabel(result.cacheControl, effectiveCc)}, TTL = ${fieldLabel(result.cacheControlTtl, "(unchanged)")}, rewrite = ${fieldLabel(result.rewriteBlockTtl, effectiveRewrite)}`);
18356
18390
  }
18357
18391
  //#endregion
18358
18392
  //#region src/commands/config/caching-summary.ts
@@ -18383,6 +18417,7 @@ function perModelTtl(current, globalCfg) {
18383
18417
  }
18384
18418
  function formatGlobalCachingSummary(cfg) {
18385
18419
  const cc = globalTriStateLabel(cfg.cacheControl, DEFAULTS.cacheControl);
18420
+ const rewrite = globalTriStateLabel(cfg.rewriteBlockTtl, DEFAULTS.rewriteBlockTtl);
18386
18421
  const sid = globalTriStateLabel(cfg.sessionId, DEFAULTS.sessionId);
18387
18422
  const nvs = globalNvsLabel(cfg.normalizeVolatileSystem);
18388
18423
  return [
@@ -18390,6 +18425,7 @@ function formatGlobalCachingSummary(cfg) {
18390
18425
  "",
18391
18426
  ` cacheControl = ${cc} (activate cache)`,
18392
18427
  ` cacheControlTtl = ${describeTtl(cfg.cacheControlTtl)}`,
18428
+ ` rewriteBlockTtl = ${rewrite} (normalize block ttl)`,
18393
18429
  ` sessionId = ${sid} (pin provider)`,
18394
18430
  ` normalizeVolatileSystem = ${nvs} (stable prefix)`,
18395
18431
  "",
@@ -18398,10 +18434,12 @@ function formatGlobalCachingSummary(cfg) {
18398
18434
  }
18399
18435
  function formatPerModelCachingSummary(modelKey, current, globalCfg) {
18400
18436
  const gCc = globalCfg.cacheControl ?? DEFAULTS.cacheControl;
18437
+ const gRewrite = globalCfg.rewriteBlockTtl ?? DEFAULTS.rewriteBlockTtl;
18401
18438
  const gSid = globalCfg.sessionId ?? DEFAULTS.sessionId;
18402
18439
  const gNvs = globalCfg.normalizeVolatileSystem ?? DEFAULTS.normalizeVolatileSystem;
18403
18440
  const cc = current.cacheControl ?? `(inherit -> ${gCc})`;
18404
18441
  const ttl = perModelTtl(current, globalCfg);
18442
+ const rewrite = current.rewriteBlockTtl ?? `(inherit -> ${gRewrite})`;
18405
18443
  const sid = current.sessionId ?? `(inherit -> ${gSid})`;
18406
18444
  const nvs = nvsLabel(current.normalizeVolatileSystem, gNvs);
18407
18445
  return [
@@ -18409,6 +18447,7 @@ function formatPerModelCachingSummary(modelKey, current, globalCfg) {
18409
18447
  "",
18410
18448
  ` cacheControl = ${cc}`,
18411
18449
  ` cacheControlTtl = ${ttl}`,
18450
+ ` rewriteBlockTtl = ${rewrite}`,
18412
18451
  ` sessionId = ${sid}`,
18413
18452
  ` normalizeVolatileSystem = ${nvs}`
18414
18453
  ].join("\n");
@@ -18426,7 +18465,7 @@ async function normalizeVolatileSystemCommand(opts) {
18426
18465
  const raw = readConfigFileRaw(configPath).normalizeVolatileSystem;
18427
18466
  const effective = raw ?? DEFAULTS.normalizeVolatileSystem;
18428
18467
  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, {
18468
+ const choice = await askNormalizeVolatileSystem("Normalize Claude Code's volatile cch and cc_version hashes in the system prompt? Stabilizes the prefix cache for non-Anthropic providers (qwen/glm/etc.).", raw, {
18430
18469
  removable: true,
18431
18470
  resetHint: `remove (default: ${DEFAULTS.normalizeVolatileSystem})`
18432
18471
  });
@@ -18456,11 +18495,12 @@ async function editSessionId(current) {
18456
18495
  }
18457
18496
  async function editCacheControl(current, configPath) {
18458
18497
  const globalTtl = readGlobalTtl(configPath);
18459
- const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl, globalTtl);
18498
+ const result = await collectCacheTriState(current.cacheControl, current.cacheControlTtl, globalTtl, current.rewriteBlockTtl);
18460
18499
  if (result === null) return current;
18461
18500
  const next = { ...current };
18462
18501
  applyField(next, "cacheControl", result.cacheControl);
18463
18502
  applyField(next, "cacheControlTtl", result.cacheControlTtl);
18503
+ applyField(next, "rewriteBlockTtl", result.rewriteBlockTtl);
18464
18504
  return next;
18465
18505
  }
18466
18506
  async function editNormalizeVolatileSystem(current) {
@@ -19353,7 +19393,7 @@ async function runConfigMenu(client) {
19353
19393
  }
19354
19394
  //#endregion
19355
19395
  //#region src/version.ts
19356
- const version = "0.12.0";
19396
+ const version = "0.14.0";
19357
19397
  //#endregion
19358
19398
  //#region src/commands/doctor.ts
19359
19399
  const DEFAULT_TIMEOUT_MS = 3e3;
@@ -22905,6 +22945,56 @@ function buildCacheControl(existing, ttl, isAnthropic) {
22905
22945
  }
22906
22946
  return base;
22907
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
+ }
22908
22998
  //#endregion
22909
22999
  //#region src/proxy/middleware/inject-cache-control.ts
22910
23000
  const injectCacheControl = createMiddleware(async (c, next) => {
@@ -22914,11 +23004,16 @@ const injectCacheControl = createMiddleware(async (c, next) => {
22914
23004
  await next();
22915
23005
  return;
22916
23006
  }
23007
+ const isAnthropic = isAnthropicEndpoint(c.var.modelName, c.req.path);
23008
+ let mutated = false;
22917
23009
  if (shouldInjectCacheControl(resolved.cacheControl, c.var.modelName, c.req.path)) {
22918
- const isAnthropic = isAnthropicEndpoint(c.var.modelName, c.req.path);
22919
23010
  parsedBody.cache_control = buildCacheControl(parsedBody.cache_control, resolved.cacheControlTtl, isAnthropic);
22920
- c.set("bodyMutated", true);
23011
+ mutated = true;
23012
+ }
23013
+ if (shouldRewriteBlockTtl(resolved.rewriteBlockTtl, resolved.cacheControl, c.var.modelName, c.req.path)) {
23014
+ if (rewriteBlockTtls(parsedBody, resolved.cacheControlTtl, isAnthropic)) mutated = true;
22921
23015
  }
23016
+ if (mutated) c.set("bodyMutated", true);
22922
23017
  await next();
22923
23018
  });
22924
23019
  //#endregion
@@ -22999,11 +23094,15 @@ const injectSessionId = createMiddleware(async (c, next) => {
22999
23094
  });
23000
23095
  //#endregion
23001
23096
  //#region src/proxy/middleware/normalize-volatile-system.ts
23002
- /** Normalize Claude Code's per-request `cch=…` hash in system[0] to a constant; it drifts every turn and breaks prefix-cache for non-Anthropic providers. */
23097
+ /** Normalize Claude Code's per-request volatile hashes in system[0] to constants; they drift every turn and break prefix-cache for non-Anthropic providers:
23098
+ * - `cch=…` per-turn hash
23099
+ * - `cc_version=<semver>.<hash>` — the trailing build hash drifts; the readable semver is preserved. */
23003
23100
  const CCH_PATTERN = /cch=[0-9a-f]+/g;
23004
23101
  const CCH_REPLACEMENT = "cch=00000";
23102
+ const CC_VERSION_PATTERN = /cc_version=(\d+\.\d+\.\d+)\.[0-9a-f]+/g;
23103
+ const CC_VERSION_REPLACEMENT = "cc_version=$1.0";
23005
23104
  function normalizeText(text) {
23006
- return text.replace(CCH_PATTERN, CCH_REPLACEMENT);
23105
+ return text.replace(CCH_PATTERN, CCH_REPLACEMENT).replace(CC_VERSION_PATTERN, CC_VERSION_REPLACEMENT);
23007
23106
  }
23008
23107
  function textOf(block) {
23009
23108
  if (block !== null && typeof block === "object" && typeof block.text === "string") return block.text;