pi-cache-optimizer 2.6.10 → 2.6.11

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
@@ -33,7 +33,7 @@ Pi extension for improving provider-side KV / prompt cache hit rates. It keeps s
33
33
  - Adds a session-id `prompt_cache_key` fallback for `openai-completions` / `openai-responses` payloads when no effective key exists.
34
34
  - Warns once for third-party OpenAI-compatible proxies missing cache/session-affinity compat flags.
35
35
  - Detects Anthropic adaptive thinking models (opus-4.6+, sonnet-4.6+, fable-5+) missing `forceAdaptiveThinking: true` compat.
36
- - Shows session-scoped footer stats for supported model families.
36
+ - Shows restart-persistent provider/model footer stats for supported model families.
37
37
  - Supports optional router-extension integration through versioned global protocols (`Symbol.for("pi.routing.registry.v1")` and `Symbol.for("pi.cache.hints.v1")`) without importing router packages.
38
38
 
39
39
  Caching is provider-side and best-effort. Third-party proxies and router extensions can still hide cache usage, reject unsupported parameters, or route requests across multiple upstreams.
@@ -59,12 +59,12 @@ On Pi 0.79.7 and newer, `pi update` updates Pi itself only. To update installed
59
59
  | Command | Effect |
60
60
  |---|---|
61
61
  | `/cache-optimizer` | Interactive menu when UI supports it; otherwise prints help and current state. |
62
- | `/cache-optimizer enable` | Enables runtime optimizations for the current Pi process, resets current-session stats, and starts a fresh “enabled” measurement. |
63
- | `/cache-optimizer disable` | Disables optimization for the current Pi process, resets current-session stats, and keeps collecting footer stats in disabled comparison mode. Run `/reload` or restart Pi to return to startup behavior. |
62
+ | `/cache-optimizer enable` | Enables runtime optimizations for the current Pi process, resets local footer stats, and starts a fresh “enabled” measurement. |
63
+ | `/cache-optimizer disable` | Disables optimization for the current Pi process, resets local footer stats, and keeps collecting footer stats in disabled comparison mode. Run `/reload` or restart Pi to return to startup behavior. |
64
64
  | `/cache-optimizer doctor` | Shows active model/provider/API/base URL/compat plus low-hit diagnosis. |
65
65
  | `/cache-optimizer compat` | Shows copyable compat advice for the active model, if applicable. |
66
- | `/cache-optimizer stats` | Shows today's session-scoped counters and recent trend for the active model. |
67
- | `/cache-optimizer reset` | Resets only local stats for the active session + model; upstream provider cache is not modified. |
66
+ | `/cache-optimizer stats` | Shows today's local provider/model counters and recent trend for the active model. |
67
+ | `/cache-optimizer reset` | Resets local footer stats for the active provider/model; upstream provider cache is not modified. |
68
68
  | `/cache-optimizer fix` | Auto-repairs safe compat issues for the active model (adaptive thinking, DeepSeek reasoning, OpenAI proxy session affinity). Shows preview + risk warning, requires confirmation. **Only modifies `models.json` after explicit user approval.** |
69
69
 
70
70
  `enable` / `disable` are current-process switches. For a persistent opt-out, use environment variables below.
@@ -214,9 +214,9 @@ If only one model should change, use `modelOverrides`:
214
214
 
215
215
  ## Footer stats
216
216
 
217
- Stats are read-only local counters stored at `~/.pi/agent/pi-cache-optimizer-stats.json` and scoped by Pi session + provider/model. They contain only dates and numeric counters — no API keys, prompts, payloads, headers, responses, or model output.
217
+ Stats are read-only local counters stored at `~/.pi/agent/pi-cache-optimizer-stats.json` and keyed by provider/model, so the same channel/model keeps today's footer counters after a Pi process or terminal restart. The file also keeps hashed session buckets for migration/reload bookkeeping. It contains only dates and numeric counters — no API keys, prompts, payloads, headers, responses, or model output.
218
218
 
219
- Pi 0.79+ also includes a built-in footer `CH` marker for the latest prompt cache hit rate. This extension complements that marker with persisted, provider/model/session-scoped counters plus proxy compat diagnostics.
219
+ Pi 0.79+ also includes a built-in footer `CH` marker for the latest prompt cache hit rate. This extension complements that marker with persisted provider/model counters plus proxy compat diagnostics.
220
220
 
221
221
  Example footer:
222
222
 
package/README.zh-CN.md CHANGED
@@ -33,7 +33,7 @@
33
33
  - 对 `openai-completions` / `openai-responses` 请求,在没有有效 key 时使用 Pi session id 补 `prompt_cache_key`。
34
34
  - 对缺少缓存 / session-affinity compat 的第三方 OpenAI-compatible 代理给出一次性提醒。
35
35
  - 检测 Anthropic adaptive thinking 模型(opus-4.6+、sonnet-4.6+、fable-5+)是否缺少 `forceAdaptiveThinking: true` compat。
