pi-cache-optimizer 2.8.18 → 2.8.19
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 +9 -9
- package/README.zh-CN.md +10 -10
- package/index.ts +38 -30
- package/package.json +3 -3
- package/src/stats-store.ts +17 -6
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ Run `/reload` in Pi after install/update/remove so extension hooks refresh.
|
|
|
62
62
|
|
|
63
63
|
On Pi 0.79.7 and newer, `pi update` updates Pi itself only. To update installed Pi packages such as this extension, run `pi update --extensions` (packages only) or `pi update --all` (Pi + packages).
|
|
64
64
|
|
|
65
|
-
This extension requires Pi 0.82+ and is validated against Pi 1.0.
|
|
65
|
+
This extension requires Pi 0.82+ and is validated against Pi 1.0.2. It uses the official Pi package types directly for type-checking, along with extension hooks, `getAgentDir()`, and prompt options shared by those versions; it does not depend on Pi 0.83+ APIs such as `ctx.scopedModels` or the bundled TypeBox 1.3 aliases. Native virtual model support and codemode nested-call coalescing activate only on Pi 0.99+ hosts that produce them; older hosts keep the previous behavior. The local development baseline uses Node.js 22.19.0 or newer, matching Pi 1.0.2's engine requirement.
|
|
66
66
|
|
|
67
67
|
## Commands
|
|
68
68
|
|
|
@@ -132,17 +132,17 @@ The fixture verifier reports numeric tool-order changes and confirms cache-marke
|
|
|
132
132
|
|
|
133
133
|
## Footer cache stats mode
|
|
134
134
|
|
|
135
|
-
Current versions store stats under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory. Each loaded extension instance owns one UUID-named shard and writes it through temp-file + atomic rename. This prevents parent/child/parallel Pi processes from overwriting one another. Upgrading from the old v6 single-file format deletes the old local stats files and starts footer counters from zero; upstream provider caches are not affected.
|
|
135
|
+
Current versions store stats under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory. Each loaded extension instance owns one active UUID-named daily shard and writes it through temp-file + atomic rename; at local midnight it closes the old shard and starts a new UUID without erasing session history. This prevents parent/child/parallel Pi processes from overwriting one another. Upgrading from the old v6 single-file format deletes the old local stats files and starts footer counters from zero; upstream provider caches are not affected.
|
|
136
136
|
|
|
137
|
-
Response stats stay with the dispatched catalog/request-local model even if you switch to another model on the same provider while waiting. Legacy response aliases use the request snapshot, not the new selection; the footer continues showing the selected direct model. Old corrupt JSON/schema shards expire by file modification time after
|
|
137
|
+
Response stats stay with the dispatched catalog/request-local model even if you switch to another model on the same provider while waiting. Legacy response aliases use the request snapshot, not the new selection; the footer continues showing the selected direct model. Old corrupt JSON/schema shards expire by file modification time after two months; young corrupt files, valid current-day shards, live old instances, and symlink targets remain protected.
|
|
138
138
|
|
|
139
139
|
The footer defaults to `session`, which reflects the current Pi conversation rather than another parallel Pi terminal using the same provider/model. Use either the command or environment variable to select the scope:
|
|
140
140
|
|
|
141
141
|
| Value | Effect |
|
|
142
142
|
|---|---|
|
|
143
|
-
| `session` (default) | Aggregate
|
|
144
|
-
| `total` | Aggregate
|
|
145
|
-
| `process` | Show only counters collected by the current extension instance. It starts at `0/0` after Pi restart or extension reload. |
|
|
143
|
+
| `session` (default) | Aggregate all retained shards carrying the current hashed Pi conversation session id and exact provider/model, including across local-day boundaries. A reload creates a new instance shard but remains in the same lifetime session scope. |
|
|
144
|
+
| `total` | Aggregate the current local day's valid shards for the exact provider/model across sessions, including child Pi agents that load this extension and share the same agent directory. |
|
|
145
|
+
| `process` | Show only today's counters collected by the current extension instance. It starts at `0/0` at local midnight, after Pi restart, or extension reload. |
|
|
146
146
|
|
|
147
147
|
Persistent command configuration takes precedence over the environment variable:
|
|
148
148
|
|
|
@@ -156,7 +156,7 @@ The explicit setting is stored in `pi-cache-optimizer-config.json` under Pi's ag
|
|
|
156
156
|
|
|
157
157
|
## Per-model `prompt_cache_key` opt-out
|
|
158
158
|
|
|
159
|
-
Some OpenAI-compatible Completions endpoints reject `prompt_cache_key` with HTTP 400 even though the same field is valid for other providers. Pi 1.0.
|
|
159
|
+
Some OpenAI-compatible Completions endpoints reject `prompt_cache_key` with HTTP 400 even though the same field is valid for other providers. Pi 1.0.2 owns the key for Responses/Codex transports; this extension's key opt-out and fallback apply only to `openai-completions`. Pi 1.0.2 has no native `supportsPromptCacheKey` compat field; do **not** add that unknown field to `models.json`. `supportsLongCacheRetention` is not an equivalent switch and should not be used for this purpose.
|
|
160
160
|
|
|
161
161
|
When the extension observes an explicit field-level `prompt_cache_key` unsupported error for the exact provider/model, ordinary `/cache-optimizer fix` offers a confirmed model-scoped repair. Value-validation failures and conditional restrictions such as “not allowed when temperature is set” do not qualify. If concurrent responses from different models cannot be correlated because Pi provides no request ID, header-only evidence is ignored unless the finalized assistant message supplies exact provider/model identity. If you already know that the endpoint rejects the field, use the explicit command:
|
|
162
162
|
|
|
@@ -196,7 +196,7 @@ Notes:
|
|
|
196
196
|
|
|
197
197
|
- `sendSessionAffinityHeaders: true` is the safe default when your proxy supports sticky routing.
|
|
198
198
|
- `supportsLongCacheRetention: true` is optional. Add it only when the endpoint explicitly supports OpenAI long prompt cache retention.
|
|
199
|
-
- Do not add `supportsPromptCacheKey` to `models.json`: Pi 1.0.
|
|
199
|
+
- Do not add `supportsPromptCacheKey` to `models.json`: Pi 1.0.2 does not define that compat field. Use `/cache-optimizer fix prompt-cache-key` to store an exact provider/model omit rule in the extension-owned config; it removes both key spellings, including a key supplied by Pi.
|
|
200
200
|
- If you see `400 Unsupported parameter: prompt_cache_retention`, remove/avoid `supportsLongCacheRetention` for that channel. Keep `sendSessionAffinityHeaders` if supported. The extension detects the explicit error from response headers or the finalized assistant error message and strips the parameter from subsequent requests in the current process.
|
|
201
201
|
- Use `/cache-optimizer compat` or `/cache-optimizer doctor` to see model-specific advice.
|
|
202
202
|
- DeepSeek model names select the `DS cache` adapter only; they do not prove a reasoning wire protocol. Generic cache/routing advice remains active for absent or non-DeepSeek formats. DeepSeek replay advice is shown only when effective `compat.thinkingFormat: "deepseek"` is explicitly configured; it never treats `thinkingFormat` as a missing fix key.
|
|
@@ -364,7 +364,7 @@ Rollback creates a new access-mode-preserving backup and uses atomic replacement
|
|
|
364
364
|
|
|
365
365
|
## Footer stats
|
|
366
366
|
|
|
367
|
-
Stats are read-only local counters stored as UUID-owned shards under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory (custom agent dirs use `PI_CODING_AGENT_DIR`). Shards contain only dates, opaque session hashes, exact provider/model counters, reset epochs, and process lifecycle metadata — no API keys, prompts, payloads, headers, responses, or model output. The footer defaults to the current conversation session; `total` aggregates
|
|
367
|
+
Stats are read-only local counters stored as UUID-owned shards under `pi-cache-optimizer-stats.d/shards/` in Pi's agent directory (custom agent dirs use `PI_CODING_AGENT_DIR`). Shards contain only dates, opaque session hashes, exact provider/model counters, reset epochs, and process lifecycle metadata — no API keys, prompts, payloads, headers, responses, or model output. The footer defaults to the current conversation session and retains that session's counters across local-day boundaries; `total` aggregates only current-day shards, and `process` shows only the current extension instance. Footer mode configuration is stored separately in `pi-cache-optimizer-config.json`. Upgrading from the old v6 shared stats files deletes those local counters instead of migrating them.
|
|
368
368
|
|
|
369
369
|
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.
|
|
370
370
|
|
package/README.zh-CN.md
CHANGED
|
@@ -62,7 +62,7 @@ pi remove npm:pi-deepseek-cache-optimizer && pi install npm:pi-cache-optimizer
|
|
|
62
62
|
|
|
63
63
|
Pi 0.79.7 及之后,`pi update` 默认只更新 Pi 本体。若要更新已安装的 Pi package(包括本扩展),请运行 `pi update --extensions`(只更新 packages)或 `pi update --all`(Pi 与 packages 一起更新)。
|
|
64
64
|
|
|
65
|
-
本扩展要求 Pi 0.82+,并已使用 Pi 1.0.
|
|
65
|
+
本扩展要求 Pi 0.82+,并已使用 Pi 1.0.2 验证。TypeScript 校验直接使用官方 Pi package 类型,同时只使用这些版本共有的 extension hooks、`getAgentDir()` 和 prompt options;不依赖 Pi 0.83+ 专有 API(例如 `ctx.scopedModels` 或 bundled TypeBox 1.3 aliases)。原生虚拟模型支持与 codemode 嵌套工具调用合并只在会产生它们的 Pi 0.99+ 上生效,较早版本保持原有行为。本地开发基线需要 Node.js 22.19.0 或更高版本,与 Pi 1.0.2 的 engine 要求一致。
|
|
66
66
|
|
|
67
67
|
## 命令
|
|
68
68
|
|
|
@@ -132,17 +132,17 @@ bun .trellis/tasks/09-03-context-epoch-tool-ordering/verify.ts
|
|
|
132
132
|
|
|
133
133
|
## Footer 缓存统计模式
|
|
134
134
|
|
|
135
|
-
当前版本把统计保存在 Pi agent 目录的 `pi-cache-optimizer-stats.d/shards/` 下。每个已加载的 extension instance
|
|
135
|
+
当前版本把统计保存在 Pi agent 目录的 `pi-cache-optimizer-stats.d/shards/` 下。每个已加载的 extension instance 独占一个当天的活跃 UUID shard,并通过临时文件 + 原子 rename 写入;本地日期切换时关闭旧 shard、创建新的 UUID,保留 session 历史,因此父/子/并行 Pi 进程不会互相覆盖。旧 v6 单文件统计在升级时直接删除,本地 footer 计数从零开始;不会影响上游 provider 的实际缓存。
|
|
136
136
|
|
|
137
|
-
即使等待响应时切换到同一 provider 的另一个模型,响应统计仍归属于实际请求的 catalog/request-local 模型。旧 transport 的响应别名只根据请求快照处理,不使用新选择的模型;footer 继续显示当前选择的 direct 模型。损坏 JSON/schema 的旧 shard
|
|
137
|
+
即使等待响应时切换到同一 provider 的另一个模型,响应统计仍归属于实际请求的 catalog/request-local 模型。旧 transport 的响应别名只根据请求快照处理,不使用新选择的模型;footer 继续显示当前选择的 direct 模型。损坏 JSON/schema 的旧 shard 按文件修改时间在两个月后清理;较新的损坏文件、有效的当天 shard、仍存活的旧 instance 和 symlink 目标均受保护。
|
|
138
138
|
|
|
139
139
|
Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 provider/model 时污染当前窗口。可以通过命令或环境变量切换显示范围:
|
|
140
140
|
|
|
141
141
|
| 值 | 作用 |
|
|
142
142
|
|---|---|
|
|
143
|
-
| `session`(默认) |
|
|
144
|
-
| `total` |
|
|
145
|
-
| `process` | 仅显示当前 extension instance
|
|
143
|
+
| `session`(默认) | 聚合所有携带当前 hashed Pi conversation session id 且精确 provider/model 相同的保留 shard,跨越本地日期仍会累计。Extension reload 会产生新 instance shard,但仍属于同一长期 session 范围。 |
|
|
144
|
+
| `total` | 聚合当前本地日期同一精确 provider/model 的全部有效本地 shard,包括加载本扩展并共享同一 agent 目录的子 Pi agent。 |
|
|
145
|
+
| `process` | 仅显示当前 extension instance 当天采集的统计。本地日期切换、Pi 重启或 extension reload 后从 `0/0` 开始。 |
|
|
146
146
|
|
|
147
147
|
持久命令配置优先于环境变量:
|
|
148
148
|
|
|
@@ -156,7 +156,7 @@ Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 prov
|
|
|
156
156
|
|
|
157
157
|
## 按模型关闭 `prompt_cache_key`
|
|
158
158
|
|
|
159
|
-
某些 OpenAI-compatible Completions endpoint 会因 `prompt_cache_key` 返回 HTTP 400,但同一个字段对其他 provider 可能有效。Pi 1.0+ 已负责 Responses/Codex transport 的 key;本扩展的 fallback 和 opt-out 只作用于 `openai-completions`。Pi 1.0.
|
|
159
|
+
某些 OpenAI-compatible Completions endpoint 会因 `prompt_cache_key` 返回 HTTP 400,但同一个字段对其他 provider 可能有效。Pi 1.0+ 已负责 Responses/Codex transport 的 key;本扩展的 fallback 和 opt-out 只作用于 `openai-completions`。Pi 1.0.2 没有原生的 `supportsPromptCacheKey` compat 字段,**不要**把这个未知字段加入 `models.json`。`supportsLongCacheRetention` 也不是等价开关,不应借用来实现此目的。
|
|
160
160
|
|
|
161
161
|
当扩展观察到精确 provider/model 对 `prompt_cache_key` 的明确字段级拒绝后,普通 `/cache-optimizer fix` 会提供经过确认的模型级修复。参数值校验失败,以及“设置 temperature 时不允许”这类条件限制都不构成证据。若不同模型的响应并发交错,而 Pi 又没有提供 request ID,扩展会忽略无法安全关联的 response-header 证据;只有最终 assistant message 提供精确 provider/model 身份时才恢复归因。如果你已经确定 endpoint 不支持该字段,可以主动执行:
|
|
162
162
|
|
|
@@ -170,7 +170,7 @@ Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 prov
|
|
|
170
170
|
|
|
171
171
|
LiteLLM / OneAPI / NewAPI / 类 OpenRouter 渠道等第三方 `openai-completions` 代理,常会把同一个 session 分散到多个上游后端,导致 provider 侧 prompt cache 被拆散。
|
|
172
172
|
|
|
173
|
-
Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention` 的模型兼容性;本扩展不按 provider 名称增加特殊分支,而是结合 `models.json` 与 runtime model,按精确 provider/model 解析有效 compat。Pi 0.81+ 也内置了使用 OpenAI-shaped transport 的 `llama.cpp` provider。本扩展只对第三方 `openai-completions` 保留 session-id key fallback;Pi 1.0+ 已负责 Responses/Codex transport 的 key。只有符合 Pi 内置 provider 明确 compat 指纹的模型会跳过通用 proxy 路由 / session-affinity 建议;仅复用 `llama.cpp` id 的自定义或覆盖 provider 仍按普通 OpenAI-compatible 渠道处理。`prompt_cache_retention` 继续遵循统一安全规则:仅官方 OpenAI 或 `models.json` 中有效配置为 `supportsLongCacheRetention: true` 时保留,否则发送前移除。Pi 1.0.
|
|
173
|
+
Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention` 的模型兼容性;本扩展不按 provider 名称增加特殊分支,而是结合 `models.json` 与 runtime model,按精确 provider/model 解析有效 compat。Pi 0.81+ 也内置了使用 OpenAI-shaped transport 的 `llama.cpp` provider。本扩展只对第三方 `openai-completions` 保留 session-id key fallback;Pi 1.0+ 已负责 Responses/Codex transport 的 key。只有符合 Pi 内置 provider 明确 compat 指纹的模型会跳过通用 proxy 路由 / session-affinity 建议;仅复用 `llama.cpp` id 的自定义或覆盖 provider 仍按普通 OpenAI-compatible 渠道处理。`prompt_cache_retention` 继续遵循统一安全规则:仅官方 OpenAI 或 `models.json` 中有效配置为 `supportsLongCacheRetention: true` 时保留,否则发送前移除。Pi 1.0.2 没有原生的 `supportsPromptCacheKey` compat 字段,因此仅对 `openai-completions` 按模型关闭 key 的策略保存在本扩展的 `pi-cache-optimizer-config.json` 中,而不是 `models.json`。扩展配置独立于 Pi 的 compat 优先级,只影响其中列出的精确 provider/model。
|
|
174
174
|
|
|
175
175
|
对真正的代理,建议先启用 session affinity:
|
|
176
176
|
|
|
@@ -196,7 +196,7 @@ Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention`
|
|
|
196
196
|
|
|
197
197
|
- `sendSessionAffinityHeaders: true` 是安全默认项,前提是你的代理支持 sticky routing。
|
|
198
198
|
- `supportsLongCacheRetention: true` 是可选项。只有 endpoint 明确支持 OpenAI long prompt cache retention 时才添加。
|
|
199
|
-
- 不要把 `supportsPromptCacheKey` 加入 `models.json`:Pi 1.0.
|
|
199
|
+
- 不要把 `supportsPromptCacheKey` 加入 `models.json`:Pi 1.0.2 没有定义这个 compat 字段。请使用 `/cache-optimizer fix prompt-cache-key` 在扩展自有配置中保存精确 provider/model 的 omit 规则;它会移除两种 key 写法,包括 Pi 提供的 key。
|
|
200
200
|
- 如果出现 `400 Unsupported parameter: prompt_cache_retention`,请为该渠道移除 / 避免 `supportsLongCacheRetention`;如支持,可保留 `sendSessionAffinityHeaders`。扩展会从响应头或最终 assistant error message 中识别这条明确错误,并在当前进程的后续请求中移除该参数。
|
|
201
201
|
- 使用 `/cache-optimizer compat` 或 `/cache-optimizer doctor` 查看当前模型的具体建议。
|
|
202
202
|
- DeepSeek 模型名只用于选择 `DS cache` adapter,不能证明 reasoning wire protocol。缺少或使用非 DeepSeek format 时仍保留通用缓存 / 路由建议;只有 effective `compat.thinkingFormat: "deepseek"` 被明确配置时,才显示 DeepSeek replay 建议,且不会把 `thinkingFormat` 列为缺失修复项。
|
|
@@ -363,7 +363,7 @@ Rollback 会创建新的、保留访问权限的备份并使用原子替换。
|
|
|
363
363
|
|
|
364
364
|
## Footer 统计
|
|
365
365
|
|
|
366
|
-
统计是只读本地计数,保存在 Pi agent 目录的 UUID shard(`pi-cache-optimizer-stats.d/shards/`;自定义 agent 目录使用 `PI_CODING_AGENT_DIR`)。Shard 只包含日期、opaque session hash、精确 provider/model 计数、reset epoch 和进程生命周期元数据,不包含 API key、prompt、payload、headers、响应或模型输出。Footer 默认显示当前 conversation session;`total`
|
|
366
|
+
统计是只读本地计数,保存在 Pi agent 目录的 UUID shard(`pi-cache-optimizer-stats.d/shards/`;自定义 agent 目录使用 `PI_CODING_AGENT_DIR`)。Shard 只包含日期、opaque session hash、精确 provider/model 计数、reset epoch 和进程生命周期元数据,不包含 API key、prompt、payload、headers、响应或模型输出。Footer 默认显示当前 conversation session,并跨本地日期累计该 session;`total` 只聚合当前本地日期的同一精确 provider/model shard,`process` 只显示当前 extension instance。旧 v6 共享统计文件升级时直接删除,不迁移旧计数。
|
|
367
367
|
|
|
368
368
|
Pi 0.79+ 已内置 footer `CH` 标记,用于显示最近一次 prompt cache hit rate。本扩展在此基础上补充持久化的 provider/model 计数,以及代理 compat 诊断。
|
|
369
369
|
|
package/index.ts
CHANGED
|
@@ -443,9 +443,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
443
443
|
let shardRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
444
444
|
let shardWatcher: ReturnType<typeof watch> | undefined;
|
|
445
445
|
const enqueuePersist = createSerializedAsyncRunner();
|
|
446
|
-
|
|
446
|
+
let instanceId = randomUUID();
|
|
447
447
|
const instanceStartedAt = Date.now();
|
|
448
|
-
|
|
448
|
+
let instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
|
|
449
|
+
let shardDay = currentLocalDay();
|
|
450
|
+
let rolloverPromise: Promise<void> | undefined;
|
|
451
|
+
// Failed day-close writes remain visible in memory until persisted or reload.
|
|
452
|
+
const retiredShards = new Map<string, PersistedStatsShardV7>();
|
|
449
453
|
const modelApiByKey = new Map<string, string>();
|
|
450
454
|
const modelNameByKey = new Map<string, string>();
|
|
451
455
|
const modelEpochByKey = new Map<string, string>();
|
|
@@ -607,9 +611,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
607
611
|
|
|
608
612
|
async function refreshShardAggregate(): Promise<ShardAggregate> {
|
|
609
613
|
const persistedShards = await readValidStatsShardsV7();
|
|
614
|
+
for (const shard of persistedShards) retiredShards.delete(shard.instanceId);
|
|
610
615
|
const shards = persistedShards.filter((shard) => shard.instanceId !== instanceId);
|
|
616
|
+
shards.push(...retiredShards.values());
|
|
611
617
|
if (currentSessionHashSet) shards.push(buildCurrentStatsShard());
|
|
618
|
+
// Keep lifetime session footer data separate from daily totals/commands.
|
|
619
|
+
// null explicitly means all retained days; undefined invokes the default day.
|
|
612
620
|
const aggregate = await aggregateStatsShardsV7(shards);
|
|
621
|
+
const sessionAggregate = await aggregateStatsShardsV7(shards, null);
|
|
613
622
|
|
|
614
623
|
// A reset from another process can advance epochs while this instance is
|
|
615
624
|
// idle. Adopt those epochs during every aggregate refresh, not only when a
|
|
@@ -636,26 +645,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
636
645
|
}
|
|
637
646
|
|
|
638
647
|
cacheStatsByModel = {};
|
|
639
|
-
for (const [sessionHash, models] of Object.entries(
|
|
648
|
+
for (const [sessionHash, models] of Object.entries(sessionAggregate.bySession)) {
|
|
640
649
|
for (const [key, stats] of Object.entries(models)) {
|
|
641
650
|
cacheStatsByModel[`${sessionHash}:${key}`] = stats;
|
|
642
651
|
}
|
|
643
652
|
}
|
|
644
653
|
cacheStatsTotalsByModel = aggregate.totalsByModel;
|
|
645
654
|
if (currentSessionHashSet) {
|
|
646
|
-
lastActualRoutedModel =
|
|
655
|
+
lastActualRoutedModel = sessionAggregate.lastRoutedModelBySession[currentSessionHash];
|
|
647
656
|
}
|
|
648
657
|
lastStatusText = undefined;
|
|
649
658
|
return aggregate;
|
|
650
659
|
}
|
|
651
660
|
|
|
652
|
-
async function ensureCurrentEpochs(): Promise<void> {
|
|
653
|
-
currentGlobalEpoch = await readGlobalStatsEpoch();
|
|
654
|
-
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
655
|
-
modelEpochByKey.set(key, await readModelStatsEpoch(key));
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
661
|
function buildCurrentStatsShard(state: "active" | "closed" = "active"): PersistedStatsShardV7 {
|
|
660
662
|
const now = Date.now();
|
|
661
663
|
const models: PersistedStatsShardV7["models"] = {};
|
|
@@ -683,7 +685,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
683
685
|
updatedAt: now,
|
|
684
686
|
...(state === "closed" ? { closedAt: now } : {}),
|
|
685
687
|
},
|
|
686
|
-
day:
|
|
688
|
+
day: shardDay,
|
|
687
689
|
globalEpoch: currentGlobalEpoch,
|
|
688
690
|
models,
|
|
689
691
|
...(lastActualRoutedModel ? { lastRoutedModel: { ...lastActualRoutedModel } } : {}),
|
|
@@ -780,9 +782,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
780
782
|
|
|
781
783
|
function persistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
|
|
782
784
|
const shard = buildCurrentStatsShard(lifecycleState);
|
|
785
|
+
const path = instanceShardPath;
|
|
783
786
|
return enqueuePersist(async () => {
|
|
784
787
|
try {
|
|
785
|
-
await writeStatsShardV7(
|
|
788
|
+
await writeStatsShardV7(path, shard);
|
|
786
789
|
} catch (error) {
|
|
787
790
|
console.warn(`${LOG_PREFIX}: failed to persist cache stats shard`, error);
|
|
788
791
|
if (!persistenceWarningShown) {
|
|
@@ -810,6 +813,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
810
813
|
clearTimeout(persistTimer);
|
|
811
814
|
persistTimer = null;
|
|
812
815
|
}
|
|
816
|
+
await rollOverStatsIfNeeded(ctx);
|
|
813
817
|
// Adopt resets performed by another Pi process before capturing the shard
|
|
814
818
|
// snapshot. Otherwise shutdown/command flush could rewrite stale local
|
|
815
819
|
// counters under an old epoch even though aggregation already hid them.
|
|
@@ -819,27 +823,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
819
823
|
}
|
|
820
824
|
|
|
821
825
|
async function rollOverStatsIfNeeded(ctx?: ExtensionContext): Promise<void> {
|
|
826
|
+
if (rolloverPromise) return rolloverPromise;
|
|
822
827
|
const day = currentLocalDay();
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
|
|
828
|
+
if (shardDay === day) return;
|
|
829
|
+
rolloverPromise = (async () => {
|
|
830
|
+
if (persistTimer !== null) {
|
|
831
|
+
clearTimeout(persistTimer);
|
|
832
|
+
persistTimer = null;
|
|
828
833
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (changed) {
|
|
834
|
+
// Preserve yesterday under its original UUID/day before clearing daily
|
|
835
|
+
// counters. Each day owns a new shard so total never includes yesterday.
|
|
836
|
+
await refreshShardAggregate();
|
|
837
|
+
await persistCacheStats(ctx, "closed");
|
|
838
|
+
retiredShards.set(instanceId, buildCurrentStatsShard("closed"));
|
|
839
|
+
instanceId = randomUUID();
|
|
840
|
+
instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
|
|
841
|
+
shardDay = day;
|
|
838
842
|
shardCreatedAt = Date.now();
|
|
843
|
+
cacheStatsProcessByModel = {};
|
|
844
|
+
cacheStatsLegacyFamily = emptyAllCacheStats();
|
|
839
845
|
lastStatusText = undefined;
|
|
840
|
-
await
|
|
841
|
-
await
|
|
842
|
-
}
|
|
846
|
+
await persistCacheStats(ctx);
|
|
847
|
+
await refreshShardAggregate();
|
|
848
|
+
})();
|
|
849
|
+
try { await rolloverPromise; }
|
|
850
|
+
finally { rolloverPromise = undefined; }
|
|
843
851
|
}
|
|
844
852
|
|
|
845
853
|
async function restoreCacheStats(reason: string, ctx: ExtensionContext): Promise<void> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cache-optimizer",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.19",
|
|
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",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"check:modules": "node scripts/check-module-graph.mjs"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@earendil-works/pi-coding-agent": "^1.0.
|
|
41
|
-
"@earendil-works/pi-server": "1.0.
|
|
40
|
+
"@earendil-works/pi-coding-agent": "^1.0.2",
|
|
41
|
+
"@earendil-works/pi-server": "1.0.2",
|
|
42
42
|
"@types/node": "^22.0.0",
|
|
43
43
|
"jiti": "^2.7.0",
|
|
44
44
|
"typescript": "^5.8.3"
|
package/src/stats-store.ts
CHANGED
|
@@ -26,7 +26,9 @@ export const SHARD_CLEANUP_LOCK_PATH = join(SHARD_MAINTENANCE_DIR, "cleanup.lock
|
|
|
26
26
|
|
|
27
27
|
export const SHARD_CLEANUP_MARKER_PATH = join(SHARD_MAINTENANCE_DIR, "last-cleanup");
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Retain closed and inactive shards for roughly two months so session-scoped
|
|
30
|
+
// statistics can survive long-running conversations without meaningful disk use.
|
|
31
|
+
export const SHARD_RETENTION_MS = 60 * 24 * 60 * 60 * 1000;
|
|
30
32
|
|
|
31
33
|
export const SHARD_TEMP_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
32
34
|
|
|
@@ -188,6 +190,15 @@ export function mergeCacheStatsForTotal(existing: CacheStats | undefined, incomi
|
|
|
188
190
|
return existing;
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
/** Merge counters while retaining historical days for an explicitly session-scoped view. */
|
|
194
|
+
export function mergeStatsAcrossDays(existing: CacheStats | undefined, incoming: CacheStats, day: string | null): CacheStats {
|
|
195
|
+
if (day !== null) return mergeCacheStatsForTotal(existing, incoming);
|
|
196
|
+
if (!existing) return cloneCacheStats(incoming);
|
|
197
|
+
addCacheStatsTotals(existing, incoming);
|
|
198
|
+
existing.day = incoming.day > existing.day ? incoming.day : existing.day;
|
|
199
|
+
return existing;
|
|
200
|
+
}
|
|
201
|
+
|
|
191
202
|
export function parsePersistedRoutedModelRef(value: unknown): PersistedRoutedModelRef | undefined {
|
|
192
203
|
const record = asRecord(value);
|
|
193
204
|
const provider = record?.provider;
|
|
@@ -352,7 +363,7 @@ export async function readValidStatsShardsV7(directory: string = SHARD_FILES_DIR
|
|
|
352
363
|
|
|
353
364
|
export async function aggregateStatsShardsV7(
|
|
354
365
|
shards: PersistedStatsShardV7[],
|
|
355
|
-
day: string = currentLocalDay(),
|
|
366
|
+
day: string | null = currentLocalDay(),
|
|
356
367
|
): Promise<ShardAggregate> {
|
|
357
368
|
const globalEpoch = await readGlobalStatsEpoch();
|
|
358
369
|
const modelEpochs = new Map<string, string>();
|
|
@@ -371,7 +382,7 @@ export async function aggregateStatsShardsV7(
|
|
|
371
382
|
const modelSessions = new Map<string, Set<string>>();
|
|
372
383
|
|
|
373
384
|
for (const shard of shards) {
|
|
374
|
-
if (shard.day !== day || shard.globalEpoch !== globalEpoch) continue;
|
|
385
|
+
if ((day !== null && shard.day !== day) || shard.globalEpoch !== globalEpoch) continue;
|
|
375
386
|
let contributed = false;
|
|
376
387
|
if (shard.lastRoutedModel && (routedUpdatedAt.get(shard.sessionHash) ?? -1) < shard.lifecycle.updatedAt) {
|
|
377
388
|
routedUpdatedAt.set(shard.sessionHash, shard.lifecycle.updatedAt);
|
|
@@ -383,11 +394,11 @@ export async function aggregateStatsShardsV7(
|
|
|
383
394
|
epoch = await readModelStatsEpoch(key);
|
|
384
395
|
modelEpochs.set(key, epoch);
|
|
385
396
|
}
|
|
386
|
-
if (entry.modelEpoch !== epoch || entry.stats.day !== day) continue;
|
|
397
|
+
if (entry.modelEpoch !== epoch || (day !== null && entry.stats.day !== day)) continue;
|
|
387
398
|
contributed = true;
|
|
388
399
|
const sessionModels = result.bySession[shard.sessionHash] ??= {};
|
|
389
|
-
sessionModels[key] =
|
|
390
|
-
result.totalsByModel[key] =
|
|
400
|
+
sessionModels[key] = mergeStatsAcrossDays(sessionModels[key], entry.stats, day);
|
|
401
|
+
result.totalsByModel[key] = mergeStatsAcrossDays(result.totalsByModel[key], entry.stats, day);
|
|
391
402
|
result.instancesByModel[key] = (result.instancesByModel[key] ?? 0) + 1;
|
|
392
403
|
if ((modelRefUpdatedAt.get(key) ?? -1) < shard.lifecycle.updatedAt) {
|
|
393
404
|
modelRefUpdatedAt.set(key, shard.lifecycle.updatedAt);
|