@agimon-ai/doompi-cache 0.0.1-alpha.44 → 0.0.1-alpha.46
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/dist/extensions/pi.cjs +1 -1
- package/dist/extensions/pi.mjs +1 -1
- package/dist/{pi-cache-optimizer-BfO9Qmqm.mjs → pi-cache-optimizer-DjqO0Quf.mjs} +710 -46
- package/dist/pi-cache-optimizer-DjqO0Quf.mjs.map +1 -0
- package/dist/{pi-cache-optimizer-A-22Ztcb.cjs → pi-cache-optimizer-wNhYQVri.cjs} +709 -45
- package/dist/pi-cache-optimizer-wNhYQVri.cjs.map +1 -0
- package/package.json +10 -10
- package/dist/pi-cache-optimizer-A-22Ztcb.cjs.map +0 -1
- package/dist/pi-cache-optimizer-BfO9Qmqm.mjs.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmod, copyFile, lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { chmod, copyFile, link, lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { constants, readFileSync, statSync, watch } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
//#region ../../../node_modules/.pnpm/pi-cache-optimizer@2.8.
|
|
7
|
+
//#region ../../../node_modules/.pnpm/pi-cache-optimizer@2.8.10_@earendil-works+pi-coding-agent@0.86.0_supports-color@7.2.0_zod@4.6.5_/node_modules/pi-cache-optimizer/index.ts
|
|
8
8
|
const PI_CACHE_RETENTION_ENV = "PI_CACHE_RETENTION";
|
|
9
9
|
const LONG_CACHE_RETENTION_VALUE = "long";
|
|
10
10
|
const PI_CACHE_RETENTION_BASELINE_SYMBOL = Symbol.for("pi.cache.optimizer.retention-baseline.v1");
|
|
@@ -70,6 +70,7 @@ const SHARD_GLOBAL_EPOCH_PATH = join(SHARD_EPOCH_DIR, "global.json");
|
|
|
70
70
|
const SHARD_CLEANUP_LOCK_PATH = join(SHARD_MAINTENANCE_DIR, "cleanup.lock");
|
|
71
71
|
const SHARD_CLEANUP_MARKER_PATH = join(SHARD_MAINTENANCE_DIR, "last-cleanup");
|
|
72
72
|
const CONFIG_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-config.json");
|
|
73
|
+
const CONFIG_RECEIPT_PATH = join(STATE_DIR, "pi-cache-optimizer-config-receipt.json");
|
|
73
74
|
const FIX_RECEIPT_FILE_NAME = "pi-cache-optimizer-fix-receipt.json";
|
|
74
75
|
const FIX_RECEIPT_PATH = join(STATE_DIR, FIX_RECEIPT_FILE_NAME);
|
|
75
76
|
const MODELS_TRANSACTION_LOCK_PATH = join(STATE_DIR, "pi-cache-optimizer-models-transaction.lock");
|
|
@@ -1036,7 +1037,7 @@ function isValidAnthropicMessagesCompat(compat) {
|
|
|
1036
1037
|
function isValidCompatRecord(value) {
|
|
1037
1038
|
if (value === void 0) return true;
|
|
1038
1039
|
const compat = asRecord(value);
|
|
1039
|
-
return !!compat && (isValidOpenAICompletionsCompat(compat) || isValidOpenAIResponsesCompat(compat) || isValidAnthropicMessagesCompat(compat));
|
|
1040
|
+
return !!compat && !Object.prototype.hasOwnProperty.call(compat, "supportsPromptCacheKey") && (isValidOpenAICompletionsCompat(compat) || isValidOpenAIResponsesCompat(compat) || isValidAnthropicMessagesCompat(compat));
|
|
1040
1041
|
}
|
|
1041
1042
|
function isValidModelsConfigForEffectiveCompat(value) {
|
|
1042
1043
|
const root = asRecord(value);
|
|
@@ -1177,6 +1178,7 @@ const CACHE_OPTIMIZER_FOOTER_MODES = [
|
|
|
1177
1178
|
"process"
|
|
1178
1179
|
];
|
|
1179
1180
|
const CACHE_OPTIMIZER_STATS_ARGUMENTS = ["all", "contributors"];
|
|
1181
|
+
const CACHE_OPTIMIZER_FIX_ARGUMENTS = ["prompt-cache-key"];
|
|
1180
1182
|
function filterCommandCompletionItems(values, prefix, argumentPath = "") {
|
|
1181
1183
|
const normalizedPrefix = prefix.trim().toLowerCase();
|
|
1182
1184
|
const matches = values.filter((value) => value.startsWith(normalizedPrefix)).map((value) => ({
|
|
@@ -1201,6 +1203,7 @@ function getCacheOptimizerArgumentCompletions(argumentPrefix) {
|
|
|
1201
1203
|
return filterCommandCompletionItems(CACHE_OPTIMIZER_COMMANDS, parts[0]);
|
|
1202
1204
|
}
|
|
1203
1205
|
if (parts[0].toLowerCase() === "stats") return parts.length === 2 ? filterCommandCompletionItems(CACHE_OPTIMIZER_STATS_ARGUMENTS, parts[1], "stats") : null;
|
|
1206
|
+
if (parts[0].toLowerCase() === "fix") return parts.length === 2 ? filterCommandCompletionItems(CACHE_OPTIMIZER_FIX_ARGUMENTS, parts[1], "fix") : null;
|
|
1204
1207
|
if (parts[0].toLowerCase() !== "config") return null;
|
|
1205
1208
|
if (parts.length === 2) {
|
|
1206
1209
|
if (parts[1].toLowerCase() === "footer-mode") return filterCommandCompletionItems(CACHE_OPTIMIZER_FOOTER_MODES, "", "config footer-mode");
|
|
@@ -1211,33 +1214,471 @@ function getCacheOptimizerArgumentCompletions(argumentPrefix) {
|
|
|
1211
1214
|
}
|
|
1212
1215
|
function parsePersistedCacheOptimizerConfig(value) {
|
|
1213
1216
|
const record = asRecord(value);
|
|
1214
|
-
if (!record || record.version !== 1) return void 0;
|
|
1217
|
+
if (!record || record.version !== 1 && record.version !== 2) return void 0;
|
|
1218
|
+
const allowedTopLevel = new Set(record.version === 1 ? ["version", "footerMode"] : [
|
|
1219
|
+
"version",
|
|
1220
|
+
"footerMode",
|
|
1221
|
+
"promptCacheKey"
|
|
1222
|
+
]);
|
|
1223
|
+
if (Object.keys(record).some((key) => !allowedTopLevel.has(key))) return void 0;
|
|
1215
1224
|
const footerMode = parseFooterStatsMode(record.footerMode);
|
|
1216
1225
|
if (record.footerMode !== void 0 && !footerMode) return void 0;
|
|
1217
|
-
return {
|
|
1226
|
+
if (record.version === 1) return {
|
|
1218
1227
|
version: 1,
|
|
1219
1228
|
...footerMode ? { footerMode } : {}
|
|
1220
1229
|
};
|
|
1230
|
+
const rawPromptCacheKey = record.promptCacheKey;
|
|
1231
|
+
if (rawPromptCacheKey !== void 0 && !asRecord(rawPromptCacheKey)) return void 0;
|
|
1232
|
+
const promptCacheKey = asRecord(rawPromptCacheKey);
|
|
1233
|
+
if (promptCacheKey && Object.keys(promptCacheKey).some((key) => key !== "omit")) return void 0;
|
|
1234
|
+
const omit = promptCacheKey?.omit;
|
|
1235
|
+
if (omit !== void 0 && (!Array.isArray(omit) || omit.some((value) => !isNonEmptyString(value)))) return void 0;
|
|
1236
|
+
const stringOmit = omit;
|
|
1237
|
+
const uniqueOmit = stringOmit ? [...new Set(stringOmit.map((value) => value.trim()))].sort() : void 0;
|
|
1238
|
+
return {
|
|
1239
|
+
version: 2,
|
|
1240
|
+
...footerMode ? { footerMode } : {},
|
|
1241
|
+
...uniqueOmit && uniqueOmit.length > 0 ? { promptCacheKey: { omit: uniqueOmit } } : {}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function normalizePersistedCacheOptimizerConfig(value) {
|
|
1245
|
+
if (!value) return { version: 2 };
|
|
1246
|
+
return value.version === 2 ? value : {
|
|
1247
|
+
version: 2,
|
|
1248
|
+
...value.footerMode ? { footerMode: value.footerMode } : {}
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function readPersistedCacheOptimizerConfig(configPath = CONFIG_FILE_PATH) {
|
|
1252
|
+
try {
|
|
1253
|
+
return normalizePersistedCacheOptimizerConfig(parsePersistedCacheOptimizerConfig(JSON.parse(readFileSync(configPath, "utf8"))));
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
if (getErrorCode(error) !== "ENOENT") console.warn(`${LOG_PREFIX}: failed to read optimizer config; using defaults`, error);
|
|
1256
|
+
return { version: 2 };
|
|
1257
|
+
}
|
|
1221
1258
|
}
|
|
1222
1259
|
function readPersistedFooterMode(configPath = CONFIG_FILE_PATH) {
|
|
1260
|
+
return readPersistedCacheOptimizerConfig(configPath).footerMode;
|
|
1261
|
+
}
|
|
1262
|
+
async function writePersistedCacheOptimizerConfigUnlocked(config, configPath) {
|
|
1263
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1264
|
+
const payloadText = JSON.stringify(normalizePersistedCacheOptimizerConfig(config), null, 2) + "\n";
|
|
1265
|
+
let targetInfo;
|
|
1266
|
+
let targetMode = 384;
|
|
1267
|
+
let targetHash;
|
|
1268
|
+
try {
|
|
1269
|
+
targetInfo = await lstat(configPath);
|
|
1270
|
+
if (targetInfo.isSymbolicLink() || !targetInfo.isFile()) throw new Error("optimizer config is not a regular file; no changes were made");
|
|
1271
|
+
const targetText = await readFile(configPath, "utf8");
|
|
1272
|
+
targetMode = targetInfo.mode & 4095;
|
|
1273
|
+
targetHash = hashText(targetText);
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
if (getErrorCode(error) !== "ENOENT") throw error;
|
|
1276
|
+
}
|
|
1277
|
+
if (targetInfo) {
|
|
1278
|
+
await atomicReplaceTextFilePreservingMode(configPath, payloadText, targetMode, "config", {
|
|
1279
|
+
identity: targetInfo,
|
|
1280
|
+
hash: targetHash,
|
|
1281
|
+
mode: targetMode
|
|
1282
|
+
});
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
await atomicCreateTextFileNoReplace(configPath, payloadText, targetMode, "config");
|
|
1286
|
+
}
|
|
1287
|
+
async function writePersistedCacheOptimizerConfig(config, configPath = CONFIG_FILE_PATH) {
|
|
1288
|
+
await withModelsJsonTransactionLock(() => writePersistedCacheOptimizerConfigUnlocked(config, configPath));
|
|
1289
|
+
}
|
|
1290
|
+
async function writePersistedFooterModeUnlocked(mode, configPath) {
|
|
1291
|
+
let version = 1;
|
|
1292
|
+
let raw;
|
|
1293
|
+
let targetExists = false;
|
|
1223
1294
|
try {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1295
|
+
raw = parsePersistedCacheOptimizerConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
1296
|
+
targetExists = true;
|
|
1297
|
+
if (!raw) throw new Error("invalid footer config schema");
|
|
1298
|
+
version = raw.version === 2 ? 2 : 1;
|
|
1227
1299
|
} catch (error) {
|
|
1228
|
-
if (getErrorCode(error) !== "ENOENT")
|
|
1300
|
+
if (getErrorCode(error) !== "ENOENT") throw new Error("invalid footer config schema");
|
|
1301
|
+
}
|
|
1302
|
+
const current = normalizePersistedCacheOptimizerConfig(raw);
|
|
1303
|
+
if (version === 1 && !current.promptCacheKey) {
|
|
1304
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1305
|
+
const targetInfo = targetExists ? await lstat(configPath) : void 0;
|
|
1306
|
+
if (targetInfo && (targetInfo.isSymbolicLink() || !targetInfo.isFile())) throw new Error("optimizer config is not a regular file; no changes were made");
|
|
1307
|
+
const targetText = targetInfo ? await readFile(configPath, "utf8") : void 0;
|
|
1308
|
+
const targetMode = targetInfo ? targetInfo.mode & 4095 : 384;
|
|
1309
|
+
const footerText = JSON.stringify({
|
|
1310
|
+
version: 1,
|
|
1311
|
+
footerMode: mode
|
|
1312
|
+
}, null, 2) + "\n";
|
|
1313
|
+
if (targetInfo && targetText !== void 0) await atomicReplaceTextFilePreservingMode(configPath, footerText, targetMode, "config-footer", {
|
|
1314
|
+
identity: targetInfo,
|
|
1315
|
+
hash: hashText(targetText),
|
|
1316
|
+
mode: targetMode
|
|
1317
|
+
});
|
|
1318
|
+
else await atomicCreateTextFileNoReplace(configPath, footerText, targetMode, "config-footer");
|
|
1229
1319
|
return;
|
|
1230
1320
|
}
|
|
1321
|
+
await writePersistedCacheOptimizerConfigUnlocked({
|
|
1322
|
+
...current,
|
|
1323
|
+
version: 2,
|
|
1324
|
+
footerMode: mode
|
|
1325
|
+
}, configPath);
|
|
1231
1326
|
}
|
|
1232
1327
|
async function writePersistedFooterMode(mode, configPath = CONFIG_FILE_PATH) {
|
|
1328
|
+
await withModelsJsonTransactionLock(() => writePersistedFooterModeUnlocked(mode, configPath));
|
|
1329
|
+
}
|
|
1330
|
+
function configReceiptBackupPath(receipt, receiptPath = CONFIG_RECEIPT_PATH) {
|
|
1331
|
+
return join(dirname(receiptPath), receipt.backupFile);
|
|
1332
|
+
}
|
|
1333
|
+
function parsePromptCacheKeyConfigReceipt(value) {
|
|
1334
|
+
const record = asRecord(value);
|
|
1335
|
+
if (!record || record.version !== 1 && record.version !== 2 || record.kind !== "pi-cache-optimizer-config-receipt") return void 0;
|
|
1336
|
+
const allowed = new Set(record.version === 1 ? [
|
|
1337
|
+
"version",
|
|
1338
|
+
"kind",
|
|
1339
|
+
"transactionId",
|
|
1340
|
+
"provider",
|
|
1341
|
+
"modelId",
|
|
1342
|
+
"beforeHash",
|
|
1343
|
+
"afterHash",
|
|
1344
|
+
"backupFile",
|
|
1345
|
+
"targetExistedBefore",
|
|
1346
|
+
"createdAt",
|
|
1347
|
+
"appliedAt",
|
|
1348
|
+
"status",
|
|
1349
|
+
"rolledBackAt"
|
|
1350
|
+
] : [
|
|
1351
|
+
"version",
|
|
1352
|
+
"kind",
|
|
1353
|
+
"transactionId",
|
|
1354
|
+
"provider",
|
|
1355
|
+
"modelId",
|
|
1356
|
+
"beforeHash",
|
|
1357
|
+
"afterHash",
|
|
1358
|
+
"backupFile",
|
|
1359
|
+
"targetExistedBefore",
|
|
1360
|
+
"targetHadModelKey",
|
|
1361
|
+
"addedModelKey",
|
|
1362
|
+
"createdAt",
|
|
1363
|
+
"appliedAt",
|
|
1364
|
+
"status",
|
|
1365
|
+
"rolledBackAt"
|
|
1366
|
+
]);
|
|
1367
|
+
if (Object.keys(record).some((key) => !allowed.has(key))) return void 0;
|
|
1368
|
+
if (![
|
|
1369
|
+
record.transactionId,
|
|
1370
|
+
record.provider,
|
|
1371
|
+
record.modelId
|
|
1372
|
+
].every(isSafeReceiptText)) return void 0;
|
|
1373
|
+
if (!isSha256(record.beforeHash) || !isSha256(record.afterHash) || record.beforeHash === record.afterHash) return void 0;
|
|
1374
|
+
if (!isSafeReceiptText(record.backupFile) || basename(record.backupFile) !== record.backupFile || !record.backupFile.startsWith("pi-cache-optimizer-config.backup-")) return void 0;
|
|
1375
|
+
if (typeof record.targetExistedBefore !== "boolean" || !isReceiptTimestamp(record.createdAt) || !isReceiptTimestamp(record.appliedAt) || record.appliedAt < record.createdAt) return void 0;
|
|
1376
|
+
const addedModelKey = `${record.provider}/${record.modelId}`;
|
|
1377
|
+
const targetHadModelKey = record.version === 1 ? false : record.targetHadModelKey;
|
|
1378
|
+
if (typeof targetHadModelKey !== "boolean") return void 0;
|
|
1379
|
+
if (record.version === 2 && (!isSafeReceiptText(record.addedModelKey) || record.addedModelKey !== addedModelKey)) return void 0;
|
|
1380
|
+
if (record.status !== void 0 && record.status !== "rolled_back") return void 0;
|
|
1381
|
+
if (record.status === "rolled_back" && (!isReceiptTimestamp(record.rolledBackAt) || record.rolledBackAt < record.appliedAt)) return void 0;
|
|
1382
|
+
if (record.status === void 0 && record.rolledBackAt !== void 0) return void 0;
|
|
1383
|
+
return {
|
|
1384
|
+
version: 2,
|
|
1385
|
+
kind: "pi-cache-optimizer-config-receipt",
|
|
1386
|
+
transactionId: String(record.transactionId),
|
|
1387
|
+
provider: String(record.provider),
|
|
1388
|
+
modelId: String(record.modelId),
|
|
1389
|
+
beforeHash: String(record.beforeHash).toLowerCase(),
|
|
1390
|
+
afterHash: String(record.afterHash).toLowerCase(),
|
|
1391
|
+
backupFile: String(record.backupFile),
|
|
1392
|
+
targetExistedBefore: record.targetExistedBefore,
|
|
1393
|
+
targetHadModelKey,
|
|
1394
|
+
addedModelKey,
|
|
1395
|
+
createdAt: Number(record.createdAt),
|
|
1396
|
+
appliedAt: Number(record.appliedAt),
|
|
1397
|
+
...record.status === "rolled_back" ? {
|
|
1398
|
+
status: "rolled_back",
|
|
1399
|
+
rolledBackAt: Number(record.rolledBackAt)
|
|
1400
|
+
} : {}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function isActionablePromptCacheKeyConfigReceipt(receipt) {
|
|
1404
|
+
return receipt !== void 0 && receipt.status === void 0;
|
|
1405
|
+
}
|
|
1406
|
+
async function assertPromptCacheKeyConfigReceiptSnapshotUnchanged(snapshot) {
|
|
1407
|
+
const info = await lstat(snapshot.receiptPath);
|
|
1408
|
+
if (info.isSymbolicLink() || !info.isFile() || !sameFileIdentity(snapshot.identity, info)) throw new Error("prompt-cache-key receipt changed since the rollback preview");
|
|
1409
|
+
const text = await readFile(snapshot.receiptPath, "utf8");
|
|
1410
|
+
const afterRead = await lstat(snapshot.receiptPath);
|
|
1411
|
+
if (afterRead.isSymbolicLink() || !afterRead.isFile() || !sameFileIdentity(info, afterRead) || hashText(text) !== snapshot.hash) throw new Error("prompt-cache-key receipt changed since the rollback preview");
|
|
1412
|
+
}
|
|
1413
|
+
async function readPromptCacheKeyConfigReceiptSnapshot(receiptPath = CONFIG_RECEIPT_PATH) {
|
|
1414
|
+
try {
|
|
1415
|
+
const info = await lstat(receiptPath);
|
|
1416
|
+
if (info.isSymbolicLink() || !info.isFile()) return void 0;
|
|
1417
|
+
const text = await readFile(receiptPath, "utf8");
|
|
1418
|
+
const afterRead = await lstat(receiptPath);
|
|
1419
|
+
if (afterRead.isSymbolicLink() || !afterRead.isFile() || !sameFileIdentity(info, afterRead)) return void 0;
|
|
1420
|
+
const receipt = parsePromptCacheKeyConfigReceipt(JSON.parse(text));
|
|
1421
|
+
if (!receipt) return void 0;
|
|
1422
|
+
return {
|
|
1423
|
+
receipt,
|
|
1424
|
+
receiptPath,
|
|
1425
|
+
hash: hashText(text),
|
|
1426
|
+
identity: afterRead
|
|
1427
|
+
};
|
|
1428
|
+
} catch {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
async function readPromptCacheKeyConfigReceipt(receiptPath = CONFIG_RECEIPT_PATH) {
|
|
1433
|
+
return (await readPromptCacheKeyConfigReceiptSnapshot(receiptPath))?.receipt;
|
|
1434
|
+
}
|
|
1435
|
+
async function writePromptCacheKeyConfigReceipt(receipt, receiptPath = CONFIG_RECEIPT_PATH, expectedSnapshot, beforeRename) {
|
|
1436
|
+
if (!parsePromptCacheKeyConfigReceipt(receipt)) throw new Error("invalid prompt cache key config receipt");
|
|
1437
|
+
if (expectedSnapshot?.receiptPath !== void 0 && expectedSnapshot.receiptPath !== receiptPath) throw new Error("prompt-cache-key receipt path changed since the rollback preview");
|
|
1438
|
+
if (expectedSnapshot) await assertPromptCacheKeyConfigReceiptSnapshotUnchanged(expectedSnapshot);
|
|
1439
|
+
await mkdir(dirname(receiptPath), { recursive: true });
|
|
1440
|
+
let existingReceiptInfo;
|
|
1441
|
+
try {
|
|
1442
|
+
const info = await lstat(receiptPath);
|
|
1443
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("invalid prompt-cache-key receipt path");
|
|
1444
|
+
existingReceiptInfo = info;
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
if (getErrorCode(error) !== "ENOENT") throw error;
|
|
1447
|
+
}
|
|
1448
|
+
const tempPath = uniqueTempPath(receiptPath, "config-receipt");
|
|
1449
|
+
try {
|
|
1450
|
+
await writeFile(tempPath, JSON.stringify(receipt, null, 2) + "\n", {
|
|
1451
|
+
encoding: "utf8",
|
|
1452
|
+
mode: 384,
|
|
1453
|
+
flag: "wx"
|
|
1454
|
+
});
|
|
1455
|
+
const tempInfo = await lstat(tempPath);
|
|
1456
|
+
if (tempInfo.isSymbolicLink() || !tempInfo.isFile()) throw new Error("invalid temporary prompt-cache-key receipt");
|
|
1457
|
+
await chmod(tempPath, 384);
|
|
1458
|
+
const assertDestinationUnchanged = async () => {
|
|
1459
|
+
try {
|
|
1460
|
+
const currentInfo = await lstat(receiptPath);
|
|
1461
|
+
if (!existingReceiptInfo || !sameFileIdentity(existingReceiptInfo, currentInfo)) throw new Error("prompt-cache-key receipt changed during atomic write");
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
if (getErrorCode(error) !== "ENOENT" || existingReceiptInfo) throw error;
|
|
1464
|
+
}
|
|
1465
|
+
if (expectedSnapshot) await assertPromptCacheKeyConfigReceiptSnapshotUnchanged(expectedSnapshot);
|
|
1466
|
+
};
|
|
1467
|
+
await assertDestinationUnchanged();
|
|
1468
|
+
if (beforeRename) await beforeRename();
|
|
1469
|
+
await assertDestinationUnchanged();
|
|
1470
|
+
if (existingReceiptInfo) await rename(tempPath, receiptPath);
|
|
1471
|
+
else {
|
|
1472
|
+
await link(tempPath, receiptPath);
|
|
1473
|
+
await unlink(tempPath).catch((cleanupError) => {
|
|
1474
|
+
console.warn(`${LOG_PREFIX}: committed prompt-cache-key receipt but failed to remove its temporary hard link`, cleanupError);
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
await unlink(tempPath).catch((cleanupError) => {
|
|
1479
|
+
if (getErrorCode(cleanupError) !== "ENOENT") console.warn(`${LOG_PREFIX}: failed to remove temporary config receipt`, cleanupError);
|
|
1480
|
+
});
|
|
1481
|
+
throw error;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
async function applyPromptCacheKeyConfigFixUnderLock(model, configPath = CONFIG_FILE_PATH, receiptPath = CONFIG_RECEIPT_PATH) {
|
|
1233
1485
|
await mkdir(dirname(configPath), { recursive: true });
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1486
|
+
let originalText = "";
|
|
1487
|
+
let targetExistedBefore = false;
|
|
1488
|
+
let mode = 384;
|
|
1489
|
+
try {
|
|
1490
|
+
const info = await lstat(configPath);
|
|
1491
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("optimizer config is not a regular file; no changes were made");
|
|
1492
|
+
originalText = await readFile(configPath, "utf8");
|
|
1493
|
+
targetExistedBefore = true;
|
|
1494
|
+
mode = info.mode & 4095;
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
if (getErrorCode(error) !== "ENOENT") throw error;
|
|
1497
|
+
}
|
|
1498
|
+
const parsedCurrent = targetExistedBefore ? parsePersistedCacheOptimizerConfig(JSON.parse(originalText)) : void 0;
|
|
1499
|
+
if (targetExistedBefore && !parsedCurrent) throw new Error("optimizer config is invalid; no changes were made");
|
|
1500
|
+
const current = normalizePersistedCacheOptimizerConfig(parsedCurrent);
|
|
1501
|
+
const key = modelKey(model);
|
|
1502
|
+
const existingOmit = current.promptCacheKey?.omit ?? [];
|
|
1503
|
+
const targetHadModelKey = existingOmit.includes(key);
|
|
1504
|
+
if (targetHadModelKey) throw new Error("prompt-cache-key is already configured; no changes were made");
|
|
1505
|
+
const omit = [.../* @__PURE__ */ new Set([...existingOmit, key])].sort();
|
|
1506
|
+
const next = {
|
|
1507
|
+
...current,
|
|
1508
|
+
version: 2,
|
|
1509
|
+
promptCacheKey: { omit }
|
|
1237
1510
|
};
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1511
|
+
const modifiedText = JSON.stringify(next, null, 2) + "\n";
|
|
1512
|
+
const beforeHash = hashText(originalText);
|
|
1513
|
+
const afterHash = hashText(modifiedText);
|
|
1514
|
+
const originalInfo = targetExistedBefore ? await lstat(configPath) : void 0;
|
|
1515
|
+
const backupFile = `pi-cache-optimizer-config.backup-${backupTimestamp()}`;
|
|
1516
|
+
const backupPath = join(dirname(configPath), backupFile);
|
|
1517
|
+
if (targetExistedBefore) {
|
|
1518
|
+
await copyFile(configPath, backupPath, constants.COPYFILE_EXCL);
|
|
1519
|
+
await chmod(backupPath, mode);
|
|
1520
|
+
}
|
|
1521
|
+
const tempPath = uniqueTempPath(configPath, "config-fix");
|
|
1522
|
+
let committedInfo;
|
|
1523
|
+
try {
|
|
1524
|
+
await writeFile(tempPath, modifiedText, {
|
|
1525
|
+
encoding: "utf8",
|
|
1526
|
+
mode,
|
|
1527
|
+
flag: "wx"
|
|
1528
|
+
});
|
|
1529
|
+
await chmod(tempPath, mode);
|
|
1530
|
+
if (originalInfo) await validateAtomicTarget(configPath, {
|
|
1531
|
+
identity: originalInfo,
|
|
1532
|
+
hash: beforeHash,
|
|
1533
|
+
mode
|
|
1534
|
+
});
|
|
1535
|
+
else try {
|
|
1536
|
+
await lstat(configPath);
|
|
1537
|
+
throw new Error("optimizer config appeared during the fix; no changes were made");
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
if (getErrorCode(error) !== "ENOENT") throw error;
|
|
1540
|
+
}
|
|
1541
|
+
if (originalInfo) {
|
|
1542
|
+
await rename(tempPath, configPath);
|
|
1543
|
+
committedInfo = await lstat(configPath);
|
|
1544
|
+
} else {
|
|
1545
|
+
await link(tempPath, configPath);
|
|
1546
|
+
committedInfo = await lstat(configPath);
|
|
1547
|
+
await unlink(tempPath).catch((cleanupError) => {
|
|
1548
|
+
console.warn(`${LOG_PREFIX}: committed optimizer config but failed to remove its temporary hard link`, cleanupError);
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
const receipt = {
|
|
1552
|
+
version: 2,
|
|
1553
|
+
kind: "pi-cache-optimizer-config-receipt",
|
|
1554
|
+
transactionId: randomUUID(),
|
|
1555
|
+
provider: model.provider,
|
|
1556
|
+
modelId: model.id,
|
|
1557
|
+
beforeHash,
|
|
1558
|
+
afterHash,
|
|
1559
|
+
backupFile,
|
|
1560
|
+
targetExistedBefore,
|
|
1561
|
+
targetHadModelKey,
|
|
1562
|
+
addedModelKey: key,
|
|
1563
|
+
createdAt: Date.now(),
|
|
1564
|
+
appliedAt: Date.now()
|
|
1565
|
+
};
|
|
1566
|
+
await writePromptCacheKeyConfigReceipt(receipt, receiptPath);
|
|
1567
|
+
return {
|
|
1568
|
+
receipt,
|
|
1569
|
+
backupPath
|
|
1570
|
+
};
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
await unlink(tempPath).catch(() => {});
|
|
1573
|
+
try {
|
|
1574
|
+
if (targetExistedBefore && committedInfo) await atomicRestoreFileFromBackup(backupPath, configPath, mode, {
|
|
1575
|
+
identity: committedInfo,
|
|
1576
|
+
hash: afterHash,
|
|
1577
|
+
mode
|
|
1578
|
+
});
|
|
1579
|
+
else if (!targetExistedBefore && committedInfo) {
|
|
1580
|
+
await validateAtomicTarget(configPath, {
|
|
1581
|
+
identity: committedInfo,
|
|
1582
|
+
hash: afterHash,
|
|
1583
|
+
mode
|
|
1584
|
+
});
|
|
1585
|
+
await unlink(configPath);
|
|
1586
|
+
}
|
|
1587
|
+
} catch (compensationError) {
|
|
1588
|
+
const writeMessage = error instanceof Error ? error.message : String(error);
|
|
1589
|
+
const compensationMessage = compensationError instanceof Error ? compensationError.message : String(compensationError);
|
|
1590
|
+
throw new Error(`prompt-cache-key fix receipt update failed (${writeMessage}) and config compensation failed (${compensationMessage})`);
|
|
1591
|
+
}
|
|
1592
|
+
throw error;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
async function applyPromptCacheKeyConfigFix(model, configPath = CONFIG_FILE_PATH, receiptPath = CONFIG_RECEIPT_PATH) {
|
|
1596
|
+
return withModelsJsonTransactionLock(() => applyPromptCacheKeyConfigFixUnderLock(model, configPath, receiptPath));
|
|
1597
|
+
}
|
|
1598
|
+
async function rollbackPromptCacheKeyConfigUnderLock(snapshot, configPath = CONFIG_FILE_PATH, receiptPath = CONFIG_RECEIPT_PATH, options = {}) {
|
|
1599
|
+
if (snapshot.receiptPath !== receiptPath) throw new Error("prompt-cache-key receipt path changed since the rollback preview");
|
|
1600
|
+
await assertPromptCacheKeyConfigReceiptSnapshotUnchanged(snapshot);
|
|
1601
|
+
const receipt = snapshot.receipt;
|
|
1602
|
+
const currentInfo = await lstat(configPath);
|
|
1603
|
+
if (currentInfo.isSymbolicLink() || !currentInfo.isFile()) throw new Error("optimizer config is not a regular file; refusing to overwrite user changes");
|
|
1604
|
+
const currentText = await readFile(configPath, "utf8");
|
|
1605
|
+
const currentHash = hashText(currentText);
|
|
1606
|
+
const currentMode = currentInfo.mode & 4095;
|
|
1607
|
+
if (currentHash !== receipt.afterHash) throw new Error("optimizer config changed after the fix; refusing to overwrite user changes");
|
|
1608
|
+
const current = parsePersistedCacheOptimizerConfig(JSON.parse(currentText));
|
|
1609
|
+
if (!current) throw new Error("optimizer config is invalid; refusing to overwrite user changes");
|
|
1610
|
+
const key = `${receipt.provider}/${receipt.modelId}`;
|
|
1611
|
+
if (receipt.addedModelKey !== key) throw new Error("prompt-cache-key receipt identity does not match; refusing to change user config");
|
|
1612
|
+
if (receipt.targetHadModelKey) throw new Error("prompt-cache-key was already configured before this fix; refusing to remove user configuration");
|
|
1613
|
+
const omit = current.version === 2 ? current.promptCacheKey?.omit ?? [] : [];
|
|
1614
|
+
if (!omit.includes(receipt.addedModelKey)) throw new Error("prompt-cache-key opt-out is no longer present; refusing to change user config");
|
|
1615
|
+
let rollbackResultText;
|
|
1616
|
+
let rollbackResultMode;
|
|
1617
|
+
let rollbackResultInfo;
|
|
1618
|
+
const backupPath = configReceiptBackupPath(receipt, receiptPath);
|
|
1619
|
+
if (receipt.targetExistedBefore) {
|
|
1620
|
+
const backupInfo = await lstat(backupPath);
|
|
1621
|
+
if (backupInfo.isSymbolicLink() || !backupInfo.isFile()) throw new Error("config backup is not a regular file");
|
|
1622
|
+
const backupText = await readFile(backupPath, "utf8");
|
|
1623
|
+
if (hashText(backupText) !== receipt.beforeHash) throw new Error("config backup hash does not match the fix receipt");
|
|
1624
|
+
rollbackResultText = backupText;
|
|
1625
|
+
rollbackResultMode = backupInfo.mode & 4095;
|
|
1626
|
+
await atomicRestoreFileFromBackup(backupPath, configPath, rollbackResultMode, {
|
|
1627
|
+
backupHash: receipt.beforeHash,
|
|
1628
|
+
identity: currentInfo,
|
|
1629
|
+
hash: currentHash,
|
|
1630
|
+
mode: currentMode
|
|
1631
|
+
});
|
|
1632
|
+
rollbackResultInfo = await lstat(configPath);
|
|
1633
|
+
} else if (current.footerMode || omit.length > 1) {
|
|
1634
|
+
const restored = {
|
|
1635
|
+
version: 2,
|
|
1636
|
+
...current.footerMode ? { footerMode: current.footerMode } : {},
|
|
1637
|
+
...omit.length > 1 ? { promptCacheKey: { omit: omit.filter((item) => item !== receipt.addedModelKey) } } : {}
|
|
1638
|
+
};
|
|
1639
|
+
rollbackResultText = JSON.stringify(restored, null, 2) + "\n";
|
|
1640
|
+
rollbackResultMode = currentMode;
|
|
1641
|
+
await atomicReplaceTextFilePreservingMode(configPath, rollbackResultText, rollbackResultMode, "config-rollback", {
|
|
1642
|
+
identity: currentInfo,
|
|
1643
|
+
hash: currentHash,
|
|
1644
|
+
mode: currentMode
|
|
1645
|
+
});
|
|
1646
|
+
rollbackResultInfo = await lstat(configPath);
|
|
1647
|
+
} else {
|
|
1648
|
+
await validateAtomicTarget(configPath, {
|
|
1649
|
+
identity: currentInfo,
|
|
1650
|
+
hash: currentHash,
|
|
1651
|
+
mode: currentMode
|
|
1652
|
+
});
|
|
1653
|
+
await unlink(configPath);
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
await writePromptCacheKeyConfigReceipt({
|
|
1657
|
+
...receipt,
|
|
1658
|
+
status: "rolled_back",
|
|
1659
|
+
rolledBackAt: Date.now()
|
|
1660
|
+
}, receiptPath, snapshot, options.beforeReceiptRename);
|
|
1661
|
+
} catch (receiptError) {
|
|
1662
|
+
try {
|
|
1663
|
+
if (rollbackResultText === void 0) await atomicCreateTextFileNoReplace(configPath, currentText, currentMode, "config-rollback-compensation");
|
|
1664
|
+
else {
|
|
1665
|
+
if (!rollbackResultInfo || rollbackResultMode === void 0) throw new Error("missing rollback result guard");
|
|
1666
|
+
await atomicReplaceTextFilePreservingMode(configPath, currentText, currentMode, "config-rollback-compensation", {
|
|
1667
|
+
identity: rollbackResultInfo,
|
|
1668
|
+
hash: hashText(rollbackResultText),
|
|
1669
|
+
mode: rollbackResultMode
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
} catch (compensationError) {
|
|
1673
|
+
const receiptMessage = receiptError instanceof Error ? receiptError.message : String(receiptError);
|
|
1674
|
+
const compensationMessage = compensationError instanceof Error ? compensationError.message : String(compensationError);
|
|
1675
|
+
throw new Error(`prompt-cache-key rollback receipt update failed (${receiptMessage}) and config compensation failed (${compensationMessage})`);
|
|
1676
|
+
}
|
|
1677
|
+
throw receiptError;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
async function rollbackPromptCacheKeyConfig(snapshot, configPath = CONFIG_FILE_PATH, receiptPath = CONFIG_RECEIPT_PATH, options = {}) {
|
|
1681
|
+
return withModelsJsonTransactionLock(() => rollbackPromptCacheKeyConfigUnderLock(snapshot, configPath, receiptPath, options));
|
|
1241
1682
|
}
|
|
1242
1683
|
function resolveFooterStatsMode(configuredMode, env = process.env) {
|
|
1243
1684
|
if (configuredMode) return {
|
|
@@ -1256,7 +1697,8 @@ function resolveFooterStatsMode(configuredMode, env = process.env) {
|
|
|
1256
1697
|
function footerStatsMode(env = process.env, configuredMode = persistedFooterStatsMode) {
|
|
1257
1698
|
return resolveFooterStatsMode(configuredMode, env).mode;
|
|
1258
1699
|
}
|
|
1259
|
-
let
|
|
1700
|
+
let persistedCacheOptimizerConfig = readPersistedCacheOptimizerConfig();
|
|
1701
|
+
let persistedFooterStatsMode = persistedCacheOptimizerConfig.footerMode;
|
|
1260
1702
|
function isDisabledEnv(value) {
|
|
1261
1703
|
if (!value) return false;
|
|
1262
1704
|
const normalized = value.trim().toLowerCase();
|
|
@@ -1339,6 +1781,22 @@ function isPiBuiltInLlamaCppModel(model) {
|
|
|
1339
1781
|
function shouldInjectOpenAIPromptCacheKeyForModel(model) {
|
|
1340
1782
|
return isOpenAICompatibleApi(model?.api);
|
|
1341
1783
|
}
|
|
1784
|
+
function isPromptCacheKeyOmittedForModel(model, config = persistedCacheOptimizerConfig) {
|
|
1785
|
+
if (!model || !isOpenAICompatibleApi(model.api)) return false;
|
|
1786
|
+
return config.promptCacheKey?.omit?.includes(modelKey(model)) === true;
|
|
1787
|
+
}
|
|
1788
|
+
function setPersistedCacheOptimizerConfig(config) {
|
|
1789
|
+
persistedCacheOptimizerConfig = normalizePersistedCacheOptimizerConfig(config);
|
|
1790
|
+
persistedFooterStatsMode = persistedCacheOptimizerConfig.footerMode;
|
|
1791
|
+
}
|
|
1792
|
+
function omitOpenAIPromptCacheKeys(payload) {
|
|
1793
|
+
const record = asRecord(payload);
|
|
1794
|
+
if (!record || !Object.prototype.hasOwnProperty.call(record, "prompt_cache_key") && !Object.prototype.hasOwnProperty.call(record, "promptCacheKey")) return void 0;
|
|
1795
|
+
const copy = { ...record };
|
|
1796
|
+
delete copy.prompt_cache_key;
|
|
1797
|
+
delete copy.promptCacheKey;
|
|
1798
|
+
return copy;
|
|
1799
|
+
}
|
|
1342
1800
|
function collectAnthropicCacheControlsInWireOrder(payload) {
|
|
1343
1801
|
const record = asRecord(payload);
|
|
1344
1802
|
if (!record) return [];
|
|
@@ -2268,6 +2726,26 @@ function hasPromptCacheRetentionUnsupportedErrorMessage(message) {
|
|
|
2268
2726
|
const record = getAssistantRecord(message);
|
|
2269
2727
|
return record?.stopReason === "error" && hasPromptCacheRetentionUnsupportedText(record.errorMessage);
|
|
2270
2728
|
}
|
|
2729
|
+
function hasPromptCacheKeyUnsupportedText(value) {
|
|
2730
|
+
const normalized = lower(value).replace(/["'`]/g, "").replace(/[\s_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
2731
|
+
const key = String.raw`(?:prompt ?cache ?key|promptcachekey)`;
|
|
2732
|
+
const field = String.raw`(?:parameter|field|input|argument)`;
|
|
2733
|
+
const unsupported = String.raw`(?:unsupported|unknown|unrecognized|unexpected|not supported|not allowed|not permitted|must be omitted|should be omitted)`;
|
|
2734
|
+
if (!new RegExp(key).test(normalized)) return false;
|
|
2735
|
+
const terminal = String.raw`(?=$|[}\]>,.;])`;
|
|
2736
|
+
return new RegExp(String.raw`(?:unsupported|unknown|unrecognized|unexpected)\s+${field}\s*[:=]?\s*${key}${terminal}`).test(normalized) || new RegExp(String.raw`(?:extra\s+inputs?|${field}\s+not\s+(?:allowed|permitted|supported))\s*[:=]\s*${key}${terminal}`).test(normalized) || new RegExp(String.raw`${key}(?:\s+${field})?\s*[:=]?\s*(?:is\s+)?${unsupported}${terminal}`).test(normalized);
|
|
2737
|
+
}
|
|
2738
|
+
function hasPromptCacheKeyUnsupportedSignal(headers) {
|
|
2739
|
+
if (!headers) return false;
|
|
2740
|
+
return Object.entries(headers).some(([key, value]) => hasPromptCacheKeyUnsupportedText(`${key}: ${value}`));
|
|
2741
|
+
}
|
|
2742
|
+
function hasPromptCacheKeyUnsupportedErrorMessage(message) {
|
|
2743
|
+
const record = getAssistantRecord(message);
|
|
2744
|
+
return record?.stopReason === "error" && getOptionalAssistantHttpStatus(record) === 400 && hasPromptCacheKeyUnsupportedText(record.errorMessage);
|
|
2745
|
+
}
|
|
2746
|
+
function isPromptCacheKeyUnsupportedApplicable(model) {
|
|
2747
|
+
return isOpenAICompatibleApi(model.api);
|
|
2748
|
+
}
|
|
2271
2749
|
function hasReasoningProtocolRejectionText(value) {
|
|
2272
2750
|
const normalized = lower(value).replace(/\s+/g, " ").trim();
|
|
2273
2751
|
if (!normalized) return false;
|
|
@@ -4532,6 +5010,12 @@ function buildDoctorDiagnosis(model, options = {}) {
|
|
|
4532
5010
|
lines.push("✅ Compat fully configured.");
|
|
4533
5011
|
appendOptionalOpenAIProxyCompatAdviceLines(lines, optionalOpenAIProxyCompat);
|
|
4534
5012
|
} else lines.push(...getCompatCheckNotApplicableLines(model));
|
|
5013
|
+
if (options.promptCacheKey400 && isPromptCacheKeyUnsupportedApplicable(model)) {
|
|
5014
|
+
lines.push("");
|
|
5015
|
+
lines.push("⚠️ This model previously rejected prompt_cache_key with an explicit HTTP 400 signal.");
|
|
5016
|
+
lines.push(" /cache-optimizer fix will offer a precise model-scoped opt-out.");
|
|
5017
|
+
lines.push(" The opt-out removes both prompt_cache_key and promptCacheKey from the final request body.");
|
|
5018
|
+
}
|
|
4535
5019
|
if (isPromptCacheRetention400Applicable(model)) {
|
|
4536
5020
|
lines.push("");
|
|
4537
5021
|
if (options.promptCacheRetention400) {
|
|
@@ -5900,6 +6384,31 @@ async function atomicReplaceTextFilePreservingMode(targetPath, content, mode, pu
|
|
|
5900
6384
|
throw error;
|
|
5901
6385
|
}
|
|
5902
6386
|
}
|
|
6387
|
+
async function atomicCreateTextFileNoReplace(targetPath, content, mode, purpose, beforeLink) {
|
|
6388
|
+
const tempPath = uniqueTempPath(targetPath, purpose);
|
|
6389
|
+
try {
|
|
6390
|
+
await writeFile(tempPath, content, {
|
|
6391
|
+
encoding: "utf8",
|
|
6392
|
+
mode,
|
|
6393
|
+
flag: "wx"
|
|
6394
|
+
});
|
|
6395
|
+
const tempInfo = await lstat(tempPath);
|
|
6396
|
+
if (tempInfo.isSymbolicLink() || !tempInfo.isFile()) throw new Error("temporary creation is not a regular file");
|
|
6397
|
+
await chmod(tempPath, mode);
|
|
6398
|
+
if (beforeLink) await beforeLink();
|
|
6399
|
+
await link(tempPath, targetPath);
|
|
6400
|
+
await unlink(tempPath).catch((cleanupError) => {
|
|
6401
|
+
console.warn(`${LOG_PREFIX}: committed config file but failed to remove its temporary hard link`, cleanupError);
|
|
6402
|
+
});
|
|
6403
|
+
} catch (error) {
|
|
6404
|
+
try {
|
|
6405
|
+
await unlink(tempPath);
|
|
6406
|
+
} catch (cleanupError) {
|
|
6407
|
+
if (getErrorCode(cleanupError) !== "ENOENT") console.warn(`${LOG_PREFIX}: failed to remove temporary config file`, cleanupError);
|
|
6408
|
+
}
|
|
6409
|
+
throw error;
|
|
6410
|
+
}
|
|
6411
|
+
}
|
|
5903
6412
|
async function atomicRestoreFileFromBackup(backupPath, targetPath, mode, guard, beforeRename) {
|
|
5904
6413
|
const backupInfo = await lstat(backupPath);
|
|
5905
6414
|
if (backupInfo.isSymbolicLink() || !backupInfo.isFile()) throw new Error("backup is not a regular file; refusing atomic restore");
|
|
@@ -6233,7 +6742,13 @@ async function writeModelsJsonFixReceipt(receipt, receiptPath = FIX_RECEIPT_PATH
|
|
|
6233
6742
|
await assertReceiptDestinationUnchanged();
|
|
6234
6743
|
if (beforeRename) await beforeRename();
|
|
6235
6744
|
await assertReceiptDestinationUnchanged();
|
|
6236
|
-
await rename(tempPath, receiptPath);
|
|
6745
|
+
if (existingReceiptInfo) await rename(tempPath, receiptPath);
|
|
6746
|
+
else {
|
|
6747
|
+
await link(tempPath, receiptPath);
|
|
6748
|
+
await unlink(tempPath).catch((cleanupError) => {
|
|
6749
|
+
console.warn(`${LOG_PREFIX}: committed fix receipt but failed to remove its temporary hard link`, cleanupError);
|
|
6750
|
+
});
|
|
6751
|
+
}
|
|
6237
6752
|
} catch (error) {
|
|
6238
6753
|
try {
|
|
6239
6754
|
await unlink(tempPath);
|
|
@@ -6557,6 +7072,8 @@ const __internals_for_tests = {
|
|
|
6557
7072
|
addOpenAIPromptCacheKey,
|
|
6558
7073
|
clampPromptCacheKey,
|
|
6559
7074
|
hasEffectivePromptCacheKey,
|
|
7075
|
+
omitOpenAIPromptCacheKeys,
|
|
7076
|
+
isPromptCacheKeyOmittedForModel,
|
|
6560
7077
|
isNonEmptyString,
|
|
6561
7078
|
shouldInjectOpenAIPromptCacheKey,
|
|
6562
7079
|
shouldInjectOpenAIPromptCacheKeyForModel,
|
|
@@ -6593,6 +7110,9 @@ const __internals_for_tests = {
|
|
|
6593
7110
|
isOpenAISdkHeader403Applicable,
|
|
6594
7111
|
hasPromptCacheRetentionUnsupportedSignal,
|
|
6595
7112
|
hasPromptCacheRetentionUnsupportedErrorMessage,
|
|
7113
|
+
hasPromptCacheKeyUnsupportedSignal,
|
|
7114
|
+
hasPromptCacheKeyUnsupportedErrorMessage,
|
|
7115
|
+
isPromptCacheKeyUnsupportedApplicable,
|
|
6596
7116
|
hasReasoningProtocolRejectionText,
|
|
6597
7117
|
hasReasoningProtocolRejectionSignal,
|
|
6598
7118
|
hasReasoningProtocolRejectionErrorMessage,
|
|
@@ -6776,11 +7296,23 @@ const __internals_for_tests = {
|
|
|
6776
7296
|
selectFooterStatsForModel,
|
|
6777
7297
|
parseFooterStatsMode,
|
|
6778
7298
|
parsePersistedCacheOptimizerConfig,
|
|
7299
|
+
readPersistedCacheOptimizerConfig,
|
|
7300
|
+
writePersistedCacheOptimizerConfig,
|
|
6779
7301
|
readPersistedFooterMode,
|
|
6780
7302
|
writePersistedFooterMode,
|
|
7303
|
+
setPersistedCacheOptimizerConfig,
|
|
6781
7304
|
resolveFooterStatsMode,
|
|
6782
7305
|
footerStatsMode,
|
|
6783
7306
|
CONFIG_FILE_PATH,
|
|
7307
|
+
CONFIG_RECEIPT_PATH,
|
|
7308
|
+
parsePromptCacheKeyConfigReceipt,
|
|
7309
|
+
isActionablePromptCacheKeyConfigReceipt,
|
|
7310
|
+
readPromptCacheKeyConfigReceipt,
|
|
7311
|
+
readPromptCacheKeyConfigReceiptSnapshot,
|
|
7312
|
+
writePromptCacheKeyConfigReceipt,
|
|
7313
|
+
applyPromptCacheKeyConfigFix,
|
|
7314
|
+
rollbackPromptCacheKeyConfig,
|
|
7315
|
+
configReceiptBackupPath,
|
|
6784
7316
|
FOOTER_MODE_ENV,
|
|
6785
7317
|
PI_ROUTING_REGISTRY_SYMBOL,
|
|
6786
7318
|
PI_CACHE_HINTS_SYMBOL,
|
|
@@ -6847,6 +7379,7 @@ const __internals_for_tests = {
|
|
|
6847
7379
|
formatCompatKeysForInsertion,
|
|
6848
7380
|
backupTimestamp,
|
|
6849
7381
|
atomicReplaceTextFilePreservingMode,
|
|
7382
|
+
atomicCreateTextFileNoReplace,
|
|
6850
7383
|
atomicRestoreFileFromBackup,
|
|
6851
7384
|
applyModelsJsonFixTransaction,
|
|
6852
7385
|
hashText,
|
|
@@ -6887,6 +7420,8 @@ function pi_cache_optimizer_default(pi) {
|
|
|
6887
7420
|
const reasoningProtocolFallbackState = getReasoningProtocolFallbackState();
|
|
6888
7421
|
const reasoningProtocolRejectedModels = reasoningProtocolFallbackState.modelKeys;
|
|
6889
7422
|
const warnedReasoningProtocolRejectedModels = reasoningProtocolFallbackState.warnedModelKeys;
|
|
7423
|
+
const promptCacheKeyRejectedModels = /* @__PURE__ */ new Set();
|
|
7424
|
+
const warnedPromptCacheKeyRejectedModels = /* @__PURE__ */ new Set();
|
|
6890
7425
|
let cacheStatsByModel = {};
|
|
6891
7426
|
let cacheStatsProcessByModel = {};
|
|
6892
7427
|
let cacheStatsTotalsByModel = {};
|
|
@@ -6910,7 +7445,7 @@ function pi_cache_optimizer_default(pi) {
|
|
|
6910
7445
|
let currentSessionHashSet = false;
|
|
6911
7446
|
let lastActualRoutedModel;
|
|
6912
7447
|
let latestCacheHint;
|
|
6913
|
-
|
|
7448
|
+
const providerRequestStates = [];
|
|
6914
7449
|
let shardCreatedAt = Date.now();
|
|
6915
7450
|
const PERSIST_DEBOUNCE_MS = 2e3;
|
|
6916
7451
|
const SHARD_REFRESH_DEBOUNCE_MS = 250;
|
|
@@ -6946,6 +7481,18 @@ function pi_cache_optimizer_default(pi) {
|
|
|
6946
7481
|
forceModelLevel: regular.forceModelLevel || observed.forceModelLevel
|
|
6947
7482
|
};
|
|
6948
7483
|
}
|
|
7484
|
+
function promptCacheKeyFixApplies(model) {
|
|
7485
|
+
return isPromptCacheKeyUnsupportedApplicable(model) && promptCacheKeyRejectedModels.has(modelKey(model));
|
|
7486
|
+
}
|
|
7487
|
+
function buildPromptCacheKeyConfigPreview(model) {
|
|
7488
|
+
return [
|
|
7489
|
+
`Target: ${modelKey(model)}`,
|
|
7490
|
+
`Action: persist prompt_cache_key mode = omit in ${CONFIG_FILE_PATH}.`,
|
|
7491
|
+
"The final request body will omit both prompt_cache_key and promptCacheKey.",
|
|
7492
|
+
"This can reduce provider-side prompt-cache reuse for this model; it does not change prompts, credentials, headers, or other models.",
|
|
7493
|
+
"The setting applies after Pi core has built the payload, including when Pi already supplied a key."
|
|
7494
|
+
];
|
|
7495
|
+
}
|
|
6949
7496
|
/** In-memory recent usage samples per model key (not persisted, cleared on reload). */
|
|
6950
7497
|
const recentSamplesByModelKey = /* @__PURE__ */ new Map();
|
|
6951
7498
|
function syncSessionHash(ctx) {
|
|
@@ -7305,7 +7852,7 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7305
7852
|
shardWatcher?.close();
|
|
7306
7853
|
shardWatcher = void 0;
|
|
7307
7854
|
latestCacheHint = void 0;
|
|
7308
|
-
|
|
7855
|
+
providerRequestStates.length = 0;
|
|
7309
7856
|
delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
|
|
7310
7857
|
uninstallCacheHintsService();
|
|
7311
7858
|
restoreCacheRetentionEnv(STARTUP_CACHE_RETENTION_ENV);
|
|
@@ -7367,7 +7914,12 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7367
7914
|
});
|
|
7368
7915
|
pi.on("before_provider_request", (event, ctx) => {
|
|
7369
7916
|
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
7370
|
-
|
|
7917
|
+
const snapshot = snapshotProviderRequestModel(requestModel);
|
|
7918
|
+
if (snapshot) providerRequestStates.push({
|
|
7919
|
+
model: snapshot,
|
|
7920
|
+
responseReceived: false,
|
|
7921
|
+
correlationAmbiguous: false
|
|
7922
|
+
});
|
|
7371
7923
|
let requestPayload = event.payload;
|
|
7372
7924
|
let toolOrderChanged = false;
|
|
7373
7925
|
if (isToolOrderEnabled() && isToolOrderingEligibleModel(requestModel)) {
|
|
@@ -7387,25 +7939,37 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7387
7939
|
}
|
|
7388
7940
|
}
|
|
7389
7941
|
}
|
|
7942
|
+
if (isPromptCacheKeyOmittedForModel(requestModel)) return omitOpenAIPromptCacheKeys(requestPayload) ?? (toolOrderChanged ? requestPayload : void 0);
|
|
7390
7943
|
if (!shouldInjectOpenAIPromptCacheKey() || !shouldInjectOpenAIPromptCacheKeyForModel(requestModel)) return toolOrderChanged ? requestPayload : void 0;
|
|
7391
7944
|
return addOpenAIPromptCacheKey(requestPayload, getSessionPromptCacheKey(ctx)) ?? (toolOrderChanged ? requestPayload : void 0);
|
|
7392
7945
|
});
|
|
7393
7946
|
pi.on("after_provider_response", async (event, ctx) => {
|
|
7394
|
-
const
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7947
|
+
const pendingStates = providerRequestStates.filter((state) => !state.responseReceived);
|
|
7948
|
+
let responseState;
|
|
7949
|
+
if (pendingStates.length === 1) responseState = pendingStates[0];
|
|
7950
|
+
else if (pendingStates.length > 1) {
|
|
7951
|
+
if (new Set(pendingStates.map((state) => modelKey(state.model))).size === 1) responseState = pendingStates[0];
|
|
7952
|
+
else for (const state of pendingStates) state.correlationAmbiguous = true;
|
|
7953
|
+
}
|
|
7954
|
+
if (responseState) responseState.responseReceived = true;
|
|
7955
|
+
const model = responseState?.model ?? (pendingStates.length === 0 ? resolveRouteModel(ctx.model, ctx) ?? ctx.model : void 0);
|
|
7956
|
+
if (!runtimeOptimizerEnabled || !model) return;
|
|
7399
7957
|
if (isReasoningProtocolRejectionSignalApplicable(model) && event.status === 400 && hasReasoningProtocolRejectionSignal(event.headers)) await notifyReasoningProtocolObservation(model, ctx, reasoningProtocolRejectedModels, warnedReasoningProtocolRejectedModels);
|
|
7400
|
-
if (event.status === 400) {
|
|
7401
|
-
|
|
7402
|
-
|
|
7958
|
+
if (event.status === 400 && isPromptCacheKeyUnsupportedApplicable(model) && hasPromptCacheKeyUnsupportedSignal(event.headers)) {
|
|
7959
|
+
const key = modelKey(model);
|
|
7960
|
+
promptCacheKeyRejectedModels.add(key);
|
|
7961
|
+
if (!warnedPromptCacheKeyRejectedModels.has(key)) {
|
|
7962
|
+
warnedPromptCacheKeyRejectedModels.add(key);
|
|
7963
|
+
ctx.ui.notify(`⚠️ ${LOG_PREFIX}: ${key} rejected prompt_cache_key. Run /cache-optimizer fix to review a precise model-scoped repair. No configuration was changed automatically.`, "warning");
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7966
|
+
if (event.status === 400 && isPromptCacheRetention400Applicable(model) && hasPromptCacheRetentionUnsupportedSignal(event.headers)) {
|
|
7403
7967
|
const key = modelKey(model);
|
|
7404
7968
|
promptCacheRetention400Models.add(key);
|
|
7405
|
-
if (warnedPromptCacheRetention400Models.has(key))
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7969
|
+
if (!warnedPromptCacheRetention400Models.has(key)) {
|
|
7970
|
+
warnedPromptCacheRetention400Models.add(key);
|
|
7971
|
+
ctx.ui.notify(`⚠️ ${LOG_PREFIX}: ${key} returned HTTP 400 while supportsLongCacheRetention is enabled. ` + getPromptCacheRetentionUnsupportedHint() + ` Run /cache-optimizer doctor for the exact edit location.`, "warning");
|
|
7972
|
+
}
|
|
7409
7973
|
}
|
|
7410
7974
|
if (event.status === 403) {
|
|
7411
7975
|
const key403 = modelKey(model);
|
|
@@ -7428,16 +7992,49 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7428
7992
|
pi.on("message_end", async (event, ctx) => {
|
|
7429
7993
|
syncSessionHash(ctx);
|
|
7430
7994
|
const msgRecord = asRecord(event.message);
|
|
7431
|
-
const
|
|
7432
|
-
|
|
7995
|
+
const requestCorrelationForMessage = msgRecord?.role === "assistant" ? (() => {
|
|
7996
|
+
const explicitModel = modelFromAssistantMessage(event.message, void 0);
|
|
7997
|
+
const explicitIndex = explicitModel ? providerRequestStates.findIndex((state) => modelKey(state.model) === modelKey(explicitModel)) : -1;
|
|
7998
|
+
const completedIndex = providerRequestStates.findIndex((state) => state.responseReceived);
|
|
7999
|
+
const contextModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
8000
|
+
const contextIndex = contextModel && !providerRequestStates.some((state) => state.correlationAmbiguous) ? providerRequestStates.findIndex((state) => modelKey(state.model) === modelKey(contextModel)) : -1;
|
|
8001
|
+
const index = explicitIndex >= 0 ? explicitIndex : completedIndex >= 0 ? completedIndex : contextIndex >= 0 ? contextIndex : 0;
|
|
8002
|
+
const state = providerRequestStates.splice(index, 1)[0];
|
|
8003
|
+
if (!state) return {
|
|
8004
|
+
model: void 0,
|
|
8005
|
+
ambiguous: false
|
|
8006
|
+
};
|
|
8007
|
+
return {
|
|
8008
|
+
model: state.correlationAmbiguous && explicitIndex < 0 ? void 0 : state.model,
|
|
8009
|
+
ambiguous: state.correlationAmbiguous && explicitIndex < 0
|
|
8010
|
+
};
|
|
8011
|
+
})() : {
|
|
8012
|
+
model: void 0,
|
|
8013
|
+
ambiguous: false
|
|
8014
|
+
};
|
|
8015
|
+
const requestModelForMessage = requestCorrelationForMessage.model;
|
|
8016
|
+
const contextualFallbackForMessage = requestCorrelationForMessage.ambiguous ? void 0 : resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
7433
8017
|
if (runtimeOptimizerEnabled && hasReasoningProtocolRejectionErrorMessage(event.message)) {
|
|
7434
|
-
const fallbackModel = requestModelForMessage ??
|
|
8018
|
+
const fallbackModel = requestModelForMessage ?? contextualFallbackForMessage;
|
|
7435
8019
|
const messageModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
|
|
7436
8020
|
const errorModel = messageModel ? findModelInRegistry(ctx.modelRegistry, messageModel.provider, messageModel.id) ?? messageModel : void 0;
|
|
7437
8021
|
if (isReasoningProtocolRejectionForModel(event.message, errorModel)) await notifyReasoningProtocolObservation(errorModel, ctx, reasoningProtocolRejectedModels, warnedReasoningProtocolRejectedModels);
|
|
7438
8022
|
}
|
|
8023
|
+
if (runtimeOptimizerEnabled && hasPromptCacheKeyUnsupportedErrorMessage(event.message)) {
|
|
8024
|
+
const fallbackModel = requestModelForMessage ?? contextualFallbackForMessage;
|
|
8025
|
+
const messageModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
|
|
8026
|
+
const errorModel = messageModel ? findModelInRegistry(ctx.modelRegistry, messageModel.provider, messageModel.id) ?? messageModel : void 0;
|
|
8027
|
+
if (errorModel && isPromptCacheKeyUnsupportedApplicable(errorModel)) {
|
|
8028
|
+
const key = modelKey(errorModel);
|
|
8029
|
+
promptCacheKeyRejectedModels.add(key);
|
|
8030
|
+
if (!warnedPromptCacheKeyRejectedModels.has(key)) {
|
|
8031
|
+
warnedPromptCacheKeyRejectedModels.add(key);
|
|
8032
|
+
ctx.ui.notify(`⚠️ ${LOG_PREFIX}: ${key} rejected prompt_cache_key. Run /cache-optimizer fix to review a precise model-scoped repair. No configuration was changed automatically.`, "warning");
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
7439
8036
|
if (runtimeOptimizerEnabled && hasPromptCacheRetentionUnsupportedErrorMessage(event.message)) {
|
|
7440
|
-
const fallbackModel = requestModelForMessage ??
|
|
8037
|
+
const fallbackModel = requestModelForMessage ?? contextualFallbackForMessage;
|
|
7441
8038
|
const messageModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
|
|
7442
8039
|
const errorModel = messageModel ? findModelInRegistry(ctx.modelRegistry, messageModel.provider, messageModel.id) ?? messageModel : void 0;
|
|
7443
8040
|
if (errorModel && isExplicitPromptCacheRetentionUnsupportedApplicable(errorModel)) {
|
|
@@ -7450,7 +8047,7 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7450
8047
|
}
|
|
7451
8048
|
}
|
|
7452
8049
|
if (hasAnthropicCacheTtlOrderError(event.message)) {
|
|
7453
|
-
const fallbackModel = requestModelForMessage ??
|
|
8050
|
+
const fallbackModel = requestModelForMessage ?? contextualFallbackForMessage;
|
|
7454
8051
|
const errorModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
|
|
7455
8052
|
if (errorModel && isAnthropicMessagesApi(errorModel.api)) {
|
|
7456
8053
|
const key = modelKey(errorModel);
|
|
@@ -7530,7 +8127,7 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7530
8127
|
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
|
|
7531
8128
|
} else if (subcommand === "disable") {
|
|
7532
8129
|
setRuntimeOptimizerEnabled(false);
|
|
7533
|
-
|
|
8130
|
+
providerRequestStates.length = 0;
|
|
7534
8131
|
await resetCurrentSessionStats();
|
|
7535
8132
|
await flushPersistCacheStats(cmdCtx);
|
|
7536
8133
|
await publishStatus(cmdCtx, model);
|
|
@@ -7543,6 +8140,7 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7543
8140
|
}
|
|
7544
8141
|
const diagnosis = buildDoctorDiagnosis(model, {
|
|
7545
8142
|
promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)),
|
|
8143
|
+
promptCacheKey400: promptCacheKeyRejectedModels.has(modelKey(model)),
|
|
7546
8144
|
anthropicTtlOrderError: anthropicTtlOrderErrorModels.has(modelKey(model)),
|
|
7547
8145
|
sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)),
|
|
7548
8146
|
openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model))
|
|
@@ -7577,7 +8175,7 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7577
8175
|
const nextMode = requestedMode;
|
|
7578
8176
|
try {
|
|
7579
8177
|
await writePersistedFooterMode(nextMode);
|
|
7580
|
-
|
|
8178
|
+
setPersistedCacheOptimizerConfig(readPersistedCacheOptimizerConfig());
|
|
7581
8179
|
lastStatusText = void 0;
|
|
7582
8180
|
await publishStatus(cmdCtx, model);
|
|
7583
8181
|
const resolved = resolveFooterStatsMode(persistedFooterStatsMode);
|
|
@@ -7598,6 +8196,28 @@ function pi_cache_optimizer_default(pi) {
|
|
|
7598
8196
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
7599
8197
|
return;
|
|
7600
8198
|
}
|
|
8199
|
+
const configReceiptSnapshot = await readPromptCacheKeyConfigReceiptSnapshot();
|
|
8200
|
+
const configReceipt = configReceiptSnapshot?.receipt;
|
|
8201
|
+
const modelsReceipt = await readModelsJsonFixReceipt();
|
|
8202
|
+
if (isActionablePromptCacheKeyConfigReceipt(configReceipt) && configReceipt.provider === model.provider && configReceipt.modelId === model.id && (!isActionableModelsJsonFixReceipt(modelsReceipt) || modelsReceipt.provider !== model.provider || modelsReceipt.modelId !== model.id || configReceipt.appliedAt >= modelsReceipt.appliedAt)) {
|
|
8203
|
+
if (!cmdCtx.hasUI) {
|
|
8204
|
+
cmdCtx.ui.notify("❌ Rollback requires interactive confirmation. No changes were made.\nRun /cache-optimizer rollback in Pi's interactive UI to restore the prompt-cache-key setting.", "warning");
|
|
8205
|
+
return;
|
|
8206
|
+
}
|
|
8207
|
+
if (!await cmdCtx.ui.confirm("Cache Optimizer — Rollback prompt_cache_key opt-out", `Model: ${modelKey(model)}\nAction: restore the extension config before the confirmed opt-out.\nFooter mode and unrelated configuration will be preserved.\nAfterward, run /reload or restart Pi.\n\nProceed with rollback?`)) {
|
|
8208
|
+
cmdCtx.ui.notify("No changes were made. Rollback canceled by user.", "info");
|
|
8209
|
+
return;
|
|
8210
|
+
}
|
|
8211
|
+
try {
|
|
8212
|
+
if (!configReceiptSnapshot) throw new Error("prompt-cache-key receipt changed since the rollback preview");
|
|
8213
|
+
await rollbackPromptCacheKeyConfig(configReceiptSnapshot);
|
|
8214
|
+
setPersistedCacheOptimizerConfig(readPersistedCacheOptimizerConfig());
|
|
8215
|
+
cmdCtx.ui.notify(`✅ Restored prompt_cache_key behavior for ${modelKey(model)}. Run /reload or restart Pi for the change to take effect.`, "info");
|
|
8216
|
+
} catch (error) {
|
|
8217
|
+
cmdCtx.ui.notify(`❌ Prompt cache key rollback refused: ${error instanceof Error ? error.message : String(error)}. No changes were made.`, "error");
|
|
8218
|
+
}
|
|
8219
|
+
return;
|
|
8220
|
+
}
|
|
7601
8221
|
if (!cmdCtx.hasUI) {
|
|
7602
8222
|
const receipt = await readModelsJsonFixReceipt();
|
|
7603
8223
|
const backupHint = receipt && isActionableModelsJsonFixReceipt(receipt) ? ` The recorded backup is ${receipt.backupFile} next to ${getModelsJsonDisplayPath()}.` : " Check the recorded models.json backup manually if a fix receipt exists.";
|
|
@@ -7673,7 +8293,48 @@ Run /cache-optimizer rollback in Pi's interactive UI.${backupHint} Then run /rel
|
|
|
7673
8293
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
7674
8294
|
return;
|
|
7675
8295
|
}
|
|
7676
|
-
const
|
|
8296
|
+
const promptCacheKeyRequested = commandParts[1] === "prompt-cache-key";
|
|
8297
|
+
const promptCacheKeyFixRequested = promptCacheKeyRequested || promptCacheKeyFixApplies(model);
|
|
8298
|
+
if (promptCacheKeyRequested && !isPromptCacheKeyUnsupportedApplicable(model)) {
|
|
8299
|
+
cmdCtx.ui.notify("ℹ️ Prompt cache key opt-out applies only to a known OpenAI-compatible provider/model endpoint. No changes were made.", "info");
|
|
8300
|
+
return;
|
|
8301
|
+
}
|
|
8302
|
+
const suggestion = promptCacheKeyFixRequested ? void 0 : buildCommandFixSuggestion(model);
|
|
8303
|
+
if (promptCacheKeyFixRequested) {
|
|
8304
|
+
if (isPromptCacheKeyOmittedForModel(model)) {
|
|
8305
|
+
cmdCtx.ui.notify(`✅ prompt_cache_key is already omitted for "${modelKey(model)}".`, "info");
|
|
8306
|
+
return;
|
|
8307
|
+
}
|
|
8308
|
+
if (!cmdCtx.hasUI) {
|
|
8309
|
+
cmdCtx.ui.notify(`❌ Non-interactive terminal detected. Prompt cache key opt-out requires UI confirmation. No changes were made.
|
|
8310
|
+
Run /cache-optimizer fix prompt-cache-key in Pi's interactive UI for ${modelKey(model)}.`, "warning");
|
|
8311
|
+
return;
|
|
8312
|
+
}
|
|
8313
|
+
const preview = [
|
|
8314
|
+
"📝 Preview of extension configuration change:",
|
|
8315
|
+
...buildPromptCacheKeyConfigPreview(model),
|
|
8316
|
+
"",
|
|
8317
|
+
"⚠️ Risk notice:",
|
|
8318
|
+
" 1. This affects all sessions using this exact provider/model.",
|
|
8319
|
+
" 2. Provider prompt-cache reuse may decrease because both key spellings are removed.",
|
|
8320
|
+
" 3. This does not modify Pi's models.json, credentials, prompts, headers, or other models.",
|
|
8321
|
+
" 4. The extension config will be backed up and Pi must be reloaded/restarted.",
|
|
8322
|
+
"",
|
|
8323
|
+
"Apply this persistent opt-out?"
|
|
8324
|
+
].join("\n");
|
|
8325
|
+
if (!await cmdCtx.ui.confirm("Cache Optimizer — Omit prompt_cache_key", preview)) {
|
|
8326
|
+
cmdCtx.ui.notify("No changes were made. Canceled by user.", "info");
|
|
8327
|
+
return;
|
|
8328
|
+
}
|
|
8329
|
+
try {
|
|
8330
|
+
const result = await applyPromptCacheKeyConfigFix(model);
|
|
8331
|
+
setPersistedCacheOptimizerConfig(readPersistedCacheOptimizerConfig());
|
|
8332
|
+
cmdCtx.ui.notify(`✅ Prompt cache key opt-out saved for ${modelKey(model)}.\nConfig backup saved to: ${result.backupPath}\nRun /reload or restart Pi for the change to take effect. Use /cache-optimizer rollback to restore it.`, "info");
|
|
8333
|
+
} catch (error) {
|
|
8334
|
+
cmdCtx.ui.notify(`❌ Could not save prompt cache key opt-out: ${error instanceof Error ? error.message : String(error)}. No changes were made.`, "error");
|
|
8335
|
+
}
|
|
8336
|
+
return;
|
|
8337
|
+
}
|
|
7677
8338
|
if (!suggestion) {
|
|
7678
8339
|
const key = modelKey(model);
|
|
7679
8340
|
cmdCtx.ui.notify(`✅ Nothing to fix for "${key}". Compat already configured.`, "info");
|
|
@@ -7841,7 +8502,8 @@ Run /cache-optimizer rollback in Pi's interactive UI.${backupHint} Then run /rel
|
|
|
7841
8502
|
"Doctor — Show cache configuration",
|
|
7842
8503
|
"Stats — Show current-session model statistics",
|
|
7843
8504
|
"Compat — Show compat suggestion",
|
|
7844
|
-
"Fix — Auto-fix compat issues (writes models.json)",
|
|
8505
|
+
"Fix — Auto-fix compat issues (writes models.json or extension config)",
|
|
8506
|
+
"Disable prompt_cache_key — Omit it for the active model",
|
|
7845
8507
|
"Rollback — Undo the latest confirmed fix",
|
|
7846
8508
|
"Footer mode — Choose total, session, or process stats",
|
|
7847
8509
|
"Reset — Reset local provider/model stats",
|
|
@@ -7854,8 +8516,9 @@ Run /cache-optimizer rollback in Pi's interactive UI.${backupHint} Then run /rel
|
|
|
7854
8516
|
else if (choice === menuOptions[3]) await handleCacheOptimizerCommand("stats", cmdCtx);
|
|
7855
8517
|
else if (choice === menuOptions[4]) await handleCacheOptimizerCommand("compat", cmdCtx);
|
|
7856
8518
|
else if (choice === menuOptions[5]) await handleCacheOptimizerCommand("fix", cmdCtx);
|
|
7857
|
-
else if (choice === menuOptions[6]) await handleCacheOptimizerCommand("
|
|
7858
|
-
else if (choice === menuOptions[7])
|
|
8519
|
+
else if (choice === menuOptions[6]) await handleCacheOptimizerCommand("fix prompt-cache-key", cmdCtx);
|
|
8520
|
+
else if (choice === menuOptions[7]) await handleCacheOptimizerCommand("rollback", cmdCtx);
|
|
8521
|
+
else if (choice === menuOptions[8]) {
|
|
7859
8522
|
const modeOptions = [
|
|
7860
8523
|
"session — Current Pi conversation session (default)",
|
|
7861
8524
|
"total — All local sessions today",
|
|
@@ -7865,7 +8528,7 @@ Run /cache-optimizer rollback in Pi's interactive UI.${backupHint} Then run /rel
|
|
|
7865
8528
|
const modeChoice = await cmdCtx.ui.select("Footer cache stats mode", modeOptions);
|
|
7866
8529
|
const nextMode = modeChoice === modeOptions[0] ? "session" : modeChoice === modeOptions[1] ? "total" : modeChoice === modeOptions[2] ? "process" : void 0;
|
|
7867
8530
|
if (nextMode) await handleCacheOptimizerCommand(`config footer-mode ${nextMode}`, cmdCtx);
|
|
7868
|
-
} else if (choice === menuOptions[
|
|
8531
|
+
} else if (choice === menuOptions[9]) await handleCacheOptimizerCommand("reset", cmdCtx);
|
|
7869
8532
|
return;
|
|
7870
8533
|
}
|
|
7871
8534
|
const diagnosis = [];
|
|
@@ -7878,7 +8541,8 @@ Run /cache-optimizer rollback in Pi's interactive UI.${backupHint} Then run /rel
|
|
|
7878
8541
|
diagnosis.push(" stats contributors — Show per-session contributors for the active model");
|
|
7879
8542
|
diagnosis.push(" compat — Show compat suggestion with edit location");
|
|
7880
8543
|
diagnosis.push(" config footer-mode total|session|process — Persist the footer stats mode");
|
|
7881
|
-
diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
|
|
8544
|
+
diagnosis.push(" fix — Auto-fix compat issues (writes models.json or extension config, requires UI)");
|
|
8545
|
+
diagnosis.push(" fix prompt-cache-key — Explicitly omit prompt_cache_key for the active model");
|
|
7882
8546
|
diagnosis.push(" rollback — Undo the latest confirmed fix (requires UI confirmation)");
|
|
7883
8547
|
diagnosis.push(" reset — Reset local provider/model stats for current model (does not affect upstream)");
|
|
7884
8548
|
diagnosis.push("");
|
|
@@ -7911,4 +8575,4 @@ Run /cache-optimizer rollback in Pi's interactive UI.${backupHint} Then run /rel
|
|
|
7911
8575
|
//#endregion
|
|
7912
8576
|
export { __internals_for_tests, pi_cache_optimizer_default as default };
|
|
7913
8577
|
|
|
7914
|
-
//# sourceMappingURL=pi-cache-optimizer-
|
|
8578
|
+
//# sourceMappingURL=pi-cache-optimizer-DjqO0Quf.mjs.map
|