pi-cache-optimizer 2.8.2 → 2.8.4
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 +24 -18
- package/README.zh-CN.md +24 -17
- package/index.ts +915 -435
- package/package.json +4 -4
package/index.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, copyFile, lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants, readFileSync, watch } 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
|
|
|
@@ -95,7 +102,19 @@ const STATUS_KEY = "pi-cache-stats";
|
|
|
95
102
|
const STATE_DIR = getAgentDir();
|
|
96
103
|
const STATE_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-stats.json");
|
|
97
104
|
const LEGACY_STATE_FILE_PATH = join(STATE_DIR, "deepseek-cache-optimizer-stats.json");
|
|
105
|
+
const SHARD_STATE_DIR = join(STATE_DIR, "pi-cache-optimizer-stats.d");
|
|
106
|
+
const SHARD_FILES_DIR = join(SHARD_STATE_DIR, "shards");
|
|
107
|
+
const SHARD_EPOCH_DIR = join(SHARD_STATE_DIR, "epochs");
|
|
108
|
+
const SHARD_MODEL_EPOCH_DIR = join(SHARD_EPOCH_DIR, "models");
|
|
109
|
+
const SHARD_MAINTENANCE_DIR = join(SHARD_STATE_DIR, "maintenance");
|
|
110
|
+
const SHARD_GLOBAL_EPOCH_PATH = join(SHARD_EPOCH_DIR, "global.json");
|
|
111
|
+
const SHARD_CLEANUP_LOCK_PATH = join(SHARD_MAINTENANCE_DIR, "cleanup.lock");
|
|
112
|
+
const SHARD_CLEANUP_MARKER_PATH = join(SHARD_MAINTENANCE_DIR, "last-cleanup");
|
|
98
113
|
const CONFIG_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-config.json");
|
|
114
|
+
const SHARD_RETENTION_MS = 48 * 60 * 60 * 1000;
|
|
115
|
+
const SHARD_TEMP_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
116
|
+
const SHARD_CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
117
|
+
const SHARD_CLEANUP_LOCK_STALE_MS = 60 * 60 * 1000;
|
|
99
118
|
const CACHE_PROVIDER_IDS: CacheProviderId[] = ["deepseek", "openai", "claude", "gemini"];
|
|
100
119
|
const OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_OPENAI_CACHE_KEY";
|
|
101
120
|
const NO_OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_NO_OPENAI_CACHE_KEY";
|
|
@@ -372,6 +391,46 @@ type OptimizedSystemPrompt = {
|
|
|
372
391
|
* Contains only numeric counters and booleans — never message content, prompts,
|
|
373
392
|
* payloads, headers, API keys, or model outputs.
|
|
374
393
|
*/
|
|
394
|
+
type PersistedStatsShardV7 = {
|
|
395
|
+
version: 7;
|
|
396
|
+
kind: "pi-cache-optimizer-shard";
|
|
397
|
+
instanceId: string;
|
|
398
|
+
sessionHash: string;
|
|
399
|
+
process: {
|
|
400
|
+
pid: number;
|
|
401
|
+
ppid: number;
|
|
402
|
+
instanceStartedAt: number;
|
|
403
|
+
};
|
|
404
|
+
lifecycle: {
|
|
405
|
+
state: "active" | "closed";
|
|
406
|
+
createdAt: number;
|
|
407
|
+
updatedAt: number;
|
|
408
|
+
closedAt?: number;
|
|
409
|
+
};
|
|
410
|
+
day: string;
|
|
411
|
+
globalEpoch: string;
|
|
412
|
+
models: Record<string, {
|
|
413
|
+
modelEpoch: string;
|
|
414
|
+
provider: string;
|
|
415
|
+
modelId: string;
|
|
416
|
+
modelName?: string;
|
|
417
|
+
api?: string;
|
|
418
|
+
stats: CacheStats;
|
|
419
|
+
}>;
|
|
420
|
+
lastRoutedModel?: PersistedRoutedModelRef;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
type ShardAggregate = {
|
|
424
|
+
bySession: Record<string, Record<string, CacheStats>>;
|
|
425
|
+
totalsByModel: Record<string, CacheStats>;
|
|
426
|
+
instancesBySession: Record<string, number>;
|
|
427
|
+
instancesBySessionModel: Record<string, Record<string, number>>;
|
|
428
|
+
sessionsByModel: Record<string, number>;
|
|
429
|
+
instancesByModel: Record<string, number>;
|
|
430
|
+
modelRefsByKey: Record<string, PersistedRoutedModelRef>;
|
|
431
|
+
lastRoutedModelBySession: Record<string, PersistedRoutedModelRef>;
|
|
432
|
+
};
|
|
433
|
+
|
|
375
434
|
type CacheUsageSample = {
|
|
376
435
|
timestamp: number;
|
|
377
436
|
hit: boolean;
|
|
@@ -1164,6 +1223,7 @@ const CACHE_OPTIMIZER_COMMANDS = [
|
|
|
1164
1223
|
] as const;
|
|
1165
1224
|
const CACHE_OPTIMIZER_CONFIG_ARGUMENTS = ["footer-mode"] as const;
|
|
1166
1225
|
const CACHE_OPTIMIZER_FOOTER_MODES = ["total", "session", "process"] as const;
|
|
1226
|
+
const CACHE_OPTIMIZER_STATS_ARGUMENTS = ["all", "contributors"] as const;
|
|
1167
1227
|
|
|
1168
1228
|
function filterCommandCompletionItems(
|
|
1169
1229
|
values: readonly string[],
|
|
@@ -1202,9 +1262,18 @@ function getCacheOptimizerArgumentCompletions(argumentPrefix: string): CommandCo
|
|
|
1202
1262
|
if (subcommandPrefix === "config") {
|
|
1203
1263
|
return filterCommandCompletionItems(CACHE_OPTIMIZER_CONFIG_ARGUMENTS, "", "config");
|
|
1204
1264
|
}
|
|
1265
|
+
if (subcommandPrefix === "stats") {
|
|
1266
|
+
return filterCommandCompletionItems(CACHE_OPTIMIZER_STATS_ARGUMENTS, "", "stats");
|
|
1267
|
+
}
|
|
1205
1268
|
return filterCommandCompletionItems(CACHE_OPTIMIZER_COMMANDS, parts[0]);
|
|
1206
1269
|
}
|
|
1207
1270
|
|
|
1271
|
+
if (parts[0].toLowerCase() === "stats") {
|
|
1272
|
+
return parts.length === 2
|
|
1273
|
+
? filterCommandCompletionItems(CACHE_OPTIMIZER_STATS_ARGUMENTS, parts[1], "stats")
|
|
1274
|
+
: null;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1208
1277
|
if (parts[0].toLowerCase() !== "config") return null;
|
|
1209
1278
|
|
|
1210
1279
|
if (parts.length === 2) {
|
|
@@ -1260,7 +1329,7 @@ function resolveFooterStatsMode(
|
|
|
1260
1329
|
): { mode: FooterStatsMode; source: FooterStatsModeSource } {
|
|
1261
1330
|
if (configuredMode) return { mode: configuredMode, source: "config" };
|
|
1262
1331
|
const envMode = parseFooterStatsMode(env[FOOTER_MODE_ENV]);
|
|
1263
|
-
return envMode ? { mode: envMode, source: "env" } : { mode: "
|
|
1332
|
+
return envMode ? { mode: envMode, source: "env" } : { mode: "session", source: "default" };
|
|
1264
1333
|
}
|
|
1265
1334
|
|
|
1266
1335
|
function footerStatsMode(
|
|
@@ -2088,7 +2157,7 @@ function isAyaLikeAssistantMessage(message: unknown, model: PiModel | undefined)
|
|
|
2088
2157
|
|
|
2089
2158
|
// ── Model key ──────────────────────────────────────────────────────
|
|
2090
2159
|
|
|
2091
|
-
function modelKey(model:
|
|
2160
|
+
function modelKey(model: ModelIdentity): string {
|
|
2092
2161
|
return `${model.provider}/${model.id}`;
|
|
2093
2162
|
}
|
|
2094
2163
|
|
|
@@ -2461,12 +2530,8 @@ function getPromptCacheRetentionUnsupportedHint(): string {
|
|
|
2461
2530
|
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
2531
|
}
|
|
2463
2532
|
|
|
2464
|
-
function
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
const normalized = Object.entries(headers)
|
|
2468
|
-
.map(([key, value]) => `${lower(key)}: ${lower(value)}`)
|
|
2469
|
-
.join("\n");
|
|
2533
|
+
function hasPromptCacheRetentionUnsupportedText(value: unknown): boolean {
|
|
2534
|
+
const normalized = lower(value);
|
|
2470
2535
|
if (!normalized.includes("prompt_cache_retention")) return false;
|
|
2471
2536
|
|
|
2472
2537
|
return [
|
|
@@ -2478,10 +2543,24 @@ function hasPromptCacheRetentionUnsupportedSignal(headers: Record<string, string
|
|
|
2478
2543
|
"extra inputs",
|
|
2479
2544
|
"not permitted",
|
|
2480
2545
|
"unrecognized",
|
|
2481
|
-
"bad request",
|
|
2482
2546
|
].some((needle) => normalized.includes(needle));
|
|
2483
2547
|
}
|
|
2484
2548
|
|
|
2549
|
+
function hasPromptCacheRetentionUnsupportedSignal(headers: Record<string, string> | undefined): boolean {
|
|
2550
|
+
if (!headers) return false;
|
|
2551
|
+
return hasPromptCacheRetentionUnsupportedText(
|
|
2552
|
+
Object.entries(headers)
|
|
2553
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
2554
|
+
.join("\n"),
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
function hasPromptCacheRetentionUnsupportedErrorMessage(message: unknown): boolean {
|
|
2559
|
+
const record = getAssistantRecord(message);
|
|
2560
|
+
return record?.stopReason === "error" &&
|
|
2561
|
+
hasPromptCacheRetentionUnsupportedText(record.errorMessage);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2485
2564
|
type CompatAdvicePlacement = {
|
|
2486
2565
|
providerLabel?: string;
|
|
2487
2566
|
modelId?: string;
|
|
@@ -3823,6 +3902,87 @@ function formatRecentTrendSummary(samples: CacheUsageSample[], maxCount: number)
|
|
|
3823
3902
|
/**
|
|
3824
3903
|
* Build the output for `/cache-optimizer stats`.
|
|
3825
3904
|
*/
|
|
3905
|
+
function formatCompactStats(stats: CacheStats): string {
|
|
3906
|
+
const percent = stats.totalInputTokens > 0
|
|
3907
|
+
? (stats.cachedInputTokens / stats.totalInputTokens) * 100
|
|
3908
|
+
: 0;
|
|
3909
|
+
return `${stats.hitRequests}/${stats.totalRequests}·${formatTokenCount(stats.cachedInputTokens)}/${formatTokenCount(stats.totalInputTokens)} ${percent.toFixed(1)}%`;
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
function modelFromStatsKey(key: string, ref?: PersistedRoutedModelRef): PiModel | undefined {
|
|
3913
|
+
const slash = key.indexOf("/");
|
|
3914
|
+
if (slash <= 0 || slash >= key.length - 1) return undefined;
|
|
3915
|
+
return routedModelRefToPiModel(ref ?? { provider: key.slice(0, slash), id: key.slice(slash + 1) });
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
function sortStatsEntries(entries: Array<[string, CacheStats]>, activeModelKey?: string): Array<[string, CacheStats]> {
|
|
3919
|
+
return entries.sort(([left], [right]) => {
|
|
3920
|
+
if (left === activeModelKey) return -1;
|
|
3921
|
+
if (right === activeModelKey) return 1;
|
|
3922
|
+
return left.localeCompare(right);
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
function buildSessionStatsOutput(
|
|
3927
|
+
sessionModels: Record<string, CacheStats>,
|
|
3928
|
+
activeModel?: PiModel,
|
|
3929
|
+
modelRefsByKey: Record<string, PersistedRoutedModelRef> = {},
|
|
3930
|
+
): string {
|
|
3931
|
+
const activeKey = activeModel ? modelKey(activeModel) : undefined;
|
|
3932
|
+
const models = { ...sessionModels };
|
|
3933
|
+
if (activeModel && selectAdapterForModel(activeModel) && !models[activeKey!]) {
|
|
3934
|
+
models[activeKey!] = emptyCacheStats();
|
|
3935
|
+
}
|
|
3936
|
+
const entries = sortStatsEntries(Object.entries(models), activeKey);
|
|
3937
|
+
if (entries.length === 0) return "ℹ️ No cache statistics recorded for the current session today.";
|
|
3938
|
+
const lines = ["Scope: current session", `Day: ${currentLocalDay()}`, `Models: ${entries.length}`];
|
|
3939
|
+
for (const [key, stats] of entries) {
|
|
3940
|
+
const model = modelFromStatsKey(key, modelRefsByKey[key]);
|
|
3941
|
+
const adapter = model ? selectAdapterForModel(model) : undefined;
|
|
3942
|
+
lines.push("", `── ${key} ──`, `Adapter: ${adapter?.label ?? "Unknown cache adapter"}`);
|
|
3943
|
+
lines.push(`Requests: ${stats.hitRequests} hit / ${stats.totalRequests} total`);
|
|
3944
|
+
lines.push(`Cached tokens: ${formatTokenCount(stats.cachedInputTokens)} / ${formatTokenCount(stats.totalInputTokens)} input · ${stats.totalInputTokens > 0 ? `${((stats.cachedInputTokens / stats.totalInputTokens) * 100).toFixed(1)}%` : "0.0%"}`);
|
|
3945
|
+
lines.push(`Summary: ${formatCompactStats(stats)}`);
|
|
3946
|
+
if (stats.cacheWriteInputTokens > 0) lines.push(`Cache write: ${formatTokenCount(stats.cacheWriteInputTokens)}`);
|
|
3947
|
+
}
|
|
3948
|
+
return lines.join("\n");
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
function buildAllStatsOutput(aggregate: ShardAggregate): string {
|
|
3952
|
+
const entries = sortStatsEntries(Object.entries(aggregate.totalsByModel));
|
|
3953
|
+
if (entries.length === 0) return "ℹ️ No local cache statistics recorded today.";
|
|
3954
|
+
const sessionCount = Object.keys(aggregate.instancesBySession).length;
|
|
3955
|
+
const instanceCount = Object.values(aggregate.instancesBySession).reduce((sum, value) => sum + value, 0);
|
|
3956
|
+
const lines = ["Scope: all local sessions", `Day: ${currentLocalDay()}`, `Sessions: ${sessionCount}`, `Instances: ${instanceCount}`];
|
|
3957
|
+
for (const [key, stats] of entries) {
|
|
3958
|
+
const model = modelFromStatsKey(key, aggregate.modelRefsByKey[key]);
|
|
3959
|
+
const adapter = model ? selectAdapterForModel(model) : undefined;
|
|
3960
|
+
lines.push("", `── ${key} ──`, `Adapter: ${adapter?.label ?? "Unknown cache adapter"}`);
|
|
3961
|
+
lines.push(`Sessions: ${aggregate.sessionsByModel[key] ?? 0} · Instances: ${aggregate.instancesByModel[key] ?? 0}`);
|
|
3962
|
+
lines.push(`Requests: ${stats.hitRequests} hit / ${stats.totalRequests} total`);
|
|
3963
|
+
lines.push(`Cached tokens: ${formatTokenCount(stats.cachedInputTokens)} / ${formatTokenCount(stats.totalInputTokens)} input · ${stats.totalInputTokens > 0 ? `${((stats.cachedInputTokens / stats.totalInputTokens) * 100).toFixed(1)}%` : "0.0%"}`);
|
|
3964
|
+
lines.push(`Summary: ${formatCompactStats(stats)}`);
|
|
3965
|
+
if (stats.cacheWriteInputTokens > 0) lines.push(`Cache write: ${formatTokenCount(stats.cacheWriteInputTokens)}`);
|
|
3966
|
+
}
|
|
3967
|
+
return lines.join("\n");
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
function buildContributorsStatsOutput(aggregate: ShardAggregate, activeModel: PiModel | undefined, currentSessionHash?: string): string {
|
|
3971
|
+
if (!activeModel) return "ℹ️ No active model selected.";
|
|
3972
|
+
const key = modelKey(activeModel);
|
|
3973
|
+
const rows = Object.entries(aggregate.bySession)
|
|
3974
|
+
.filter(([, models]) => models[key])
|
|
3975
|
+
.sort(([left], [right]) => left === currentSessionHash ? -1 : right === currentSessionHash ? 1 : left.localeCompare(right));
|
|
3976
|
+
if (rows.length === 0) return `ℹ️ No contributors recorded for ${key} today.`;
|
|
3977
|
+
const lines = [`Model: ${key}`, "Scope: contributing local sessions today"];
|
|
3978
|
+
let otherIndex = 0;
|
|
3979
|
+
for (const [sessionHash, models] of rows) {
|
|
3980
|
+
const label = sessionHash === currentSessionHash ? "Current session" : `Other session ${++otherIndex}`;
|
|
3981
|
+
lines.push("", label, ` Instances: ${aggregate.instancesBySessionModel[sessionHash]?.[key] ?? 0}`, ` ${formatCompactStats(models[key])}`);
|
|
3982
|
+
}
|
|
3983
|
+
return lines.join("\n");
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3826
3986
|
function buildStatsOutput(model: PiModel | undefined, adapter: CacheProviderAdapter | undefined, stats: CacheStats | undefined, recentSamples: CacheUsageSample[]): string {
|
|
3827
3987
|
const lines: string[] = [];
|
|
3828
3988
|
|
|
@@ -4231,8 +4391,8 @@ function filterRestorableStatsForSession(
|
|
|
4231
4391
|
* `restoreCacheStats` can migrate them on the next load before the session
|
|
4232
4392
|
* id is known.
|
|
4233
4393
|
*
|
|
4234
|
-
*
|
|
4235
|
-
*
|
|
4394
|
+
* Historical v1-v6 pure helper (no runtime I/O). Permanent migration tests
|
|
4395
|
+
* exercise it directly; v7 startup never reads or writes the obsolete file.
|
|
4236
4396
|
*/
|
|
4237
4397
|
function mergeCacheSessions(
|
|
4238
4398
|
existingSessions: Record<string, Record<string, CacheStats>>,
|
|
@@ -4392,7 +4552,304 @@ async function writePersistedCacheStats(
|
|
|
4392
4552
|
await rename(tempPath, STATE_FILE_PATH);
|
|
4393
4553
|
}
|
|
4394
4554
|
|
|
4555
|
+
function modelEpochPath(modelKeyValue: string): string {
|
|
4556
|
+
return join(SHARD_MODEL_EPOCH_DIR, `${createHash("sha256").update(modelKeyValue).digest("hex")}.json`);
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
function initialEpoch(scope: string): string {
|
|
4560
|
+
return `initial:${scope}`;
|
|
4561
|
+
}
|
|
4395
4562
|
|
|
4563
|
+
function parseEpochRecord(value: unknown): string | undefined {
|
|
4564
|
+
const record = asRecord(value);
|
|
4565
|
+
return record?.version === 1 && isNonEmptyString(record.epoch) ? record.epoch.trim() : undefined;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
async function readEpochFile(path: string, fallback: string): Promise<string> {
|
|
4569
|
+
try {
|
|
4570
|
+
return parseEpochRecord(JSON.parse(await readFile(path, "utf8"))) ?? fallback;
|
|
4571
|
+
} catch {
|
|
4572
|
+
return fallback;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
async function readGlobalStatsEpoch(): Promise<string> {
|
|
4577
|
+
return readEpochFile(SHARD_GLOBAL_EPOCH_PATH, initialEpoch("global"));
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
async function readModelStatsEpoch(modelKeyValue: string): Promise<string> {
|
|
4581
|
+
return readEpochFile(modelEpochPath(modelKeyValue), initialEpoch(`model:${modelKeyValue}`));
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
async function writeStatsEpoch(path: string, epoch: string): Promise<void> {
|
|
4585
|
+
await mkdir(dirname(path), { recursive: true });
|
|
4586
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
4587
|
+
await writeFile(tempPath, JSON.stringify({ version: 1, epoch, createdAt: Date.now() }, null, 2) + "\n", "utf8");
|
|
4588
|
+
await rename(tempPath, path);
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
async function advanceGlobalStatsEpoch(): Promise<string> {
|
|
4592
|
+
const epoch = `${Date.now()}-${randomUUID()}`;
|
|
4593
|
+
await writeStatsEpoch(SHARD_GLOBAL_EPOCH_PATH, epoch);
|
|
4594
|
+
return epoch;
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
async function advanceModelStatsEpoch(modelKeyValue: string): Promise<string> {
|
|
4598
|
+
const epoch = `${Date.now()}-${randomUUID()}`;
|
|
4599
|
+
await writeStatsEpoch(modelEpochPath(modelKeyValue), epoch);
|
|
4600
|
+
return epoch;
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
function parsePersistedStatsShardV7(value: unknown): PersistedStatsShardV7 | undefined {
|
|
4604
|
+
const record = asRecord(value);
|
|
4605
|
+
const processRecord = asRecord(record?.process);
|
|
4606
|
+
const lifecycle = asRecord(record?.lifecycle);
|
|
4607
|
+
const rawModels = asRecord(record?.models);
|
|
4608
|
+
if (
|
|
4609
|
+
record?.version !== 7 ||
|
|
4610
|
+
record.kind !== "pi-cache-optimizer-shard" ||
|
|
4611
|
+
!isNonEmptyString(record.instanceId) ||
|
|
4612
|
+
!isNonEmptyString(record.sessionHash) ||
|
|
4613
|
+
!processRecord ||
|
|
4614
|
+
!Number.isInteger(processRecord.pid) ||
|
|
4615
|
+
!Number.isInteger(processRecord.ppid) ||
|
|
4616
|
+
typeof processRecord.instanceStartedAt !== "number" ||
|
|
4617
|
+
!lifecycle ||
|
|
4618
|
+
(lifecycle.state !== "active" && lifecycle.state !== "closed") ||
|
|
4619
|
+
typeof lifecycle.createdAt !== "number" ||
|
|
4620
|
+
typeof lifecycle.updatedAt !== "number" ||
|
|
4621
|
+
!isNonEmptyString(record.day) ||
|
|
4622
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(record.day) ||
|
|
4623
|
+
!isNonEmptyString(record.globalEpoch) ||
|
|
4624
|
+
!rawModels
|
|
4625
|
+
) return undefined;
|
|
4626
|
+
|
|
4627
|
+
const models: PersistedStatsShardV7["models"] = {};
|
|
4628
|
+
for (const [key, rawEntry] of Object.entries(rawModels)) {
|
|
4629
|
+
const entry = asRecord(rawEntry);
|
|
4630
|
+
const stats = parseCacheStats(entry?.stats);
|
|
4631
|
+
if (
|
|
4632
|
+
!entry || !stats || !isNonEmptyString(entry.modelEpoch) ||
|
|
4633
|
+
!isNonEmptyString(entry.provider) || !isNonEmptyString(entry.modelId) ||
|
|
4634
|
+
key !== `${entry.provider.trim()}/${entry.modelId.trim()}`
|
|
4635
|
+
) continue;
|
|
4636
|
+
models[key] = {
|
|
4637
|
+
modelEpoch: entry.modelEpoch.trim(),
|
|
4638
|
+
provider: entry.provider.trim(),
|
|
4639
|
+
modelId: entry.modelId.trim(),
|
|
4640
|
+
...(isNonEmptyString(entry.modelName) ? { modelName: entry.modelName.trim() } : {}),
|
|
4641
|
+
...(isNonEmptyString(entry.api) ? { api: entry.api.trim() } : {}),
|
|
4642
|
+
stats,
|
|
4643
|
+
};
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
const lastRoutedModel = parsePersistedRoutedModelRef(record.lastRoutedModel);
|
|
4647
|
+
return {
|
|
4648
|
+
version: 7,
|
|
4649
|
+
kind: "pi-cache-optimizer-shard",
|
|
4650
|
+
instanceId: record.instanceId.trim(),
|
|
4651
|
+
sessionHash: record.sessionHash.trim(),
|
|
4652
|
+
process: {
|
|
4653
|
+
pid: Number(processRecord.pid),
|
|
4654
|
+
ppid: Number(processRecord.ppid),
|
|
4655
|
+
instanceStartedAt: processRecord.instanceStartedAt,
|
|
4656
|
+
},
|
|
4657
|
+
lifecycle: {
|
|
4658
|
+
state: lifecycle.state,
|
|
4659
|
+
createdAt: lifecycle.createdAt,
|
|
4660
|
+
updatedAt: lifecycle.updatedAt,
|
|
4661
|
+
...(typeof lifecycle.closedAt === "number" ? { closedAt: lifecycle.closedAt } : {}),
|
|
4662
|
+
},
|
|
4663
|
+
day: record.day,
|
|
4664
|
+
globalEpoch: record.globalEpoch.trim(),
|
|
4665
|
+
models,
|
|
4666
|
+
...(lastRoutedModel ? { lastRoutedModel } : {}),
|
|
4667
|
+
};
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4670
|
+
async function writeStatsShardV7(path: string, shard: PersistedStatsShardV7): Promise<void> {
|
|
4671
|
+
await mkdir(dirname(path), { recursive: true });
|
|
4672
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
4673
|
+
await writeFile(tempPath, JSON.stringify(shard, null, 2) + "\n", "utf8");
|
|
4674
|
+
await rename(tempPath, path);
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
async function readValidStatsShardsV7(directory: string = SHARD_FILES_DIR): Promise<PersistedStatsShardV7[]> {
|
|
4678
|
+
let names: string[];
|
|
4679
|
+
try {
|
|
4680
|
+
names = await readdir(directory);
|
|
4681
|
+
} catch (error) {
|
|
4682
|
+
if (getErrorCode(error) === "ENOENT") return [];
|
|
4683
|
+
throw error;
|
|
4684
|
+
}
|
|
4685
|
+
const shards: PersistedStatsShardV7[] = [];
|
|
4686
|
+
for (const name of names) {
|
|
4687
|
+
if (!/^[0-9a-f-]+\.json$/i.test(name)) continue;
|
|
4688
|
+
const path = join(directory, name);
|
|
4689
|
+
try {
|
|
4690
|
+
const info = await lstat(path);
|
|
4691
|
+
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
4692
|
+
const parsed = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
|
|
4693
|
+
const filenameInstanceId = name.slice(0, -".json".length);
|
|
4694
|
+
if (parsed && parsed.instanceId === filenameInstanceId) shards.push(parsed);
|
|
4695
|
+
} catch {
|
|
4696
|
+
// Ignore malformed or transiently unavailable shards. Atomic writers will
|
|
4697
|
+
// publish a complete replacement on the next successful update.
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
return shards;
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
async function aggregateStatsShardsV7(
|
|
4704
|
+
shards: PersistedStatsShardV7[],
|
|
4705
|
+
day: string = currentLocalDay(),
|
|
4706
|
+
): Promise<ShardAggregate> {
|
|
4707
|
+
const globalEpoch = await readGlobalStatsEpoch();
|
|
4708
|
+
const modelEpochs = new Map<string, string>();
|
|
4709
|
+
const result: ShardAggregate = {
|
|
4710
|
+
bySession: {},
|
|
4711
|
+
totalsByModel: {},
|
|
4712
|
+
instancesBySession: {},
|
|
4713
|
+
instancesBySessionModel: {},
|
|
4714
|
+
sessionsByModel: {},
|
|
4715
|
+
instancesByModel: {},
|
|
4716
|
+
modelRefsByKey: {},
|
|
4717
|
+
lastRoutedModelBySession: {},
|
|
4718
|
+
};
|
|
4719
|
+
const routedUpdatedAt = new Map<string, number>();
|
|
4720
|
+
const modelRefUpdatedAt = new Map<string, number>();
|
|
4721
|
+
const modelSessions = new Map<string, Set<string>>();
|
|
4722
|
+
|
|
4723
|
+
for (const shard of shards) {
|
|
4724
|
+
if (shard.day !== day || shard.globalEpoch !== globalEpoch) continue;
|
|
4725
|
+
let contributed = false;
|
|
4726
|
+
if (shard.lastRoutedModel && (routedUpdatedAt.get(shard.sessionHash) ?? -1) < shard.lifecycle.updatedAt) {
|
|
4727
|
+
routedUpdatedAt.set(shard.sessionHash, shard.lifecycle.updatedAt);
|
|
4728
|
+
result.lastRoutedModelBySession[shard.sessionHash] = shard.lastRoutedModel;
|
|
4729
|
+
}
|
|
4730
|
+
for (const [key, entry] of Object.entries(shard.models)) {
|
|
4731
|
+
let epoch = modelEpochs.get(key);
|
|
4732
|
+
if (!epoch) {
|
|
4733
|
+
epoch = await readModelStatsEpoch(key);
|
|
4734
|
+
modelEpochs.set(key, epoch);
|
|
4735
|
+
}
|
|
4736
|
+
if (entry.modelEpoch !== epoch || entry.stats.day !== day) continue;
|
|
4737
|
+
contributed = true;
|
|
4738
|
+
const sessionModels = result.bySession[shard.sessionHash] ??= {};
|
|
4739
|
+
sessionModels[key] = mergeCacheStatsForTotal(sessionModels[key], entry.stats);
|
|
4740
|
+
result.totalsByModel[key] = mergeCacheStatsForTotal(result.totalsByModel[key], entry.stats);
|
|
4741
|
+
result.instancesByModel[key] = (result.instancesByModel[key] ?? 0) + 1;
|
|
4742
|
+
if ((modelRefUpdatedAt.get(key) ?? -1) < shard.lifecycle.updatedAt) {
|
|
4743
|
+
modelRefUpdatedAt.set(key, shard.lifecycle.updatedAt);
|
|
4744
|
+
result.modelRefsByKey[key] = {
|
|
4745
|
+
provider: entry.provider,
|
|
4746
|
+
id: entry.modelId,
|
|
4747
|
+
name: entry.modelName ?? entry.modelId,
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
const sessionInstances = result.instancesBySessionModel[shard.sessionHash] ??= {};
|
|
4751
|
+
sessionInstances[key] = (sessionInstances[key] ?? 0) + 1;
|
|
4752
|
+
const sessions = modelSessions.get(key) ?? new Set<string>();
|
|
4753
|
+
sessions.add(shard.sessionHash);
|
|
4754
|
+
modelSessions.set(key, sessions);
|
|
4755
|
+
}
|
|
4756
|
+
if (contributed) {
|
|
4757
|
+
result.instancesBySession[shard.sessionHash] = (result.instancesBySession[shard.sessionHash] ?? 0) + 1;
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
for (const [key, sessions] of modelSessions) result.sessionsByModel[key] = sessions.size;
|
|
4761
|
+
return result;
|
|
4762
|
+
}
|
|
4763
|
+
|
|
4764
|
+
async function loadStatsShardAggregateV7(directory: string = SHARD_FILES_DIR): Promise<ShardAggregate> {
|
|
4765
|
+
return aggregateStatsShardsV7(await readValidStatsShardsV7(directory));
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
function isProcessAlive(pid: number): boolean {
|
|
4769
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
4770
|
+
try {
|
|
4771
|
+
process.kill(pid, 0);
|
|
4772
|
+
return true;
|
|
4773
|
+
} catch (error) {
|
|
4774
|
+
return getErrorCode(error) === "EPERM";
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
async function removeLegacyStatsFiles(): Promise<void> {
|
|
4779
|
+
for (const path of [STATE_FILE_PATH, LEGACY_STATE_FILE_PATH]) {
|
|
4780
|
+
try {
|
|
4781
|
+
await unlink(path);
|
|
4782
|
+
} catch (error) {
|
|
4783
|
+
if (getErrorCode(error) !== "ENOENT") console.warn(`${LOG_PREFIX}: failed to remove obsolete stats file ${path}`, error);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
async function cleanupStatsShardsV7(now = Date.now(), directory: string = SHARD_FILES_DIR): Promise<number> {
|
|
4789
|
+
let names: string[];
|
|
4790
|
+
try {
|
|
4791
|
+
names = await readdir(directory);
|
|
4792
|
+
} catch (error) {
|
|
4793
|
+
return getErrorCode(error) === "ENOENT" ? 0 : Promise.reject(error);
|
|
4794
|
+
}
|
|
4795
|
+
const today = currentLocalDay();
|
|
4796
|
+
let removed = 0;
|
|
4797
|
+
for (const name of names) {
|
|
4798
|
+
const isShard = /^[0-9a-f-]+\.json$/i.test(name);
|
|
4799
|
+
const isTemp = name.endsWith(".tmp");
|
|
4800
|
+
if (!isShard && !isTemp) continue;
|
|
4801
|
+
const path = join(directory, name);
|
|
4802
|
+
try {
|
|
4803
|
+
const info = await lstat(path);
|
|
4804
|
+
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
4805
|
+
if (isTemp) {
|
|
4806
|
+
if (now - info.mtimeMs < SHARD_TEMP_RETENTION_MS) continue;
|
|
4807
|
+
} else {
|
|
4808
|
+
const parsed = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
|
|
4809
|
+
if (parsed?.day === today) continue;
|
|
4810
|
+
const updatedAt = parsed?.lifecycle.updatedAt ?? info.mtimeMs;
|
|
4811
|
+
if (now - updatedAt < SHARD_RETENTION_MS) continue;
|
|
4812
|
+
if (parsed?.lifecycle.state === "active" && isProcessAlive(parsed.process.pid)) continue;
|
|
4813
|
+
}
|
|
4814
|
+
await unlink(path);
|
|
4815
|
+
removed += 1;
|
|
4816
|
+
} catch {
|
|
4817
|
+
// Best-effort maintenance must never block the extension.
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
return removed;
|
|
4821
|
+
}
|
|
4822
|
+
|
|
4823
|
+
async function maybeCleanupStatsShardsV7(now = Date.now()): Promise<void> {
|
|
4824
|
+
await mkdir(SHARD_MAINTENANCE_DIR, { recursive: true });
|
|
4825
|
+
try {
|
|
4826
|
+
const marker = await stat(SHARD_CLEANUP_MARKER_PATH);
|
|
4827
|
+
if (now - marker.mtimeMs < SHARD_CLEANUP_INTERVAL_MS) return;
|
|
4828
|
+
} catch {}
|
|
4829
|
+
|
|
4830
|
+
try {
|
|
4831
|
+
await mkdir(SHARD_CLEANUP_LOCK_PATH);
|
|
4832
|
+
} catch (error) {
|
|
4833
|
+
if (getErrorCode(error) !== "EEXIST") return;
|
|
4834
|
+
try {
|
|
4835
|
+
const lock = await stat(SHARD_CLEANUP_LOCK_PATH);
|
|
4836
|
+
if (now - lock.mtimeMs <= SHARD_CLEANUP_LOCK_STALE_MS) return;
|
|
4837
|
+
await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
|
|
4838
|
+
await mkdir(SHARD_CLEANUP_LOCK_PATH);
|
|
4839
|
+
} catch {
|
|
4840
|
+
return;
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
try {
|
|
4845
|
+
await cleanupStatsShardsV7(now);
|
|
4846
|
+
const tempPath = `${SHARD_CLEANUP_MARKER_PATH}.${process.pid}.${randomUUID()}.tmp`;
|
|
4847
|
+
await writeFile(tempPath, `${now}\n`, "utf8");
|
|
4848
|
+
await rename(tempPath, SHARD_CLEANUP_MARKER_PATH);
|
|
4849
|
+
} finally {
|
|
4850
|
+
await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4396
4853
|
|
|
4397
4854
|
function isCompatCheckApplicable(model: PiModel): boolean {
|
|
4398
4855
|
return isOpenAICompatibleProxyApi(model.api) && !isOfficialOpenAIBaseUrl(model) && !isPiBuiltInLlamaCppModel(model);
|
|
@@ -4405,6 +4862,16 @@ function isPromptCacheRetention400Applicable(model: PiModel): boolean {
|
|
|
4405
4862
|
getCompat(model).supportsLongCacheRetention === true;
|
|
4406
4863
|
}
|
|
4407
4864
|
|
|
4865
|
+
function isExplicitPromptCacheRetentionUnsupportedApplicable(model: PiModel): boolean {
|
|
4866
|
+
// A finalized assistant error with an explicit unsupported-parameter signal
|
|
4867
|
+
// proves that prompt_cache_retention reached this provider/model. Do not
|
|
4868
|
+
// require compat inherited from the active fallback model: router shells may
|
|
4869
|
+
// have no upstream compat metadata when no live routing registry is present.
|
|
4870
|
+
return isOpenAICompatibleApi(model.api) &&
|
|
4871
|
+
!isOfficialOpenAIBaseUrl(model) &&
|
|
4872
|
+
!isPiBuiltInLlamaCppModel(model);
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4408
4875
|
/**
|
|
4409
4876
|
* Whether the 403 sendSessionAffinityHeaders diagnostic applies to a model.
|
|
4410
4877
|
*
|
|
@@ -6528,18 +6995,121 @@ function formatCompatKeysForInsertion(compatKeys: Record<string, unknown>): stri
|
|
|
6528
6995
|
}
|
|
6529
6996
|
|
|
6530
6997
|
/**
|
|
6531
|
-
* Generate
|
|
6532
|
-
*
|
|
6998
|
+
* Generate a unique UTC timestamp component for backup filenames.
|
|
6999
|
+
* Milliseconds, process id, and an in-process sequence prevent collisions.
|
|
6533
7000
|
*/
|
|
6534
|
-
|
|
6535
|
-
|
|
7001
|
+
let backupSequence = 0;
|
|
7002
|
+
|
|
7003
|
+
function backupTimestamp(now: Date = new Date()): string {
|
|
6536
7004
|
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
|
-
|
|
7005
|
+
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
7006
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
7007
|
+
const h = String(now.getUTCHours()).padStart(2, "0");
|
|
7008
|
+
const min = String(now.getUTCMinutes()).padStart(2, "0");
|
|
7009
|
+
const s = String(now.getUTCSeconds()).padStart(2, "0");
|
|
7010
|
+
const ms = String(now.getUTCMilliseconds()).padStart(3, "0");
|
|
7011
|
+
return `${y}${m}${d}T${h}${min}${s}${ms}Z-${process.pid}-${backupSequence++}`;
|
|
7012
|
+
}
|
|
7013
|
+
|
|
7014
|
+
function uniqueTempPath(targetPath: string, purpose: string): string {
|
|
7015
|
+
return `${targetPath}.${process.pid}.${Date.now()}.${backupSequence++}.${purpose}.tmp`;
|
|
7016
|
+
}
|
|
7017
|
+
|
|
7018
|
+
async function atomicReplaceTextFilePreservingMode(
|
|
7019
|
+
targetPath: string,
|
|
7020
|
+
content: string,
|
|
7021
|
+
mode: number,
|
|
7022
|
+
purpose: string,
|
|
7023
|
+
): Promise<void> {
|
|
7024
|
+
const tempPath = uniqueTempPath(targetPath, purpose);
|
|
7025
|
+
try {
|
|
7026
|
+
await writeFile(tempPath, content, { encoding: "utf8", mode });
|
|
7027
|
+
await chmod(tempPath, mode);
|
|
7028
|
+
await rename(tempPath, targetPath);
|
|
7029
|
+
} catch (error) {
|
|
7030
|
+
try {
|
|
7031
|
+
await unlink(tempPath);
|
|
7032
|
+
} catch (cleanupError) {
|
|
7033
|
+
if (getErrorCode(cleanupError) !== "ENOENT") {
|
|
7034
|
+
console.warn(`${LOG_PREFIX}: failed to remove temporary models.json file`, cleanupError);
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
throw error;
|
|
7038
|
+
}
|
|
7039
|
+
}
|
|
7040
|
+
|
|
7041
|
+
async function atomicRestoreFileFromBackup(
|
|
7042
|
+
backupPath: string,
|
|
7043
|
+
targetPath: string,
|
|
7044
|
+
mode: number,
|
|
7045
|
+
): Promise<void> {
|
|
7046
|
+
const tempPath = uniqueTempPath(targetPath, "restore");
|
|
7047
|
+
try {
|
|
7048
|
+
await copyFile(backupPath, tempPath, fsConstants.COPYFILE_EXCL);
|
|
7049
|
+
await chmod(tempPath, mode);
|
|
7050
|
+
await rename(tempPath, targetPath);
|
|
7051
|
+
} catch (error) {
|
|
7052
|
+
try {
|
|
7053
|
+
await unlink(tempPath);
|
|
7054
|
+
} catch (cleanupError) {
|
|
7055
|
+
if (getErrorCode(cleanupError) !== "ENOENT") {
|
|
7056
|
+
console.warn(`${LOG_PREFIX}: failed to remove temporary restore file`, cleanupError);
|
|
7057
|
+
}
|
|
7058
|
+
}
|
|
7059
|
+
throw error;
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
|
|
7063
|
+
type ModelsJsonFixTransactionResult =
|
|
7064
|
+
| { ok: true }
|
|
7065
|
+
| { ok: false; postCheckError: string };
|
|
7066
|
+
|
|
7067
|
+
async function applyModelsJsonFixTransaction(
|
|
7068
|
+
modifiedText: string,
|
|
7069
|
+
backupPath: string,
|
|
7070
|
+
validateWrittenText: (writtenText: string) => string | null,
|
|
7071
|
+
): Promise<ModelsJsonFixTransactionResult> {
|
|
7072
|
+
const originalMode = (await stat(MODELS_JSON_PATH)).mode & 0o7777;
|
|
7073
|
+
await copyFile(MODELS_JSON_PATH, backupPath, fsConstants.COPYFILE_EXCL);
|
|
7074
|
+
await chmod(backupPath, originalMode);
|
|
7075
|
+
|
|
7076
|
+
let targetReplaced = false;
|
|
7077
|
+
try {
|
|
7078
|
+
await atomicReplaceTextFilePreservingMode(
|
|
7079
|
+
MODELS_JSON_PATH,
|
|
7080
|
+
modifiedText,
|
|
7081
|
+
originalMode,
|
|
7082
|
+
"fix",
|
|
7083
|
+
);
|
|
7084
|
+
targetReplaced = true;
|
|
7085
|
+
|
|
7086
|
+
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
7087
|
+
const postCheckError = validateWrittenText(writtenText);
|
|
7088
|
+
const writtenMode = (await stat(MODELS_JSON_PATH)).mode & 0o7777;
|
|
7089
|
+
const effectiveError = postCheckError ?? (
|
|
7090
|
+
writtenMode === originalMode
|
|
7091
|
+
? null
|
|
7092
|
+
: `models.json access mode changed from ${originalMode.toString(8)} to ${writtenMode.toString(8)}`
|
|
7093
|
+
);
|
|
7094
|
+
if (effectiveError !== null) {
|
|
7095
|
+
await atomicRestoreFileFromBackup(backupPath, MODELS_JSON_PATH, originalMode);
|
|
7096
|
+
return { ok: false, postCheckError: effectiveError };
|
|
7097
|
+
}
|
|
7098
|
+
|
|
7099
|
+
return { ok: true };
|
|
7100
|
+
} catch (error) {
|
|
7101
|
+
if (targetReplaced) {
|
|
7102
|
+
try {
|
|
7103
|
+
await atomicRestoreFileFromBackup(backupPath, MODELS_JSON_PATH, originalMode);
|
|
7104
|
+
} catch (restoreError) {
|
|
7105
|
+
throw new AggregateError(
|
|
7106
|
+
[error, restoreError],
|
|
7107
|
+
"models.json fix failed and the atomic backup restore also failed",
|
|
7108
|
+
);
|
|
7109
|
+
}
|
|
7110
|
+
}
|
|
7111
|
+
throw error;
|
|
7112
|
+
}
|
|
6543
7113
|
}
|
|
6544
7114
|
|
|
6545
7115
|
// Internal helpers exported only so the task verification script
|
|
@@ -6593,6 +7163,7 @@ export const __internals_for_tests = {
|
|
|
6593
7163
|
isSessionAffinity403Applicable,
|
|
6594
7164
|
isOpenAISdkHeader403Applicable,
|
|
6595
7165
|
hasPromptCacheRetentionUnsupportedSignal,
|
|
7166
|
+
hasPromptCacheRetentionUnsupportedErrorMessage,
|
|
6596
7167
|
// Non-GPT OpenAI-compatible model detection
|
|
6597
7168
|
isKimiLikeModel,
|
|
6598
7169
|
isKimiLikeAssistantMessage,
|
|
@@ -6745,6 +7316,10 @@ export const __internals_for_tests = {
|
|
|
6745
7316
|
// Recent sample / stats output / diagnosis helpers
|
|
6746
7317
|
MAX_RECENT_SAMPLES,
|
|
6747
7318
|
buildStatsOutput,
|
|
7319
|
+
buildSessionStatsOutput,
|
|
7320
|
+
buildAllStatsOutput,
|
|
7321
|
+
buildContributorsStatsOutput,
|
|
7322
|
+
formatCompactStats,
|
|
6748
7323
|
buildLowHitDiagnosis,
|
|
6749
7324
|
formatRecentTrendSummary,
|
|
6750
7325
|
formatHitRatio,
|
|
@@ -6792,8 +7367,23 @@ export const __internals_for_tests = {
|
|
|
6792
7367
|
createSerializedAsyncRunner,
|
|
6793
7368
|
writePersistedCacheStats,
|
|
6794
7369
|
readPersistedCacheStats,
|
|
7370
|
+
parsePersistedStatsShardV7,
|
|
7371
|
+
writeStatsShardV7,
|
|
7372
|
+
readValidStatsShardsV7,
|
|
7373
|
+
aggregateStatsShardsV7,
|
|
7374
|
+
loadStatsShardAggregateV7,
|
|
7375
|
+
cleanupStatsShardsV7,
|
|
7376
|
+
removeLegacyStatsFiles,
|
|
7377
|
+
readGlobalStatsEpoch,
|
|
7378
|
+
readModelStatsEpoch,
|
|
7379
|
+
advanceGlobalStatsEpoch,
|
|
7380
|
+
advanceModelStatsEpoch,
|
|
7381
|
+
modelEpochPath,
|
|
6795
7382
|
STATE_FILE_PATH,
|
|
6796
7383
|
LEGACY_STATE_FILE_PATH,
|
|
7384
|
+
SHARD_STATE_DIR,
|
|
7385
|
+
SHARD_FILES_DIR,
|
|
7386
|
+
SHARD_GLOBAL_EPOCH_PATH,
|
|
6797
7387
|
STATE_DIR,
|
|
6798
7388
|
// JSONC surgical edit helpers
|
|
6799
7389
|
MODELS_JSON_PATH,
|
|
@@ -6816,6 +7406,9 @@ export const __internals_for_tests = {
|
|
|
6816
7406
|
deepEqualIgnoringKeys,
|
|
6817
7407
|
formatCompatKeysForInsertion,
|
|
6818
7408
|
backupTimestamp,
|
|
7409
|
+
atomicReplaceTextFilePreservingMode,
|
|
7410
|
+
atomicRestoreFileFromBackup,
|
|
7411
|
+
applyModelsJsonFixTransaction,
|
|
6819
7412
|
// Fix suggestion builder
|
|
6820
7413
|
buildFixSuggestion,
|
|
6821
7414
|
// Adaptive thinking compat helpers
|
|
@@ -6847,17 +7440,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
6847
7440
|
let lastStatusText: string | undefined;
|
|
6848
7441
|
let persistenceWarningShown = false;
|
|
6849
7442
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
7443
|
+
let shardRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
7444
|
+
let shardWatcher: ReturnType<typeof watch> | undefined;
|
|
6850
7445
|
const enqueuePersist = createSerializedAsyncRunner();
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
const
|
|
7446
|
+
const instanceId = randomUUID();
|
|
7447
|
+
const instanceStartedAt = Date.now();
|
|
7448
|
+
const instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
|
|
7449
|
+
const modelApiByKey = new Map<string, string>();
|
|
7450
|
+
const modelNameByKey = new Map<string, string>();
|
|
7451
|
+
const modelEpochByKey = new Map<string, string>();
|
|
7452
|
+
let currentGlobalEpoch = initialEpoch("global");
|
|
6854
7453
|
let integrityNotificationShown = false;
|
|
6855
7454
|
let currentSessionId = "";
|
|
6856
7455
|
let currentSessionHash = "";
|
|
6857
7456
|
let currentSessionHashSet = false;
|
|
6858
7457
|
let lastActualRoutedModel: PersistedRoutedModelRef | undefined;
|
|
6859
7458
|
let latestCacheHint: PiCacheHintSnapshot | undefined;
|
|
7459
|
+
let shardCreatedAt = Date.now();
|
|
6860
7460
|
const PERSIST_DEBOUNCE_MS = 2000;
|
|
7461
|
+
const SHARD_REFRESH_DEBOUNCE_MS = 250;
|
|
6861
7462
|
|
|
6862
7463
|
function buildObservedRuntimeFixSuggestion(model: PiModel): FixSuggestion | undefined {
|
|
6863
7464
|
const key = modelKey(model);
|
|
@@ -6972,17 +7573,102 @@ export default function (pi: ExtensionAPI) {
|
|
|
6972
7573
|
recentSamplesByModelKey.clear();
|
|
6973
7574
|
}
|
|
6974
7575
|
|
|
6975
|
-
function
|
|
7576
|
+
async function refreshShardAggregate(): Promise<ShardAggregate> {
|
|
7577
|
+
const persistedShards = await readValidStatsShardsV7();
|
|
7578
|
+
const shards = persistedShards.filter((shard) => shard.instanceId !== instanceId);
|
|
7579
|
+
if (currentSessionHashSet) shards.push(buildCurrentStatsShard());
|
|
7580
|
+
const aggregate = await aggregateStatsShardsV7(shards);
|
|
7581
|
+
|
|
7582
|
+
// A reset from another process can advance epochs while this instance is
|
|
7583
|
+
// idle. Adopt those epochs during every aggregate refresh, not only when a
|
|
7584
|
+
// later message_end arrives, so this instance cannot keep an old in-memory
|
|
7585
|
+
// process bucket visible in `process` mode or rewrite stale counters during
|
|
7586
|
+
// shutdown. Model-scoped resets clear only the affected local bucket.
|
|
7587
|
+
const latestGlobalEpoch = await readGlobalStatsEpoch();
|
|
7588
|
+
if (currentGlobalEpoch !== latestGlobalEpoch) {
|
|
7589
|
+
currentGlobalEpoch = latestGlobalEpoch;
|
|
7590
|
+
cacheStatsProcessByModel = {};
|
|
7591
|
+
modelEpochByKey.clear();
|
|
7592
|
+
modelApiByKey.clear();
|
|
7593
|
+
modelNameByKey.clear();
|
|
7594
|
+
lastActualRoutedModel = undefined;
|
|
7595
|
+
} else {
|
|
7596
|
+
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7597
|
+
const latestModelEpoch = await readModelStatsEpoch(key);
|
|
7598
|
+
if ((modelEpochByKey.get(key) ?? initialEpoch(`model:${key}`)) === latestModelEpoch) continue;
|
|
7599
|
+
delete cacheStatsProcessByModel[key];
|
|
7600
|
+
modelEpochByKey.set(key, latestModelEpoch);
|
|
7601
|
+
modelApiByKey.delete(key);
|
|
7602
|
+
modelNameByKey.delete(key);
|
|
7603
|
+
}
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
cacheStatsByModel = {};
|
|
7607
|
+
for (const [sessionHash, models] of Object.entries(aggregate.bySession)) {
|
|
7608
|
+
for (const [key, stats] of Object.entries(models)) {
|
|
7609
|
+
cacheStatsByModel[`${sessionHash}:${key}`] = stats;
|
|
7610
|
+
}
|
|
7611
|
+
}
|
|
7612
|
+
cacheStatsTotalsByModel = aggregate.totalsByModel;
|
|
7613
|
+
if (currentSessionHashSet) {
|
|
7614
|
+
lastActualRoutedModel = aggregate.lastRoutedModelBySession[currentSessionHash];
|
|
7615
|
+
}
|
|
7616
|
+
lastStatusText = undefined;
|
|
7617
|
+
return aggregate;
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
async function ensureCurrentEpochs(): Promise<void> {
|
|
7621
|
+
currentGlobalEpoch = await readGlobalStatsEpoch();
|
|
7622
|
+
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7623
|
+
modelEpochByKey.set(key, await readModelStatsEpoch(key));
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7626
|
+
|
|
7627
|
+
function buildCurrentStatsShard(state: "active" | "closed" = "active"): PersistedStatsShardV7 {
|
|
7628
|
+
const now = Date.now();
|
|
7629
|
+
const models: PersistedStatsShardV7["models"] = {};
|
|
7630
|
+
for (const [key, stats] of Object.entries(cacheStatsProcessByModel)) {
|
|
7631
|
+
const slash = key.indexOf("/");
|
|
7632
|
+
if (slash <= 0 || slash >= key.length - 1) continue;
|
|
7633
|
+
models[key] = {
|
|
7634
|
+
modelEpoch: modelEpochByKey.get(key) ?? initialEpoch(`model:${key}`),
|
|
7635
|
+
provider: key.slice(0, slash),
|
|
7636
|
+
modelId: key.slice(slash + 1),
|
|
7637
|
+
...(modelNameByKey.get(key) ? { modelName: modelNameByKey.get(key) } : {}),
|
|
7638
|
+
...(modelApiByKey.get(key) ? { api: modelApiByKey.get(key) } : {}),
|
|
7639
|
+
stats: cloneCacheStats(stats),
|
|
7640
|
+
};
|
|
7641
|
+
}
|
|
6976
7642
|
return {
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
7643
|
+
version: 7,
|
|
7644
|
+
kind: "pi-cache-optimizer-shard",
|
|
7645
|
+
instanceId,
|
|
7646
|
+
sessionHash: currentSessionHash || "_nosession",
|
|
7647
|
+
process: { pid: process.pid, ppid: process.ppid, instanceStartedAt },
|
|
7648
|
+
lifecycle: {
|
|
7649
|
+
state,
|
|
7650
|
+
createdAt: shardCreatedAt,
|
|
7651
|
+
updatedAt: now,
|
|
7652
|
+
...(state === "closed" ? { closedAt: now } : {}),
|
|
7653
|
+
},
|
|
7654
|
+
day: currentLocalDay(),
|
|
7655
|
+
globalEpoch: currentGlobalEpoch,
|
|
7656
|
+
models,
|
|
7657
|
+
...(lastActualRoutedModel ? { lastRoutedModel: { ...lastActualRoutedModel } } : {}),
|
|
6983
7658
|
};
|
|
6984
7659
|
}
|
|
6985
7660
|
|
|
7661
|
+
function scheduleShardRefresh(ctx?: ExtensionContext, model?: PiModel): void {
|
|
7662
|
+
if (shardRefreshTimer !== null) clearTimeout(shardRefreshTimer);
|
|
7663
|
+
shardRefreshTimer = setTimeout(() => {
|
|
7664
|
+
shardRefreshTimer = null;
|
|
7665
|
+
void refreshShardAggregate()
|
|
7666
|
+
.then(() => publishStatus(ctx as ExtensionContext, model ?? ctx?.model))
|
|
7667
|
+
.catch(() => undefined);
|
|
7668
|
+
}, SHARD_REFRESH_DEBOUNCE_MS);
|
|
7669
|
+
shardRefreshTimer.unref?.();
|
|
7670
|
+
}
|
|
7671
|
+
|
|
6986
7672
|
/** Look up visible cumulative stats for a model, falling back to legacy family. */
|
|
6987
7673
|
function getStatsForModel(model: PiModel | undefined, adapter: CacheProviderAdapter): CacheStats {
|
|
6988
7674
|
if (model) {
|
|
@@ -7032,79 +7718,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
7032
7718
|
return created;
|
|
7033
7719
|
}
|
|
7034
7720
|
|
|
7035
|
-
function resetStatsForModel(model: PiModel): void {
|
|
7721
|
+
async function resetStatsForModel(model: PiModel): Promise<void> {
|
|
7036
7722
|
const displayKey = modelKey(model);
|
|
7723
|
+
const nextEpoch = await advanceModelStatsEpoch(displayKey);
|
|
7724
|
+
modelEpochByKey.set(displayKey, nextEpoch);
|
|
7725
|
+
delete cacheStatsProcessByModel[displayKey];
|
|
7726
|
+
delete cacheStatsTotalsByModel[displayKey];
|
|
7037
7727
|
for (const key of Object.keys(cacheStatsByModel)) {
|
|
7038
7728
|
if (modelKeyFromSessionScoped(key) === displayKey) delete cacheStatsByModel[key];
|
|
7039
7729
|
}
|
|
7040
|
-
delete cacheStatsTotalsByModel[displayKey];
|
|
7041
|
-
delete cacheStatsProcessByModel[displayKey];
|
|
7042
|
-
pendingDeletedTotalModelKeys.add(displayKey);
|
|
7043
7730
|
for (const key of Array.from(recentSamplesByModelKey.keys())) {
|
|
7044
7731
|
if (modelKeyFromSessionScoped(key) === displayKey) recentSamplesByModelKey.delete(key);
|
|
7045
7732
|
}
|
|
7046
7733
|
lastStatusText = undefined;
|
|
7047
7734
|
}
|
|
7048
7735
|
|
|
7049
|
-
function resetCurrentSessionStats(): void {
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
if (key.startsWith(prefix)) delete cacheStatsByModel[key];
|
|
7053
|
-
}
|
|
7054
|
-
cacheStatsProcessByModel = {};
|
|
7736
|
+
async function resetCurrentSessionStats(): Promise<void> {
|
|
7737
|
+
currentGlobalEpoch = await advanceGlobalStatsEpoch();
|
|
7738
|
+
cacheStatsByModel = {};
|
|
7055
7739
|
cacheStatsTotalsByModel = {};
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7740
|
+
cacheStatsProcessByModel = {};
|
|
7741
|
+
modelEpochByKey.clear();
|
|
7742
|
+
modelApiByKey.clear();
|
|
7743
|
+
modelNameByKey.clear();
|
|
7744
|
+
clearRecentSamples();
|
|
7060
7745
|
lastActualRoutedModel = undefined;
|
|
7061
7746
|
lastStatusText = undefined;
|
|
7062
7747
|
}
|
|
7063
7748
|
|
|
7064
|
-
function
|
|
7065
|
-
const
|
|
7066
|
-
const statsByModel = Object.fromEntries(
|
|
7067
|
-
Object.entries(state.statsByModel).map(([key, stats]) => [key, { ...stats }]),
|
|
7068
|
-
);
|
|
7069
|
-
const totalsByModel = Object.fromEntries(
|
|
7070
|
-
Object.entries(state.totalsByModel).map(([key, stats]) => [key, { ...stats }]),
|
|
7071
|
-
);
|
|
7072
|
-
const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
|
|
7073
|
-
for (const id of CACHE_PROVIDER_IDS) {
|
|
7074
|
-
const stats = state.legacyFamily[id];
|
|
7075
|
-
if (stats) legacyFamily[id] = { ...stats };
|
|
7076
|
-
}
|
|
7077
|
-
const lastRoutedModelBySession = state.lastRoutedModelBySession
|
|
7078
|
-
? Object.fromEntries(
|
|
7079
|
-
Object.entries(state.lastRoutedModelBySession).map(([key, model]) => [key, { ...model }]),
|
|
7080
|
-
)
|
|
7081
|
-
: undefined;
|
|
7082
|
-
return { statsByModel, totalsByModel, legacyFamily, lastRoutedModelBySession };
|
|
7083
|
-
}
|
|
7084
|
-
|
|
7085
|
-
function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
7086
|
-
const state = snapshotCacheStatsState();
|
|
7087
|
-
const sessionHash = currentSessionHashSet ? currentSessionHash : undefined;
|
|
7088
|
-
const deleteModelKeys = Array.from(pendingDeletedTotalModelKeys);
|
|
7089
|
-
const replaceTotals = replacePersistedTotalsOnNextWrite;
|
|
7090
|
-
// This queued write owns the current intents. Later mutations can enqueue
|
|
7091
|
-
// independent intents without an earlier completion clearing them.
|
|
7092
|
-
pendingDeletedTotalModelKeys.clear();
|
|
7093
|
-
replacePersistedTotalsOnNextWrite = false;
|
|
7094
|
-
|
|
7749
|
+
function persistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
|
|
7750
|
+
const shard = buildCurrentStatsShard(lifecycleState);
|
|
7095
7751
|
return enqueuePersist(async () => {
|
|
7096
|
-
const recoverFromEarlierFailure = forceReplaceTotalsAfterFailure;
|
|
7097
7752
|
try {
|
|
7098
|
-
await
|
|
7099
|
-
deleteModelKeys,
|
|
7100
|
-
replaceTotals: replaceTotals || recoverFromEarlierFailure,
|
|
7101
|
-
});
|
|
7102
|
-
if (recoverFromEarlierFailure) forceReplaceTotalsAfterFailure = false;
|
|
7753
|
+
await writeStatsShardV7(instanceShardPath, shard);
|
|
7103
7754
|
} catch (error) {
|
|
7104
|
-
|
|
7105
|
-
// reset/delete cannot merge stale persisted counters back into state.
|
|
7106
|
-
forceReplaceTotalsAfterFailure = true;
|
|
7107
|
-
console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
|
|
7755
|
+
console.warn(`${LOG_PREFIX}: failed to persist cache stats shard`, error);
|
|
7108
7756
|
if (!persistenceWarningShown) {
|
|
7109
7757
|
persistenceWarningShown = true;
|
|
7110
7758
|
ctx?.ui.notify(
|
|
@@ -7116,61 +7764,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
7116
7764
|
});
|
|
7117
7765
|
}
|
|
7118
7766
|
|
|
7119
|
-
/** Schedule a debounced persist. Coalesces rapid message_end writes
|
|
7120
|
-
* into a single disk write after PERSIST_DEBOUNCE_MS of silence.
|
|
7121
|
-
* In-memory stats remain instantly up-to-date for the footer; only
|
|
7122
|
-
* the on-disk persistence is delayed. */
|
|
7123
7767
|
function schedulePersistCacheStats(ctx?: ExtensionContext): void {
|
|
7124
7768
|
if (persistTimer !== null) clearTimeout(persistTimer);
|
|
7125
7769
|
persistTimer = setTimeout(() => {
|
|
7126
7770
|
persistTimer = null;
|
|
7127
|
-
persistCacheStats(ctx).catch((
|
|
7128
|
-
console.warn(`${LOG_PREFIX}: debounced persist failed`, err);
|
|
7129
|
-
});
|
|
7771
|
+
void persistCacheStats(ctx).then(() => refreshShardAggregate()).catch(() => undefined);
|
|
7130
7772
|
}, PERSIST_DEBOUNCE_MS);
|
|
7773
|
+
persistTimer.unref?.();
|
|
7131
7774
|
}
|
|
7132
7775
|
|
|
7133
|
-
|
|
7134
|
-
* Used on reload and day-rollover where immediate durability matters. */
|
|
7135
|
-
async function flushPersistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
7776
|
+
async function flushPersistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
|
|
7136
7777
|
if (persistTimer !== null) {
|
|
7137
7778
|
clearTimeout(persistTimer);
|
|
7138
7779
|
persistTimer = null;
|
|
7139
7780
|
}
|
|
7140
|
-
|
|
7781
|
+
// Adopt resets performed by another Pi process before capturing the shard
|
|
7782
|
+
// snapshot. Otherwise shutdown/command flush could rewrite stale local
|
|
7783
|
+
// counters under an old epoch even though aggregation already hid them.
|
|
7784
|
+
await refreshShardAggregate();
|
|
7785
|
+
await persistCacheStats(ctx, lifecycleState);
|
|
7786
|
+
await refreshShardAggregate();
|
|
7141
7787
|
}
|
|
7142
7788
|
|
|
7143
7789
|
async function rollOverStatsIfNeeded(ctx?: ExtensionContext): Promise<void> {
|
|
7144
7790
|
const day = currentLocalDay();
|
|
7145
7791
|
let changed = false;
|
|
7146
|
-
|
|
7147
|
-
// Roll over per-model entries.
|
|
7148
|
-
for (const key of Object.keys(cacheStatsByModel)) {
|
|
7149
|
-
const stats = cacheStatsByModel[key];
|
|
7150
|
-
if (stats && stats.day !== day) {
|
|
7151
|
-
cacheStatsByModel[key] = emptyCacheStats(day);
|
|
7152
|
-
changed = true;
|
|
7153
|
-
}
|
|
7154
|
-
}
|
|
7155
|
-
|
|
7156
|
-
// Roll over cumulative provider/model totals.
|
|
7157
7792
|
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7158
|
-
|
|
7159
|
-
if (stats && stats.day !== day) {
|
|
7793
|
+
if (cacheStatsProcessByModel[key]?.day !== day) {
|
|
7160
7794
|
cacheStatsProcessByModel[key] = emptyCacheStats(day);
|
|
7161
7795
|
changed = true;
|
|
7162
7796
|
}
|
|
7163
7797
|
}
|
|
7164
|
-
|
|
7165
|
-
for (const key of Object.keys(cacheStatsTotalsByModel)) {
|
|
7166
|
-
const stats = cacheStatsTotalsByModel[key];
|
|
7167
|
-
if (stats && stats.day !== day) {
|
|
7168
|
-
cacheStatsTotalsByModel[key] = emptyCacheStats(day);
|
|
7169
|
-
changed = true;
|
|
7170
|
-
}
|
|
7171
|
-
}
|
|
7172
|
-
|
|
7173
|
-
// Roll over legacy family entries.
|
|
7174
7798
|
for (const id of CACHE_PROVIDER_IDS) {
|
|
7175
7799
|
const stats = cacheStatsLegacyFamily[id];
|
|
7176
7800
|
if (stats && stats.day !== day) {
|
|
@@ -7178,57 +7802,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
7178
7802
|
changed = true;
|
|
7179
7803
|
}
|
|
7180
7804
|
}
|
|
7181
|
-
|
|
7182
7805
|
if (changed) {
|
|
7806
|
+
shardCreatedAt = Date.now();
|
|
7183
7807
|
lastStatusText = undefined;
|
|
7184
|
-
await
|
|
7808
|
+
await ensureCurrentEpochs();
|
|
7809
|
+
await flushPersistCacheStats(ctx);
|
|
7185
7810
|
}
|
|
7186
7811
|
}
|
|
7187
7812
|
|
|
7188
7813
|
async function restoreCacheStats(reason: string, ctx: ExtensionContext): Promise<void> {
|
|
7189
7814
|
syncSessionHash(ctx);
|
|
7190
|
-
|
|
7815
|
+
lastStatusText = undefined;
|
|
7816
|
+
cacheStatsProcessByModel = {};
|
|
7817
|
+
cacheStatsLegacyFamily = emptyAllCacheStats();
|
|
7818
|
+
modelEpochByKey.clear();
|
|
7819
|
+
modelApiByKey.clear();
|
|
7820
|
+
modelNameByKey.clear();
|
|
7821
|
+
currentGlobalEpoch = await readGlobalStatsEpoch();
|
|
7191
7822
|
if (reason === "reload") {
|
|
7192
|
-
// /reload: preserve session-scoped stats (same session hash).
|
|
7193
|
-
// Pi extension reload creates a fresh closure, so cacheStatsByModel
|
|
7194
|
-
// starts empty. Read persisted data and filter for current session.
|
|
7195
|
-
lastStatusText = undefined;
|
|
7196
7823
|
lastPromptIntegrityWarningAt = 0;
|
|
7197
7824
|
integrityNotificationShown = false;
|
|
7198
7825
|
clearRecentSamples();
|
|
7199
|
-
|
|
7200
|
-
const persisted = await readPersistedCacheStats();
|
|
7201
|
-
cacheStatsByModel = filterRestorableStatsForSession(
|
|
7202
|
-
persisted,
|
|
7203
|
-
currentSessionHashSet ? currentSessionHash : undefined,
|
|
7204
|
-
);
|
|
7205
|
-
cacheStatsProcessByModel = {};
|
|
7206
|
-
cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
|
|
7207
|
-
cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
|
|
7208
|
-
lastActualRoutedModel = currentSessionHashSet
|
|
7209
|
-
? persisted?.lastRoutedModelBySession?.[currentSessionHash]
|
|
7210
|
-
: undefined;
|
|
7211
|
-
|
|
7212
|
-
await rollOverStatsIfNeeded(ctx);
|
|
7213
|
-
return;
|
|
7214
7826
|
}
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
);
|
|
7224
|
-
cacheStatsProcessByModel = {};
|
|
7225
|
-
cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
|
|
7226
|
-
cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
|
|
7227
|
-
lastActualRoutedModel = currentSessionHashSet
|
|
7228
|
-
? persisted?.lastRoutedModelBySession?.[currentSessionHash]
|
|
7229
|
-
: undefined;
|
|
7230
|
-
lastStatusText = undefined;
|
|
7231
|
-
await rollOverStatsIfNeeded(ctx);
|
|
7827
|
+
await removeLegacyStatsFiles();
|
|
7828
|
+
await mkdir(SHARD_FILES_DIR, { recursive: true });
|
|
7829
|
+
await refreshShardAggregate();
|
|
7830
|
+
await flushPersistCacheStats(ctx);
|
|
7831
|
+
await maybeCleanupStatsShardsV7();
|
|
7832
|
+
// Maintenance may remove expired shard files after the initial refresh.
|
|
7833
|
+
// Re-scan before publishing so startup/footer command state cannot retain
|
|
7834
|
+
// counters from files that no longer exist.
|
|
7835
|
+
await refreshShardAggregate();
|
|
7232
7836
|
}
|
|
7233
7837
|
|
|
7234
7838
|
async function publishStatus(ctx: ExtensionContext, model: PiModel | undefined = ctx.model): Promise<void> {
|
|
@@ -7273,8 +7877,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7273
7877
|
}
|
|
7274
7878
|
|
|
7275
7879
|
if (adapter) {
|
|
7276
|
-
// Footer mode defaults to
|
|
7277
|
-
// current-
|
|
7880
|
+
// Footer mode defaults to current-session provider/model totals. Users may
|
|
7881
|
+
// select all-current-day or current-process counters through persistent
|
|
7882
|
+
// command config or the environment variable.
|
|
7278
7883
|
const stats = displayModel
|
|
7279
7884
|
? selectFooterStatsForModel(mode, sessionHash, cacheStatsByModel, cacheStatsTotalsByModel, displayModel, cacheStatsProcessByModel)
|
|
7280
7885
|
: undefined;
|
|
@@ -7358,13 +7963,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
7358
7963
|
pi.on("session_start", async (event, ctx) => {
|
|
7359
7964
|
if (runtimeOptimizerEnabled) requestLongCacheRetention();
|
|
7360
7965
|
await restoreCacheStats(event.reason, ctx);
|
|
7966
|
+
if (ctx.mode === "tui" && !shardWatcher) {
|
|
7967
|
+
try {
|
|
7968
|
+
shardWatcher = watch(SHARD_FILES_DIR, () => scheduleShardRefresh(ctx));
|
|
7969
|
+
shardWatcher.unref?.();
|
|
7970
|
+
} catch {
|
|
7971
|
+
// Lifecycle and explicit command refreshes remain authoritative.
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7974
|
+
await publishStatus(ctx);
|
|
7975
|
+
});
|
|
7976
|
+
|
|
7977
|
+
pi.on("tool_execution_end", async (_event, ctx) => {
|
|
7978
|
+
await refreshShardAggregate();
|
|
7979
|
+
await publishStatus(ctx);
|
|
7980
|
+
});
|
|
7981
|
+
|
|
7982
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
7983
|
+
await refreshShardAggregate();
|
|
7361
7984
|
await publishStatus(ctx);
|
|
7362
7985
|
});
|
|
7363
7986
|
|
|
7364
7987
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
7365
7988
|
try {
|
|
7366
|
-
await flushPersistCacheStats(ctx);
|
|
7989
|
+
await flushPersistCacheStats(ctx, "closed");
|
|
7367
7990
|
} finally {
|
|
7991
|
+
if (shardRefreshTimer !== null) clearTimeout(shardRefreshTimer);
|
|
7992
|
+
shardRefreshTimer = null;
|
|
7993
|
+
shardWatcher?.close();
|
|
7994
|
+
shardWatcher = undefined;
|
|
7368
7995
|
latestCacheHint = undefined;
|
|
7369
7996
|
delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
|
|
7370
7997
|
uninstallCacheHintsService();
|
|
@@ -7374,6 +8001,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7374
8001
|
|
|
7375
8002
|
pi.on("model_select", async (event, ctx) => {
|
|
7376
8003
|
if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(event.model, ctx) ?? event.model, ctx, warnedModels);
|
|
8004
|
+
await refreshShardAggregate();
|
|
7377
8005
|
await publishStatus(ctx, event.model);
|
|
7378
8006
|
});
|
|
7379
8007
|
|
|
@@ -7615,6 +8243,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
7615
8243
|
syncSessionHash(ctx);
|
|
7616
8244
|
const msgRecord = asRecord(event.message);
|
|
7617
8245
|
|
|
8246
|
+
// Some providers expose an HTTP 400 error body only through the finalized
|
|
8247
|
+
// assistant error message; after_provider_response may contain the status
|
|
8248
|
+
// and no diagnostic response headers. Record the same model-scoped
|
|
8249
|
+
// prompt_cache_retention fallback from that authoritative message identity.
|
|
8250
|
+
if (runtimeOptimizerEnabled && hasPromptCacheRetentionUnsupportedErrorMessage(event.message)) {
|
|
8251
|
+
const fallbackModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
8252
|
+
const messageModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
|
|
8253
|
+
const errorModel = messageModel
|
|
8254
|
+
? findModelInRegistry(ctx.modelRegistry, messageModel.provider, messageModel.id) ?? messageModel
|
|
8255
|
+
: undefined;
|
|
8256
|
+
if (errorModel && isExplicitPromptCacheRetentionUnsupportedApplicable(errorModel)) {
|
|
8257
|
+
const key = modelKey(errorModel);
|
|
8258
|
+
promptCacheRetention400Models.add(key);
|
|
8259
|
+
if (!warnedPromptCacheRetention400Models.has(key)) {
|
|
8260
|
+
warnedPromptCacheRetention400Models.add(key);
|
|
8261
|
+
ctx.ui.notify(
|
|
8262
|
+
`⚠️ ${LOG_PREFIX}: ${key} rejected prompt_cache_retention. ` +
|
|
8263
|
+
getPromptCacheRetentionUnsupportedHint() +
|
|
8264
|
+
` Run /cache-optimizer doctor for the exact edit location.`,
|
|
8265
|
+
"warning",
|
|
8266
|
+
);
|
|
8267
|
+
}
|
|
8268
|
+
}
|
|
8269
|
+
}
|
|
8270
|
+
|
|
7618
8271
|
// Record only Anthropic's explicit mixed-TTL ordering error. This is a
|
|
7619
8272
|
// non-retryable 400 in Pi 0.82.1, so the fallback applies to the next
|
|
7620
8273
|
// subsequent request (and to a retry only if another layer initiates one).
|
|
@@ -7703,6 +8356,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
7703
8356
|
// Update session, process-local, and cumulative buckets for the actual
|
|
7704
8357
|
// routed model. The process bucket is intentionally never persisted.
|
|
7705
8358
|
if (statsModel) {
|
|
8359
|
+
const key = modelKey(statsModel);
|
|
8360
|
+
const latestGlobalEpoch = await readGlobalStatsEpoch();
|
|
8361
|
+
const latestModelEpoch = await readModelStatsEpoch(key);
|
|
8362
|
+
if (currentGlobalEpoch !== latestGlobalEpoch) {
|
|
8363
|
+
currentGlobalEpoch = latestGlobalEpoch;
|
|
8364
|
+
cacheStatsProcessByModel = {};
|
|
8365
|
+
modelEpochByKey.clear();
|
|
8366
|
+
}
|
|
8367
|
+
if (modelEpochByKey.get(key) !== latestModelEpoch) {
|
|
8368
|
+
delete cacheStatsProcessByModel[key];
|
|
8369
|
+
modelEpochByKey.set(key, latestModelEpoch);
|
|
8370
|
+
}
|
|
8371
|
+
modelApiByKey.set(key, statsModel.api ?? "");
|
|
8372
|
+
modelNameByKey.set(key, statsModel.name || statsModel.id);
|
|
7706
8373
|
const sk = sessionModelKey(statsModel);
|
|
7707
8374
|
addUsageToCacheStats(getOrCreateStatsByModelKey(sk), usage);
|
|
7708
8375
|
addUsageToCacheStats(getOrCreateProcessStatsForModel(statsModel), usage);
|
|
@@ -7712,6 +8379,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7712
8379
|
}
|
|
7713
8380
|
|
|
7714
8381
|
schedulePersistCacheStats(ctx);
|
|
8382
|
+
await refreshShardAggregate();
|
|
7715
8383
|
await publishStatus(ctx, statsModel);
|
|
7716
8384
|
});
|
|
7717
8385
|
|
|
@@ -7722,36 +8390,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
7722
8390
|
// disable — disable runtime prompt/cache optimizations for this process
|
|
7723
8391
|
// doctor — show current model/provider/api/baseUrl/compat status
|
|
7724
8392
|
// with low-hit diagnosis
|
|
7725
|
-
// stats — show
|
|
8393
|
+
// stats — show detailed current-session models; `stats all` shows all local shards
|
|
7726
8394
|
// compat — show compat suggestion with file path
|
|
7727
8395
|
// config footer-mode total|session|process — persist footer mode override
|
|
7728
8396
|
// fix — auto-fix compat issues (writes models.json, requires UI)
|
|
7729
8397
|
// reset — reset current provider/model footer stats bucket (local only)
|
|
7730
8398
|
// (no args) — interactive menu (with UI) or help summary
|
|
7731
8399
|
// ────────────────────────────────────────────────────────────────
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
8400
|
+
async function handleCacheOptimizerCommand(
|
|
8401
|
+
args: string,
|
|
8402
|
+
cmdCtx: ExtensionCommandContext,
|
|
8403
|
+
): Promise<void> {
|
|
8404
|
+
syncSessionHash(cmdCtx);
|
|
8405
|
+
const selectedModel = cmdCtx.model;
|
|
8406
|
+
const model = resolveRouteModel(selectedModel, cmdCtx) ?? selectedModel;
|
|
8407
|
+
const commandParts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
8408
|
+
const subcommand = commandParts[0] || "help";
|
|
7741
8409
|
|
|
7742
8410
|
if (subcommand === "enable") {
|
|
7743
8411
|
setRuntimeOptimizerEnabled(true);
|
|
7744
|
-
resetCurrentSessionStats();
|
|
7745
|
-
await flushPersistCacheStats(cmdCtx
|
|
7746
|
-
await publishStatus(cmdCtx
|
|
8412
|
+
await resetCurrentSessionStats();
|
|
8413
|
+
await flushPersistCacheStats(cmdCtx);
|
|
8414
|
+
await publishStatus(cmdCtx, model);
|
|
7747
8415
|
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
|
|
7748
8416
|
} else if (subcommand === "disable") {
|
|
7749
8417
|
setRuntimeOptimizerEnabled(false);
|
|
7750
|
-
resetCurrentSessionStats();
|
|
7751
|
-
await flushPersistCacheStats(cmdCtx
|
|
7752
|
-
await publishStatus(cmdCtx
|
|
8418
|
+
await resetCurrentSessionStats();
|
|
8419
|
+
await flushPersistCacheStats(cmdCtx);
|
|
8420
|
+
await publishStatus(cmdCtx, model);
|
|
7753
8421
|
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
8422
|
} else if (subcommand === "doctor") {
|
|
8423
|
+
await refreshShardAggregate();
|
|
7755
8424
|
if (!model) {
|
|
7756
8425
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
7757
8426
|
return;
|
|
@@ -7767,16 +8436,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7767
8436
|
: diagnosis;
|
|
7768
8437
|
cmdCtx.ui.notify(fullDiagnosis, "info");
|
|
7769
8438
|
} else if (subcommand === "stats") {
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
8439
|
+
const aggregate = await refreshShardAggregate();
|
|
8440
|
+
const statsMode = commandParts[1];
|
|
8441
|
+
if (statsMode === "all") {
|
|
8442
|
+
cmdCtx.ui.notify(buildAllStatsOutput(aggregate), "info");
|
|
8443
|
+
} else if (statsMode === "contributors") {
|
|
8444
|
+
cmdCtx.ui.notify(buildContributorsStatsOutput(aggregate, model, currentSessionHashSet ? currentSessionHash : undefined), "info");
|
|
8445
|
+
} else if (statsMode) {
|
|
8446
|
+
cmdCtx.ui.notify("Usage: /cache-optimizer stats [all|contributors]", "info");
|
|
8447
|
+
} else {
|
|
8448
|
+
const sessionModels = currentSessionHashSet ? aggregate.bySession[currentSessionHash] ?? {} : {};
|
|
8449
|
+
cmdCtx.ui.notify(buildSessionStatsOutput(sessionModels, model, aggregate.modelRefsByKey), "info");
|
|
7773
8450
|
}
|
|
7774
|
-
const adapter = selectAdapterForModel(model);
|
|
7775
|
-
const sk = model ? sessionModelKey(model) : undefined;
|
|
7776
|
-
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
7777
|
-
const samples = sk ? getRecentSamples(sk) : [];
|
|
7778
|
-
const output = buildStatsOutput(model, adapter, statsState, samples);
|
|
7779
|
-
cmdCtx.ui.notify(output, "info");
|
|
7780
8451
|
} else if (subcommand === "config") {
|
|
7781
8452
|
const configKey = commandParts[1];
|
|
7782
8453
|
const requestedMode = commandParts[2];
|
|
@@ -7795,7 +8466,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7795
8466
|
await writePersistedFooterMode(nextMode);
|
|
7796
8467
|
persistedFooterStatsMode = nextMode;
|
|
7797
8468
|
lastStatusText = undefined;
|
|
7798
|
-
await publishStatus(cmdCtx
|
|
8469
|
+
await publishStatus(cmdCtx, model);
|
|
7799
8470
|
const resolved = resolveFooterStatsMode(persistedFooterStatsMode);
|
|
7800
8471
|
cmdCtx.ui.notify(
|
|
7801
8472
|
`✅ Footer mode set to ${resolved.mode}. Persistent config overrides ${FOOTER_MODE_ENV}.`,
|
|
@@ -7839,13 +8510,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
7839
8510
|
// Reset local footer stats for the effective active model. If the
|
|
7840
8511
|
// selected model is a virtual router and the protocol exposes a live
|
|
7841
8512
|
// route, this clears the real upstream bucket, not the router shell.
|
|
7842
|
-
resetStatsForModel(model);
|
|
8513
|
+
await resetStatsForModel(model);
|
|
7843
8514
|
|
|
7844
8515
|
// Persist immediately.
|
|
7845
|
-
await flushPersistCacheStats(cmdCtx
|
|
8516
|
+
await flushPersistCacheStats(cmdCtx);
|
|
7846
8517
|
|
|
7847
8518
|
// Update footer to show 0/0.
|
|
7848
|
-
await publishStatus(cmdCtx
|
|
8519
|
+
await publishStatus(cmdCtx, model);
|
|
7849
8520
|
|
|
7850
8521
|
cmdCtx.ui.notify(
|
|
7851
8522
|
`✅ Reset local footer cache stats for "${displayKey}". ` +
|
|
@@ -7998,20 +8669,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
7998
8669
|
);
|
|
7999
8670
|
if (confirmed) {
|
|
8000
8671
|
try {
|
|
8001
|
-
await
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8672
|
+
const result = await applyModelsJsonFixTransaction(
|
|
8673
|
+
plan.modifiedText,
|
|
8674
|
+
backupPath,
|
|
8675
|
+
(writtenText) => selfCheckMissingEntryInsertion(
|
|
8676
|
+
originalText,
|
|
8677
|
+
writtenText,
|
|
8678
|
+
suggestion.providerLabel,
|
|
8679
|
+
suggestion.modelId,
|
|
8680
|
+
suggestion.compatKeys,
|
|
8681
|
+
),
|
|
8010
8682
|
);
|
|
8011
|
-
if (
|
|
8012
|
-
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
8683
|
+
if ("postCheckError" in result) {
|
|
8013
8684
|
cmdCtx.ui.notify(
|
|
8014
|
-
`❌ Post-write self-check failed: ${
|
|
8685
|
+
`❌ Post-write self-check failed: ${result.postCheckError}\n` +
|
|
8015
8686
|
`The backup at ${backupPath} has been restored. No changes applied.`,
|
|
8016
8687
|
"error",
|
|
8017
8688
|
);
|
|
@@ -8025,7 +8696,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
8025
8696
|
);
|
|
8026
8697
|
} catch (e) {
|
|
8027
8698
|
cmdCtx.ui.notify(
|
|
8028
|
-
`❌ Write failed: ${e instanceof Error ? e.message : String(e)}
|
|
8699
|
+
`❌ Write failed: ${e instanceof Error ? e.message : String(e)}\n` +
|
|
8700
|
+
`Backup may be at: ${backupPath}`,
|
|
8029
8701
|
"error",
|
|
8030
8702
|
);
|
|
8031
8703
|
}
|
|
@@ -8147,22 +8819,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
8147
8819
|
|
|
8148
8820
|
// Write: backup → temp + rename → self-check again
|
|
8149
8821
|
try {
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
8822
|
+
const result = await applyModelsJsonFixTransaction(
|
|
8823
|
+
modifiedText,
|
|
8824
|
+
backupPath,
|
|
8825
|
+
(writtenText) => selfCheckFix(
|
|
8826
|
+
originalText,
|
|
8827
|
+
writtenText,
|
|
8828
|
+
suggestion.providerLabel,
|
|
8829
|
+
suggestion.modelId,
|
|
8830
|
+
suggestion.compatKeys,
|
|
8831
|
+
decision.placement,
|
|
8832
|
+
),
|
|
8833
|
+
);
|
|
8834
|
+
if ("postCheckError" in result) {
|
|
8164
8835
|
cmdCtx.ui.notify(
|
|
8165
|
-
`❌ Post-write self-check failed: ${postCheckError}\n` +
|
|
8836
|
+
`❌ Post-write self-check failed: ${result.postCheckError}\n` +
|
|
8166
8837
|
`The backup at ${backupPath} has been restored. No changes applied.`,
|
|
8167
8838
|
"error",
|
|
8168
8839
|
);
|
|
@@ -8189,7 +8860,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8189
8860
|
"Enable — Turn on runtime optimizations",
|
|
8190
8861
|
"Disable — Turn off runtime optimizations",
|
|
8191
8862
|
"Doctor — Show cache configuration",
|
|
8192
|
-
"Stats — Show
|
|
8863
|
+
"Stats — Show current-session model statistics",
|
|
8193
8864
|
"Compat — Show compat suggestion",
|
|
8194
8865
|
"Fix — Auto-fix compat issues (writes models.json)",
|
|
8195
8866
|
"Footer mode — Choose total, session, or process stats",
|
|
@@ -8198,228 +8869,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
8198
8869
|
];
|
|
8199
8870
|
const choice = await cmdCtx.ui.select("Cache Optimizer", menuOptions);
|
|
8200
8871
|
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");
|
|
8872
|
+
await handleCacheOptimizerCommand("enable", cmdCtx);
|
|
8206
8873
|
} 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");
|
|
8874
|
+
await handleCacheOptimizerCommand("disable", cmdCtx);
|
|
8212
8875
|
} 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
|
-
}
|
|
8876
|
+
await handleCacheOptimizerCommand("doctor", cmdCtx);
|
|
8227
8877
|
} 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
|
-
}
|
|
8878
|
+
await handleCacheOptimizerCommand("stats", cmdCtx);
|
|
8238
8879
|
} 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
|
-
}
|
|
8880
|
+
await handleCacheOptimizerCommand("compat", cmdCtx);
|
|
8254
8881
|
} 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
|
-
}
|
|
8882
|
+
await handleCacheOptimizerCommand("fix", cmdCtx);
|
|
8377
8883
|
} else if (choice === menuOptions[6]) {
|
|
8378
|
-
const modeOptions = ["
|
|
8884
|
+
const modeOptions = ["session — Current Pi conversation session (default)", "total — All local sessions today", "process — Current extension instance only", "Cancel"];
|
|
8379
8885
|
const modeChoice = await cmdCtx.ui.select("Footer cache stats mode", modeOptions);
|
|
8380
8886
|
const nextMode = modeChoice === modeOptions[0]
|
|
8381
|
-
? "
|
|
8887
|
+
? "session"
|
|
8382
8888
|
: modeChoice === modeOptions[1]
|
|
8383
|
-
? "
|
|
8889
|
+
? "total"
|
|
8384
8890
|
: modeChoice === modeOptions[2]
|
|
8385
8891
|
? "process"
|
|
8386
8892
|
: 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
|
-
}
|
|
8893
|
+
if (nextMode) await handleCacheOptimizerCommand(`config footer-mode ${nextMode}`, cmdCtx);
|
|
8404
8894
|
} 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
|
-
}
|
|
8895
|
+
await handleCacheOptimizerCommand("reset", cmdCtx);
|
|
8423
8896
|
}
|
|
8424
8897
|
// choice === "cancel" or undefined → no action
|
|
8425
8898
|
return;
|
|
@@ -8431,7 +8904,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
8431
8904
|
diagnosis.push(" enable — Enable prompt/cache optimizations for this Pi process");
|
|
8432
8905
|
diagnosis.push(" disable — Disable prompt/cache optimizations for this Pi process");
|
|
8433
8906
|
diagnosis.push(" doctor — Show current model/provider/api/baseUrl/compat and low-hit diagnosis");
|
|
8434
|
-
diagnosis.push(" stats — Show
|
|
8907
|
+
diagnosis.push(" stats — Show detailed statistics for every model in the current session");
|
|
8908
|
+
diagnosis.push(" stats all — Show detailed totals for every model across all local sessions");
|
|
8909
|
+
diagnosis.push(" stats contributors — Show per-session contributors for the active model");
|
|
8435
8910
|
diagnosis.push(" compat — Show compat suggestion with edit location");
|
|
8436
8911
|
diagnosis.push(" config footer-mode total|session|process — Persist the footer stats mode");
|
|
8437
8912
|
diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
|
|
@@ -8459,6 +8934,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
8459
8934
|
}
|
|
8460
8935
|
cmdCtx.ui.notify(diagnosis.join("\n"), "info");
|
|
8461
8936
|
}
|
|
8462
|
-
|
|
8937
|
+
}
|
|
8938
|
+
|
|
8939
|
+
pi.registerCommand("cache-optimizer", {
|
|
8940
|
+
description: "Configure and diagnose Pi cache behavior",
|
|
8941
|
+
getArgumentCompletions: getCacheOptimizerArgumentCompletions,
|
|
8942
|
+
handler: handleCacheOptimizerCommand,
|
|
8463
8943
|
});
|
|
8464
8944
|
}
|