@agimon-ai/doompi-cache 0.0.1-alpha.45 → 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.
@@ -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_@earendil-works+pi-coding-agent@0.86.0_supports-color@7.2.0_zod@4.6.2_/node_modules/pi-cache-optimizer/index.ts
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
- const parsed = parsePersistedCacheOptimizerConfig(JSON.parse(readFileSync(configPath, "utf8")));
1225
- if (!parsed) throw new Error("invalid footer config schema");
1226
- return parsed.footerMode;
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") console.warn(`${LOG_PREFIX}: failed to read footer config; using environment/default mode`, error);
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
- const payload = {
1235
- version: 1,
1236
- footerMode: mode
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 tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
1239
- await writeFile(tempPath, JSON.stringify(payload, null, 2) + "\n", "utf8");
1240
- await rename(tempPath, configPath);
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 persistedFooterStatsMode = readPersistedFooterMode();
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
- let pendingProviderResponseModel;
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
- pendingProviderResponseModel = void 0;
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
- pendingProviderResponseModel = runtimeOptimizerEnabled ? snapshotProviderRequestModel(requestModel) : void 0;
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 model = pendingProviderResponseModel ?? resolveRouteModel(ctx.model, ctx) ?? ctx.model;
7395
- if (!runtimeOptimizerEnabled || !model) {
7396
- pendingProviderResponseModel = void 0;
7397
- return;
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
- if (!isPromptCacheRetention400Applicable(model)) return;
7402
- if (!hasPromptCacheRetentionUnsupportedSignal(event.headers)) return;
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)) return;
7406
- warnedPromptCacheRetention400Models.add(key);
7407
- ctx.ui.notify(`⚠️ ${LOG_PREFIX}: ${key} returned HTTP 400 while supportsLongCacheRetention is enabled. ` + getPromptCacheRetentionUnsupportedHint() + ` Run /cache-optimizer doctor for the exact edit location.`, "warning");
7408
- return;
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 requestModelForMessage = msgRecord?.role === "assistant" ? pendingProviderResponseModel : void 0;
7432
- if (msgRecord?.role === "assistant") pendingProviderResponseModel = void 0;
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 ?? resolveRouteModel(ctx.model, ctx) ?? ctx.model;
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 ?? resolveRouteModel(ctx.model, ctx) ?? ctx.model;
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 ?? resolveRouteModel(ctx.model, ctx) ?? ctx.model;
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
- pendingProviderResponseModel = void 0;
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
- persistedFooterStatsMode = nextMode;
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 suggestion = buildCommandFixSuggestion(model);
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("rollback", cmdCtx);
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[8]) await handleCacheOptimizerCommand("reset", cmdCtx);
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-BfO9Qmqm.mjs.map
8578
+ //# sourceMappingURL=pi-cache-optimizer-DjqO0Quf.mjs.map