pi-cache-optimizer 2.6.21 → 2.6.23

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
@@ -113,6 +113,14 @@ Notes:
113
113
  - For DeepSeek models, the Pi Mono guidance expects `compat.requiresReasoningContentOnAssistantMessages: true` and `compat.thinkingFormat: "deepseek"` alongside cache/session-affinity flags when the endpoint supports them.
114
114
  - This extension's `doctor` and `compat` commands only advise; they do not modify `models.json`.
115
115
 
116
+ ## Anthropic cache TTL compatibility
117
+
118
+ Anthropic processes cache breakpoints in `tools → system → messages` order and rejects a `ttl: "1h"` breakpoint that appears after a 5-minute breakpoint. An ephemeral `cache_control` without `ttl` uses the default 5-minute retention.
119
+
120
+ For `anthropic-messages` channels, the extension checks the final serialized payload before sending. If it detects an invalid short-to-long transition, it conservatively downgrades that request's `1h` breakpoints to the default 5-minute TTL. Legal long-only and `1h → 5m` payloads are left unchanged. This is based on payload structure, not provider names such as `pipi-cc`.
121
+
122
+ If a proxy adds its own conflicting cache controls after Pi's request hook, set `compat.supportsLongCacheRetention: false` for that provider/model so Pi does not emit `ttl: "1h"`.
123
+
116
124
  ## Adaptive thinking models
117
125
 
118
126
  Claude models from opus-4.6 / sonnet-4.6 (including Sonnet 5) / fable-5 onwards require `forceAdaptiveThinking: true` in compat. Kimi Coding K3 (`k3`) and `kimi-for-coding` also use adaptive thinking and need `allowEmptySignature: true` so replayed empty-signature thinking blocks remain valid. Without the required compat, Pi may send a legacy thinking payload or replay thinking incorrectly.
package/README.zh-CN.md CHANGED
@@ -113,6 +113,14 @@ Pi 0.81+ 也内置了使用 OpenAI-shaped transport 的 `llama.cpp` provider。P
113
113
  - 对 DeepSeek 模型,Pi Mono 指南期望在支持时同时设置 `compat.requiresReasoningContentOnAssistantMessages: true` 和 `compat.thinkingFormat: "deepseek"`,再配合缓存 / session-affinity 相关 compat。
114
114
  - 本扩展的 `doctor` 和 `compat` 命令只给建议,不会修改 `models.json`。
115
115
 
116
+ ## Anthropic 缓存 TTL 兼容
117
+
118
+ Anthropic 按 `tools → system → messages` 顺序处理 cache breakpoint,并拒绝位于 5 分钟 breakpoint 之后的 `ttl: "1h"` breakpoint。省略 `ttl` 的 ephemeral `cache_control` 使用默认 5 分钟保留时间。
119
+
120
+ 对于 `anthropic-messages` 渠道,本扩展会在发送前检查最终序列化 payload。如果检测到非法的短 TTL → 长 TTL 顺序,会保守地把该请求中的 `1h` breakpoint 降级为默认 5 分钟 TTL。合法的纯 `1h` 和 `1h → 5m` payload 保持不变。判断依据是 payload 结构,而不是 `pipi-cc` 等 provider 名称。
121
+
122
+ 如果代理在 Pi request hook 之后自行添加冲突的 cache control,可为该 provider/model 设置 `compat.supportsLongCacheRetention: false`,使 Pi 不再发送 `ttl: "1h"`。
123
+
116
124
  ## Adaptive thinking 模型
117
125
 
118
126
  Claude 从 opus-4.6 / sonnet-4.6(含 Sonnet 5)/ fable-5 开始需要在 compat 中设置 `forceAdaptiveThinking: true`。Kimi Coding K3(`k3`)和 `kimi-for-coding` 也使用 adaptive thinking,并需要 `allowEmptySignature: true`,以正确重放空 signature 的 thinking block。缺少这些 compat 时,Pi 可能发送旧版 thinking payload 或错误重放 thinking。
