pi-cache-optimizer 2.8.2 → 2.8.3
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 +5 -4
- package/README.zh-CN.md +5 -4
- package/index.ts +238 -272
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Run `/reload` in Pi after install/update/remove so extension hooks refresh.
|
|
|
55
55
|
|
|
56
56
|
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).
|
|
57
57
|
|
|
58
|
-
This extension is validated against Pi 0.84.
|
|
58
|
+
This extension requires Pi 0.82+ and is validated against Pi 0.84.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.
|
|
59
59
|
|
|
60
60
|
## Commands
|
|
61
61
|
|
|
@@ -135,7 +135,7 @@ Notes:
|
|
|
135
135
|
|
|
136
136
|
- `sendSessionAffinityHeaders: true` is the safe default when your proxy supports sticky routing.
|
|
137
137
|
- `supportsLongCacheRetention: true` is optional. Add it only when the endpoint explicitly supports OpenAI long prompt cache retention.
|
|
138
|
-
- If you see `400 Unsupported parameter: prompt_cache_retention`, remove/avoid `supportsLongCacheRetention` for that channel. Keep `sendSessionAffinityHeaders` if supported.
|
|
138
|
+
- 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.
|
|
139
139
|
- Use `/cache-optimizer compat` or `/cache-optimizer doctor` to see model-specific advice.
|
|
140
140
|
- For DeepSeek models, the Pi Mono guidance expects `compat.requiresReasoningContentOnAssistantMessages: true` and `compat.thinkingFormat: "deepseek"` alongside cache/session-affinity flags when the endpoint supports them.
|
|
141
141
|
- This extension's `doctor` and `compat` commands only advise; they do not modify `models.json`.
|
|
@@ -229,8 +229,9 @@ Pi 0.80.9+ already includes Kimi K3 in built-in Kimi Coding, Moonshot AI / China
|
|
|
229
229
|
2. Warns: ① changes affect all sessions using that channel, ② automatic backup created at `models.json.backup-cache-optimizer-<timestamp>`, ③ Pi reload required
|
|
230
230
|
3. Uses comment-preserving surgical editor — existing comments, indentation, and existing key order are preserved
|
|
231
231
|
4. Requires explicit user confirmation (interactive prompt or `ui.select`)
|
|
232
|
-
5. Writes atomically (temp + rename); self-validates after write
|
|
233
|
-
6.
|
|
232
|
+
5. Writes and restores atomically (temp + rename); self-validates after write
|
|
233
|
+
6. Preserves the existing `models.json` access mode exactly — it does not tighten or loosen permissions (for example, `0600` stays `0600`, `0644` stays `0644`)
|
|
234
|
+
7. Uses unique, non-overwriting backup names and falls back to manual guidance if the JSONC scanner cannot confidently locate the target
|
|
234
235
|
|
|
235
236
|
Existing `modelOverrides[modelId]` entries have Pi's highest precedence, so `fix` repairs them directly. For built-in or API-login models without a custom `models[]` entry, `fix` creates a compat-only `modelOverrides` entry instead of inventing a custom model definition. Self-validation checks the effective three-layer compat result, so a lower-level edit shadowed by an override is rejected.
|
|
236
237
|
|
package/README.zh-CN.md
CHANGED
|
@@ -55,7 +55,7 @@ pi remove npm:pi-deepseek-cache-optimizer && pi install npm:pi-cache-optimizer
|
|
|
55
55
|
|
|
56
56
|
Pi 0.79.7 及之后,`pi update` 默认只更新 Pi 本体。若要更新已安装的 Pi package(包括本扩展),请运行 `pi update --extensions`(只更新 packages)或 `pi update --all`(Pi 与 packages 一起更新)。
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
本扩展要求 Pi 0.82+,并已使用 Pi 0.84.2 验证。TypeScript 校验直接使用官方 Pi package 类型,同时只使用这些版本共有的 extension hooks、`getAgentDir()` 和 prompt options;不依赖 Pi 0.83+ 专有 API(例如 `ctx.scopedModels` 或 bundled TypeBox 1.3 aliases)。
|
|
59
59
|
|
|
60
60
|
## 命令
|
|
61
61
|
|
|
@@ -134,7 +134,7 @@ Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention`
|
|
|
134
134
|
|
|
135
135
|
- `sendSessionAffinityHeaders: true` 是安全默认项,前提是你的代理支持 sticky routing。
|
|
136
136
|
- `supportsLongCacheRetention: true` 是可选项。只有 endpoint 明确支持 OpenAI long prompt cache retention 时才添加。
|
|
137
|
-
- 如果出现 `400 Unsupported parameter: prompt_cache_retention`,请为该渠道移除 / 避免 `supportsLongCacheRetention`;如支持,可保留 `sendSessionAffinityHeaders
|
|
137
|
+
- 如果出现 `400 Unsupported parameter: prompt_cache_retention`,请为该渠道移除 / 避免 `supportsLongCacheRetention`;如支持,可保留 `sendSessionAffinityHeaders`。扩展会从响应头或最终 assistant error message 中识别这条明确错误,并在当前进程的后续请求中移除该参数。
|
|
138
138
|
- 使用 `/cache-optimizer compat` 或 `/cache-optimizer doctor` 查看当前模型的具体建议。
|
|
139
139
|
- 对 DeepSeek 模型,Pi Mono 指南期望在支持时同时设置 `compat.requiresReasoningContentOnAssistantMessages: true` 和 `compat.thinkingFormat: "deepseek"`,再配合缓存 / session-affinity 相关 compat。
|
|
140
140
|
- 本扩展的 `doctor` 和 `compat` 命令只给建议,不会修改 `models.json`。
|
|
@@ -228,8 +228,9 @@ Pi 0.80.9+ 已在内置 Kimi Coding、Moonshot AI / 中国区、OpenRouter 和 V
|
|
|
228
228
|
2. 警告:① 修改影响使用该渠道的所有 session,② 自动备份到 `models.json.backup-cache-optimizer-<timestamp>`,③ 需重启 Pi 或 reload
|
|
229
229
|
3. 使用保留注释的精确编辑器 —— 现有注释、缩进和已有 key 顺序都会保留
|
|
230
230
|
4. 需要用户明确确认(交互式提示或 `ui.select`)
|
|
231
|
-
5.
|
|
232
|
-
6.
|
|
231
|
+
5. 写入与失败恢复都使用原子替换(temp + rename);写入后自我验证
|
|
232
|
+
6. 完全保留 `models.json` 原有访问权限,不主动收紧或放宽(例如 `0600` 保持 `0600`,`0644` 保持 `0644`)
|
|
233
|
+
7. 备份名唯一且不会覆盖已有备份;如果 JSONC 扫描器无法置信定位目标,则回退到手动修改指引
|
|
233
234
|
|
|
234
235
|
已有的 `modelOverrides[modelId]` 具有 Pi 的最高优先级,因此 `fix` 会直接修复该 entry。对于没有自定义 `models[]` entry 的内置模型或 API-login 模型,`fix` 会创建仅含 compat 的 `modelOverrides` entry,而不会凭空添加自定义模型定义。自检会验证三层合并后的有效 compat,因此被更高层 override 遮蔽的低层修改会被拒绝。
|
|
235
236
|
|
package/index.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { chmod, copyFile, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants, readFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getAgentDir,
|
|
8
|
+
type BuildSystemPromptOptions,
|
|
9
|
+
type ExtensionAPI,
|
|
10
|
+
type ExtensionCommandContext,
|
|
11
|
+
type ExtensionContext,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
13
|
|
|
8
14
|
type MutableEnv = Record<string, string | undefined>;
|
|
9
15
|
|
|
@@ -85,6 +91,7 @@ const STARTUP_CACHE_RETENTION_ENV = getOrCaptureCacheRetentionBaseline();
|
|
|
85
91
|
requestLongCacheRetention();
|
|
86
92
|
|
|
87
93
|
type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
94
|
+
type ModelIdentity = Pick<PiModel, "provider" | "id">;
|
|
88
95
|
type UnknownRecord = Record<string, unknown>;
|
|
89
96
|
type CacheProviderId = "deepseek" | "openai" | "claude" | "gemini";
|
|
90
97
|
|
|
@@ -2088,7 +2095,7 @@ function isAyaLikeAssistantMessage(message: unknown, model: PiModel | undefined)
|
|
|
2088
2095
|
|
|
2089
2096
|
// ── Model key ──────────────────────────────────────────────────────
|
|
2090
2097
|
|
|
2091
|
-
function modelKey(model:
|
|
2098
|
+
function modelKey(model: ModelIdentity): string {
|
|
2092
2099
|
return `${model.provider}/${model.id}`;
|
|
2093
2100
|
}
|
|
2094
2101
|
|
|
@@ -2461,12 +2468,8 @@ function getPromptCacheRetentionUnsupportedHint(): string {
|
|
|
2461
2468
|
return "If this channel returns `400 Unsupported parameter: prompt_cache_retention`, remove/avoid `supportsLongCacheRetention`; this extension does not write that field directly, but Pi may send it when long retention is requested and compat says the proxy supports it.";
|
|
2462
2469
|
}
|
|
2463
2470
|
|
|
2464
|
-
function
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
const normalized = Object.entries(headers)
|
|
2468
|
-
.map(([key, value]) => `${lower(key)}: ${lower(value)}`)
|
|
2469
|
-
.join("\n");
|
|
2471
|
+
function hasPromptCacheRetentionUnsupportedText(value: unknown): boolean {
|
|
2472
|
+
const normalized = lower(value);
|
|
2470
2473
|
if (!normalized.includes("prompt_cache_retention")) return false;
|
|
2471
2474
|
|
|
2472
2475
|
return [
|
|
@@ -2478,10 +2481,24 @@ function hasPromptCacheRetentionUnsupportedSignal(headers: Record<string, string
|
|
|
2478
2481
|
"extra inputs",
|
|
2479
2482
|
"not permitted",
|
|
2480
2483
|
"unrecognized",
|
|
2481
|
-
"bad request",
|
|
2482
2484
|
].some((needle) => normalized.includes(needle));
|
|
2483
2485
|
}
|
|
2484
2486
|
|
|
2487
|
+
function hasPromptCacheRetentionUnsupportedSignal(headers: Record<string, string> | undefined): boolean {
|
|
2488
|
+
if (!headers) return false;
|
|
2489
|
+
return hasPromptCacheRetentionUnsupportedText(
|
|
2490
|
+
Object.entries(headers)
|
|
2491
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
2492
|
+
.join("\n"),
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
function hasPromptCacheRetentionUnsupportedErrorMessage(message: unknown): boolean {
|
|
2497
|
+
const record = getAssistantRecord(message);
|
|
2498
|
+
return record?.stopReason === "error" &&
|
|
2499
|
+
hasPromptCacheRetentionUnsupportedText(record.errorMessage);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2485
2502
|
type CompatAdvicePlacement = {
|
|
2486
2503
|
providerLabel?: string;
|
|
2487
2504
|
modelId?: string;
|
|
@@ -4405,6 +4422,16 @@ function isPromptCacheRetention400Applicable(model: PiModel): boolean {
|
|
|
4405
4422
|
getCompat(model).supportsLongCacheRetention === true;
|
|
4406
4423
|
}
|
|
4407
4424
|
|
|
4425
|
+
function isExplicitPromptCacheRetentionUnsupportedApplicable(model: PiModel): boolean {
|
|
4426
|
+
// A finalized assistant error with an explicit unsupported-parameter signal
|
|
4427
|
+
// proves that prompt_cache_retention reached this provider/model. Do not
|
|
4428
|
+
// require compat inherited from the active fallback model: router shells may
|
|
4429
|
+
// have no upstream compat metadata when no live routing registry is present.
|
|
4430
|
+
return isOpenAICompatibleApi(model.api) &&
|
|
4431
|
+
!isOfficialOpenAIBaseUrl(model) &&
|
|
4432
|
+
!isPiBuiltInLlamaCppModel(model);
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4408
4435
|
/**
|
|
4409
4436
|
* Whether the 403 sendSessionAffinityHeaders diagnostic applies to a model.
|
|
4410
4437
|
*
|
|
@@ -6528,18 +6555,121 @@ function formatCompatKeysForInsertion(compatKeys: Record<string, unknown>): stri
|
|
|
6528
6555
|
}
|
|
6529
6556
|
|
|
6530
6557
|
/**
|
|
6531
|
-
* Generate
|
|
6532
|
-
*
|
|
6558
|
+
* Generate a unique UTC timestamp component for backup filenames.
|
|
6559
|
+
* Milliseconds, process id, and an in-process sequence prevent collisions.
|
|
6533
6560
|
*/
|
|
6534
|
-
|
|
6535
|
-
|
|
6561
|
+
let backupSequence = 0;
|
|
6562
|
+
|
|
6563
|
+
function backupTimestamp(now: Date = new Date()): string {
|
|
6536
6564
|
const y = now.getUTCFullYear();
|
|
6537
|
-
const m = String(now.getUTCMonth() + 1).padStart(2,
|
|
6538
|
-
const d = String(now.getUTCDate()).padStart(2,
|
|
6539
|
-
const h = String(now.getUTCHours()).padStart(2,
|
|
6540
|
-
const min = String(now.getUTCMinutes()).padStart(2,
|
|
6541
|
-
const s = String(now.getUTCSeconds()).padStart(2,
|
|
6542
|
-
|
|
6565
|
+
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
6566
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
6567
|
+
const h = String(now.getUTCHours()).padStart(2, "0");
|
|
6568
|
+
const min = String(now.getUTCMinutes()).padStart(2, "0");
|
|
6569
|
+
const s = String(now.getUTCSeconds()).padStart(2, "0");
|
|
6570
|
+
const ms = String(now.getUTCMilliseconds()).padStart(3, "0");
|
|
6571
|
+
return `${y}${m}${d}T${h}${min}${s}${ms}Z-${process.pid}-${backupSequence++}`;
|
|
6572
|
+
}
|
|
6573
|
+
|
|
6574
|
+
function uniqueTempPath(targetPath: string, purpose: string): string {
|
|
6575
|
+
return `${targetPath}.${process.pid}.${Date.now()}.${backupSequence++}.${purpose}.tmp`;
|
|
6576
|
+
}
|
|
6577
|
+
|
|
6578
|
+
async function atomicReplaceTextFilePreservingMode(
|
|
6579
|
+
targetPath: string,
|
|
6580
|
+
content: string,
|
|
6581
|
+
mode: number,
|
|
6582
|
+
purpose: string,
|
|
6583
|
+
): Promise<void> {
|
|
6584
|
+
const tempPath = uniqueTempPath(targetPath, purpose);
|
|
6585
|
+
try {
|
|
6586
|
+
await writeFile(tempPath, content, { encoding: "utf8", mode });
|
|
6587
|
+
await chmod(tempPath, mode);
|
|
6588
|
+
await rename(tempPath, targetPath);
|
|
6589
|
+
} catch (error) {
|
|
6590
|
+
try {
|
|
6591
|
+
await unlink(tempPath);
|
|
6592
|
+
} catch (cleanupError) {
|
|
6593
|
+
if (getErrorCode(cleanupError) !== "ENOENT") {
|
|
6594
|
+
console.warn(`${LOG_PREFIX}: failed to remove temporary models.json file`, cleanupError);
|
|
6595
|
+
}
|
|
6596
|
+
}
|
|
6597
|
+
throw error;
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
async function atomicRestoreFileFromBackup(
|
|
6602
|
+
backupPath: string,
|
|
6603
|
+
targetPath: string,
|
|
6604
|
+
mode: number,
|
|
6605
|
+
): Promise<void> {
|
|
6606
|
+
const tempPath = uniqueTempPath(targetPath, "restore");
|
|
6607
|
+
try {
|
|
6608
|
+
await copyFile(backupPath, tempPath, fsConstants.COPYFILE_EXCL);
|
|
6609
|
+
await chmod(tempPath, mode);
|
|
6610
|
+
await rename(tempPath, targetPath);
|
|
6611
|
+
} catch (error) {
|
|
6612
|
+
try {
|
|
6613
|
+
await unlink(tempPath);
|
|
6614
|
+
} catch (cleanupError) {
|
|
6615
|
+
if (getErrorCode(cleanupError) !== "ENOENT") {
|
|
6616
|
+
console.warn(`${LOG_PREFIX}: failed to remove temporary restore file`, cleanupError);
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
6619
|
+
throw error;
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
|
|
6623
|
+
type ModelsJsonFixTransactionResult =
|
|
6624
|
+
| { ok: true }
|
|
6625
|
+
| { ok: false; postCheckError: string };
|
|
6626
|
+
|
|
6627
|
+
async function applyModelsJsonFixTransaction(
|
|
6628
|
+
modifiedText: string,
|
|
6629
|
+
backupPath: string,
|
|
6630
|
+
validateWrittenText: (writtenText: string) => string | null,
|
|
6631
|
+
): Promise<ModelsJsonFixTransactionResult> {
|
|
6632
|
+
const originalMode = (await stat(MODELS_JSON_PATH)).mode & 0o7777;
|
|
6633
|
+
await copyFile(MODELS_JSON_PATH, backupPath, fsConstants.COPYFILE_EXCL);
|
|
6634
|
+
await chmod(backupPath, originalMode);
|
|
6635
|
+
|
|
6636
|
+
let targetReplaced = false;
|
|
6637
|
+
try {
|
|
6638
|
+
await atomicReplaceTextFilePreservingMode(
|
|
6639
|
+
MODELS_JSON_PATH,
|
|
6640
|
+
modifiedText,
|
|
6641
|
+
originalMode,
|
|
6642
|
+
"fix",
|
|
6643
|
+
);
|
|
6644
|
+
targetReplaced = true;
|
|
6645
|
+
|
|
6646
|
+
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
6647
|
+
const postCheckError = validateWrittenText(writtenText);
|
|
6648
|
+
const writtenMode = (await stat(MODELS_JSON_PATH)).mode & 0o7777;
|
|
6649
|
+
const effectiveError = postCheckError ?? (
|
|
6650
|
+
writtenMode === originalMode
|
|
6651
|
+
? null
|
|
6652
|
+
: `models.json access mode changed from ${originalMode.toString(8)} to ${writtenMode.toString(8)}`
|
|
6653
|
+
);
|
|
6654
|
+
if (effectiveError !== null) {
|
|
6655
|
+
await atomicRestoreFileFromBackup(backupPath, MODELS_JSON_PATH, originalMode);
|
|
6656
|
+
return { ok: false, postCheckError: effectiveError };
|
|
6657
|
+
}
|
|
6658
|
+
|
|
6659
|
+
return { ok: true };
|
|
6660
|
+
} catch (error) {
|
|
6661
|
+
if (targetReplaced) {
|
|
6662
|
+
try {
|
|
6663
|
+
await atomicRestoreFileFromBackup(backupPath, MODELS_JSON_PATH, originalMode);
|
|
6664
|
+
} catch (restoreError) {
|
|
6665
|
+
throw new AggregateError(
|
|
6666
|
+
[error, restoreError],
|
|
6667
|
+
"models.json fix failed and the atomic backup restore also failed",
|
|
6668
|
+
);
|
|
6669
|
+
}
|
|
6670
|
+
}
|
|
6671
|
+
throw error;
|
|
6672
|
+
}
|
|
6543
6673
|
}
|
|
6544
6674
|
|
|
6545
6675
|
// Internal helpers exported only so the task verification script
|
|
@@ -6593,6 +6723,7 @@ export const __internals_for_tests = {
|
|
|
6593
6723
|
isSessionAffinity403Applicable,
|
|
6594
6724
|
isOpenAISdkHeader403Applicable,
|
|
6595
6725
|
hasPromptCacheRetentionUnsupportedSignal,
|
|
6726
|
+
hasPromptCacheRetentionUnsupportedErrorMessage,
|
|
6596
6727
|
// Non-GPT OpenAI-compatible model detection
|
|
6597
6728
|
isKimiLikeModel,
|
|
6598
6729
|
isKimiLikeAssistantMessage,
|
|
@@ -6816,6 +6947,9 @@ export const __internals_for_tests = {
|
|
|
6816
6947
|
deepEqualIgnoringKeys,
|
|
6817
6948
|
formatCompatKeysForInsertion,
|
|
6818
6949
|
backupTimestamp,
|
|
6950
|
+
atomicReplaceTextFilePreservingMode,
|
|
6951
|
+
atomicRestoreFileFromBackup,
|
|
6952
|
+
applyModelsJsonFixTransaction,
|
|
6819
6953
|
// Fix suggestion builder
|
|
6820
6954
|
buildFixSuggestion,
|
|
6821
6955
|
// Adaptive thinking compat helpers
|
|
@@ -7615,6 +7749,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
7615
7749
|
syncSessionHash(ctx);
|
|
7616
7750
|
const msgRecord = asRecord(event.message);
|
|
7617
7751
|
|
|
7752
|
+
// Some providers expose an HTTP 400 error body only through the finalized
|
|
7753
|
+
// assistant error message; after_provider_response may contain the status
|
|
7754
|
+
// and no diagnostic response headers. Record the same model-scoped
|
|
7755
|
+
// prompt_cache_retention fallback from that authoritative message identity.
|
|
7756
|
+
if (runtimeOptimizerEnabled && hasPromptCacheRetentionUnsupportedErrorMessage(event.message)) {
|
|
7757
|
+
const fallbackModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
7758
|
+
const messageModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
|
|
7759
|
+
const errorModel = messageModel
|
|
7760
|
+
? findModelInRegistry(ctx.modelRegistry, messageModel.provider, messageModel.id) ?? messageModel
|
|
7761
|
+
: undefined;
|
|
7762
|
+
if (errorModel && isExplicitPromptCacheRetentionUnsupportedApplicable(errorModel)) {
|
|
7763
|
+
const key = modelKey(errorModel);
|
|
7764
|
+
promptCacheRetention400Models.add(key);
|
|
7765
|
+
if (!warnedPromptCacheRetention400Models.has(key)) {
|
|
7766
|
+
warnedPromptCacheRetention400Models.add(key);
|
|
7767
|
+
ctx.ui.notify(
|
|
7768
|
+
`⚠️ ${LOG_PREFIX}: ${key} rejected prompt_cache_retention. ` +
|
|
7769
|
+
getPromptCacheRetentionUnsupportedHint() +
|
|
7770
|
+
` Run /cache-optimizer doctor for the exact edit location.`,
|
|
7771
|
+
"warning",
|
|
7772
|
+
);
|
|
7773
|
+
}
|
|
7774
|
+
}
|
|
7775
|
+
}
|
|
7776
|
+
|
|
7618
7777
|
// Record only Anthropic's explicit mixed-TTL ordering error. This is a
|
|
7619
7778
|
// non-retryable 400 in Pi 0.82.1, so the fallback applies to the next
|
|
7620
7779
|
// subsequent request (and to a retry only if another layer initiates one).
|
|
@@ -7729,27 +7888,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
7729
7888
|
// reset — reset current provider/model footer stats bucket (local only)
|
|
7730
7889
|
// (no args) — interactive menu (with UI) or help summary
|
|
7731
7890
|
// ────────────────────────────────────────────────────────────────
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7891
|
+
async function handleCacheOptimizerCommand(
|
|
7892
|
+
args: string,
|
|
7893
|
+
cmdCtx: ExtensionCommandContext,
|
|
7894
|
+
): Promise<void> {
|
|
7895
|
+
syncSessionHash(cmdCtx);
|
|
7896
|
+
const selectedModel = cmdCtx.model;
|
|
7897
|
+
const model = resolveRouteModel(selectedModel, cmdCtx) ?? selectedModel;
|
|
7898
|
+
const commandParts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
7899
|
+
const subcommand = commandParts[0] || "help";
|
|
7741
7900
|
|
|
7742
7901
|
if (subcommand === "enable") {
|
|
7743
7902
|
setRuntimeOptimizerEnabled(true);
|
|
7744
7903
|
resetCurrentSessionStats();
|
|
7745
|
-
await flushPersistCacheStats(cmdCtx
|
|
7746
|
-
await publishStatus(cmdCtx
|
|
7904
|
+
await flushPersistCacheStats(cmdCtx);
|
|
7905
|
+
await publishStatus(cmdCtx, model);
|
|
7747
7906
|
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
|
|
7748
7907
|
} else if (subcommand === "disable") {
|
|
7749
7908
|
setRuntimeOptimizerEnabled(false);
|
|
7750
7909
|
resetCurrentSessionStats();
|
|
7751
|
-
await flushPersistCacheStats(cmdCtx
|
|
7752
|
-
await publishStatus(cmdCtx
|
|
7910
|
+
await flushPersistCacheStats(cmdCtx);
|
|
7911
|
+
await publishStatus(cmdCtx, model);
|
|
7753
7912
|
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");
|
|
7754
7913
|
} else if (subcommand === "doctor") {
|
|
7755
7914
|
if (!model) {
|
|
@@ -7795,7 +7954,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7795
7954
|
await writePersistedFooterMode(nextMode);
|
|
7796
7955
|
persistedFooterStatsMode = nextMode;
|
|
7797
7956
|
lastStatusText = undefined;
|
|
7798
|
-
await publishStatus(cmdCtx
|
|
7957
|
+
await publishStatus(cmdCtx, model);
|
|
7799
7958
|
const resolved = resolveFooterStatsMode(persistedFooterStatsMode);
|
|
7800
7959
|
cmdCtx.ui.notify(
|
|
7801
7960
|
`✅ Footer mode set to ${resolved.mode}. Persistent config overrides ${FOOTER_MODE_ENV}.`,
|
|
@@ -7842,10 +8001,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
7842
8001
|
resetStatsForModel(model);
|
|
7843
8002
|
|
|
7844
8003
|
// Persist immediately.
|
|
7845
|
-
await flushPersistCacheStats(cmdCtx
|
|
8004
|
+
await flushPersistCacheStats(cmdCtx);
|
|
7846
8005
|
|
|
7847
8006
|
// Update footer to show 0/0.
|
|
7848
|
-
await publishStatus(cmdCtx
|
|
8007
|
+
await publishStatus(cmdCtx, model);
|
|
7849
8008
|
|
|
7850
8009
|
cmdCtx.ui.notify(
|
|
7851
8010
|
`✅ Reset local footer cache stats for "${displayKey}". ` +
|
|
@@ -7998,20 +8157,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
7998
8157
|
);
|
|
7999
8158
|
if (confirmed) {
|
|
8000
8159
|
try {
|
|
8001
|
-
await
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8160
|
+
const result = await applyModelsJsonFixTransaction(
|
|
8161
|
+
plan.modifiedText,
|
|
8162
|
+
backupPath,
|
|
8163
|
+
(writtenText) => selfCheckMissingEntryInsertion(
|
|
8164
|
+
originalText,
|
|
8165
|
+
writtenText,
|
|
8166
|
+
suggestion.providerLabel,
|
|
8167
|
+
suggestion.modelId,
|
|
8168
|
+
suggestion.compatKeys,
|
|
8169
|
+
),
|
|
8010
8170
|
);
|
|
8011
|
-
if (
|
|
8012
|
-
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
8171
|
+
if ("postCheckError" in result) {
|
|
8013
8172
|
cmdCtx.ui.notify(
|
|
8014
|
-
`❌ Post-write self-check failed: ${
|
|
8173
|
+
`❌ Post-write self-check failed: ${result.postCheckError}\n` +
|
|
8015
8174
|
`The backup at ${backupPath} has been restored. No changes applied.`,
|
|
8016
8175
|
"error",
|
|
8017
8176
|
);
|
|
@@ -8025,7 +8184,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
8025
8184
|
);
|
|
8026
8185
|
} catch (e) {
|
|
8027
8186
|
cmdCtx.ui.notify(
|
|
8028
|
-
`❌ Write failed: ${e instanceof Error ? e.message : String(e)}
|
|
8187
|
+
`❌ Write failed: ${e instanceof Error ? e.message : String(e)}\n` +
|
|
8188
|
+
`Backup may be at: ${backupPath}`,
|
|
8029
8189
|
"error",
|
|
8030
8190
|
);
|
|
8031
8191
|
}
|
|
@@ -8147,22 +8307,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
8147
8307
|
|
|
8148
8308
|
// Write: backup → temp + rename → self-check again
|
|
8149
8309
|
try {
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
8310
|
+
const result = await applyModelsJsonFixTransaction(
|
|
8311
|
+
modifiedText,
|
|
8312
|
+
backupPath,
|
|
8313
|
+
(writtenText) => selfCheckFix(
|
|
8314
|
+
originalText,
|
|
8315
|
+
writtenText,
|
|
8316
|
+
suggestion.providerLabel,
|
|
8317
|
+
suggestion.modelId,
|
|
8318
|
+
suggestion.compatKeys,
|
|
8319
|
+
decision.placement,
|
|
8320
|
+
),
|
|
8321
|
+
);
|
|
8322
|
+
if ("postCheckError" in result) {
|
|
8164
8323
|
cmdCtx.ui.notify(
|
|
8165
|
-
`❌ Post-write self-check failed: ${postCheckError}\n` +
|
|
8324
|
+
`❌ Post-write self-check failed: ${result.postCheckError}\n` +
|
|
8166
8325
|
`The backup at ${backupPath} has been restored. No changes applied.`,
|
|
8167
8326
|
"error",
|
|
8168
8327
|
);
|
|
@@ -8198,182 +8357,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
8198
8357
|
];
|
|
8199
8358
|
const choice = await cmdCtx.ui.select("Cache Optimizer", menuOptions);
|
|
8200
8359
|
if (choice === menuOptions[0]) {
|
|
8201
|
-
|
|
8202
|
-
resetCurrentSessionStats();
|
|
8203
|
-
await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
|
|
8204
|
-
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
8205
|
-
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
|
|
8360
|
+
await handleCacheOptimizerCommand("enable", cmdCtx);
|
|
8206
8361
|
} else if (choice === menuOptions[1]) {
|
|
8207
|
-
|
|
8208
|
-
resetCurrentSessionStats();
|
|
8209
|
-
await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
|
|
8210
|
-
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
8211
|
-
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");
|
|
8362
|
+
await handleCacheOptimizerCommand("disable", cmdCtx);
|
|
8212
8363
|
} else if (choice === menuOptions[2]) {
|
|
8213
|
-
|
|
8214
|
-
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
8215
|
-
} else {
|
|
8216
|
-
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), anthropicTtlOrderError: anthropicTtlOrderErrorModels.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
|
|
8217
|
-
const adapter = selectAdapterForModel(model);
|
|
8218
|
-
const sk = model ? sessionModelKey(model) : undefined;
|
|
8219
|
-
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
8220
|
-
const samples = sk ? getRecentSamples(sk) : [];
|
|
8221
|
-
const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
|
|
8222
|
-
const fullDiagnosis = lowHitLines.length > 0
|
|
8223
|
-
? diagnosis + "\n" + lowHitLines.join("\n")
|
|
8224
|
-
: diagnosis;
|
|
8225
|
-
cmdCtx.ui.notify(fullDiagnosis, "info");
|
|
8226
|
-
}
|
|
8364
|
+
await handleCacheOptimizerCommand("doctor", cmdCtx);
|
|
8227
8365
|
} else if (choice === menuOptions[3]) {
|
|
8228
|
-
|
|
8229
|
-
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
8230
|
-
} else {
|
|
8231
|
-
const adapter = selectAdapterForModel(model);
|
|
8232
|
-
const sk = model ? sessionModelKey(model) : undefined;
|
|
8233
|
-
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
8234
|
-
const samples = sk ? getRecentSamples(sk) : [];
|
|
8235
|
-
const output = buildStatsOutput(model, adapter, statsState, samples);
|
|
8236
|
-
cmdCtx.ui.notify(output, "info");
|
|
8237
|
-
}
|
|
8366
|
+
await handleCacheOptimizerCommand("stats", cmdCtx);
|
|
8238
8367
|
} else if (choice === menuOptions[4]) {
|
|
8239
|
-
|
|
8240
|
-
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
8241
|
-
} else {
|
|
8242
|
-
const compatResult = buildCompatDiagnosis(model);
|
|
8243
|
-
if (compatResult) {
|
|
8244
|
-
cmdCtx.ui.notify(compatResult, "warning");
|
|
8245
|
-
} else {
|
|
8246
|
-
cmdCtx.ui.notify(
|
|
8247
|
-
isAdaptiveThinkingCompatApplicable(model) || isDeepSeekCompatCheckApplicable(model) || isCompatCheckApplicable(model)
|
|
8248
|
-
? "✅ Compat fully configured."
|
|
8249
|
-
: getCompatCheckNotApplicableLines(model).join("\n"),
|
|
8250
|
-
"info",
|
|
8251
|
-
);
|
|
8252
|
-
}
|
|
8253
|
-
}
|
|
8368
|
+
await handleCacheOptimizerCommand("compat", cmdCtx);
|
|
8254
8369
|
} else if (choice === menuOptions[5]) {
|
|
8255
|
-
|
|
8256
|
-
if (!model) {
|
|
8257
|
-
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
8258
|
-
return;
|
|
8259
|
-
}
|
|
8260
|
-
const suggestion = buildCommandFixSuggestion(model);
|
|
8261
|
-
if (!suggestion) {
|
|
8262
|
-
const key = modelKey(model);
|
|
8263
|
-
cmdCtx.ui.notify(`✅ Nothing to fix for "${key}". Compat already configured.`, "info");
|
|
8264
|
-
return;
|
|
8265
|
-
}
|
|
8266
|
-
|
|
8267
|
-
// Read models.json
|
|
8268
|
-
let originalText: string;
|
|
8269
|
-
try {
|
|
8270
|
-
originalText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
8271
|
-
} catch {
|
|
8272
|
-
cmdCtx.ui.notify(`❌ Could not read ${MODELS_JSON_PATH}. File may not exist.`, "error");
|
|
8273
|
-
return;
|
|
8274
|
-
}
|
|
8275
|
-
|
|
8276
|
-
const location = locateModelInJsonc(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
8277
|
-
const menuDecision = location
|
|
8278
|
-
? chooseFixPlacement(
|
|
8279
|
-
originalText,
|
|
8280
|
-
location,
|
|
8281
|
-
suggestion.compatKeys,
|
|
8282
|
-
suggestion.providerLabel,
|
|
8283
|
-
suggestion.forceModelLevel,
|
|
8284
|
-
)
|
|
8285
|
-
: undefined;
|
|
8286
|
-
const overridePlan = location
|
|
8287
|
-
? undefined
|
|
8288
|
-
: composeModelOverrideInsertion(
|
|
8289
|
-
originalText,
|
|
8290
|
-
suggestion.providerLabel,
|
|
8291
|
-
suggestion.modelId,
|
|
8292
|
-
suggestion.compatKeys,
|
|
8293
|
-
);
|
|
8294
|
-
if (!location && !overridePlan) {
|
|
8295
|
-
cmdCtx.ui.notify(
|
|
8296
|
-
`❌ Could not safely locate a modelOverrides insertion point in ${getModelsJsonDisplayPath()}.\n` +
|
|
8297
|
-
`Manual edit required:\n${formatMissingEntryManualSnippet(suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys)}\n` +
|
|
8298
|
-
`Then run /reload.`,
|
|
8299
|
-
"warning",
|
|
8300
|
-
);
|
|
8301
|
-
return;
|
|
8302
|
-
}
|
|
8303
|
-
const modifiedText = location && menuDecision
|
|
8304
|
-
? composeFixInsertion(originalText, location, suggestion.compatKeys, menuDecision.placement)
|
|
8305
|
-
: overridePlan!.modifiedText;
|
|
8306
|
-
const checkError = location && menuDecision
|
|
8307
|
-
? selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement)
|
|
8308
|
-
: selfCheckMissingEntryInsertion(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
8309
|
-
if (checkError !== null) {
|
|
8310
|
-
cmdCtx.ui.notify(`❌ Self-check failed: ${checkError}\nNo changes made.`, "error");
|
|
8311
|
-
return;
|
|
8312
|
-
}
|
|
8313
|
-
|
|
8314
|
-
const keysPreview = JSON.stringify(suggestion.compatKeys, null, 2);
|
|
8315
|
-
const ts = backupTimestamp();
|
|
8316
|
-
const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
|
|
8317
|
-
|
|
8318
|
-
const menuLocationDesc = overridePlan?.placementLabel ?? (menuDecision?.placement === "provider"
|
|
8319
|
-
? `providers["${suggestion.providerLabel}"] -> compat (provider level)`
|
|
8320
|
-
: menuDecision?.placement === "modelOverride"
|
|
8321
|
-
? `providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat`
|
|
8322
|
-
: `providers["${suggestion.providerLabel}"] -> models -> "${suggestion.modelId}" -> compat (model level)`);
|
|
8323
|
-
const menuScopeRiskLine = menuDecision?.placement === "provider"
|
|
8324
|
-
? ` 1. This change applies to ALL ${location?.allModelIds.length || 1} model(s) in the "${suggestion.providerLabel}" provider, across all sessions.`
|
|
8325
|
-
: ` 1. This change affects ALL sessions using the "${suggestion.providerLabel}" provider/channel (scoped to model "${suggestion.modelId}").`;
|
|
8326
|
-
|
|
8327
|
-
const previewLines = [
|
|
8328
|
-
`📝 Preview of changes to ${getModelsJsonDisplayPath()}:`,
|
|
8329
|
-
`Location: ${menuLocationDesc}`,
|
|
8330
|
-
`Placement: ${menuDecision ? `${menuDecision.placement} level — ${menuDecision.reason}` : "modelOverrides — built-in/API-login-safe model override"}`,
|
|
8331
|
-
`Compat JSON to write:`,
|
|
8332
|
-
keysPreview,
|
|
8333
|
-
``,
|
|
8334
|
-
`⚠️ Risk notice:`,
|
|
8335
|
-
menuScopeRiskLine,
|
|
8336
|
-
` 2. A timestamped backup will be written to: ${backupPath}`,
|
|
8337
|
-
` 3. You must restart Pi / run /reload for the change to take effect.`,
|
|
8338
|
-
` 4. If the file contains comments, verify the result after write.`,
|
|
8339
|
-
``,
|
|
8340
|
-
`Apply these changes?`,
|
|
8341
|
-
];
|
|
8342
|
-
|
|
8343
|
-
const confirmed = await cmdCtx.ui.confirm("Cache Optimizer — Fix", previewLines.join("\n"));
|
|
8344
|
-
if (!confirmed) {
|
|
8345
|
-
cmdCtx.ui.notify("No changes were made. Canceled by user.", "info");
|
|
8346
|
-
return;
|
|
8347
|
-
}
|
|
8348
|
-
|
|
8349
|
-
try {
|
|
8350
|
-
await copyFile(MODELS_JSON_PATH, backupPath);
|
|
8351
|
-
const tempPath = `${MODELS_JSON_PATH}.${process.pid}.${Date.now()}.fix.tmp`;
|
|
8352
|
-
await writeFile(tempPath, modifiedText, "utf8");
|
|
8353
|
-
await rename(tempPath, MODELS_JSON_PATH);
|
|
8354
|
-
|
|
8355
|
-
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
8356
|
-
const postCheck = location && menuDecision
|
|
8357
|
-
? selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement)
|
|
8358
|
-
: selfCheckMissingEntryInsertion(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
8359
|
-
if (postCheck !== null) {
|
|
8360
|
-
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
8361
|
-
cmdCtx.ui.notify(`❌ Post-write check failed: ${postCheck}\nBackup restored.`, "error");
|
|
8362
|
-
return;
|
|
8363
|
-
}
|
|
8364
|
-
|
|
8365
|
-
cmdCtx.ui.notify(
|
|
8366
|
-
`✅ Fix applied to ${getModelsJsonDisplayPath()}.` +
|
|
8367
|
-
`\nBackup: ${backupPath}` +
|
|
8368
|
-
`\nRun /reload or restart Pi for the change to take effect.`,
|
|
8369
|
-
"info",
|
|
8370
|
-
);
|
|
8371
|
-
} catch (writeError) {
|
|
8372
|
-
cmdCtx.ui.notify(
|
|
8373
|
-
`❌ Write failed: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
8374
|
-
"error",
|
|
8375
|
-
);
|
|
8376
|
-
}
|
|
8370
|
+
await handleCacheOptimizerCommand("fix", cmdCtx);
|
|
8377
8371
|
} else if (choice === menuOptions[6]) {
|
|
8378
8372
|
const modeOptions = ["total — Daily cumulative totals (default)", "session — Current Pi conversation session", "process — Current Pi process only", "Cancel"];
|
|
8379
8373
|
const modeChoice = await cmdCtx.ui.select("Footer cache stats mode", modeOptions);
|
|
@@ -8384,42 +8378,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
8384
8378
|
: modeChoice === modeOptions[2]
|
|
8385
8379
|
? "process"
|
|
8386
8380
|
: undefined;
|
|
8387
|
-
if (nextMode) {
|
|
8388
|
-
try {
|
|
8389
|
-
await writePersistedFooterMode(nextMode);
|
|
8390
|
-
persistedFooterStatsMode = nextMode;
|
|
8391
|
-
lastStatusText = undefined;
|
|
8392
|
-
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
8393
|
-
cmdCtx.ui.notify(
|
|
8394
|
-
`✅ Footer mode set to ${nextMode}. Persistent config overrides ${FOOTER_MODE_ENV}.`,
|
|
8395
|
-
"info",
|
|
8396
|
-
);
|
|
8397
|
-
} catch (error) {
|
|
8398
|
-
cmdCtx.ui.notify(
|
|
8399
|
-
`❌ Could not update footer mode config: ${error instanceof Error ? error.message : String(error)}`,
|
|
8400
|
-
"error",
|
|
8401
|
-
);
|
|
8402
|
-
}
|
|
8403
|
-
}
|
|
8381
|
+
if (nextMode) await handleCacheOptimizerCommand(`config footer-mode ${nextMode}`, cmdCtx);
|
|
8404
8382
|
} else if (choice === menuOptions[7]) {
|
|
8405
|
-
|
|
8406
|
-
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
8407
|
-
} else {
|
|
8408
|
-
const adapter = selectAdapterForModel(model);
|
|
8409
|
-
if (!adapter) {
|
|
8410
|
-
cmdCtx.ui.notify("ℹ️ Active model does not match a cache adapter. No stats to reset.", "info");
|
|
8411
|
-
} else {
|
|
8412
|
-
const displayKey = modelKey(model);
|
|
8413
|
-
resetStatsForModel(model);
|
|
8414
|
-
await flushPersistCacheStats(cmdCtx as unknown as ExtensionContext);
|
|
8415
|
-
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
8416
|
-
cmdCtx.ui.notify(
|
|
8417
|
-
`✅ Reset local footer cache stats for "${displayKey}". ` +
|
|
8418
|
-
"Upstream provider prompt cache was not modified.",
|
|
8419
|
-
"info",
|
|
8420
|
-
);
|
|
8421
|
-
}
|
|
8422
|
-
}
|
|
8383
|
+
await handleCacheOptimizerCommand("reset", cmdCtx);
|
|
8423
8384
|
}
|
|
8424
8385
|
// choice === "cancel" or undefined → no action
|
|
8425
8386
|
return;
|
|
@@ -8459,6 +8420,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
8459
8420
|
}
|
|
8460
8421
|
cmdCtx.ui.notify(diagnosis.join("\n"), "info");
|
|
8461
8422
|
}
|
|
8462
|
-
|
|
8423
|
+
}
|
|
8424
|
+
|
|
8425
|
+
pi.registerCommand("cache-optimizer", {
|
|
8426
|
+
description: "Configure and diagnose Pi cache behavior",
|
|
8427
|
+
getArgumentCompletions: getCacheOptimizerArgumentCompletions,
|
|
8428
|
+
handler: handleCacheOptimizerCommand,
|
|
8463
8429
|
});
|
|
8464
8430
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cache-optimizer",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.3",
|
|
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",
|
|
@@ -28,20 +28,20 @@
|
|
|
28
28
|
"#extension": "./index.ts"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"test": "node --import jiti/register tests
|
|
31
|
+
"test": "node --import jiti/register --test tests/*.test.ts",
|
|
32
32
|
"typecheck": "tsc --noEmit --pretty false",
|
|
33
33
|
"check:diff": "git diff --check",
|
|
34
34
|
"check:pack": "npm pack --dry-run --cache node_modules/.cache/npm",
|
|
35
35
|
"check": "npm run typecheck && npm test && npm run check:diff && npm run check:pack"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@earendil-works/pi-coding-agent": "^0.84.
|
|
38
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
39
39
|
"@types/node": "^22.0.0",
|
|
40
40
|
"jiti": "^2.7.0",
|
|
41
41
|
"typescript": "^5.8.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@earendil-works/pi-coding-agent": "
|
|
44
|
+
"@earendil-works/pi-coding-agent": ">=0.82.0"
|
|
45
45
|
},
|
|
46
46
|
"repository": {
|
|
47
47
|
"type": "git",
|