pi-cache-optimizer 2.8.13 → 2.8.15
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 +19 -2
- package/README.zh-CN.md +19 -2
- package/index.ts +145 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,10 +92,25 @@ The interactive `/cache-optimizer` menu includes `Footer mode`, where you can ch
|
|
|
92
92
|
| Env var | Effect |
|
|
93
93
|
|---|---|
|
|
94
94
|
| `PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE=1` | Disable prompt mutations only; footer stats and cache-key fallback remain active. |
|
|
95
|
+
| `PI_CACHE_OPTIMIZER_VIRTUAL_REWRITE=1` | Explicitly allow native virtual prompt rewriting only when the routing registry exposes a non-Responses, non-Codex candidate chain. Unknown or incomplete routes remain unchanged. |
|
|
95
96
|
| `PI_CACHE_OPTIMIZER_NO_SKILL_COMPRESSION=1` | Keep Pi's verbose skill XML. |
|
|
96
97
|
| `PI_CACHE_OPTIMIZER_NO_OPENAI_CACHE_KEY=1` | Disable the OpenAI-compatible `prompt_cache_key` fallback. Preferred explicit opt-out. |
|
|
97
98
|
| `PI_CACHE_OPTIMIZER_OPENAI_CACHE_KEY=0` | Disable the same fallback via the legacy inverse switch. Values `0`, `false`, `no`, or `off` disable it. |
|
|
98
99
|
|
|
100
|
+
Persistent feature settings are managed under the Pi agent directory by the native command interface. They take precedence over their corresponding environment variables:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
/cache-optimizer config prompt-rewrite on|off
|
|
104
|
+
/cache-optimizer config virtual-rewrite on|off
|
|
105
|
+
/cache-optimizer config skill-compression on|off
|
|
106
|
+
/cache-optimizer config openai-cache-key on|off
|
|
107
|
+
/cache-optimizer config tool-order on|off
|
|
108
|
+
/cache-optimizer config footer-mode total|session|process
|
|
109
|
+
/cache-optimizer config reset
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`config reset` removes persistent feature overrides while preserving the existing footer mode and model-specific prompt-cache-key settings. These commands do not modify shell startup files or `PI_CACHE_RETENTION`; `enable` and `disable` remain current-process runtime switches.
|
|
113
|
+
|
|
99
114
|
## Opt-in deterministic tool ordering
|
|
100
115
|
|
|
101
116
|
`PI_CACHE_OPTIMIZER_TOOL_ORDER=1` enables deterministic ordering for verified tool definitions in Pi's built-in OpenAI Completions, Anthropic, Google, and Bedrock payload shapes. Truthy values are `1`, `true`, `yes`, or `on` (case-insensitive). The feature is off by default, process-local, and suppressed by `/cache-optimizer disable`.
|
|
@@ -367,12 +382,14 @@ Pi 0.99 lets extensions register virtual models with `pi.registerVirtualModel()`
|
|
|
367
382
|
- Request hooks read the dispatched model id from the provider payload and match it against physical models with configured credentials. The `prompt_cache_key` fallback, `prompt_cache_retention` safety, Anthropic TTL repair, and per-model `prompt_cache_key` omit rules then apply to that physical model. If several credentialed providers share the id and would be treated differently, the extension does not guess; identity-dependent request changes are skipped.
|
|
368
383
|
- Footer stats and `/cache-optimizer doctor`, `compat`, `stats`, `reset`, and `fix` use the physical model that answered last on the current session branch, matching Pi's own routed-model display and context limits. Doctor and compat name both the virtual selection and that physical model.
|
|
369
384
|
- Before Pi routes the first request, the footer stays empty and diagnostics ask you to send a prompt first.
|
|
370
|
-
- Prompt rewriting is skipped for virtual selections: Pi picks the physical model after the system prompt is built, and a reordered prompt must not reach a safety-filtered Codex route.
|
|
385
|
+
- Prompt rewriting is skipped for virtual selections by default: Pi picks the physical model after the system prompt is built, and a reordered prompt must not reach a safety-filtered Codex route. `PI_CACHE_OPTIMIZER_VIRTUAL_REWRITE=1` is an explicit opt-in only when the routing registry exposes a complete candidate chain and every candidate is a known non-Responses, non-Codex transport; unknown or incomplete routes remain unchanged.
|
|
371
386
|
- The session-affinity header bridge is skipped as well, because Pi builds request headers before the payload exists. Pi still sends the physical model's own configured affinity headers.
|
|
372
387
|
|
|
373
388
|
## For router / virtual-channel extension authors
|
|
374
389
|
|
|
375
|
-
If your Pi extension provides a virtual routing provider (for example `router/auto`, `router/smart`, or a profile/channel that forwards to a real upstream), this extension can show cache stats for the real upstream provider/model instead of the virtual shell. Integration is optional, versioned, and does **not** require importing this package.
|
|
390
|
+
If your Pi extension provides a virtual routing provider (for example [`pi-router`](https://github.com/jiangge/pi-router)'s `router/auto`, `router/smart`, or a profile/channel that forwards to a real upstream), this extension can show cache stats for the real upstream provider/model instead of the virtual shell. Integration is optional, versioned, and does **not** require importing this package.
|
|
391
|
+
|
|
392
|
+
For [`pi-router`](https://github.com/jiangge/pi-router), use a current release that implements `pi.routing.registry.v1`. Direct `router/*` selections continue to use the normal prompt optimizer. When a native Pi 0.99+ virtual model is routed through a registry-aware router, the optional `PI_CACHE_OPTIMIZER_VIRTUAL_REWRITE=1` path inspects the router's candidate chain before rewriting; candidates using OpenAI Responses/Codex APIs, incomplete metadata, and unavailable route information fail closed.
|
|
376
393
|
|
|
377
394
|
### Minimum integration: final assistant message metadata
|
|
378
395
|
|
package/README.zh-CN.md
CHANGED
|
@@ -92,10 +92,25 @@ Pi 0.79.7 及之后,`pi update` 默认只更新 Pi 本体。若要更新已安
|
|
|
92
92
|
| 环境变量 | 作用 |
|
|
93
93
|
|---|---|
|
|
94
94
|
| `PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE=1` | 只关闭 prompt 改写;footer 统计和 cache-key fallback 仍启用。 |
|
|
95
|
+
| `PI_CACHE_OPTIMIZER_VIRTUAL_REWRITE=1` | 显式允许原生 virtual prompt 改写,但仅当 routing registry 暴露的候选链不包含 Responses/Codex 时生效。路由未知或信息不完整时仍保持原 prompt。 |
|
|
95
96
|
| `PI_CACHE_OPTIMIZER_NO_SKILL_COMPRESSION=1` | 保留 Pi 原始 verbose skill XML。 |
|
|
96
97
|
| `PI_CACHE_OPTIMIZER_NO_OPENAI_CACHE_KEY=1` | 关闭 OpenAI-compatible `prompt_cache_key` fallback。推荐使用这个显式 opt-out。 |
|
|
97
98
|
| `PI_CACHE_OPTIMIZER_OPENAI_CACHE_KEY=0` | 通过旧的反向开关关闭同一个 fallback。取值 `0`、`false`、`no`、`off` 时关闭。 |
|
|
98
99
|
|
|
100
|
+
持久化 feature 配置由 Pi agent 目录中的原生命令管理,并且优先于对应环境变量:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
/cache-optimizer config prompt-rewrite on|off
|
|
104
|
+
/cache-optimizer config virtual-rewrite on|off
|
|
105
|
+
/cache-optimizer config skill-compression on|off
|
|
106
|
+
/cache-optimizer config openai-cache-key on|off
|
|
107
|
+
/cache-optimizer config tool-order on|off
|
|
108
|
+
/cache-optimizer config footer-mode total|session|process
|
|
109
|
+
/cache-optimizer config reset
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`config reset` 会移除持久化 feature 覆盖,同时保留现有 footer mode 和按模型配置的 prompt-cache-key 设置。这些命令不会修改 shell 启动文件或 `PI_CACHE_RETENTION`;`enable` 和 `disable` 仍然是当前进程的运行时开关。
|
|
113
|
+
|
|
99
114
|
## Opt-in 确定性工具排序
|
|
100
115
|
|
|
101
116
|
`PI_CACHE_OPTIMIZER_TOOL_ORDER=1` 会对 Pi 内置 OpenAI Completions、Anthropic、Google 与 Bedrock payload 中已验证的工具定义进行确定性排序。Truthy 取值为(不区分大小写)`1`、`true`、`yes` 或 `on`。该能力默认关闭、仅在当前进程生效,并会被 `/cache-optimizer disable` 抑制。
|
|
@@ -366,12 +381,14 @@ Pi 0.99 允许扩展通过 `pi.registerVirtualModel()` 注册虚拟模型。选
|
|
|
366
381
|
- 请求 hook 从 provider payload 中读取实际派发的模型 id,并在已配置凭证的物理模型中匹配。`prompt_cache_key` fallback、`prompt_cache_retention` 安全规则、Anthropic TTL 修复以及按模型关闭 `prompt_cache_key` 的规则都按该物理模型生效。如果多个已配置凭证的 provider 共用同一个 id 且处理方式不同,扩展不会猜测,而是跳过依赖模型身份的请求修改。
|
|
367
382
|
- Footer 统计以及 `/cache-optimizer doctor`、`compat`、`stats`、`reset`、`fix` 使用当前会话分支上最近一次应答的物理模型,与 Pi 自身显示的路由模型和 context 上限一致。doctor 与 compat 会同时标出虚拟选择和该物理模型。
|
|
368
383
|
- 在 Pi 路由第一个请求之前,footer 保持为空,诊断会提示先发送一个 prompt。
|
|
369
|
-
-
|
|
384
|
+
- 虚拟选择默认不做 prompt 改写:Pi 在构建 system prompt 之后才决定物理模型,重排后的 prompt 不应被送到有安全过滤的 Codex 路由。只有设置 `PI_CACHE_OPTIMIZER_VIRTUAL_REWRITE=1` 且 routing registry 暴露完整候选链、所有候选都是已知的非 Responses / 非 Codex transport 时,才允许改写;路由未知或信息不完整时仍保持原 prompt。
|
|
370
385
|
- Session-affinity header 桥接同样跳过,因为 Pi 在 payload 生成之前就构造请求 header。Pi 仍会发送物理模型自身配置的 affinity header。
|
|
371
386
|
|
|
372
387
|
## Router / Virtual-channel 扩展作者指南
|
|
373
388
|
|
|
374
|
-
如果你的 Pi 扩展提供虚拟 routing provider(例如 `router/auto`、`router/smart`,或会转发到真实上游的 profile/channel),本扩展可以为真实上游 provider/model 显示缓存统计,而不是把统计记到虚拟外壳上。集成是可选、版本化的,并且**不需要导入本包**。
|
|
389
|
+
如果你的 Pi 扩展提供虚拟 routing provider(例如 [`pi-router`](https://github.com/jiangge/pi-router) 的 `router/auto`、`router/smart`,或会转发到真实上游的 profile/channel),本扩展可以为真实上游 provider/model 显示缓存统计,而不是把统计记到虚拟外壳上。集成是可选、版本化的,并且**不需要导入本包**。
|
|
390
|
+
|
|
391
|
+
对于 [`pi-router`](https://github.com/jiangge/pi-router),请使用实现 `pi.routing.registry.v1` 的较新版本。直接选择 `router/*` 模型时仍使用普通 prompt optimizer;当 Pi 0.99+ 原生 virtual model 通过支持该 registry 的 router 路由时,可选的 `PI_CACHE_OPTIMIZER_VIRTUAL_REWRITE=1` 会先检查 router candidate chain 再决定是否改写。候选包含 OpenAI Responses/Codex API、元数据不完整或路由信息不可用时,会 fail closed。
|
|
375
392
|
|
|
376
393
|
### 最小集成:最终 assistant message metadata
|
|
377
394
|
|
package/index.ts
CHANGED
|
@@ -131,6 +131,7 @@ const NO_OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_NO_OPENAI_CACHE_KEY";
|
|
|
131
131
|
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
132
132
|
const NO_SKILL_COMPRESSION_ENV = "PI_CACHE_OPTIMIZER_NO_SKILL_COMPRESSION";
|
|
133
133
|
const NO_PROMPT_REWRITE_ENV = "PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE";
|
|
134
|
+
const VIRTUAL_REWRITE_ENV = "PI_CACHE_OPTIMIZER_VIRTUAL_REWRITE";
|
|
134
135
|
const TOOL_ORDER_ENV = "PI_CACHE_OPTIMIZER_TOOL_ORDER";
|
|
135
136
|
const FOOTER_MODE_ENV = "PI_CACHE_OPTIMIZER_FOOTER_MODE";
|
|
136
137
|
type FooterStatsMode = "session" | "total" | "process";
|
|
@@ -179,6 +180,7 @@ type PersistedCacheOptimizerConfigV1 = {
|
|
|
179
180
|
version: 1;
|
|
180
181
|
footerMode?: FooterStatsMode;
|
|
181
182
|
};
|
|
183
|
+
type PersistedCacheOptimizerFeature = "promptRewrite" | "virtualRewrite" | "skillCompression" | "openAICacheKey" | "toolOrder";
|
|
182
184
|
type PersistedCacheOptimizerConfigV2 = {
|
|
183
185
|
version: 2;
|
|
184
186
|
footerMode?: FooterStatsMode;
|
|
@@ -186,7 +188,13 @@ type PersistedCacheOptimizerConfigV2 = {
|
|
|
186
188
|
omit?: string[];
|
|
187
189
|
};
|
|
188
190
|
};
|
|
189
|
-
type
|
|
191
|
+
type PersistedCacheOptimizerConfigV3 = {
|
|
192
|
+
version: 2 | 3;
|
|
193
|
+
footerMode?: FooterStatsMode;
|
|
194
|
+
promptCacheKey?: { omit?: string[] };
|
|
195
|
+
features?: Partial<Record<PersistedCacheOptimizerFeature, boolean>>;
|
|
196
|
+
};
|
|
197
|
+
type PersistedCacheOptimizerConfig = PersistedCacheOptimizerConfigV1 | PersistedCacheOptimizerConfigV2 | PersistedCacheOptimizerConfigV3;
|
|
190
198
|
type PromptCacheKeyConfigReceipt = {
|
|
191
199
|
version: 2;
|
|
192
200
|
kind: "pi-cache-optimizer-config-receipt";
|
|
@@ -922,7 +930,7 @@ function compressSkillsInSystemPrompt(
|
|
|
922
930
|
prompt: string,
|
|
923
931
|
opts: BuildSystemPromptOptions,
|
|
924
932
|
): string {
|
|
925
|
-
if (
|
|
933
|
+
if (!featureEnabled("skillCompression", NO_SKILL_COMPRESSION_ENV, true)) return prompt;
|
|
926
934
|
if (!opts.skills || opts.skills.length === 0) return prompt;
|
|
927
935
|
|
|
928
936
|
const visible = opts.skills.filter((skill) => !skill.disableModelInvocation);
|
|
@@ -1334,6 +1342,43 @@ function resolveActiveRouteSnapshot(
|
|
|
1334
1342
|
}
|
|
1335
1343
|
}
|
|
1336
1344
|
|
|
1345
|
+
function resolveNativeVirtualCandidateModels(
|
|
1346
|
+
model: PiModel | undefined,
|
|
1347
|
+
ctx?: ContextWithOptionalModelRegistry,
|
|
1348
|
+
): PiModel[] | undefined {
|
|
1349
|
+
if (!isNativeVirtualModel(model)) return undefined;
|
|
1350
|
+
const adapter = model ? getRoutingRegistry()?.getRouter(model.provider) : undefined;
|
|
1351
|
+
if (!adapter?.resolveCandidateRoutes) return undefined;
|
|
1352
|
+
|
|
1353
|
+
try {
|
|
1354
|
+
const rawSnapshots = adapter.resolveCandidateRoutes(model.id);
|
|
1355
|
+
if (!Array.isArray(rawSnapshots) || rawSnapshots.length === 0) return undefined;
|
|
1356
|
+
const candidates: PiModel[] = [];
|
|
1357
|
+
for (const rawSnapshot of rawSnapshots) {
|
|
1358
|
+
const snapshot = parseRouteSnapshot(rawSnapshot, model.provider, model.id);
|
|
1359
|
+
if (!snapshot) return undefined;
|
|
1360
|
+
const candidate = findModelInRegistry(ctx?.modelRegistry, snapshot.provider, snapshot.modelId)
|
|
1361
|
+
?? routeSnapshotToPiModel(snapshot, model);
|
|
1362
|
+
if (!isNonEmptyString(candidate.api)) return undefined;
|
|
1363
|
+
candidates.push(candidate);
|
|
1364
|
+
}
|
|
1365
|
+
return candidates;
|
|
1366
|
+
} catch {
|
|
1367
|
+
return undefined;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function canRewriteNativeVirtualPrompt(
|
|
1372
|
+
model: PiModel | undefined,
|
|
1373
|
+
ctx?: ContextWithOptionalModelRegistry,
|
|
1374
|
+
): boolean {
|
|
1375
|
+
if (!isNativeVirtualModel(model) || !featureEnabled("virtualRewrite", VIRTUAL_REWRITE_ENV, false)) return false;
|
|
1376
|
+
const candidates = resolveNativeVirtualCandidateModels(model, ctx);
|
|
1377
|
+
return !!candidates?.length && candidates.every((candidate) =>
|
|
1378
|
+
candidate.api === "openai-completions" || candidate.api === "anthropic-messages",
|
|
1379
|
+
) && candidates.every((candidate) => !isResponsesPromptRewriteBypassApi(candidate.api));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1337
1382
|
function routeSnapshotToPiModel(snapshot: PiRouteSnapshot, fallback?: PiModel): PiModel {
|
|
1338
1383
|
const sameIdentity = fallback?.provider === snapshot.provider && fallback?.id === snapshot.modelId;
|
|
1339
1384
|
return {
|
|
@@ -2044,8 +2089,18 @@ function isEnabledEnv(value: string | undefined): boolean {
|
|
|
2044
2089
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
2045
2090
|
}
|
|
2046
2091
|
|
|
2092
|
+
function featureEnabled(feature: PersistedCacheOptimizerFeature, envName: string, defaultValue: boolean): boolean {
|
|
2093
|
+
const configured = (persistedCacheOptimizerConfig as PersistedCacheOptimizerConfigV3).features?.[feature];
|
|
2094
|
+
if (configured !== undefined) return configured;
|
|
2095
|
+
if (feature === "virtualRewrite") return isEnabledEnv(process.env[envName]);
|
|
2096
|
+
if (feature === "promptRewrite" || feature === "skillCompression" || feature === "openAICacheKey") {
|
|
2097
|
+
return !isEnabledEnv(process.env[envName]);
|
|
2098
|
+
}
|
|
2099
|
+
return isEnabledEnv(process.env[envName]) || defaultValue;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2047
2102
|
function isToolOrderEnabled(env: MutableEnv = process.env): boolean {
|
|
2048
|
-
return runtimeOptimizerEnabled &&
|
|
2103
|
+
return runtimeOptimizerEnabled && featureEnabled("toolOrder", TOOL_ORDER_ENV, false);
|
|
2049
2104
|
}
|
|
2050
2105
|
|
|
2051
2106
|
function parseFooterStatsMode(value: unknown): FooterStatsMode | undefined {
|
|
@@ -2071,8 +2126,17 @@ const CACHE_OPTIMIZER_COMMANDS = [
|
|
|
2071
2126
|
"fix",
|
|
2072
2127
|
"rollback",
|
|
2073
2128
|
] as const;
|
|
2074
|
-
const CACHE_OPTIMIZER_CONFIG_ARGUMENTS = ["footer-mode"] as const;
|
|
2129
|
+
const CACHE_OPTIMIZER_CONFIG_ARGUMENTS = ["footer-mode", "prompt-rewrite", "virtual-rewrite", "skill-compression", "openai-cache-key", "tool-order", "reset"] as const;
|
|
2130
|
+
const CACHE_OPTIMIZER_FEATURE_COMMANDS = ["prompt-rewrite", "virtual-rewrite", "skill-compression", "openai-cache-key", "tool-order"] as const;
|
|
2131
|
+
const CACHE_OPTIMIZER_FEATURE_VALUES = ["on", "off"] as const;
|
|
2075
2132
|
const CACHE_OPTIMIZER_FOOTER_MODES = ["total", "session", "process"] as const;
|
|
2133
|
+
const FEATURE_COMMAND_MAP: Record<string, PersistedCacheOptimizerFeature> = {
|
|
2134
|
+
"prompt-rewrite": "promptRewrite",
|
|
2135
|
+
"virtual-rewrite": "virtualRewrite",
|
|
2136
|
+
"skill-compression": "skillCompression",
|
|
2137
|
+
"openai-cache-key": "openAICacheKey",
|
|
2138
|
+
"tool-order": "toolOrder",
|
|
2139
|
+
};
|
|
2076
2140
|
const CACHE_OPTIMIZER_STATS_ARGUMENTS = ["all", "contributors"] as const;
|
|
2077
2141
|
const CACHE_OPTIMIZER_FIX_ARGUMENTS = ["prompt-cache-key"] as const;
|
|
2078
2142
|
|
|
@@ -2144,19 +2208,28 @@ function getCacheOptimizerArgumentCompletions(argumentPrefix: string): CommandCo
|
|
|
2144
2208
|
if (parts.length === 3 && parts[1].toLowerCase() === "footer-mode") {
|
|
2145
2209
|
return filterCommandCompletionItems(CACHE_OPTIMIZER_FOOTER_MODES, parts[2], "config footer-mode");
|
|
2146
2210
|
}
|
|
2211
|
+
if (parts.length === 3 && CACHE_OPTIMIZER_FEATURE_COMMANDS.includes(parts[1] as typeof CACHE_OPTIMIZER_FEATURE_COMMANDS[number])) {
|
|
2212
|
+
return filterCommandCompletionItems(CACHE_OPTIMIZER_FEATURE_VALUES, parts[2], `config ${parts[1]}`);
|
|
2213
|
+
}
|
|
2147
2214
|
|
|
2148
2215
|
return null;
|
|
2149
2216
|
}
|
|
2150
2217
|
|
|
2151
2218
|
function parsePersistedCacheOptimizerConfig(value: unknown): PersistedCacheOptimizerConfig | undefined {
|
|
2152
2219
|
const record = asRecord(value);
|
|
2153
|
-
if (!record || (record.version !== 1 && record.version !== 2)) return undefined;
|
|
2154
|
-
const allowedTopLevel = new Set(record.version === 1 ? ["version", "footerMode"] : ["version", "footerMode", "promptCacheKey"]);
|
|
2220
|
+
if (!record || (record.version !== 1 && record.version !== 2 && record.version !== 3)) return undefined;
|
|
2221
|
+
const allowedTopLevel = new Set(record.version === 1 ? ["version", "footerMode"] : record.version === 2 ? ["version", "footerMode", "promptCacheKey"] : ["version", "footerMode", "promptCacheKey", "features"]);
|
|
2155
2222
|
if (Object.keys(record).some((key) => !allowedTopLevel.has(key))) return undefined;
|
|
2156
2223
|
const footerMode = parseFooterStatsMode(record.footerMode);
|
|
2157
2224
|
if (record.footerMode !== undefined && !footerMode) return undefined;
|
|
2158
2225
|
if (record.version === 1) return { version: 1, ...(footerMode ? { footerMode } : {}) };
|
|
2159
2226
|
|
|
2227
|
+
const rawFeatures = record.version === 3 ? record.features : undefined;
|
|
2228
|
+
if (rawFeatures !== undefined && !asRecord(rawFeatures)) return undefined;
|
|
2229
|
+
const featuresRecord = asRecord(rawFeatures);
|
|
2230
|
+
const featureNames = new Set<PersistedCacheOptimizerFeature>(["promptRewrite", "virtualRewrite", "skillCompression", "openAICacheKey", "toolOrder"]);
|
|
2231
|
+
if (featuresRecord && Object.keys(featuresRecord).some((key) => !featureNames.has(key as PersistedCacheOptimizerFeature) || typeof featuresRecord[key] !== "boolean")) return undefined;
|
|
2232
|
+
|
|
2160
2233
|
const rawPromptCacheKey = record.promptCacheKey;
|
|
2161
2234
|
if (rawPromptCacheKey !== undefined && !asRecord(rawPromptCacheKey)) return undefined;
|
|
2162
2235
|
const promptCacheKey = asRecord(rawPromptCacheKey);
|
|
@@ -2166,20 +2239,25 @@ function parsePersistedCacheOptimizerConfig(value: unknown): PersistedCacheOptim
|
|
|
2166
2239
|
const stringOmit = omit as string[] | undefined;
|
|
2167
2240
|
const uniqueOmit = stringOmit ? [...new Set(stringOmit.map((value) => value.trim()))].sort() : undefined;
|
|
2168
2241
|
return {
|
|
2169
|
-
version: 2,
|
|
2242
|
+
version: record.version === 3 ? 3 : 2,
|
|
2170
2243
|
...(footerMode ? { footerMode } : {}),
|
|
2171
2244
|
...(uniqueOmit && uniqueOmit.length > 0 ? { promptCacheKey: { omit: uniqueOmit } } : {}),
|
|
2172
|
-
|
|
2245
|
+
...(featuresRecord ? { features: Object.fromEntries(Object.entries(featuresRecord)) as Partial<Record<PersistedCacheOptimizerFeature, boolean>> } : {}),
|
|
2246
|
+
} as PersistedCacheOptimizerConfigV3;
|
|
2173
2247
|
}
|
|
2174
2248
|
|
|
2175
|
-
function normalizePersistedCacheOptimizerConfig(value: PersistedCacheOptimizerConfig | undefined):
|
|
2249
|
+
function normalizePersistedCacheOptimizerConfig(value: PersistedCacheOptimizerConfig | undefined): PersistedCacheOptimizerConfigV3 {
|
|
2176
2250
|
if (!value) return { version: 2 };
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2251
|
+
const features = value.version === 3 ? value.features : undefined;
|
|
2252
|
+
return {
|
|
2253
|
+
version: features ? 3 : 2,
|
|
2254
|
+
...(value.footerMode ? { footerMode: value.footerMode } : {}),
|
|
2255
|
+
...(value.version !== 1 && value.promptCacheKey ? { promptCacheKey: value.promptCacheKey } : {}),
|
|
2256
|
+
...(features ? { features } : {}),
|
|
2257
|
+
};
|
|
2180
2258
|
}
|
|
2181
2259
|
|
|
2182
|
-
function readPersistedCacheOptimizerConfig(configPath: string = CONFIG_FILE_PATH):
|
|
2260
|
+
function readPersistedCacheOptimizerConfig(configPath: string = CONFIG_FILE_PATH): PersistedCacheOptimizerConfigV3 {
|
|
2183
2261
|
try {
|
|
2184
2262
|
return normalizePersistedCacheOptimizerConfig(parsePersistedCacheOptimizerConfig(JSON.parse(readFileSync(configPath, "utf8"))));
|
|
2185
2263
|
} catch (error) {
|
|
@@ -2193,7 +2271,7 @@ function readPersistedFooterMode(configPath: string = CONFIG_FILE_PATH): FooterS
|
|
|
2193
2271
|
}
|
|
2194
2272
|
|
|
2195
2273
|
async function writePersistedCacheOptimizerConfigUnlocked(
|
|
2196
|
-
config:
|
|
2274
|
+
config: PersistedCacheOptimizerConfig | PersistedCacheOptimizerConfigV3,
|
|
2197
2275
|
configPath: string,
|
|
2198
2276
|
): Promise<void> {
|
|
2199
2277
|
await mkdir(dirname(configPath), { recursive: true });
|
|
@@ -2223,7 +2301,7 @@ async function writePersistedCacheOptimizerConfigUnlocked(
|
|
|
2223
2301
|
}
|
|
2224
2302
|
|
|
2225
2303
|
async function writePersistedCacheOptimizerConfig(
|
|
2226
|
-
config:
|
|
2304
|
+
config: PersistedCacheOptimizerConfig | PersistedCacheOptimizerConfigV3,
|
|
2227
2305
|
configPath: string = CONFIG_FILE_PATH,
|
|
2228
2306
|
): Promise<void> {
|
|
2229
2307
|
await withModelsJsonTransactionLock(() => writePersistedCacheOptimizerConfigUnlocked(config, configPath));
|
|
@@ -2233,14 +2311,14 @@ async function writePersistedFooterModeUnlocked(
|
|
|
2233
2311
|
mode: FooterStatsMode,
|
|
2234
2312
|
configPath: string,
|
|
2235
2313
|
): Promise<void> {
|
|
2236
|
-
let version: 1 | 2 = 1;
|
|
2314
|
+
let version: 1 | 2 | 3 = 1;
|
|
2237
2315
|
let raw: PersistedCacheOptimizerConfig | undefined;
|
|
2238
2316
|
let targetExists = false;
|
|
2239
2317
|
try {
|
|
2240
2318
|
raw = parsePersistedCacheOptimizerConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
2241
2319
|
targetExists = true;
|
|
2242
2320
|
if (!raw) throw new Error("invalid footer config schema");
|
|
2243
|
-
version = raw.version === 2 ? 2 : 1;
|
|
2321
|
+
version = raw.version === 3 ? 3 : raw.version === 2 ? 2 : 1;
|
|
2244
2322
|
} catch (error) {
|
|
2245
2323
|
if (getErrorCode(error) !== "ENOENT") throw new Error("invalid footer config schema");
|
|
2246
2324
|
}
|
|
@@ -2263,7 +2341,7 @@ async function writePersistedFooterModeUnlocked(
|
|
|
2263
2341
|
}
|
|
2264
2342
|
return;
|
|
2265
2343
|
}
|
|
2266
|
-
await writePersistedCacheOptimizerConfigUnlocked({ ...current, version:
|
|
2344
|
+
await writePersistedCacheOptimizerConfigUnlocked({ ...current, version: 3, footerMode: mode } as PersistedCacheOptimizerConfigV3, configPath);
|
|
2267
2345
|
}
|
|
2268
2346
|
|
|
2269
2347
|
async function writePersistedFooterMode(
|
|
@@ -2273,6 +2351,17 @@ async function writePersistedFooterMode(
|
|
|
2273
2351
|
await withModelsJsonTransactionLock(() => writePersistedFooterModeUnlocked(mode, configPath));
|
|
2274
2352
|
}
|
|
2275
2353
|
|
|
2354
|
+
async function writePersistedFeature(feature: PersistedCacheOptimizerFeature, enabled: boolean): Promise<void> {
|
|
2355
|
+
const current = readPersistedCacheOptimizerConfig();
|
|
2356
|
+
const next: PersistedCacheOptimizerConfigV3 = {
|
|
2357
|
+
...current,
|
|
2358
|
+
version: 3,
|
|
2359
|
+
features: { ...(current.features ?? {}), [feature]: enabled },
|
|
2360
|
+
};
|
|
2361
|
+
await writePersistedCacheOptimizerConfig(next);
|
|
2362
|
+
setPersistedCacheOptimizerConfig(readPersistedCacheOptimizerConfig());
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2276
2365
|
function configReceiptBackupPath(receipt: PromptCacheKeyConfigReceipt, receiptPath: string = CONFIG_RECEIPT_PATH): string {
|
|
2277
2366
|
return join(dirname(receiptPath), receipt.backupFile);
|
|
2278
2367
|
}
|
|
@@ -2665,7 +2754,7 @@ function isDisabledEnv(value: string | undefined): boolean {
|
|
|
2665
2754
|
|
|
2666
2755
|
function shouldInjectOpenAIPromptCacheKey(): boolean {
|
|
2667
2756
|
if (!runtimeOptimizerEnabled) return false;
|
|
2668
|
-
if (
|
|
2757
|
+
if (!featureEnabled("openAICacheKey", NO_OPENAI_CACHE_KEY_ENV, true)) return false;
|
|
2669
2758
|
if (isDisabledEnv(process.env[OPENAI_CACHE_KEY_ENV])) return false;
|
|
2670
2759
|
return true;
|
|
2671
2760
|
}
|
|
@@ -2687,7 +2776,9 @@ function getOptimizerRuntimeModeLines(): string[] {
|
|
|
2687
2776
|
const state = runtimeOptimizerEnabled ? "enabled" : "disabled";
|
|
2688
2777
|
const lines: string[] = [];
|
|
2689
2778
|
lines.push(`Runtime state: ${state}`);
|
|
2690
|
-
lines.push(`• Prompt rewrite: ${runtimeOptimizerEnabled &&
|
|
2779
|
+
lines.push(`• Prompt rewrite: ${runtimeOptimizerEnabled && featureEnabled("promptRewrite", NO_PROMPT_REWRITE_ENV, true) ? "on" : "off"}`);
|
|
2780
|
+
lines.push(`• Native virtual rewrite: ${featureEnabled("virtualRewrite", VIRTUAL_REWRITE_ENV, false) ? "opt-in" : "off"}`);
|
|
2781
|
+
lines.push(`• Skill compression: ${featureEnabled("skillCompression", NO_SKILL_COMPRESSION_ENV, true) ? "on" : "off"}`);
|
|
2691
2782
|
lines.push(`• Deterministic tool ordering: ${isToolOrderEnabled() ? "on (verified built-in payloads, opt-in)" : "off"}`);
|
|
2692
2783
|
lines.push(`• OpenAI prompt_cache_key fallback: ${shouldInjectOpenAIPromptCacheKey() ? "on" : "off"}`);
|
|
2693
2784
|
lines.push(`• Footer cache stats: on${runtimeOptimizerEnabled ? "" : " (comparison mode)"}`);
|
|
@@ -2695,7 +2786,7 @@ function getOptimizerRuntimeModeLines(): string[] {
|
|
|
2695
2786
|
lines.push(`• ${PI_CACHE_RETENTION_ENV}: ${process.env[PI_CACHE_RETENTION_ENV] ?? "(unset)"}`);
|
|
2696
2787
|
if (!runtimeOptimizerEnabled) {
|
|
2697
2788
|
lines.push("This is a current-process switch. Run /reload or restart Pi to return to startup behavior.");
|
|
2698
|
-
} else if (
|
|
2789
|
+
} else if (!featureEnabled("promptRewrite", NO_PROMPT_REWRITE_ENV, true) || !shouldInjectOpenAIPromptCacheKey()) {
|
|
2699
2790
|
lines.push("Some features are still disabled by environment variables.");
|
|
2700
2791
|
}
|
|
2701
2792
|
return lines;
|
|
@@ -2782,12 +2873,12 @@ function shouldInjectOpenAIPromptCacheKeyForModel(model: PiModel | undefined): b
|
|
|
2782
2873
|
return isOpenAICompatibleApi(model?.api);
|
|
2783
2874
|
}
|
|
2784
2875
|
|
|
2785
|
-
function isPromptCacheKeyOmittedForModel(model: PiModel | undefined, config:
|
|
2876
|
+
function isPromptCacheKeyOmittedForModel(model: PiModel | undefined, config: PersistedCacheOptimizerConfig | PersistedCacheOptimizerConfigV3 = persistedCacheOptimizerConfig): boolean {
|
|
2786
2877
|
if (!model || !isOpenAICompatibleApi(model.api)) return false;
|
|
2787
|
-
return config.promptCacheKey?.omit?.includes(modelKey(model)) === true;
|
|
2878
|
+
return "promptCacheKey" in config && config.promptCacheKey?.omit?.includes(modelKey(model)) === true;
|
|
2788
2879
|
}
|
|
2789
2880
|
|
|
2790
|
-
function setPersistedCacheOptimizerConfig(config:
|
|
2881
|
+
function setPersistedCacheOptimizerConfig(config: PersistedCacheOptimizerConfig | PersistedCacheOptimizerConfigV3): void {
|
|
2791
2882
|
persistedCacheOptimizerConfig = normalizePersistedCacheOptimizerConfig(config);
|
|
2792
2883
|
persistedFooterStatsMode = persistedCacheOptimizerConfig.footerMode;
|
|
2793
2884
|
}
|
|
@@ -10160,6 +10251,8 @@ export const __internals_for_tests = {
|
|
|
10160
10251
|
normalizeAnthropicCacheControlTtlOrder,
|
|
10161
10252
|
isPiBuiltInLlamaCppModel,
|
|
10162
10253
|
isResponsesPromptRewriteBypassApi,
|
|
10254
|
+
canRewriteNativeVirtualPrompt,
|
|
10255
|
+
VIRTUAL_REWRITE_ENV,
|
|
10163
10256
|
isMistralConversationsApi,
|
|
10164
10257
|
isOpenAIFamilyModel,
|
|
10165
10258
|
isOpenAIFamilyAssistantMessage,
|
|
@@ -11202,7 +11295,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
11202
11295
|
// after this system prompt is built, so the bypass above cannot be decided
|
|
11203
11296
|
// here. A route may reach the safety-filtered Codex backend; keep Pi's
|
|
11204
11297
|
// prompt byte-for-byte instead of reordering it.
|
|
11205
|
-
if (isNativeVirtualModel(_ctx.model)) {
|
|
11298
|
+
if (isNativeVirtualModel(_ctx.model) && !canRewriteNativeVirtualPrompt(_ctx.model, _ctx)) {
|
|
11206
11299
|
return {};
|
|
11207
11300
|
}
|
|
11208
11301
|
|
|
@@ -11212,7 +11305,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
11212
11305
|
// prompt mutations below (session-overview churn strip, skill compression,
|
|
11213
11306
|
// and stable-prefix reordering). Footer stats and the OpenAI
|
|
11214
11307
|
// prompt_cache_key fallback remain active.
|
|
11215
|
-
if (
|
|
11308
|
+
if (!featureEnabled("promptRewrite", NO_PROMPT_REWRITE_ENV, true)) return {};
|
|
11216
11309
|
|
|
11217
11310
|
// Step 1: strip per-turn churn from <session-overview>.
|
|
11218
11311
|
// Removing RECENT COMMITS, Working directory status, and
|
|
@@ -11781,10 +11874,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
11781
11874
|
} else if (subcommand === "config") {
|
|
11782
11875
|
const configKey = commandParts[1];
|
|
11783
11876
|
const requestedMode = commandParts[2];
|
|
11877
|
+
const feature = configKey ? FEATURE_COMMAND_MAP[configKey] : undefined;
|
|
11878
|
+
if (feature && (requestedMode === "on" || requestedMode === "off")) {
|
|
11879
|
+
try {
|
|
11880
|
+
await writePersistedFeature(feature, requestedMode === "on");
|
|
11881
|
+
lastStatusText = undefined;
|
|
11882
|
+
await publishStatus(cmdCtx, model);
|
|
11883
|
+
cmdCtx.ui.notify(`✅ ${configKey} set to ${requestedMode}. Persistent config overrides its environment variable.`, "info");
|
|
11884
|
+
} catch (error) {
|
|
11885
|
+
cmdCtx.ui.notify(`❌ Could not update ${configKey}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
11886
|
+
}
|
|
11887
|
+
return;
|
|
11888
|
+
}
|
|
11889
|
+
if (configKey === "reset") {
|
|
11890
|
+
try {
|
|
11891
|
+
await writePersistedCacheOptimizerConfig({ version: 2, footerMode: persistedFooterStatsMode, promptCacheKey: persistedCacheOptimizerConfig.promptCacheKey });
|
|
11892
|
+
setPersistedCacheOptimizerConfig(readPersistedCacheOptimizerConfig());
|
|
11893
|
+
cmdCtx.ui.notify("✅ Feature configuration reset. Environment variables now apply again.", "info");
|
|
11894
|
+
} catch (error) {
|
|
11895
|
+
cmdCtx.ui.notify(`❌ Could not reset feature configuration: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
11896
|
+
}
|
|
11897
|
+
return;
|
|
11898
|
+
}
|
|
11784
11899
|
if (configKey !== "footer-mode" || !requestedMode || !["session", "total", "process"].includes(requestedMode)) {
|
|
11785
11900
|
const resolved = resolveFooterStatsMode(persistedFooterStatsMode);
|
|
11786
11901
|
cmdCtx.ui.notify(
|
|
11787
11902
|
`Usage: /cache-optimizer config footer-mode total|session|process\n` +
|
|
11903
|
+
` /cache-optimizer config prompt-rewrite|virtual-rewrite|skill-compression|openai-cache-key|tool-order on|off\n` +
|
|
11904
|
+
` /cache-optimizer config reset\n` +
|
|
11788
11905
|
`Current footer mode: ${resolved.mode} (${resolved.source})`,
|
|
11789
11906
|
"info",
|
|
11790
11907
|
);
|
|
@@ -12601,7 +12718,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
12601
12718
|
diagnosis.push(" stats all — Show detailed totals for every model across all local sessions");
|
|
12602
12719
|
diagnosis.push(" stats contributors — Show per-session contributors for the active model");
|
|
12603
12720
|
diagnosis.push(" compat — Show compat suggestion with edit location");
|
|
12721
|
+
diagnosis.push(" config prompt-rewrite|virtual-rewrite|skill-compression|openai-cache-key|tool-order on|off — Persist feature settings");
|
|
12604
12722
|
diagnosis.push(" config footer-mode total|session|process — Persist the footer stats mode");
|
|
12723
|
+
diagnosis.push(" config reset — Remove persistent feature overrides");
|
|
12605
12724
|
diagnosis.push(" fix — Auto-fix compat issues (writes models.json or extension config, requires UI)");
|
|
12606
12725
|
diagnosis.push(" fix prompt-cache-key — Explicitly omit prompt_cache_key for the active model");
|
|
12607
12726
|
diagnosis.push(" rollback — Undo the latest confirmed fix (requires UI confirmation)");
|
package/package.json
CHANGED