package/index.ts CHANGED
@@ -14,6 +14,12 @@ type CacheRetentionEnvSnapshot = {
14
14
 
15
15
  const PI_CACHE_RETENTION_ENV = "PI_CACHE_RETENTION";
16
16
  const LONG_CACHE_RETENTION_VALUE = "long";
17
+ const PI_CACHE_RETENTION_BASELINE_SYMBOL = Symbol.for("pi.cache.optimizer.retention-baseline.v1");
18
+
19
+ type CacheRetentionBaselineV1 = {
20
+ version: 1;
21
+ snapshot: CacheRetentionEnvSnapshot;
22
+ };
17
23
 
18
24
  function captureCacheRetentionEnv(env: MutableEnv = process.env): CacheRetentionEnvSnapshot {
19
25
  return {
@@ -36,7 +42,28 @@ function restoreCacheRetentionEnv(snapshot: CacheRetentionEnvSnapshot, env: Muta
36
42
  }
37
43
  }
38
44
 
39
- const STARTUP_CACHE_RETENTION_ENV = captureCacheRetentionEnv();
45
+ function isCacheRetentionBaselineV1(value: unknown): value is CacheRetentionBaselineV1 {
46
+ if (typeof value !== "object" || value === null) return false;
47
+ const record = value as { version?: unknown; snapshot?: unknown };
48
+ if (record.version !== 1 || typeof record.snapshot !== "object" || record.snapshot === null) return false;
49
+ const snapshot = record.snapshot as { wasSet?: unknown; value?: unknown };
50
+ return typeof snapshot.wasSet === "boolean" &&
51
+ (snapshot.value === undefined || typeof snapshot.value === "string");
52
+ }
53
+
54
+ function getOrCaptureCacheRetentionBaseline(
55
+ env: MutableEnv = process.env,
56
+ globals: Record<symbol, unknown> = globalThis as Record<symbol, unknown>,
57
+ ): CacheRetentionEnvSnapshot {
58
+ const existing = globals[PI_CACHE_RETENTION_BASELINE_SYMBOL];
59
+ if (isCacheRetentionBaselineV1(existing)) return { ...existing.snapshot };
60
+
61
+ const snapshot = captureCacheRetentionEnv(env);
62
+ globals[PI_CACHE_RETENTION_BASELINE_SYMBOL] = { version: 1, snapshot: { ...snapshot } };
63
+ return snapshot;
64
+ }
65
+
66
+ const STARTUP_CACHE_RETENTION_ENV = getOrCaptureCacheRetentionBaseline();
40
67
 
41
68
  /**
42
69
  * Pi Cache Optimizer (formerly pi-deepseek-cache-optimizer)
@@ -76,6 +103,7 @@ const NO_SKILL_COMPRESSION_ENV = "PI_CACHE_OPTIMIZER_NO_SKILL_COMPRESSION";
76
103
  const NO_PROMPT_REWRITE_ENV = "PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE";
77
104
  const PI_ROUTING_REGISTRY_SYMBOL = Symbol.for("pi.routing.registry.v1");
78
105
  const PI_CACHE_HINTS_SYMBOL = Symbol.for("pi.cache.hints.v1");
106
+ const PI_CACHE_HINTS_OWNER_SYMBOL = Symbol.for("pi.cache.optimizer.hints-owner.v1");
79
107
 
80
108
  let runtimeOptimizerEnabled = true;
81
109
 
@@ -921,16 +949,29 @@ function getCacheHintsService(): PiCacheHintsV1 | undefined {
921
949
  return isCacheHintsServiceV1(candidate) ? candidate : undefined;
922
950
  }
923
951
 
924
- function installCacheHintsService(service: PiCacheHintsV1): () => void {
952
+ function markOptimizerOwnedCacheHintsService(service: PiCacheHintsV1): PiCacheHintsV1 {
953
+ (service as PiCacheHintsV1 & Record<symbol, unknown>)[PI_CACHE_HINTS_OWNER_SYMBOL] = true;
954
+ return service;
955
+ }
956
+
957
+ function isOptimizerOwnedCacheHintsService(value: unknown): boolean {
958
+ return typeof value === "object" && value !== null &&
959
+ (value as Record<symbol, unknown>)[PI_CACHE_HINTS_OWNER_SYMBOL] === true;
960
+ }
961
+
962
+ function installCacheHintsService(
963
+ service: PiCacheHintsV1,
964
+ options: { discardPrevious?: (value: unknown) => boolean } = {},
965
+ ): () => void {
925
966
  const globals = getProtocolGlobal();
926
967
  const previous = globals[PI_CACHE_HINTS_SYMBOL];
927
968
  globals[PI_CACHE_HINTS_SYMBOL] = service;
928
969
  return () => {
929
970
  if (globals[PI_CACHE_HINTS_SYMBOL] !== service) return;
930
- if (previous === undefined) {
931
- delete globals[PI_CACHE_HINTS_SYMBOL];
932
- } else {
971
+ if (previous !== undefined && !options.discardPrevious?.(previous)) {
933
972
  globals[PI_CACHE_HINTS_SYMBOL] = previous;
973
+ } else {
974
+ delete globals[PI_CACHE_HINTS_SYMBOL];
934
975
  }
935
976
  };
936
977
  }
@@ -1129,6 +1170,10 @@ function isOpenAICompatibleApi(api: unknown): boolean {
1129
1170
  return value === "openai-completions" || value === "openai-responses";
1130
1171
  }
1131
1172
 
1173
+ function isAnthropicMessagesApi(api: unknown): boolean {
1174
+ return lower(api) === "anthropic-messages";
1175
+ }
1176
+
1132
1177
  function isOpenAICompatibleProxyApi(api: unknown): boolean {
1133
1178
  return lower(api) === "openai-completions";
1134
1179
  }
@@ -1155,6 +1200,62 @@ function shouldInjectOpenAIPromptCacheKeyForModel(model: PiModel | undefined): b
1155
1200
  return isOpenAICompatibleApi(model?.api);
1156
1201
  }
1157
1202
 
1203
+ function collectAnthropicCacheControlsInWireOrder(payload: unknown): UnknownRecord[] {
1204
+ const record = asRecord(payload);
1205
+ if (!record) return [];
1206
+
1207
+ const controls: UnknownRecord[] = [];
1208
+ const collectFromBlock = (value: unknown): void => {
1209
+ const cacheControl = asRecord(asRecord(value)?.cache_control);
1210
+ if (cacheControl?.type === "ephemeral") controls.push(cacheControl);
1211
+ };
1212
+
1213
+ // Anthropic processes cache breakpoints in tools → system → messages order.
1214
+ if (Array.isArray(record.tools)) {
1215
+ for (const tool of record.tools) collectFromBlock(tool);
1216
+ }
1217
+ if (Array.isArray(record.system)) {
1218
+ for (const block of record.system) collectFromBlock(block);
1219
+ }
1220
+ if (Array.isArray(record.messages)) {
1221
+ for (const message of record.messages) {
1222
+ const content = asRecord(message)?.content;
1223
+ if (!Array.isArray(content)) continue;
1224
+ for (const block of content) collectFromBlock(block);
1225
+ }
1226
+ }
1227
+
1228
+ return controls;
1229
+ }
1230
+
1231
+ /**
1232
+ * Anthropic requires every 1h cache breakpoint to precede every 5m breakpoint.
1233
+ * An ephemeral cache_control without ttl uses Anthropic's default 5m retention.
1234
+ * If the final serialized payload contains a short → long transition, downgrade
1235
+ * all 1h breakpoints to the default 5m so the request remains valid.
1236
+ */
1237
+ function normalizeAnthropicCacheControlTtlOrder(payload: unknown): boolean {
1238
+ const controls = collectAnthropicCacheControlsInWireOrder(payload);
1239
+ let seenShort = false;
1240
+ let hasInvalidLongAfterShort = false;
1241
+
1242
+ for (const control of controls) {
1243
+ const ttl = control.ttl;
1244
+ if (ttl === undefined || ttl === "5m") {
1245
+ seenShort = true;
1246
+ } else if (ttl === "1h" && seenShort) {
1247
+ hasInvalidLongAfterShort = true;
1248
+ break;
1249
+ }
1250
+ }
1251
+ if (!hasInvalidLongAfterShort) return false;
1252
+
1253
+ for (const control of controls) {
1254
+ if (control.ttl === "1h") delete control.ttl;
1255
+ }
1256
+ return true;
1257
+ }
1258
+
1158
1259
  function isResponsesPromptRewriteBypassApi(api: unknown): boolean {
1159
1260
  const value = lower(api);
1160
1261
  return value === "openai-codex-responses" || value === "openai-responses" || value === "azure-openai-responses";
@@ -3919,6 +4020,15 @@ function mergeCacheSessions(
3919
4020
  return sessions;
3920
4021
  }
3921
4022
 
4023
+ function createSerializedAsyncRunner(): <T>(operation: () => Promise<T>) => Promise<T> {
4024
+ let tail: Promise<unknown> = Promise.resolve();
4025
+ return <T>(operation: () => Promise<T>): Promise<T> => {
4026
+ const result = tail.then(operation);
4027
+ tail = result.catch(() => undefined);
4028
+ return result;
4029
+ };
4030
+ }
4031
+
3922
4032
  function mergeCacheTotals(
3923
4033
  existingTotalsByModel: Record<string, CacheStats>,
3924
4034
  stateTotalsByModel: Record<string, CacheStats>,
@@ -5826,7 +5936,10 @@ export const __internals_for_tests = {
5826
5936
  shouldInjectOpenAIPromptCacheKey,
5827
5937
  shouldInjectOpenAIPromptCacheKeyForModel,
5828
5938
  isOpenAICompatibleApi,
5939
+ isAnthropicMessagesApi,
5829
5940
  isOpenAICompatibleProxyApi,
5941
+ collectAnthropicCacheControlsInWireOrder,
5942
+ normalizeAnthropicCacheControlTtlOrder,
5830
5943
  isPiBuiltInLlamaCppModel,
5831
5944
  isResponsesPromptRewriteBypassApi,
5832
5945
  isMistralConversationsApi,
@@ -6025,10 +6138,15 @@ export const __internals_for_tests = {
6025
6138
  isVirtualRoutingModel,
6026
6139
  installCacheHintsService,
6027
6140
  getCacheHintsService,
6141
+ markOptimizerOwnedCacheHintsService,
6142
+ isOptimizerOwnedCacheHintsService,
6143
+ getOrCaptureCacheRetentionBaseline,
6144
+ PI_CACHE_RETENTION_BASELINE_SYMBOL,
6028
6145
  // Persistence helpers (for reload/reset tests)
6029
6146
  mergeCacheSessions,
6030
6147
  mergeCacheTotals,
6031
6148
  mergeLastRoutedModels,
6149
+ createSerializedAsyncRunner,
6032
6150
  writePersistedCacheStats,
6033
6151
  readPersistedCacheStats,
6034
6152
  STATE_FILE_PATH,
@@ -6078,7 +6196,9 @@ export default function (pi: ExtensionAPI) {
6078
6196
  let lastStatusText: string | undefined;
6079
6197
  let persistenceWarningShown = false;
6080
6198
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
6199
+ const enqueuePersist = createSerializedAsyncRunner();
6081
6200
  let replacePersistedTotalsOnNextWrite = false;
6201
+ let forceReplaceTotalsAfterFailure = false;
6082
6202
  const pendingDeletedTotalModelKeys = new Set<string>();
6083
6203
  let integrityNotificationShown = false;
6084
6204
  let currentSessionId = "";
@@ -6100,7 +6220,7 @@ export default function (pi: ExtensionAPI) {
6100
6220
  }
6101
6221
  }
6102
6222
 
6103
- const uninstallCacheHintsService = installCacheHintsService({
6223
+ const uninstallCacheHintsService = installCacheHintsService(markOptimizerOwnedCacheHintsService({
6104
6224
  version: 1,
6105
6225
  getHints(input: PiCacheHintsInput): PiCacheHintsOutput | undefined {
6106
6226
  if (!runtimeOptimizerEnabled || isEnabledEnv(process.env[NO_PROMPT_REWRITE_ENV])) return undefined;
@@ -6119,8 +6239,7 @@ export default function (pi: ExtensionAPI) {
6119
6239
  cacheRetention: hint.cacheRetention,
6120
6240
  };
6121
6241
  },
6122
- });
6123
- void uninstallCacheHintsService;
6242
+ }), { discardPrevious: isOptimizerOwnedCacheHintsService });
6124
6243
 
6125
6244
  /**
6126
6245
  * Build a session-scoped stats key from the current session hash + model key.
@@ -6243,28 +6362,59 @@ export default function (pi: ExtensionAPI) {
6243
6362
  lastStatusText = undefined;
6244
6363
  }
6245
6364
 
6246
- async function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
6247
- try {
6248
- await writePersistedCacheStats(
6249
- getCacheStatsState(),
6250
- currentSessionHashSet ? currentSessionHash : undefined,
6251
- {
6252
- deleteModelKeys: Array.from(pendingDeletedTotalModelKeys),
6253
- replaceTotals: replacePersistedTotalsOnNextWrite,
6254
- },
6255
- );
6256
- pendingDeletedTotalModelKeys.clear();
6257
- replacePersistedTotalsOnNextWrite = false;
6258
- } catch (error) {
6259
- console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
6260
- if (!persistenceWarningShown) {
6261
- persistenceWarningShown = true;
6262
- ctx?.ui.notify(
6263
- `${LOG_PREFIX}: failed to persist footer stats; using in-memory stats for this process.`,
6264
- "warning",
6265
- );
6266
- }
6365
+ function snapshotCacheStatsState(): CacheStatsState {
6366
+ const state = getCacheStatsState();
6367
+ const statsByModel = Object.fromEntries(
6368
+ Object.entries(state.statsByModel).map(([key, stats]) => [key, { ...stats }]),
6369
+ );
6370
+ const totalsByModel = Object.fromEntries(
6371
+ Object.entries(state.totalsByModel).map(([key, stats]) => [key, { ...stats }]),
6372
+ );
6373
+ const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
6374
+ for (const id of CACHE_PROVIDER_IDS) {
6375
+ const stats = state.legacyFamily[id];
6376
+ if (stats) legacyFamily[id] = { ...stats };
6267
6377
  }
6378
+ const lastRoutedModelBySession = state.lastRoutedModelBySession
6379
+ ? Object.fromEntries(
6380
+ Object.entries(state.lastRoutedModelBySession).map(([key, model]) => [key, { ...model }]),
6381
+ )
6382
+ : undefined;
6383
+ return { statsByModel, totalsByModel, legacyFamily, lastRoutedModelBySession };
6384
+ }
6385
+
6386
+ function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
6387
+ const state = snapshotCacheStatsState();
6388
+ const sessionHash = currentSessionHashSet ? currentSessionHash : undefined;
6389
+ const deleteModelKeys = Array.from(pendingDeletedTotalModelKeys);
6390
+ const replaceTotals = replacePersistedTotalsOnNextWrite;
6391
+ // This queued write owns the current intents. Later mutations can enqueue
6392
+ // independent intents without an earlier completion clearing them.
6393
+ pendingDeletedTotalModelKeys.clear();
6394
+ replacePersistedTotalsOnNextWrite = false;
6395
+
6396
+ return enqueuePersist(async () => {
6397
+ const recoverFromEarlierFailure = forceReplaceTotalsAfterFailure;
6398
+ try {
6399
+ await writePersistedCacheStats(state, sessionHash, {
6400
+ deleteModelKeys,
6401
+ replaceTotals: replaceTotals || recoverFromEarlierFailure,
6402
+ });
6403
+ if (recoverFromEarlierFailure) forceReplaceTotalsAfterFailure = false;
6404
+ } catch (error) {
6405
+ // The next queued write must replace totals authoritatively so a failed
6406
+ // reset/delete cannot merge stale persisted counters back into state.
6407
+ forceReplaceTotalsAfterFailure = true;
6408
+ console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
6409
+ if (!persistenceWarningShown) {
6410
+ persistenceWarningShown = true;
6411
+ ctx?.ui.notify(
6412
+ `${LOG_PREFIX}: failed to persist footer stats; using in-memory stats for this process.`,
6413
+ "warning",
6414
+ );
6415
+ }
6416
+ }
6417
+ });
6268
6418
  }
6269
6419
 
6270
6420
  /** Schedule a debounced persist. Coalesces rapid message_end writes
@@ -6554,10 +6704,22 @@ export default function (pi: ExtensionAPI) {
6554
6704
  }
6555
6705
 
6556
6706
  pi.on("session_start", async (event, ctx) => {
6707
+ if (runtimeOptimizerEnabled) requestLongCacheRetention();
6557
6708
  await restoreCacheStats(event.reason, ctx);
6558
6709
  await publishStatus(ctx);
6559
6710
  });
6560
6711
 
6712
+ pi.on("session_shutdown", async (_event, ctx) => {
6713
+ try {
6714
+ await flushPersistCacheStats(ctx);
6715
+ } finally {
6716
+ latestCacheHint = undefined;
6717
+ delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
6718
+ uninstallCacheHintsService();
6719
+ restoreCacheRetentionEnv(STARTUP_CACHE_RETENTION_ENV);
6720
+ }
6721
+ });
6722
+
6561
6723
  pi.on("model_select", async (event, ctx) => {
6562
6724
  if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(event.model, ctx) ?? event.model, ctx, warnedModels);
6563
6725
  await publishStatus(ctx, event.model);
@@ -6684,6 +6846,16 @@ export default function (pi: ExtensionAPI) {
6684
6846
  });
6685
6847
 
6686
6848
  pi.on("before_provider_request", (event, ctx) => {
6849
+ const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
6850
+
6851
+ // Anthropic rejects mixed cache breakpoints when a 1h block appears after
6852
+ // a 5m/default block in wire order (tools → system → messages). Normalize
6853
+ // only provably-invalid final payloads; legal long-only and long→short
6854
+ // payloads keep their original retention.
6855
+ if (isAnthropicMessagesApi(requestModel?.api)) {
6856
+ normalizeAnthropicCacheControlTtlOrder(event.payload);
6857
+ }
6858
+
6687
6859
  // ── Safety: strip prompt_cache_retention from payload for models that
6688
6860
  // are not authorised to send it. Pi defaults supportsLongCacheRetention
6689
6861
  // to true for all openai-completions models, but most third-party APIs
@@ -6700,14 +6872,13 @@ export default function (pi: ExtensionAPI) {
6700
6872
  if (runtimeOptimizerEnabled) {
6701
6873
  const payload = event.payload as UnknownRecord;
6702
6874
  if (payload && typeof payload.prompt_cache_retention === 'string') {
6703
- const rModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
6704
- if (rModel) {
6705
- if (isOfficialOpenAIBaseUrl(rModel)) {
6875
+ if (requestModel) {
6876
+ if (isOfficialOpenAIBaseUrl(requestModel)) {
6706
6877
  // Gate 1: Official OpenAI → keep
6707
- } else if (promptCacheRetention400Models.has(modelKey(rModel))) {
6878
+ } else if (promptCacheRetention400Models.has(modelKey(requestModel))) {
6708
6879
  // Gate 2: 400 history → strip (overrides user opt-in)
6709
6880
  delete payload.prompt_cache_retention;
6710
- } else if (hasExplicitLongRetentionOptIn(rModel)) {
6881
+ } else if (hasExplicitLongRetentionOptIn(requestModel)) {
6711
6882
  // Gate 3: Explicit user opt-in → keep
6712
6883
  } else {
6713
6884
  // Gate 4: Safe default → strip
@@ -6718,7 +6889,6 @@ export default function (pi: ExtensionAPI) {
6718
6889
  }
6719
6890
 
6720
6891
  if (!shouldInjectOpenAIPromptCacheKey()) return undefined;
6721
- const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
6722
6892
  if (!shouldInjectOpenAIPromptCacheKeyForModel(requestModel)) return undefined;
6723
6893
 
6724
6894
  return addOpenAIPromptCacheKey(event.payload, getSessionPromptCacheKey(ctx));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.6.21",
3
+ "version": "2.6.23",
4
4
  "description": "Improve Pi prompt/KV cache hit rates with stable prompts, OpenAI-compatible cache keys, proxy compat warnings, and footer cache stats.",
5
5
  "keywords": [
6
6
  "pi-package",