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 +7 -7
- package/README.zh-CN.md +7 -7
- package/index.ts +218 -62
- package/package.json +1 -1
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
|
|
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
|
|
63
|
-
| `/cache-optimizer disable` | Disables optimization for the current Pi process, resets
|
|
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
|
|
67
|
-
| `/cache-optimizer reset` | Resets
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
63
|
-
| `/cache-optimizer disable` | 在当前 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` |
|
|
67
|
-
| `/cache-optimizer reset` |
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
3775
|
-
version:
|
|
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
|
|
4871
|
-
|
|
4872
|
-
|
|
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
|
-
|
|
4907
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5397
|
-
|
|
5398
|
-
|
|
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
|
|
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
|
|
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 =
|
|
5803
|
-
const existing =
|
|
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
|
|
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
|
|
5829
|
-
|
|
5830
|
-
|
|
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(
|
|
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 [
|
|
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
|
|
6037
|
-
//
|
|
6038
|
-
//
|
|
6039
|
-
const
|
|
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
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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