pi-cache-optimizer 2.6.10 → 2.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/index.ts +168 -47
- 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
|
};
|
|
@@ -5660,6 +5741,8 @@ export const __internals_for_tests = {
|
|
|
5660
5741
|
emptyAllCacheStats,
|
|
5661
5742
|
parseCacheStats,
|
|
5662
5743
|
parsePersistedCacheStats,
|
|
5744
|
+
deriveTotalsByModelFromSessionStats,
|
|
5745
|
+
parsePersistedTotalsByModel,
|
|
5663
5746
|
// Recent sample / stats output / diagnosis helpers
|
|
5664
5747
|
MAX_RECENT_SAMPLES,
|
|
5665
5748
|
buildStatsOutput,
|
|
@@ -5691,6 +5774,7 @@ export const __internals_for_tests = {
|
|
|
5691
5774
|
getCacheHintsService,
|
|
5692
5775
|
// Persistence helpers (for reload/reset tests)
|
|
5693
5776
|
mergeCacheSessions,
|
|
5777
|
+
mergeCacheTotals,
|
|
5694
5778
|
mergeLastRoutedModels,
|
|
5695
5779
|
writePersistedCacheStats,
|
|
5696
5780
|
readPersistedCacheStats,
|
|
@@ -5730,10 +5814,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
5730
5814
|
const promptCacheRetention400Models = new Set<string>();
|
|
5731
5815
|
const warnedPromptCacheRetention400Models = new Set<string>();
|
|
5732
5816
|
let cacheStatsByModel: Record<string, CacheStats> = {};
|
|
5817
|
+
let cacheStatsTotalsByModel: Record<string, CacheStats> = {};
|
|
5733
5818
|
let cacheStatsLegacyFamily: Partial<Record<CacheProviderId, CacheStats>> = emptyAllCacheStats();
|
|
5734
5819
|
let lastStatusText: string | undefined;
|
|
5735
5820
|
let persistenceWarningShown = false;
|
|
5736
5821
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
5822
|
+
let replacePersistedTotalsOnNextWrite = false;
|
|
5823
|
+
const pendingDeletedTotalModelKeys = new Set<string>();
|
|
5737
5824
|
let integrityNotificationShown = false;
|
|
5738
5825
|
let currentSessionId = "";
|
|
5739
5826
|
let currentSessionHash = "";
|
|
@@ -5824,6 +5911,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5824
5911
|
function getCacheStatsState(): CacheStatsState {
|
|
5825
5912
|
return {
|
|
5826
5913
|
statsByModel: cacheStatsByModel,
|
|
5914
|
+
totalsByModel: cacheStatsTotalsByModel,
|
|
5827
5915
|
legacyFamily: cacheStatsLegacyFamily,
|
|
5828
5916
|
...(currentSessionHashSet && lastActualRoutedModel
|
|
5829
5917
|
? { lastRoutedModelBySession: { [currentSessionHash]: lastActualRoutedModel } }
|
|
@@ -5831,16 +5919,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
5831
5919
|
};
|
|
5832
5920
|
}
|
|
5833
5921
|
|
|
5834
|
-
/** Look up
|
|
5922
|
+
/** Look up visible cumulative stats for a model, falling back to legacy family. */
|
|
5835
5923
|
function getStatsForModel(model: PiModel | undefined, adapter: CacheProviderAdapter): CacheStats {
|
|
5836
5924
|
if (model) {
|
|
5837
|
-
const key =
|
|
5838
|
-
const existing =
|
|
5925
|
+
const key = modelKey(model);
|
|
5926
|
+
const existing = cacheStatsTotalsByModel[key];
|
|
5839
5927
|
if (existing) return existing;
|
|
5840
5928
|
}
|
|
5841
5929
|
|
|
5842
|
-
// Fallback: legacy family bucket — used when model key is unknown
|
|
5843
|
-
// or this model hasn't been seen yet in this session.
|
|
5930
|
+
// Fallback: legacy family bucket — used when model key is unknown.
|
|
5844
5931
|
const family = cacheStatsLegacyFamily[adapter.id];
|
|
5845
5932
|
if (family) return family;
|
|
5846
5933
|
|
|
@@ -5849,7 +5936,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5849
5936
|
return created;
|
|
5850
5937
|
}
|
|
5851
5938
|
|
|
5852
|
-
/** Get or create a stats entry for the given
|
|
5939
|
+
/** Get or create a session-scoped stats entry for the given key. */
|
|
5853
5940
|
function getOrCreateStatsByModelKey(key: string): CacheStats {
|
|
5854
5941
|
const existing = cacheStatsByModel[key];
|
|
5855
5942
|
if (existing) return existing;
|
|
@@ -5859,10 +5946,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
5859
5946
|
return created;
|
|
5860
5947
|
}
|
|
5861
5948
|
|
|
5949
|
+
/** Get or create the cumulative provider/model stats entry shown in the footer. */
|
|
5950
|
+
function getOrCreateTotalStatsForModel(model: PiModel): CacheStats {
|
|
5951
|
+
const key = modelKey(model);
|
|
5952
|
+
const existing = cacheStatsTotalsByModel[key];
|
|
5953
|
+
if (existing) return existing;
|
|
5954
|
+
|
|
5955
|
+
const created = emptyCacheStats();
|
|
5956
|
+
cacheStatsTotalsByModel[key] = created;
|
|
5957
|
+
return created;
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5862
5960
|
function resetStatsForModel(model: PiModel): void {
|
|
5863
|
-
const
|
|
5864
|
-
|
|
5865
|
-
|
|
5961
|
+
const displayKey = modelKey(model);
|
|
5962
|
+
for (const key of Object.keys(cacheStatsByModel)) {
|
|
5963
|
+
if (modelKeyFromSessionScoped(key) === displayKey) delete cacheStatsByModel[key];
|
|
5964
|
+
}
|
|
5965
|
+
delete cacheStatsTotalsByModel[displayKey];
|
|
5966
|
+
pendingDeletedTotalModelKeys.add(displayKey);
|
|
5967
|
+
for (const key of Array.from(recentSamplesByModelKey.keys())) {
|
|
5968
|
+
if (modelKeyFromSessionScoped(key) === displayKey) recentSamplesByModelKey.delete(key);
|
|
5969
|
+
}
|
|
5866
5970
|
lastStatusText = undefined;
|
|
5867
5971
|
}
|
|
5868
5972
|
|
|
@@ -5871,6 +5975,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
5871
5975
|
for (const key of Object.keys(cacheStatsByModel)) {
|
|
5872
5976
|
if (key.startsWith(prefix)) delete cacheStatsByModel[key];
|
|
5873
5977
|
}
|
|
5978
|
+
cacheStatsTotalsByModel = {};
|
|
5979
|
+
replacePersistedTotalsOnNextWrite = true;
|
|
5874
5980
|
for (const key of Array.from(recentSamplesByModelKey.keys())) {
|
|
5875
5981
|
if (key.startsWith(prefix)) recentSamplesByModelKey.delete(key);
|
|
5876
5982
|
}
|
|
@@ -5880,7 +5986,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
5880
5986
|
|
|
5881
5987
|
async function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
5882
5988
|
try {
|
|
5883
|
-
await writePersistedCacheStats(
|
|
5989
|
+
await writePersistedCacheStats(
|
|
5990
|
+
getCacheStatsState(),
|
|
5991
|
+
currentSessionHashSet ? currentSessionHash : undefined,
|
|
5992
|
+
{
|
|
5993
|
+
deleteModelKeys: Array.from(pendingDeletedTotalModelKeys),
|
|
5994
|
+
replaceTotals: replacePersistedTotalsOnNextWrite,
|
|
5995
|
+
},
|
|
5996
|
+
);
|
|
5997
|
+
pendingDeletedTotalModelKeys.clear();
|
|
5998
|
+
replacePersistedTotalsOnNextWrite = false;
|
|
5884
5999
|
} catch (error) {
|
|
5885
6000
|
console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
|
|
5886
6001
|
if (!persistenceWarningShown) {
|
|
@@ -5930,6 +6045,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
5930
6045
|
}
|
|
5931
6046
|
}
|
|
5932
6047
|
|
|
6048
|
+
// Roll over cumulative provider/model totals.
|
|
6049
|
+
for (const key of Object.keys(cacheStatsTotalsByModel)) {
|
|
6050
|
+
const stats = cacheStatsTotalsByModel[key];
|
|
6051
|
+
if (stats && stats.day !== day) {
|
|
6052
|
+
cacheStatsTotalsByModel[key] = emptyCacheStats(day);
|
|
6053
|
+
changed = true;
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
|
|
5933
6057
|
// Roll over legacy family entries.
|
|
5934
6058
|
for (const id of CACHE_PROVIDER_IDS) {
|
|
5935
6059
|
const stats = cacheStatsLegacyFamily[id];
|
|
@@ -5962,6 +6086,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5962
6086
|
persisted,
|
|
5963
6087
|
currentSessionHashSet ? currentSessionHash : undefined,
|
|
5964
6088
|
);
|
|
6089
|
+
cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
|
|
5965
6090
|
cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
|
|
5966
6091
|
lastActualRoutedModel = currentSessionHashSet
|
|
5967
6092
|
? persisted?.lastRoutedModelBySession?.[currentSessionHash]
|
|
@@ -5979,6 +6104,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5979
6104
|
persisted,
|
|
5980
6105
|
currentSessionHashSet ? currentSessionHash : undefined,
|
|
5981
6106
|
);
|
|
6107
|
+
cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
|
|
5982
6108
|
cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
|
|
5983
6109
|
lastActualRoutedModel = currentSessionHashSet
|
|
5984
6110
|
? persisted?.lastRoutedModelBySession?.[currentSessionHash]
|
|
@@ -5996,15 +6122,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
5996
6122
|
* meaningful footer content on session_start after reload.
|
|
5997
6123
|
*/
|
|
5998
6124
|
function findBestRouterModelStats(): { adapter: CacheProviderAdapter; stats: CacheStats } | undefined {
|
|
5999
|
-
if (!currentSessionHash) return undefined;
|
|
6000
|
-
const prefix = `${currentSessionHash}:`;
|
|
6001
6125
|
let best: { adapter: CacheProviderAdapter; stats: CacheStats; total: number } | undefined;
|
|
6002
6126
|
|
|
6003
|
-
for (const [
|
|
6004
|
-
if (!key.startsWith(prefix)) continue;
|
|
6005
|
-
|
|
6006
|
-
// Extract provider/id from key like "abc123:run-claude/claude-opus-4-8"
|
|
6007
|
-
const modelKeyPart = key.slice(prefix.length);
|
|
6127
|
+
for (const [modelKeyPart, stats] of Object.entries(cacheStatsTotalsByModel)) {
|
|
6008
6128
|
const slashIdx = modelKeyPart.indexOf("/");
|
|
6009
6129
|
if (slashIdx < 0 || slashIdx >= modelKeyPart.length - 1) continue;
|
|
6010
6130
|
const modelId = modelKeyPart.slice(slashIdx + 1);
|
|
@@ -6058,6 +6178,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6058
6178
|
currentSessionHashSet ? currentSessionHash : undefined,
|
|
6059
6179
|
cacheStatsByModel,
|
|
6060
6180
|
lastActualRoutedModel,
|
|
6181
|
+
cacheStatsTotalsByModel,
|
|
6061
6182
|
) ?? findBestRouterModelStats();
|
|
6062
6183
|
if (realEntry) {
|
|
6063
6184
|
const statsText = formatCacheStats(realEntry.adapter, realEntry.stats);
|
|
@@ -6068,11 +6189,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
6068
6189
|
}
|
|
6069
6190
|
|
|
6070
6191
|
if (adapter) {
|
|
6071
|
-
// Display
|
|
6072
|
-
//
|
|
6073
|
-
//
|
|
6074
|
-
const
|
|
6075
|
-
const stats = sk ? cacheStatsByModel[sk] : undefined;
|
|
6192
|
+
// Display provider/model cumulative stats. A model that has never
|
|
6193
|
+
// been used locally shows 0/0. The message_end hook updates both the
|
|
6194
|
+
// current session bucket and this restart-persistent totals bucket.
|
|
6195
|
+
const stats = displayModel ? cacheStatsTotalsByModel[modelKey(displayModel)] : undefined;
|
|
6076
6196
|
const statsText = formatCacheStats(adapter, stats ?? emptyCacheStats());
|
|
6077
6197
|
statusText = runtimeOptimizerEnabled ? statsText : `Cache Optimizer disabled · ${statsText}`;
|
|
6078
6198
|
}
|
|
@@ -6406,6 +6526,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6406
6526
|
if (statsModel) {
|
|
6407
6527
|
const sk = sessionModelKey(statsModel);
|
|
6408
6528
|
addUsageToCacheStats(getOrCreateStatsByModelKey(sk), usage);
|
|
6529
|
+
addUsageToCacheStats(getOrCreateTotalStatsForModel(statsModel), usage);
|
|
6409
6530
|
} else {
|
|
6410
6531
|
addUsageToCacheStats(getStatsForModel(undefined, adapter), usage);
|
|
6411
6532
|
}
|
|
@@ -6424,7 +6545,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6424
6545
|
// stats — show active model stats bucket, recent trend, usage
|
|
6425
6546
|
// compat — show compat suggestion with file path
|
|
6426
6547
|
// fix — auto-fix compat issues (writes models.json, requires UI)
|
|
6427
|
-
// reset — reset current
|
|
6548
|
+
// reset — reset current provider/model footer stats bucket (local only)
|
|
6428
6549
|
// (no args) — interactive menu (with UI) or help summary
|
|
6429
6550
|
// ────────────────────────────────────────────────────────────────
|
|
6430
6551
|
pi.registerCommand("cache-optimizer", {
|
|
@@ -6440,13 +6561,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
6440
6561
|
resetCurrentSessionStats();
|
|
6441
6562
|
await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
|
|
6442
6563
|
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
6443
|
-
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process.
|
|
6564
|
+
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
|
|
6444
6565
|
} else if (subcommand === "disable") {
|
|
6445
6566
|
setRuntimeOptimizerEnabled(false);
|
|
6446
6567
|
resetCurrentSessionStats();
|
|
6447
6568
|
await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
|
|
6448
6569
|
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
6449
|
-
cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process.
|
|
6570
|
+
cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process. Local footer stats were reset and will keep collecting while disabled for comparison.\n${formatOptimizerRuntimeMode()}`, "warning");
|
|
6450
6571
|
} else if (subcommand === "doctor") {
|
|
6451
6572
|
if (!model) {
|
|
6452
6573
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
@@ -6455,7 +6576,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6455
6576
|
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
|
|
6456
6577
|
const adapter = selectAdapterForModel(model);
|
|
6457
6578
|
const sk = model ? sessionModelKey(model) : undefined;
|
|
6458
|
-
const statsState =
|
|
6579
|
+
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
6459
6580
|
const samples = sk ? getRecentSamples(sk) : [];
|
|
6460
6581
|
const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
|
|
6461
6582
|
const fullDiagnosis = lowHitLines.length > 0
|
|
@@ -6469,7 +6590,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6469
6590
|
}
|
|
6470
6591
|
const adapter = selectAdapterForModel(model);
|
|
6471
6592
|
const sk = model ? sessionModelKey(model) : undefined;
|
|
6472
|
-
const statsState =
|
|
6593
|
+
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
6473
6594
|
const samples = sk ? getRecentSamples(sk) : [];
|
|
6474
6595
|
const output = buildStatsOutput(model, adapter, statsState, samples);
|
|
6475
6596
|
cmdCtx.ui.notify(output, "info");
|
|
@@ -6502,7 +6623,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6502
6623
|
|
|
6503
6624
|
const displayKey = modelKey(model);
|
|
6504
6625
|
|
|
6505
|
-
// Reset
|
|
6626
|
+
// Reset local footer stats for the effective active model. If the
|
|
6506
6627
|
// selected model is a virtual router and the protocol exposes a live
|
|
6507
6628
|
// route, this clears the real upstream bucket, not the router shell.
|
|
6508
6629
|
resetStatsForModel(model);
|
|
@@ -6514,9 +6635,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
6514
6635
|
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
6515
6636
|
|
|
6516
6637
|
cmdCtx.ui.notify(
|
|
6517
|
-
`✅ Reset local
|
|
6638
|
+
`✅ Reset local footer cache stats for "${displayKey}". ` +
|
|
6518
6639
|
"Upstream provider prompt cache was not modified. " +
|
|
6519
|
-
"New requests will start a fresh stats bucket for this
|
|
6640
|
+
"New requests will start a fresh local stats bucket for this provider/model.",
|
|
6520
6641
|
"info",
|
|
6521
6642
|
);
|
|
6522
6643
|
} else if (subcommand === "fix") {
|
|
@@ -6831,7 +6952,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6831
6952
|
"Stats — Show cache stats and trend",
|
|
6832
6953
|
"Compat — Show compat suggestion",
|
|
6833
6954
|
"Fix — Auto-fix compat issues (writes models.json)",
|
|
6834
|
-
"Reset — Reset local
|
|
6955
|
+
"Reset — Reset local provider/model stats",
|
|
6835
6956
|
"Cancel",
|
|
6836
6957
|
];
|
|
6837
6958
|
const choice = await cmdCtx.ui.select("Cache Optimizer", menuOptions);
|
|
@@ -6840,13 +6961,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
6840
6961
|
resetCurrentSessionStats();
|
|
6841
6962
|
await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
|
|
6842
6963
|
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
6843
|
-
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process.
|
|
6964
|
+
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
|
|
6844
6965
|
} else if (choice === menuOptions[1]) {
|
|
6845
6966
|
setRuntimeOptimizerEnabled(false);
|
|
6846
6967
|
resetCurrentSessionStats();
|
|
6847
6968
|
await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
|
|
6848
6969
|
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
6849
|
-
cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process.
|
|
6970
|
+
cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process. Local footer stats were reset and will keep collecting while disabled for comparison.\n${formatOptimizerRuntimeMode()}`, "warning");
|
|
6850
6971
|
} else if (choice === menuOptions[2]) {
|
|
6851
6972
|
if (!model) {
|
|
6852
6973
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
@@ -6854,7 +6975,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6854
6975
|
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
|
|
6855
6976
|
const adapter = selectAdapterForModel(model);
|
|
6856
6977
|
const sk = model ? sessionModelKey(model) : undefined;
|
|
6857
|
-
const statsState =
|
|
6978
|
+
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
6858
6979
|
const samples = sk ? getRecentSamples(sk) : [];
|
|
6859
6980
|
const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
|
|
6860
6981
|
const fullDiagnosis = lowHitLines.length > 0
|
|
@@ -6868,7 +6989,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6868
6989
|
} else {
|
|
6869
6990
|
const adapter = selectAdapterForModel(model);
|
|
6870
6991
|
const sk = model ? sessionModelKey(model) : undefined;
|
|
6871
|
-
const statsState =
|
|
6992
|
+
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
6872
6993
|
const samples = sk ? getRecentSamples(sk) : [];
|
|
6873
6994
|
const output = buildStatsOutput(model, adapter, statsState, samples);
|
|
6874
6995
|
cmdCtx.ui.notify(output, "info");
|
|
@@ -7003,7 +7124,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7003
7124
|
await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
|
|
7004
7125
|
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
7005
7126
|
cmdCtx.ui.notify(
|
|
7006
|
-
`✅ Reset local
|
|
7127
|
+
`✅ Reset local footer cache stats for "${displayKey}". ` +
|
|
7007
7128
|
"Upstream provider prompt cache was not modified.",
|
|
7008
7129
|
"info",
|
|
7009
7130
|
);
|
|
@@ -7023,7 +7144,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7023
7144
|
diagnosis.push(" stats — Show active model stats bucket and recent trend");
|
|
7024
7145
|
diagnosis.push(" compat — Show compat suggestion with edit location");
|
|
7025
7146
|
diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
|
|
7026
|
-
diagnosis.push(" reset — Reset local
|
|
7147
|
+
diagnosis.push(" reset — Reset local provider/model stats for current model (does not affect upstream)");
|
|
7027
7148
|
diagnosis.push("");
|
|
7028
7149
|
diagnosis.push(formatOptimizerRuntimeMode());
|
|
7029
7150
|
diagnosis.push("");
|
package/package.json
CHANGED