pi-cache-optimizer 2.6.9 → 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
  };
@@ -4833,6 +4914,19 @@ function composeMissingEntryInsertion(
4833
4914
  modelId: string,
4834
4915
  compatKeys: Record<string, unknown>,
4835
4916
  ): { modifiedText: string; placementLabel: string } {
4917
+ // Comments preserve length when stripped (`stripJsoncComments` replaces
4918
+ // comment bytes 1-for-1 with spaces), so offsets derived from the
4919
+ // comment-stripped text map cleanly back to the original. However,
4920
+ // `lastIndexOf("{", pos)` / `lastIndexOf("[", pos)` must NOT be run
4921
+ // against the raw original: a comment like `// add [more] here with a {
4922
+ // brace` would surface `[` / `{` bytes that have no structural meaning,
4923
+ // contaminating the `hasExisting`/`hasExistingElements` decision below
4924
+ // and producing a stray leading comma. Run the structural searches
4925
+ // against the comment-stripped version; keep the indentation lookups
4926
+ // (which only care about newlines + leading whitespace) on the
4927
+ // original since comments never contain forward-scan-relevant bytes.
4928
+ const cleanText = stripJsoncComments(originalText);
4929
+
4836
4930
  // Resolve a sensible indentation step from an arbitrary byte offset in
4837
4931
  // the original file.
4838
4932
  const indentUnitAt = (offset: number): string => {
@@ -4867,9 +4961,11 @@ function composeMissingEntryInsertion(
4867
4961
  const inner1 = inner0 + unit; // indent of compat keys inside the model
4868
4962
  const inner2 = inner1 + unit; // indent of compat values
4869
4963
 
4870
- // Determine whether the array is empty (need to skip the leading newline).
4871
- const arrayInterior = originalText.slice(
4872
- originalText.lastIndexOf("[", diagnosis.modelsEnd) + 1,
4964
+ // Determine whether the array is empty (need to skip the leading comma).
4965
+ // Search for the models `[` on the comment-stripped text so a `[` inside
4966
+ // a comment cannot be mistaken for the array opener.
4967
+ const arrayInterior = cleanText.slice(
4968
+ cleanText.lastIndexOf("[", diagnosis.modelsEnd) + 1,
4873
4969
  diagnosis.modelsEnd,
4874
4970
  ).trim();
4875
4971
  const hasExistingElements = arrayInterior.length > 0;
@@ -4903,8 +4999,10 @@ function composeMissingEntryInsertion(
4903
4999
  const inner3 = inner2 + unit;
4904
5000
 
4905
5001
  const compatBlock = formatCompactCompat(inner3);
4906
- const providersInterior = originalText.slice(
4907
- originalText.lastIndexOf("{", diagnosis.providersEnd) + 1,
5002
+ // Search for the providers `{` on the comment-stripped text so a `{`
5003
+ // inside a comment cannot be mistaken for the providers object opener.
5004
+ const providersInterior = cleanText.slice(
5005
+ cleanText.lastIndexOf("{", diagnosis.providersEnd) + 1,
4908
5006
  diagnosis.providersEnd,
4909
5007
  ).trim();
4910
5008
  const hasExisting = providersInterior.length > 0;
@@ -5279,6 +5377,7 @@ function selfCheckFix(
5279
5377
  providerLabel: string,
5280
5378
  modelId: string,
5281
5379
  compatKeys: Record<string, unknown>,
5380
+ placement: "provider" | "model" = "model",
5282
5381
  ): string | null {
5283
5382
  try {
5284
5383
  // Step 1: Parse both versions as JSONC (comments + trailing commas allowed).
@@ -5371,7 +5470,18 @@ function selfCheckFix(
5371
5470
  } else {
5372
5471
  const origCompat = origObj[key] as Record<string, unknown>;
5373
5472
  const modCompat = modObj[key] as Record<string, unknown>;
5374
- const mayRepairThisCompat = origObj === origProvider || origObj === origTargetModelRecord;
5473
+ // Only the compat object at the level ACTUALLY edited may have
5474
+ // its values repaired by this fix. The un-edited level must
5475
+ // remain byte/structure-equivalent, so its same-name keys stay
5476
+ // under full validation. Using a disjunction OR (provider ||
5477
+ // target) here would silently skip validation at the un-edited
5478
+ // level, masking corruption (e.g. a buggy editor accidentally
5479
+ // breaking provider.compat.sendSessionAffinityHeaders while
5480
+ // the fix was a model-level repair). Track placement — only
5481
+ // the placement-resolved object's own compat may be exempt.
5482
+ const mayRepairThisCompat =
5483
+ (placement === "provider" && origObj === origProvider) ||
5484
+ (placement === "model" && origObj === origTargetModelRecord);
5375
5485
  for (const ck of Object.keys(origCompat)) {
5376
5486
  if (!(ck in modCompat)) return false;
5377
5487
  // The fix may repair an existing wrong compat value (for example
@@ -5393,12 +5503,15 @@ function selfCheckFix(
5393
5503
  return "Modified file: original structure was altered (data loss detected)";
5394
5504
  }
5395
5505
 
5396
- // Step 8: Basic format sanity checks
5397
- if (modified.length < original.length) {
5398
- return "Modified file: content is shorter than original (possible truncation)";
5399
- }
5506
+ // Note: we intentionally do NOT enforce `modified.length >= original.length`.
5507
+ // The surgical editor may replace an existing compat value with a shorter one
5508
+ // (e.g. `false` -> `true`), which legitimately shrinks the file by a byte.
5509
+ // Real data loss / truncation is already caught by Step 7's isSubset
5510
+ // (every original key still present) and Step 8's root-bracket integrity
5511
+ // check below — a surviving length heuristic would false-positive on every
5512
+ // such value repair. (Tracked: the mofas glm-5.2 self-check failure path.)
5400
5513
 
5401
- // Step 9: Validate root bracket integrity with the same string/comment-aware
5514
+ // Step 8: Validate root bracket integrity with the same string/comment-aware
5402
5515
  // scanner used for edits. Do not count raw braces: comments or strings may
5403
5516
  // legitimately contain unmatched `{` / `}` bytes.
5404
5517
  const modifiedClean = stripJsoncComments(modified);
@@ -5628,6 +5741,8 @@ export const __internals_for_tests = {
5628
5741
  emptyAllCacheStats,
5629
5742
  parseCacheStats,
5630
5743
  parsePersistedCacheStats,
5744
+ deriveTotalsByModelFromSessionStats,
5745
+ parsePersistedTotalsByModel,
5631
5746
  // Recent sample / stats output / diagnosis helpers
5632
5747
  MAX_RECENT_SAMPLES,
5633
5748
  buildStatsOutput,
@@ -5659,6 +5774,7 @@ export const __internals_for_tests = {
5659
5774
  getCacheHintsService,
5660
5775
  // Persistence helpers (for reload/reset tests)
5661
5776
  mergeCacheSessions,
5777
+ mergeCacheTotals,
5662
5778
  mergeLastRoutedModels,
5663
5779
  writePersistedCacheStats,
5664
5780
  readPersistedCacheStats,
@@ -5673,6 +5789,9 @@ export const __internals_for_tests = {
5673
5789
  locateModelInJsonc,
5674
5790
  composeFixInsertion,
5675
5791
  selfCheckFix,
5792
+ analyzeModelsJsonForMissingEntry,
5793
+ composeMissingEntryInsertion,
5794
+ selfCheckMissingEntryInsertion,
5676
5795
  decideFixPlacement,
5677
5796
  chooseFixPlacement,
5678
5797
  findExistingCompatKeysInJsonc,
@@ -5695,10 +5814,13 @@ export default function (pi: ExtensionAPI) {
5695
5814
  const promptCacheRetention400Models = new Set<string>();
5696
5815
  const warnedPromptCacheRetention400Models = new Set<string>();
5697
5816
  let cacheStatsByModel: Record<string, CacheStats> = {};
5817
+ let cacheStatsTotalsByModel: Record<string, CacheStats> = {};
5698
5818
  let cacheStatsLegacyFamily: Partial<Record<CacheProviderId, CacheStats>> = emptyAllCacheStats();
5699
5819
  let lastStatusText: string | undefined;
5700
5820
  let persistenceWarningShown = false;
5701
5821
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
5822
+ let replacePersistedTotalsOnNextWrite = false;
5823
+ const pendingDeletedTotalModelKeys = new Set<string>();
5702
5824
  let integrityNotificationShown = false;
5703
5825
  let currentSessionId = "";
5704
5826
  let currentSessionHash = "";
@@ -5789,6 +5911,7 @@ export default function (pi: ExtensionAPI) {
5789
5911
  function getCacheStatsState(): CacheStatsState {
5790
5912
  return {
5791
5913
  statsByModel: cacheStatsByModel,
5914
+ totalsByModel: cacheStatsTotalsByModel,
5792
5915
  legacyFamily: cacheStatsLegacyFamily,
5793
5916
  ...(currentSessionHashSet && lastActualRoutedModel
5794
5917
  ? { lastRoutedModelBySession: { [currentSessionHash]: lastActualRoutedModel } }
@@ -5796,16 +5919,15 @@ export default function (pi: ExtensionAPI) {
5796
5919
  };
5797
5920
  }
5798
5921
 
5799
- /** 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. */
5800
5923
  function getStatsForModel(model: PiModel | undefined, adapter: CacheProviderAdapter): CacheStats {
5801
5924
  if (model) {
5802
- const key = sessionModelKey(model);
5803
- const existing = cacheStatsByModel[key];
5925
+ const key = modelKey(model);
5926
+ const existing = cacheStatsTotalsByModel[key];
5804
5927
  if (existing) return existing;
5805
5928
  }
5806
5929
 
5807
- // Fallback: legacy family bucket — used when model key is unknown
5808
- // or this model hasn't been seen yet in this session.
5930
+ // Fallback: legacy family bucket — used when model key is unknown.
5809
5931
  const family = cacheStatsLegacyFamily[adapter.id];
5810
5932
  if (family) return family;
5811
5933
 
@@ -5814,7 +5936,7 @@ export default function (pi: ExtensionAPI) {
5814
5936
  return created;
5815
5937
  }
5816
5938
 
5817
- /** Get or create a stats entry for the given model key. */
5939
+ /** Get or create a session-scoped stats entry for the given key. */
5818
5940
  function getOrCreateStatsByModelKey(key: string): CacheStats {
5819
5941
  const existing = cacheStatsByModel[key];
5820
5942
  if (existing) return existing;
@@ -5824,10 +5946,27 @@ export default function (pi: ExtensionAPI) {
5824
5946
  return created;
5825
5947
  }
5826
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
+
5827
5960
  function resetStatsForModel(model: PiModel): void {
5828
- const sk = sessionModelKey(model);
5829
- delete cacheStatsByModel[sk];
5830
- 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
+ }
5831
5970
  lastStatusText = undefined;
5832
5971
  }
5833
5972
 
@@ -5836,6 +5975,8 @@ export default function (pi: ExtensionAPI) {
5836
5975
  for (const key of Object.keys(cacheStatsByModel)) {
5837
5976
  if (key.startsWith(prefix)) delete cacheStatsByModel[key];
5838
5977
  }
5978
+ cacheStatsTotalsByModel = {};
5979
+ replacePersistedTotalsOnNextWrite = true;
5839
5980
  for (const key of Array.from(recentSamplesByModelKey.keys())) {
5840
5981
  if (key.startsWith(prefix)) recentSamplesByModelKey.delete(key);
5841
5982
  }
@@ -5845,7 +5986,16 @@ export default function (pi: ExtensionAPI) {
5845
5986
 
5846
5987
  async function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
5847
5988
  try {
5848
- 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;
5849
5999
  } catch (error) {
5850
6000
  console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
5851
6001
  if (!persistenceWarningShown) {
@@ -5895,6 +6045,15 @@ export default function (pi: ExtensionAPI) {
5895
6045
  }
5896
6046
  }
5897
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
+
5898
6057
  // Roll over legacy family entries.
5899
6058
  for (const id of CACHE_PROVIDER_IDS) {
5900
6059
  const stats = cacheStatsLegacyFamily[id];
@@ -5927,6 +6086,7 @@ export default function (pi: ExtensionAPI) {
5927
6086
  persisted,
5928
6087
  currentSessionHashSet ? currentSessionHash : undefined,
5929
6088
  );
6089
+ cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
5930
6090
  cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
5931
6091
  lastActualRoutedModel = currentSessionHashSet
5932
6092
  ? persisted?.lastRoutedModelBySession?.[currentSessionHash]
@@ -5944,6 +6104,7 @@ export default function (pi: ExtensionAPI) {
5944
6104
  persisted,
5945
6105
  currentSessionHashSet ? currentSessionHash : undefined,
5946
6106
  );
6107
+ cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
5947
6108
  cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
5948
6109
  lastActualRoutedModel = currentSessionHashSet
5949
6110
  ? persisted?.lastRoutedModelBySession?.[currentSessionHash]
@@ -5961,15 +6122,9 @@ export default function (pi: ExtensionAPI) {
5961
6122
  * meaningful footer content on session_start after reload.
5962
6123
  */
5963
6124
  function findBestRouterModelStats(): { adapter: CacheProviderAdapter; stats: CacheStats } | undefined {
5964
- if (!currentSessionHash) return undefined;
5965
- const prefix = `${currentSessionHash}:`;
5966
6125
  let best: { adapter: CacheProviderAdapter; stats: CacheStats; total: number } | undefined;
5967
6126
 
5968
- for (const [key, stats] of Object.entries(cacheStatsByModel)) {
5969
- if (!key.startsWith(prefix)) continue;
5970
-
5971
- // Extract provider/id from key like "abc123:run-claude/claude-opus-4-8"
5972
- const modelKeyPart = key.slice(prefix.length);
6127
+ for (const [modelKeyPart, stats] of Object.entries(cacheStatsTotalsByModel)) {
5973
6128
  const slashIdx = modelKeyPart.indexOf("/");
5974
6129
  if (slashIdx < 0 || slashIdx >= modelKeyPart.length - 1) continue;
5975
6130
  const modelId = modelKeyPart.slice(slashIdx + 1);
@@ -6023,6 +6178,7 @@ export default function (pi: ExtensionAPI) {
6023
6178
  currentSessionHashSet ? currentSessionHash : undefined,
6024
6179
  cacheStatsByModel,
6025
6180
  lastActualRoutedModel,
6181
+ cacheStatsTotalsByModel,
6026
6182
  ) ?? findBestRouterModelStats();
6027
6183
  if (realEntry) {
6028
6184
  const statsText = formatCacheStats(realEntry.adapter, realEntry.stats);
@@ -6033,11 +6189,10 @@ export default function (pi: ExtensionAPI) {
6033
6189
  }
6034
6190
 
6035
6191
  if (adapter) {
6036
- // Display session-scoped stats. A model that has never been used
6037
- // in this session shows 0/0. The message_end hook populates
6038
- // cacheStatsByModel[sessionModelKey(displayModel)] on first use.
6039
- const sk = displayModel ? sessionModelKey(displayModel) : undefined;
6040
- 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;
6041
6196
  const statsText = formatCacheStats(adapter, stats ?? emptyCacheStats());
6042
6197
  statusText = runtimeOptimizerEnabled ? statsText : `Cache Optimizer disabled · ${statsText}`;
6043
6198
  }
@@ -6371,6 +6526,7 @@ export default function (pi: ExtensionAPI) {
6371
6526
  if (statsModel) {
6372
6527
  const sk = sessionModelKey(statsModel);
6373
6528
  addUsageToCacheStats(getOrCreateStatsByModelKey(sk), usage);
6529
+ addUsageToCacheStats(getOrCreateTotalStatsForModel(statsModel), usage);
6374
6530
  } else {
6375
6531
  addUsageToCacheStats(getStatsForModel(undefined, adapter), usage);
6376
6532
  }
@@ -6389,7 +6545,7 @@ export default function (pi: ExtensionAPI) {
6389
6545
  // stats — show active model stats bucket, recent trend, usage
6390
6546
  // compat — show compat suggestion with file path
6391
6547
  // fix — auto-fix compat issues (writes models.json, requires UI)
6392
- // reset — reset current session model stats bucket (local only)
6548
+ // reset — reset current provider/model footer stats bucket (local only)
6393
6549
  // (no args) — interactive menu (with UI) or help summary
6394
6550
  // ────────────────────────────────────────────────────────────────
6395
6551
  pi.registerCommand("cache-optimizer", {
@@ -6405,13 +6561,13 @@ export default function (pi: ExtensionAPI) {
6405
6561
  resetCurrentSessionStats();
6406
6562
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6407
6563
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6408
- 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");
6409
6565
  } else if (subcommand === "disable") {
6410
6566
  setRuntimeOptimizerEnabled(false);
6411
6567
  resetCurrentSessionStats();
6412
6568
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6413
6569
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6414
- 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");
6415
6571
  } else if (subcommand === "doctor") {
6416
6572
  if (!model) {
6417
6573
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
@@ -6420,7 +6576,7 @@ export default function (pi: ExtensionAPI) {
6420
6576
  const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
6421
6577
  const adapter = selectAdapterForModel(model);
6422
6578
  const sk = model ? sessionModelKey(model) : undefined;
6423
- const statsState = sk ? cacheStatsByModel[sk] : undefined;
6579
+ const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
6424
6580
  const samples = sk ? getRecentSamples(sk) : [];
6425
6581
  const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
6426
6582
  const fullDiagnosis = lowHitLines.length > 0
@@ -6434,7 +6590,7 @@ export default function (pi: ExtensionAPI) {
6434
6590
  }
6435
6591
  const adapter = selectAdapterForModel(model);
6436
6592
  const sk = model ? sessionModelKey(model) : undefined;
6437
- const statsState = sk ? cacheStatsByModel[sk] : undefined;
6593
+ const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
6438
6594
  const samples = sk ? getRecentSamples(sk) : [];
6439
6595
  const output = buildStatsOutput(model, adapter, statsState, samples);
6440
6596
  cmdCtx.ui.notify(output, "info");
@@ -6467,7 +6623,7 @@ export default function (pi: ExtensionAPI) {
6467
6623
 
6468
6624
  const displayKey = modelKey(model);
6469
6625
 
6470
- // Reset session-scoped stats for the effective active model. If the
6626
+ // Reset local footer stats for the effective active model. If the
6471
6627
  // selected model is a virtual router and the protocol exposes a live
6472
6628
  // route, this clears the real upstream bucket, not the router shell.
6473
6629
  resetStatsForModel(model);
@@ -6479,9 +6635,9 @@ export default function (pi: ExtensionAPI) {
6479
6635
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6480
6636
 
6481
6637
  cmdCtx.ui.notify(
6482
- `✅ Reset local session cache stats for "${displayKey}". ` +
6638
+ `✅ Reset local footer cache stats for "${displayKey}". ` +
6483
6639
  "Upstream provider prompt cache was not modified. " +
6484
- "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.",
6485
6641
  "info",
6486
6642
  );
6487
6643
  } else if (subcommand === "fix") {
@@ -6692,7 +6848,7 @@ export default function (pi: ExtensionAPI) {
6692
6848
  const modifiedText = composeFixInsertion(originalText, location, suggestion.compatKeys, decision.placement);
6693
6849
 
6694
6850
  // Self-check
6695
- const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
6851
+ const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, decision.placement);
6696
6852
  if (checkError !== null) {
6697
6853
  cmdCtx.ui.notify(
6698
6854
  `❌ Self-check failed before write: ${checkError}\n` +
@@ -6761,7 +6917,7 @@ export default function (pi: ExtensionAPI) {
6761
6917
 
6762
6918
  // Post-write self-check (read back)
6763
6919
  const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
6764
- const postCheckError = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
6920
+ const postCheckError = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, decision.placement);
6765
6921
  if (postCheckError !== null) {
6766
6922
  // Restore from backup
6767
6923
  await copyFile(backupPath, MODELS_JSON_PATH);
@@ -6796,7 +6952,7 @@ export default function (pi: ExtensionAPI) {
6796
6952
  "Stats — Show cache stats and trend",
6797
6953
  "Compat — Show compat suggestion",
6798
6954
  "Fix — Auto-fix compat issues (writes models.json)",
6799
- "Reset — Reset local session stats",
6955
+ "Reset — Reset local provider/model stats",
6800
6956
  "Cancel",
6801
6957
  ];
6802
6958
  const choice = await cmdCtx.ui.select("Cache Optimizer", menuOptions);
@@ -6805,13 +6961,13 @@ export default function (pi: ExtensionAPI) {
6805
6961
  resetCurrentSessionStats();
6806
6962
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6807
6963
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6808
- 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");
6809
6965
  } else if (choice === menuOptions[1]) {
6810
6966
  setRuntimeOptimizerEnabled(false);
6811
6967
  resetCurrentSessionStats();
6812
6968
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6813
6969
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6814
- 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");
6815
6971
  } else if (choice === menuOptions[2]) {
6816
6972
  if (!model) {
6817
6973
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
@@ -6819,7 +6975,7 @@ export default function (pi: ExtensionAPI) {
6819
6975
  const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
6820
6976
  const adapter = selectAdapterForModel(model);
6821
6977
  const sk = model ? sessionModelKey(model) : undefined;
6822
- const statsState = sk ? cacheStatsByModel[sk] : undefined;
6978
+ const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
6823
6979
  const samples = sk ? getRecentSamples(sk) : [];
6824
6980
  const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
6825
6981
  const fullDiagnosis = lowHitLines.length > 0
@@ -6833,7 +6989,7 @@ export default function (pi: ExtensionAPI) {
6833
6989
  } else {
6834
6990
  const adapter = selectAdapterForModel(model);
6835
6991
  const sk = model ? sessionModelKey(model) : undefined;
6836
- const statsState = sk ? cacheStatsByModel[sk] : undefined;
6992
+ const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
6837
6993
  const samples = sk ? getRecentSamples(sk) : [];
6838
6994
  const output = buildStatsOutput(model, adapter, statsState, samples);
6839
6995
  cmdCtx.ui.notify(output, "info");
@@ -6890,7 +7046,7 @@ export default function (pi: ExtensionAPI) {
6890
7046
 
6891
7047
  const menuDecision = chooseFixPlacement(originalText, location, suggestion.compatKeys, suggestion.providerLabel);
6892
7048
  const modifiedText = composeFixInsertion(originalText, location, suggestion.compatKeys, menuDecision.placement);
6893
- const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
7049
+ const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement);
6894
7050
  if (checkError !== null) {
6895
7051
  cmdCtx.ui.notify(`❌ Self-check failed: ${checkError}\nNo changes made.`, "error");
6896
7052
  return;
@@ -6936,7 +7092,7 @@ export default function (pi: ExtensionAPI) {
6936
7092
  await rename(tempPath, MODELS_JSON_PATH);
6937
7093
 
6938
7094
  const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
6939
- const postCheck = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
7095
+ const postCheck = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement);
6940
7096
  if (postCheck !== null) {
6941
7097
  await copyFile(backupPath, MODELS_JSON_PATH);
6942
7098
  cmdCtx.ui.notify(`❌ Post-write check failed: ${postCheck}\nBackup restored.`, "error");
@@ -6968,7 +7124,7 @@ export default function (pi: ExtensionAPI) {
6968
7124
  await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
6969
7125
  await publishStatus(cmdCtx as unknown as ExtensionContext, model);
6970
7126
  cmdCtx.ui.notify(
6971
- `✅ Reset local session cache stats for "${displayKey}". ` +
7127
+ `✅ Reset local footer cache stats for "${displayKey}". ` +
6972
7128
  "Upstream provider prompt cache was not modified.",
6973
7129
  "info",
6974
7130
  );
@@ -6988,7 +7144,7 @@ export default function (pi: ExtensionAPI) {
6988
7144
  diagnosis.push(" stats — Show active model stats bucket and recent trend");
6989
7145
  diagnosis.push(" compat — Show compat suggestion with edit location");
6990
7146
  diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
6991
- 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)");
6992
7148
  diagnosis.push("");
6993
7149
  diagnosis.push(formatOptimizerRuntimeMode());
6994
7150
  diagnosis.push("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.6.9",
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",