36
- - 为支持的模型家族显示按 session 隔离的底部缓存统计。
36
+ - 为支持的模型家族显示可跨 Pi 进程 / 终端重启延续的 provider/model footer 缓存统计。
37
37
  - 通过版本化全局协议(`Symbol.for("pi.routing.registry.v1")` 与 `Symbol.for("pi.cache.hints.v1")`)支持可选的 router extension 集成,而不导入任何 router 包。
38
38
 
39
39
  缓存是 provider 侧的 best-effort 行为。第三方代理和 router extension 仍可能隐藏缓存 usage、拒绝不支持的参数,或把请求路由到多个上游。
@@ -59,12 +59,12 @@ Pi 0.79.7 及之后,`pi update` 默认只更新 Pi 本体。若要更新已安
59
59
  | 命令 | 作用 |
60
60
  |---|---|
61
61
  | `/cache-optimizer` | UI 支持时打开交互菜单;否则打印帮助和当前状态。 |
62
- | `/cache-optimizer enable` | 在当前 Pi 进程中开启运行时优化,清零当前 session 统计,并开始新的“开启状态”测量。 |
63
- | `/cache-optimizer disable` | 在当前 Pi 进程中关闭优化,清零当前 session 统计,并继续以 disabled 对比模式采集 footer 统计。运行 `/reload` 或重启 Pi 后回到启动时行为。 |
62
+ | `/cache-optimizer enable` | 在当前 Pi 进程中开启运行时优化,清零本地 footer 统计,并开始新的“开启状态”测量。 |
63
+ | `/cache-optimizer disable` | 在当前 Pi 进程中关闭优化,清零本地 footer 统计,并继续以 disabled 对比模式采集 footer 统计。运行 `/reload` 或重启 Pi 后回到启动时行为。 |
64
64
  | `/cache-optimizer doctor` | 显示当前模型 / provider / API / base URL / compat 与低命中诊断。 |
65
65
  | `/cache-optimizer compat` | 对当前模型显示可复制的 compat 建议(如适用)。 |
66
- | `/cache-optimizer stats` | 显示当前模型今天的 session-scoped 统计和近期趋势。 |
67
- | `/cache-optimizer reset` | 只重置当前 session + 当前模型的本地统计;不会修改上游 provider 缓存。 |
66
+ | `/cache-optimizer stats` | 显示当前模型今天的本地 provider/model 统计和近期趋势。 |
67
+ | `/cache-optimizer reset` | 重置当前 provider/model 的本地 footer 统计;不会修改上游 provider 缓存。 |
68
68
  | `/cache-optimizer fix` | 为当前模型自动修复安全的 compat 问题(adaptive thinking、DeepSeek reasoning、OpenAI proxy session affinity)。展示预览 + 风险提示,需要用户确认。**仅在用户明确批准后才修改 `models.json`。** |
69
69
 
70
70
  `enable` / `disable` 是当前进程内开关。若要持久关闭某些能力,请使用下面的环境变量。
@@ -214,9 +214,9 @@ Provider 级最小 override:
214
214
 
215
215
  ## Footer 统计
216
216
 
217
- 统计是只读本地计数,保存在 `~/.pi/agent/pi-cache-optimizer-stats.json`,按 Pi session + provider/model 隔离。文件只包含日期和数字计数,不包含 API key、prompt、payload、headers、响应或模型输出。
217
+ 统计是只读本地计数,保存在 `~/.pi/agent/pi-cache-optimizer-stats.json`,按 provider/model 作为 footer 展示维度,因此同一个渠道/模型在 Pi 进程或终端重启后会延续今天的统计。文件也保留 hashed session buckets,用于迁移和 reload 记录。文件只包含日期和数字计数,不包含 API key、prompt、payload、headers、响应或模型输出。
218
218
 
219
- Pi 0.79+ 已内置 footer `CH` 标记,用于显示最近一次 prompt cache hit rate。本扩展在此基础上补充持久化的 provider/model/session-scoped 计数,以及代理 compat 诊断。
219
+ Pi 0.79+ 已内置 footer `CH` 标记,用于显示最近一次 prompt cache hit rate。本扩展在此基础上补充持久化的 provider/model 计数,以及代理 compat 诊断。
220
220
 
221
221
  示例 footer:
222
222
 
package/index.ts CHANGED
@@ -247,6 +247,7 @@ type ContextWithOptionalModelRegistry = Pick<ExtensionContext, "sessionManager">
247
247
 
248
248
  type CacheStatsState = {
249
249
  statsByModel: Record<string, CacheStats>;
250
+ totalsByModel: Record<string, CacheStats>;
250
251
  legacyFamily: Partial<Record<CacheProviderId, CacheStats>>;
251
252
  lastRoutedModelBySession?: Record<string, PersistedRoutedModelRef>;
252
253
  };
@@ -277,6 +278,14 @@ type PersistedCacheStatsV5 = {
277
278
  lastRoutedModelBySession?: Record<string, PersistedRoutedModelRef>;
278
279
  };
279
280
 
