pi-cache-optimizer 2.4.0 → 2.4.1
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/index.ts +166 -30
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -102,6 +102,18 @@ type PersistedCacheStatsV2 = {
|
|
|
102
102
|
statsByProvider: Partial<Record<CacheProviderId, CacheStats>>;
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
/** Per-model-key scoped state. Used in memory and for v3 persistence. */
|
|
106
|
+
type CacheStatsState = {
|
|
107
|
+
statsByModel: Record<string, CacheStats>;
|
|
108
|
+
legacyFamily: Partial<Record<CacheProviderId, CacheStats>>;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type PersistedCacheStatsV3 = {
|
|
112
|
+
version: 3;
|
|
113
|
+
statsByModel: Record<string, CacheStats>;
|
|
114
|
+
legacyFamily: Partial<Record<CacheProviderId, CacheStats>>;
|
|
115
|
+
};
|
|
116
|
+
|
|
105
117
|
type UsageSnapshot = {
|
|
106
118
|
cacheRead: number;
|
|
107
119
|
cacheWrite: number;
|
|
@@ -831,7 +843,7 @@ function describeMissingOpenAIFamilyProxyCompat(model: PiModel): string[] {
|
|
|
831
843
|
const missing: string[] = [];
|
|
832
844
|
|
|
833
845
|
if (!isOpenAIFamilyModel(model)) return missing;
|
|
834
|
-
if (model.api !== "openai-completions") return missing;
|
|
846
|
+
if (lower(model.api) !== "openai-completions") return missing;
|
|
835
847
|
if (isOfficialOpenAIBaseUrl(model)) return missing;
|
|
836
848
|
|
|
837
849
|
if (compat.supportsLongCacheRetention !== true) {
|
|
@@ -1061,30 +1073,56 @@ function parseCacheStats(value: unknown): CacheStats | undefined {
|
|
|
1061
1073
|
};
|
|
1062
1074
|
}
|
|
1063
1075
|
|
|
1064
|
-
function parsePersistedCacheStats(value: unknown):
|
|
1076
|
+
function parsePersistedCacheStats(value: unknown): CacheStatsState | undefined {
|
|
1065
1077
|
const record = asRecord(value);
|
|
1066
1078
|
if (!record) return undefined;
|
|
1067
1079
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1080
|
+
// version 3: model-scoped stats + legacy family fallback
|
|
1081
|
+
if (record.version === 3) {
|
|
1082
|
+
const statsByModel: Record<string, CacheStats> = {};
|
|
1083
|
+
const rawModelMap = asRecord(record.statsByModel);
|
|
1084
|
+
if (rawModelMap) {
|
|
1085
|
+
for (const [key, val] of Object.entries(rawModelMap)) {
|
|
1086
|
+
const parsed = parseCacheStats(val);
|
|
1087
|
+
if (parsed) statsByModel[key] = parsed;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1072
1090
|
|
|
1073
|
-
|
|
1091
|
+
const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
|
|
1092
|
+
const rawFamily = asRecord(record.legacyFamily);
|
|
1093
|
+
if (rawFamily) {
|
|
1094
|
+
for (const id of CACHE_PROVIDER_IDS) {
|
|
1095
|
+
const stats = parseCacheStats(rawFamily[id]);
|
|
1096
|
+
if (stats) legacyFamily[id] = stats;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1074
1099
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1100
|
+
return { statsByModel, legacyFamily };
|
|
1101
|
+
}
|
|
1077
1102
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1103
|
+
// version 2: migrate statsByProvider into legacyFamily
|
|
1104
|
+
if (record.version === 2) {
|
|
1105
|
+
const statsByProvider = asRecord(record.statsByProvider);
|
|
1106
|
+
const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
|
|
1107
|
+
if (statsByProvider) {
|
|
1108
|
+
for (const id of CACHE_PROVIDER_IDS) {
|
|
1109
|
+
const stats = parseCacheStats(statsByProvider[id]);
|
|
1110
|
+
if (stats) legacyFamily[id] = stats;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return { statsByModel: {}, legacyFamily };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// version 1: single DeepSeek stats -> migrate to legacyFamily.deepseek
|
|
1117
|
+
if (record.version === 1) {
|
|
1118
|
+
const migrated = parseCacheStats(record.stats);
|
|
1119
|
+
return migrated ? { statsByModel: {}, legacyFamily: { deepseek: migrated } } : undefined;
|
|
1082
1120
|
}
|
|
1083
1121
|
|
|
1084
|
-
return
|
|
1122
|
+
return undefined;
|
|
1085
1123
|
}
|
|
1086
1124
|
|
|
1087
|
-
async function readPersistedCacheStats(): Promise<
|
|
1125
|
+
async function readPersistedCacheStats(): Promise<CacheStatsState | undefined> {
|
|
1088
1126
|
try {
|
|
1089
1127
|
const raw = await readFile(STATE_FILE_PATH, "utf8");
|
|
1090
1128
|
return parsePersistedCacheStats(JSON.parse(raw));
|
|
@@ -1124,9 +1162,13 @@ async function readPersistedCacheStats(): Promise<Partial<Record<CacheProviderId
|
|
|
1124
1162
|
return undefined;
|
|
1125
1163
|
}
|
|
1126
1164
|
|
|
1127
|
-
async function writePersistedCacheStats(
|
|
1165
|
+
async function writePersistedCacheStats(state: CacheStatsState): Promise<void> {
|
|
1128
1166
|
await mkdir(STATE_DIR, { recursive: true });
|
|
1129
|
-
const payload:
|
|
1167
|
+
const payload: PersistedCacheStatsV3 = {
|
|
1168
|
+
version: 3,
|
|
1169
|
+
statsByModel: state.statsByModel,
|
|
1170
|
+
legacyFamily: state.legacyFamily,
|
|
1171
|
+
};
|
|
1130
1172
|
const tempPath = `${STATE_FILE_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
1131
1173
|
|
|
1132
1174
|
await writeFile(tempPath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
@@ -1164,26 +1206,59 @@ export const __internals_for_tests = {
|
|
|
1164
1206
|
getAssistantMessageModelTokenValues,
|
|
1165
1207
|
getCompat,
|
|
1166
1208
|
modelKey,
|
|
1209
|
+
// Cache stats helpers (module-level, usable from verify script)
|
|
1210
|
+
addUsageToCacheStats,
|
|
1211
|
+
formatCacheStats,
|
|
1212
|
+
emptyCacheStats,
|
|
1213
|
+
emptyAllCacheStats,
|
|
1214
|
+
parseCacheStats,
|
|
1215
|
+
parsePersistedCacheStats,
|
|
1167
1216
|
};
|
|
1168
1217
|
|
|
1169
1218
|
export default function (pi: ExtensionAPI) {
|
|
1170
1219
|
const warnedModels = new Set<string>();
|
|
1171
|
-
let
|
|
1220
|
+
let cacheStatsByModel: Record<string, CacheStats> = {};
|
|
1221
|
+
let cacheStatsLegacyFamily: Partial<Record<CacheProviderId, CacheStats>> = emptyAllCacheStats();
|
|
1172
1222
|
let lastStatusText: string | undefined;
|
|
1173
1223
|
let persistenceWarningShown = false;
|
|
1224
|
+
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1225
|
+
const PERSIST_DEBOUNCE_MS = 2000;
|
|
1226
|
+
|
|
1227
|
+
function getCacheStatsState(): CacheStatsState {
|
|
1228
|
+
return { statsByModel: cacheStatsByModel, legacyFamily: cacheStatsLegacyFamily };
|
|
1229
|
+
}
|
|
1174
1230
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1231
|
+
/** Look up active stats for a model, falling back to legacy family. */
|
|
1232
|
+
function getStatsForModel(model: PiModel | undefined, adapter: CacheProviderAdapter): CacheStats {
|
|
1233
|
+
if (model) {
|
|
1234
|
+
const key = modelKey(model);
|
|
1235
|
+
const existing = cacheStatsByModel[key];
|
|
1236
|
+
if (existing) return existing;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Fallback: legacy family bucket — used when model key is unknown
|
|
1240
|
+
// or this model hasn't been seen yet in this session.
|
|
1241
|
+
const family = cacheStatsLegacyFamily[adapter.id];
|
|
1242
|
+
if (family) return family;
|
|
1243
|
+
|
|
1244
|
+
const created = emptyCacheStats();
|
|
1245
|
+
cacheStatsLegacyFamily[adapter.id] = created;
|
|
1246
|
+
return created;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** Get or create a stats entry for the given model key. */
|
|
1250
|
+
function getOrCreateStatsByModelKey(key: string): CacheStats {
|
|
1251
|
+
const existing = cacheStatsByModel[key];
|
|
1177
1252
|
if (existing) return existing;
|
|
1178
1253
|
|
|
1179
1254
|
const created = emptyCacheStats();
|
|
1180
|
-
|
|
1255
|
+
cacheStatsByModel[key] = created;
|
|
1181
1256
|
return created;
|
|
1182
1257
|
}
|
|
1183
1258
|
|
|
1184
1259
|
async function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
1185
1260
|
try {
|
|
1186
|
-
await writePersistedCacheStats(
|
|
1261
|
+
await writePersistedCacheStats(getCacheStatsState());
|
|
1187
1262
|
} catch (error) {
|
|
1188
1263
|
console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
|
|
1189
1264
|
if (!persistenceWarningShown) {
|
|
@@ -1196,14 +1271,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
1196
1271
|
}
|
|
1197
1272
|
}
|
|
1198
1273
|
|
|
1274
|
+
/** Schedule a debounced persist. Coalesces rapid message_end writes
|
|
1275
|
+
* into a single disk write after PERSIST_DEBOUNCE_MS of silence.
|
|
1276
|
+
* In-memory stats remain instantly up-to-date for the footer; only
|
|
1277
|
+
* the on-disk persistence is delayed. */
|
|
1278
|
+
function schedulePersistCacheStats(ctx?: ExtensionContext): void {
|
|
1279
|
+
if (persistTimer !== null) clearTimeout(persistTimer);
|
|
1280
|
+
persistTimer = setTimeout(() => {
|
|
1281
|
+
persistTimer = null;
|
|
1282
|
+
persistCacheStats(ctx).catch((err) => {
|
|
1283
|
+
console.warn(`${LOG_PREFIX}: debounced persist failed`, err);
|
|
1284
|
+
});
|
|
1285
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/** Flush any pending debounced persist immediately (cancels timer + writes).
|
|
1289
|
+
* Used on reload and day-rollover where immediate durability matters. */
|
|
1290
|
+
async function flushPersistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
1291
|
+
if (persistTimer !== null) {
|
|
1292
|
+
clearTimeout(persistTimer);
|
|
1293
|
+
persistTimer = null;
|
|
1294
|
+
}
|
|
1295
|
+
await persistCacheStats(ctx);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1199
1298
|
async function rollOverStatsIfNeeded(ctx?: ExtensionContext): Promise<void> {
|
|
1200
1299
|
const day = currentLocalDay();
|
|
1201
1300
|
let changed = false;
|
|
1202
1301
|
|
|
1302
|
+
// Roll over per-model entries.
|
|
1303
|
+
for (const key of Object.keys(cacheStatsByModel)) {
|
|
1304
|
+
const stats = cacheStatsByModel[key];
|
|
1305
|
+
if (stats && stats.day !== day) {
|
|
1306
|
+
cacheStatsByModel[key] = emptyCacheStats(day);
|
|
1307
|
+
changed = true;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Roll over legacy family entries.
|
|
1203
1312
|
for (const id of CACHE_PROVIDER_IDS) {
|
|
1204
|
-
const stats =
|
|
1313
|
+
const stats = cacheStatsLegacyFamily[id];
|
|
1205
1314
|
if (stats && stats.day !== day) {
|
|
1206
|
-
|
|
1315
|
+
cacheStatsLegacyFamily[id] = emptyCacheStats(day);
|
|
1207
1316
|
changed = true;
|
|
1208
1317
|
}
|
|
1209
1318
|
}
|
|
@@ -1216,13 +1325,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
1216
1325
|
|
|
1217
1326
|
async function restoreCacheStats(reason: string, ctx: ExtensionContext): Promise<void> {
|
|
1218
1327
|
if (reason === "reload") {
|
|
1219
|
-
|
|
1328
|
+
cacheStatsByModel = {};
|
|
1329
|
+
cacheStatsLegacyFamily = emptyAllCacheStats();
|
|
1220
1330
|
lastStatusText = undefined;
|
|
1221
|
-
await
|
|
1331
|
+
await flushPersistCacheStats(ctx);
|
|
1222
1332
|
return;
|
|
1223
1333
|
}
|
|
1224
1334
|
|
|
1225
|
-
|
|
1335
|
+
const persisted = await readPersistedCacheStats();
|
|
1336
|
+
if (persisted) {
|
|
1337
|
+
cacheStatsByModel = persisted.statsByModel;
|
|
1338
|
+
cacheStatsLegacyFamily = persisted.legacyFamily;
|
|
1339
|
+
} else {
|
|
1340
|
+
cacheStatsByModel = {};
|
|
1341
|
+
cacheStatsLegacyFamily = emptyAllCacheStats();
|
|
1342
|
+
}
|
|
1226
1343
|
lastStatusText = undefined;
|
|
1227
1344
|
await rollOverStatsIfNeeded(ctx);
|
|
1228
1345
|
}
|
|
@@ -1231,7 +1348,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1231
1348
|
await rollOverStatsIfNeeded(ctx);
|
|
1232
1349
|
|
|
1233
1350
|
const adapter = selectAdapterForModel(model);
|
|
1234
|
-
let statusText: string | undefined
|
|
1351
|
+
let statusText: string | undefined;
|
|
1352
|
+
if (adapter) {
|
|
1353
|
+
// Display only per-model scoped stats. A model that has never been
|
|
1354
|
+
// used in this session shows 0/0 rather than falling back to legacy
|
|
1355
|
+
// family aggregated stats (which could span different providers with
|
|
1356
|
+
// the same model-family name). The message_end hook populates
|
|
1357
|
+
// cacheStatsByModel[key] on first use with that model.
|
|
1358
|
+
const key = model ? modelKey(model) : undefined;
|
|
1359
|
+
const stats = key ? cacheStatsByModel[key] : undefined;
|
|
1360
|
+
statusText = formatCacheStats(adapter, stats ?? emptyCacheStats());
|
|
1361
|
+
}
|
|
1235
1362
|
|
|
1236
1363
|
// If optimizeSystemPrompt detected structural truncation on this or
|
|
1237
1364
|
// a recent turn, flag it once in the footer so the user knows to
|
|
@@ -1351,8 +1478,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1351
1478
|
if (!usage) return;
|
|
1352
1479
|
|
|
1353
1480
|
await rollOverStatsIfNeeded(ctx);
|
|
1354
|
-
|
|
1355
|
-
|
|
1481
|
+
|
|
1482
|
+
// Update stats scoped to the active model (provider/id key).
|
|
1483
|
+
// Falls back to legacy family when ctx.model is undefined.
|
|
1484
|
+
if (ctx.model) {
|
|
1485
|
+
const key = modelKey(ctx.model);
|
|
1486
|
+
addUsageToCacheStats(getOrCreateStatsByModelKey(key), usage);
|
|
1487
|
+
} else {
|
|
1488
|
+
addUsageToCacheStats(getStatsForModel(undefined, adapter), usage);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
schedulePersistCacheStats(ctx);
|
|
1356
1492
|
await publishStatus(ctx);
|
|
1357
1493
|
});
|
|
1358
1494
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cache-optimizer",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Pi extension that improves provider-side KV/prompt cache hit rates (DeepSeek, OpenAI, Claude, Gemini) by reordering the system prompt, requesting long retention, and showing footer cache stats. Renamed from pi-deepseek-cache-optimizer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|