281
+ type PersistedCacheStatsV6 = {
282
+ version: 6;
283
+ sessions: Record<string, Record<string, CacheStats>>;
284
+ totalsByModel: Record<string, CacheStats>;
285
+ legacyFamily: Partial<Record<CacheProviderId, CacheStats>>;
286
+ lastRoutedModelBySession?: Record<string, PersistedRoutedModelRef>;
287
+ };
288
+
280
289
  type UsageSnapshot = {
281
290
  cacheRead: number;
282
291
  cacheWrite: number;
@@ -3426,6 +3435,49 @@ function parseCacheStats(value: unknown): CacheStats | undefined {
3426
3435
  };
3427
3436
  }
3428
3437
 
3438
+ function cloneCacheStats(stats: CacheStats): CacheStats {
3439
+ return { ...stats };
3440
+ }
3441
+
3442
+ function addCacheStatsTotals(target: CacheStats, source: CacheStats): void {
3443
+ target.totalRequests += source.totalRequests;
3444
+ target.hitRequests += source.hitRequests;
3445
+ target.cachedInputTokens += source.cachedInputTokens;
3446
+ target.cacheWriteInputTokens += source.cacheWriteInputTokens;
3447
+ target.totalInputTokens += source.totalInputTokens;
3448
+ }
3449
+
3450
+ function mergeCacheStatsForTotal(existing: CacheStats | undefined, incoming: CacheStats): CacheStats {
3451
+ if (!existing) return cloneCacheStats(incoming);
3452
+ if (incoming.day > existing.day) return cloneCacheStats(incoming);
3453
+ if (incoming.day < existing.day) return existing;
3454
+ addCacheStatsTotals(existing, incoming);
3455
+ return existing;
3456
+ }
3457
+
3458
+ function deriveTotalsByModelFromSessionStats(statsByModel: Record<string, CacheStats>): Record<string, CacheStats> {
3459
+ const totals: Record<string, CacheStats> = {};
3460
+ for (const [fullKey, stats] of Object.entries(statsByModel)) {
3461
+ totals[modelKeyFromSessionKey(fullKey)] = mergeCacheStatsForTotal(
3462
+ totals[modelKeyFromSessionKey(fullKey)],
3463
+ stats,
3464
+ );
3465
+ }
3466
+ return totals;
3467
+ }
3468
+
3469
+ function parsePersistedTotalsByModel(value: unknown): Record<string, CacheStats> | undefined {
3470
+ const record = asRecord(value);
3471
+ if (!record) return undefined;
3472
+
3473
+ const totals: Record<string, CacheStats> = {};
3474
+ for (const [modelKeyStr, rawStats] of Object.entries(record)) {
3475
+ const stats = parseCacheStats(rawStats);
3476
+ if (stats) totals[modelKeyStr] = stats;
3477
+ }
3478
+ return totals;
3479
+ }
3480
+
3429
3481
  function parsePersistedRoutedModelRef(value: unknown): PersistedRoutedModelRef | undefined {
3430
3482
  const record = asRecord(value);
3431
3483
  const provider = record?.provider;
@@ -3459,6 +3511,7 @@ function buildExactRouterStatusEntry(
3459
3511
  sessionHash: string | undefined,
3460
3512
  statsByModel: Record<string, CacheStats>,
3461
3513
  lastRoutedModel: PersistedRoutedModelRef | undefined,
3514
+ totalsByModel: Record<string, CacheStats> = {},
3462
3515
  ): { model: PiModel; adapter: CacheProviderAdapter; stats: CacheStats } | undefined {
3463
3516
  if (!sessionHash || !lastRoutedModel) return undefined;
3464
3517
 
@@ -3467,17 +3520,20 @@ function buildExactRouterStatusEntry(
3467
3520
  if (!adapter) return undefined;
3468
3521
 
3469
3522
  const key = makeSessionModelKey(sessionHash, lastRoutedModel.provider, lastRoutedModel.id);
3470
- return { model, adapter, stats: statsByModel[key] ?? emptyCacheStats() };
3523
+ return { model, adapter, stats: totalsByModel[modelKey(model)] ?? statsByModel[key] ?? emptyCacheStats() };
3471
3524
  }
3472
3525
 
3473
3526
  function parsePersistedCacheStats(value: unknown): CacheStatsState | undefined {
3474
3527
  const record = asRecord(value);
3475
3528
  if (!record) return undefined;
3476
3529
 
3477
- // version 4/5: session-scoped stats + legacy family fallback.
3530
+ // version 4/5/6: session-scoped stats + legacy family fallback.
3478
3531
  // v5 additionally persists the last actual routed model per session so
3479
3532
  // router/auto can restore the exact upstream footer after /reload.
3480
- if (record.version === 4 || record.version === 5) {
3533
+ // v6 adds provider/model totals used for footer continuity across Pi
3534
+ // process/terminal restarts, while retaining session buckets for migration
3535
+ // and best-effort preservation of older data.
3536
+ if (record.version === 4 || record.version === 5 || record.version === 6) {
3481
3537
  const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
3482
3538
  const rawFamily = asRecord(record.legacyFamily);
3483
3539
  if (rawFamily) {
@@ -3514,7 +3570,10 @@ function parsePersistedCacheStats(value: unknown): CacheStatsState | undefined {
3514
3570
  }
3515
3571
  }
3516
3572
 
3517
- return { statsByModel, legacyFamily, lastRoutedModelBySession };
3573
+ const parsedTotals = parsePersistedTotalsByModel(record.totalsByModel);
3574
+ const totalsByModel = parsedTotals ?? deriveTotalsByModelFromSessionStats(statsByModel);
3575
+
3576
+ return { statsByModel, totalsByModel, legacyFamily, lastRoutedModelBySession };
3518
3577
  }
3519
3578
 
3520
3579
  // version 3: migrate to v4/v5 semantics by wrapping statsByModel into sessions
@@ -3537,7 +3596,7 @@ function parsePersistedCacheStats(value: unknown): CacheStatsState | undefined {
3537
3596
  }
3538
3597
  }
3539
3598
 
3540
- return { statsByModel, legacyFamily };
3599
+ return { statsByModel, totalsByModel: deriveTotalsByModelFromSessionStats(statsByModel), legacyFamily };
3541
3600
  }
3542
3601
 
3543
3602
  // version 2: migrate statsByProvider into legacyFamily
@@ -3550,13 +3609,13 @@ function parsePersistedCacheStats(value: unknown): CacheStatsState | undefined {
3550
3609
  if (stats) legacyFamily[id] = stats;
3551
3610
  }
3552
3611
  }
3553
- return { statsByModel: {}, legacyFamily };
3612
+ return { statsByModel: {}, totalsByModel: {}, legacyFamily };
3554
3613
  }
3555
3614
 
3556
3615
  // version 1: single DeepSeek stats -> migrate to legacyFamily.deepseek
3557
3616
  if (record.version === 1) {
3558
3617
  const migrated = parseCacheStats(record.stats);
3559
- return migrated ? { statsByModel: {}, legacyFamily: { deepseek: migrated } } : undefined;
3618
+ return migrated ? { statsByModel: {}, totalsByModel: {}, legacyFamily: { deepseek: migrated } } : undefined;
3560
3619
  }
3561
3620
 
3562
3621
  return undefined;
@@ -3711,6 +3770,20 @@ function mergeCacheSessions(
3711
3770
  return sessions;
3712
3771
  }
3713
3772
 
3773
+ function mergeCacheTotals(
3774
+ existingTotalsByModel: Record<string, CacheStats>,
3775
+ stateTotalsByModel: Record<string, CacheStats>,
3776
+ options: { deleteModelKeys?: string[]; replaceTotals?: boolean } = {},
3777
+ ): Record<string, CacheStats> {
3778
+ const totals = options.replaceTotals
3779
+ ? { ...stateTotalsByModel }
3780
+ : { ...existingTotalsByModel, ...stateTotalsByModel };
3781
+ for (const key of options.deleteModelKeys ?? []) {
3782
+ delete totals[key];
3783
+ }
3784
+ return totals;
3785
+ }
3786
+
3714
3787
  function mergeLastRoutedModels(
3715
3788
  existingLastRoutedModelBySession: Record<string, PersistedRoutedModelRef>,
3716
3789
  state: CacheStatsState,
@@ -3737,11 +3810,16 @@ function mergeLastRoutedModels(
3737
3810
  return merged;
3738
3811
  }
3739
3812
 
3740
- async function writePersistedCacheStats(state: CacheStatsState, currentSessionHash?: string): Promise<void> {
3813
+ async function writePersistedCacheStats(
3814
+ state: CacheStatsState,
3815
+ currentSessionHash?: string,
3816
+ options: { deleteModelKeys?: string[]; replaceTotals?: boolean } = {},
3817
+ ): Promise<void> {
3741
3818
  await mkdir(STATE_DIR, { recursive: true });
3742
3819
 
3743
3820
  // Read existing file to preserve other sessions' data.
3744
3821
  let existingSessions: Record<string, Record<string, CacheStats>> = {};
3822
+ let existingTotalsByModel: Record<string, CacheStats> = {};
3745
3823
  let existingLastRoutedModelBySession: Record<string, PersistedRoutedModelRef> = {};
3746
3824
  try {
3747
3825
  const raw = await readFile(STATE_FILE_PATH, "utf8");
@@ -3758,6 +3836,7 @@ async function writePersistedCacheStats(state: CacheStatsState, currentSessionHa
3758
3836
  existingSessions[hash][modelKey] = stats;
3759
3837
  }
3760
3838
  }
3839
+ existingTotalsByModel = { ...(parsed.totalsByModel ?? {}) };
3761
3840
  existingLastRoutedModelBySession = { ...(parsed.lastRoutedModelBySession ?? {}) };
3762
3841
  }
3763
3842
  } catch {
@@ -3765,15 +3844,17 @@ async function writePersistedCacheStats(state: CacheStatsState, currentSessionHa
3765
3844
  }
3766
3845
 
3767
3846
  const sessions = mergeCacheSessions(existingSessions, state, currentSessionHash);
3847
+ const totalsByModel = mergeCacheTotals(existingTotalsByModel, state.totalsByModel, options);
3768
3848
  const lastRoutedModelBySession = mergeLastRoutedModels(
3769
3849
  existingLastRoutedModelBySession,
3770
3850
  state,
3771
3851
  currentSessionHash,
3772
3852
  );
3773
3853
 
3774
- const payload: PersistedCacheStatsV5 = {
3775
- version: 5,
3854
+ const payload: PersistedCacheStatsV6 = {
3855
+ version: 6,
3776
3856
  sessions,
3857
+ totalsByModel,
3777
3858
  legacyFamily: state.legacyFamily,
3778
3859
  ...(Object.keys(lastRoutedModelBySession).length > 0 ? { lastRoutedModelBySession } : {}),
3779
3860
  };
@@ -5660,6 +5741,8 @@ export const __internals_for_tests = {
5660
5741
  emptyAllCacheStats,
5661
5742
  parseCacheStats,
5662
5743
  parsePersistedCacheStats,
5744
+ deriveTotalsByModelFromSessionStats,
5745
+ parsePersistedTotalsByModel,
5663
5746
  // Recent sample / stats output / diagnosis helpers
5664
5747
  MAX_RECENT_SAMPLES,
5665
5748
  buildStatsOutput,
@@ -5691,6 +5774,7 @@ export const __internals_for_tests = {
5691
5774
  getCacheHintsService,
5692
5775
  // Persistence helpers (for reload/reset tests)
5693
5776
  mergeCacheSessions,
5777
+ mergeCacheTotals,
5694
5778
  mergeLastRoutedModels,
5695
5779
  writePersistedCacheStats,
5696
5780
  readPersistedCacheStats,
@@ -5730,10 +5814,13 @@ export default function (pi: ExtensionAPI) {
5730
5814
  const promptCacheRetention400Models = new Set<string>();
5731
5815
  const warnedPromptCacheRetention400Models = new Set<string>();
5732
5816
  let cacheStatsByModel: Record<string, CacheStats> = {};
5817
+ let cacheStatsTotalsByModel: Record<string, CacheStats> = {};
5733
5818
  let cacheStatsLegacyFamily: Partial<Record<CacheProviderId, CacheStats>> = emptyAllCacheStats();
5734
5819
  let lastStatusText: string | undefined;
5735
5820
  let persistenceWarningShown = false;
5736
5821
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
5822
+ let replacePersistedTotalsOnNextWrite = false;
5823
+ const pendingDeletedTotalModelKeys = new Set<string>();
5737
5824
  let integrityNotificationShown = false;
5738
5825
  let currentSessionId = "";
5739
5826
  let currentSessionHash = "";
@@ -5824,6 +5911,7 @@ export default function (pi: ExtensionAPI) {
5824
5911
  function getCacheStatsState(): CacheStatsState {
5825
5912
  return {
5826
5913
  statsByModel: cacheStatsByModel,
5914
+ totalsByModel: cacheStatsTotalsByModel,
5827
5915
  legacyFamily: cacheStatsLegacyFamily,
5828
5916
  ...(currentSessionHashSet && lastActualRoutedModel
5829
5917
  ? { lastRoutedModelBySession: { [currentSessionHash]: lastActualRoutedModel } }
@@ -5831,16 +5919,15 @@ export default function (pi: ExtensionAPI) {
5831
5919
  };
5832
5920
  }
5833
5921
 
5834
- /** Look up active stats for a model, falling back to legacy family. */
5922
+ /** Look up visible cumulative stats for a model, falling back to legacy family. */
5835
5923
  function getStatsForModel(model: PiModel | undefined, adapter: CacheProviderAdapter): CacheStats {
5836
5924
  if (model) {
5837
- const key = sessionModelKey(model);
5838
- const existing = cacheStatsByModel[key];
5925
+ const key = modelKey(model);
5926
+ const existing = cacheStatsTotalsByModel[key];
5839
5927
  if (existing) return existing;
5840
5928
  }
5841
5929
 
5842
- // Fallback: legacy family bucket — used when model key is unknown
5843
- // or this model hasn't been seen yet in this session.
5930
+ // Fallback: legacy family bucket — used when model key is unknown.
5844
5931
  const family = cacheStatsLegacyFamily[adapter.id];
5845
5932
  if (family) return family;
5846
5933
 
@@ -5849,7 +5936,7 @@ export default function (pi: ExtensionAPI) {
5849
5936
  return created;
5850
5937
  }
5851
5938
 
5852
- /** Get or create a stats entry for the given model key. */
5939
+ /** Get or create a session-scoped stats entry for the given key. */
5853
5940
  function getOrCreateStatsByModelKey(key: string): CacheStats {
5854
5941
  const existing = cacheStatsByModel[key];
5855
5942
  if (existing) return existing;
@@ -5859,10 +5946,27 @@ export default function (pi: ExtensionAPI) {
5859
5946
  return created;
5860
5947
  }
5861
5948
 
5949
+ /** Get or create the cumulative provider/model stats entry shown in the footer. */
5950
+ function getOrCreateTotalStatsForModel(model: PiModel): CacheStats {
5951
+ const key = modelKey(model);
5952
+ const existing = cacheStatsTotalsByModel[key];
5953
+ if (existing) return existing;
5954
+
5955
+ const created = emptyCacheStats();
5956
+ cacheStatsTotalsByModel[key] = created;
5957
+ return created;
5958
+ }
5959
+
5862
5960
  function resetStatsForModel(model: PiModel): void {
5863
- const sk = sessionModelKey(model);
5864
- delete cacheStatsByModel[sk];
5865
- recentSamplesByModelKey.delete(sk);
5961
+ const displayKey = modelKey(model);
5962
+ for (const key of Object.keys(cacheStatsByModel)) {
5963
+ if (modelKeyFromSessionScoped(key) === displayKey) delete cacheStatsByModel[key];
5964
+ }
5965
+ delete cacheStatsTotalsByModel[displayKey];
5966
+ pendingDeletedTotalModelKeys.add(displayKey);
5967
+ for (const key of Array.from(recentSamplesByModelKey.keys())) {
5968
+ if (modelKeyFromSessionScoped(key) === displayKey) recentSamplesByModelKey.delete(key);
5969
+ }
5866
5970
  lastStatusText = undefined;
5867
5971
  }
5868
5972
 
@@ -5871,6 +5975,8 @@ export default function (pi: ExtensionAPI) {
5871
5975
  for (const key of Object.keys(cacheStatsByModel)) {
5872
5976
  if (key.startsWith(prefix)) delete cacheStatsByModel[key];
5873
5977
  }
5978
+ cacheStatsTotalsByModel = {};
5979
+ replacePersistedTotalsOnNextWrite = true;
5874
5980
  for (const key of Array.from(recentSamplesByModelKey.keys())) {
5875
5981
  if (key.startsWith(prefix)) recentSamplesByModelKey.delete(key);
5876
5982
  }
@@ -5880,7 +5986,16 @@ export default function (pi: ExtensionAPI) {
5880
5986
 
5881
5987
  async function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
5882
5988
  try {
5883
- await writePersistedCacheStats(getCacheStatsState(), currentSessionHashSet ? currentSessionHash : undefined);
5989
+ await writePersistedCacheStats(
5990
+ getCacheStatsState(),
5991
+ currentSessionHashSet ? currentSessionHash : undefined,
5992
+ {
5993
+ deleteModelKeys: Array.from(pendingDeletedTotalModelKeys),
5994
+ replaceTotals: replacePersistedTotalsOnNextWrite,
5995
+ },
5996
+ );
5997
+ pendingDeletedTotalModelKeys.clear();
5998
+ replacePersistedTotalsOnNextWrite = false;
5884
5999
  } catch (error) {
5885
6000
  console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
5886
6001
  if (!persistenceWarningShown) {
@@ -5930,6 +6045,15 @@ export default function (pi: ExtensionAPI) {
5930
6045
  }
5931
6046
  }
5932
6047
 
6048
+ // Roll over cumulative provider/model totals.
6049
+ for (const key of Object.keys(cacheStatsTotalsByModel)) {
6050
+ const stats = cacheStatsTotalsByModel[key];
6051
+ if (stats && stats.day !== day) {
6052
+ cacheStatsTotalsByModel[key] = emptyCacheStats(day);
6053
+ changed = true;
6054
+ }
6055
+ }
6056
+
5933
6057
  // Roll over legacy family entries.
5934
6058
  for (const id of CACHE_PROVIDER_IDS) {
5935
6059
  const stats = cacheStatsLegacyFamily[id];
@@ -5962,6 +6086,7 @@ export default function (pi: ExtensionAPI) {
5962
6086
  persisted,
5963
6087
  currentSessionHashSet ? currentSessionHash : undefined,
5964
6088
  );
6089
+ cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
5965
6090
  cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
5966
6091
  lastActualRoutedModel = currentSessionHashSet
5967
6092
  ? persisted?.lastRoutedModelBySession?.[currentSessionHash]
@@ -5979,6 +6104,7 @@ export default function (pi: ExtensionAPI) {
5979
6104
  persisted,
5980
6105
  currentSessionHashSet ? currentSessionHash : undefined,
5981
6106
  );
6107
+ cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
5982
6108
  cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
5983
6109
  lastActualRoutedModel = currentSessionHashSet
5984
6110
  ? persisted?.lastRoutedModelBySession?.[currentSessionHash]
@@ -5996,15 +6122,9 @@ export default function (pi: ExtensionAPI) {
5996
6122
  * meaningful footer content on session_start after reload.
5997
6123
  */
5998
6124
  function findBestRouterModelStats(): { adapter: CacheProviderAdapter; stats: CacheStats } | undefined {
5999
- if (!currentSessionHash) return undefined;
6000
- const prefix = `${currentSessionHash}:`;
6001
6125
  let best: { adapter: CacheProviderAdapter; stats: CacheStats; total: number } | undefined;
6002
6126
 
6003
- for (const [key, stats] of Object.entries(cacheStatsByModel)) {
6004
- if (!key.startsWith(prefix)) continue;
6005
-
6006
- // Extract provider/id from key like "abc123:run-claude/claude-opus-4-8"
6007
- const modelKeyPart = key.slice(prefix.length);
6127
+ for (const [modelKeyPart, stats] of Object.entries(cacheStatsTotalsByModel)) {
6008
6128
  const slashIdx = modelKeyPart.indexOf("/");
6009
6129
  if (slashIdx < 0 || slashIdx >= modelKeyPart.length - 1) continue;
6010
6130
  const modelId = modelKeyPart.slice(slashIdx + 1);
@@ -6058,6 +6178,7 @@ export default function (pi: ExtensionAPI) {
6058
6178
  currentSessionHashSet ? currentSessionHash : undefined,
6059
6179
  cacheStatsByModel,
6060
6180
  lastActualRoutedModel,
6181
+ cacheStatsTotalsByModel,
6061
6182
  ) ?? findBestRouterModelStats();
6062
6183
  if (realEntry) {
6063
6184
  const statsText = formatCacheStats(realEntry.adapter, realEntry.stats);
@@ -6068,11 +6189,10 @@ export default function (pi: ExtensionAPI) {
6068
6189
  }
6069
6190
 
6070
6191
  if (adapter) {
6071
- // Display session-scoped stats. A model that has never been used
6072
- // in this session shows 0/0. The message_end hook populates
6073
- // cacheStatsByModel[sessionModelKey(displayModel)] on first use.
6074
- const sk = displayModel ? sessionModelKey(displayModel) : undefined;
6075
- const stats = sk ? cacheStatsByModel[sk] : undefined;
6192
+ // Display provider/model cumulative stats. A model that has never
6193
+ // been used locally shows 0/0. The message_end hook updates both the
6194
+ // current session bucket and this restart-persistent totals bucket.
6195
+ const stats = displayModel ? cacheStatsTotalsByModel[modelKey(displayModel)] : undefined;
6076
6196
  const statsText = formatCacheStats(adapter, stats ?? emptyCacheStats());
6077
6197
  statusText = runtimeOptimizerEnabled ? statsText : `Cache Optimizer disabled · ${statsText}`;
6078
6198
  }
@@ -6406,6 +6526,7 @@ export default function (pi: ExtensionAPI) {
6406
6526
  if (statsModel) {
6407
6527
  const sk = sessionModelKey(statsModel);
6408
6528
  addUsageToCacheStats(getOrCreateStatsByModelKey(sk), usage);
6529
+ addUsageToCacheStats(getOrCreateTotalStatsForModel(statsModel), usage);
6409
6530
  } else {
6410
6531
  addUsageToCacheStats(getStatsForModel(undefined, adapter), usage);
6411
6532
  }
@@ -6424,7 +6545,7 @@ export default function (pi: ExtensionAPI) {
6424
6545
  // stats — show active model stats bucket, recent trend, usage
6425
6546
  // compat — show compat suggestion with file path
6426
6547
  // fix — auto-fix compat issues (writes models.json, requires UI)
6427
- // reset — reset current session model stats bucket (local only)
6548
+ // reset — reset current provider/model footer stats bucket (local only)
6428
6549
  // (no args) — interactive menu (with UI) or help summary
6429
6550
  // ────────────────────────────────────────────────────────────────
6430
6551
  pi.registerCommand("cache-optimizer", {
@@ -6440,13 +6561,13 @@ export default function (pi: ExtensionAPI) {
6440
6561
  resetCurrentSessionStats();
6441
6562
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6442
6563
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6443
- cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Current-session stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
6564
+ cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
6444
6565
  } else if (subcommand === "disable") {
6445
6566
  setRuntimeOptimizerEnabled(false);
6446
6567
  resetCurrentSessionStats();
6447
6568
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6448
6569
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6449
- cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process. Current-session stats were reset and will keep collecting while disabled for comparison.\n${formatOptimizerRuntimeMode()}`, "warning");
6570
+ cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process. Local footer stats were reset and will keep collecting while disabled for comparison.\n${formatOptimizerRuntimeMode()}`, "warning");
6450
6571
  } else if (subcommand === "doctor") {
6451
6572
  if (!model) {
6452
6573
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
@@ -6455,7 +6576,7 @@ export default function (pi: ExtensionAPI) {
6455
6576
  const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
6456
6577
  const adapter = selectAdapterForModel(model);
6457
6578
  const sk = model ? sessionModelKey(model) : undefined;
6458
- const statsState = sk ? cacheStatsByModel[sk] : undefined;
6579
+ const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
6459
6580
  const samples = sk ? getRecentSamples(sk) : [];
6460
6581
  const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
6461
6582
  const fullDiagnosis = lowHitLines.length > 0
@@ -6469,7 +6590,7 @@ export default function (pi: ExtensionAPI) {
6469
6590
  }
6470
6591
  const adapter = selectAdapterForModel(model);
6471
6592
  const sk = model ? sessionModelKey(model) : undefined;
6472
- const statsState = sk ? cacheStatsByModel[sk] : undefined;
6593
+ const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
6473
6594
  const samples = sk ? getRecentSamples(sk) : [];
6474
6595
  const output = buildStatsOutput(model, adapter, statsState, samples);
6475
6596
  cmdCtx.ui.notify(output, "info");
@@ -6502,7 +6623,7 @@ export default function (pi: ExtensionAPI) {
6502
6623
 
6503
6624
  const displayKey = modelKey(model);
6504
6625
 
6505
- // Reset session-scoped stats for the effective active model. If the
6626
+ // Reset local footer stats for the effective active model. If the
6506
6627
  // selected model is a virtual router and the protocol exposes a live
6507
6628
  // route, this clears the real upstream bucket, not the router shell.
6508
6629
  resetStatsForModel(model);
@@ -6514,9 +6635,9 @@ export default function (pi: ExtensionAPI) {
6514
6635
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6515
6636
 
6516
6637
  cmdCtx.ui.notify(
6517
- `✅ Reset local session cache stats for "${displayKey}". ` +
6638
+ `✅ Reset local footer cache stats for "${displayKey}". ` +
6518
6639
  "Upstream provider prompt cache was not modified. " +
6519
- "New requests will start a fresh stats bucket for this Pi session.",
6640
+ "New requests will start a fresh local stats bucket for this provider/model.",
6520
6641
  "info",
6521
6642
  );
6522
6643
  } else if (subcommand === "fix") {
@@ -6831,7 +6952,7 @@ export default function (pi: ExtensionAPI) {
6831
6952
  "Stats — Show cache stats and trend",
6832
6953
  "Compat — Show compat suggestion",
6833
6954
  "Fix — Auto-fix compat issues (writes models.json)",
6834
- "Reset — Reset local session stats",
6955
+ "Reset — Reset local provider/model stats",
6835
6956
  "Cancel",
6836
6957
  ];
6837
6958
  const choice = await cmdCtx.ui.select("Cache Optimizer", menuOptions);
@@ -6840,13 +6961,13 @@ export default function (pi: ExtensionAPI) {
6840
6961
  resetCurrentSessionStats();
6841
6962
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6842
6963
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6843
- cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Current-session stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
6964
+ cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
6844
6965
  } else if (choice === menuOptions[1]) {
6845
6966
  setRuntimeOptimizerEnabled(false);
6846
6967
  resetCurrentSessionStats();
6847
6968
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6848
6969
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6849
- cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process. Current-session stats were reset and will keep collecting while disabled for comparison.\n${formatOptimizerRuntimeMode()}`, "warning");
6970
+ cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process. Local footer stats were reset and will keep collecting while disabled for comparison.\n${formatOptimizerRuntimeMode()}`, "warning");
6850
6971
  } else if (choice === menuOptions[2]) {
6851
6972
  if (!model) {
6852
6973
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
@@ -6854,7 +6975,7 @@ export default function (pi: ExtensionAPI) {
6854
6975
  const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
6855
6976
  const adapter = selectAdapterForModel(model);
6856
6977
  const sk = model ? sessionModelKey(model) : undefined;
6857
- const statsState = sk ? cacheStatsByModel[sk] : undefined;
6978
+ const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
6858
6979
  const samples = sk ? getRecentSamples(sk) : [];
6859
6980
  const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
6860
6981
  const fullDiagnosis = lowHitLines.length > 0
@@ -6868,7 +6989,7 @@ export default function (pi: ExtensionAPI) {
6868
6989
  } else {
6869
6990
  const adapter = selectAdapterForModel(model);
6870
6991
  const sk = model ? sessionModelKey(model) : undefined;
6871
- const statsState = sk ? cacheStatsByModel[sk] : undefined;
6992
+ const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
6872
6993
  const samples = sk ? getRecentSamples(sk) : [];
6873
6994
  const output = buildStatsOutput(model, adapter, statsState, samples);
6874
6995
  cmdCtx.ui.notify(output, "info");
@@ -7003,7 +7124,7 @@ export default function (pi: ExtensionAPI) {
7003
7124
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
7004
7125
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
7005
7126
  cmdCtx.ui.notify(
7006
- `✅ Reset local session cache stats for "${displayKey}". ` +
7127
+ `✅ Reset local footer cache stats for "${displayKey}". ` +
7007
7128
  "Upstream provider prompt cache was not modified.",
7008
7129
  "info",
7009
7130
  );
@@ -7023,7 +7144,7 @@ export default function (pi: ExtensionAPI) {
7023
7144
  diagnosis.push(" stats — Show active model stats bucket and recent trend");
7024
7145
  diagnosis.push(" compat — Show compat suggestion with edit location");
7025
7146
  diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
7026
- diagnosis.push(" reset — Reset local session stats for current model (does not affect upstream)");
7147
+ diagnosis.push(" reset — Reset local provider/model stats for current model (does not affect upstream)");
7027
7148
  diagnosis.push("");
7028
7149
  diagnosis.push(formatOptimizerRuntimeMode());
7029
7150
  diagnosis.push("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.6.10",
3
+ "version": "2.6.11",
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",