pi-cache-optimizer 2.6.21 → 2.6.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/README.zh-CN.md +8 -0
- package/index.ts +205 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,14 @@ Notes:
|
|
|
113
113
|
- For DeepSeek models, the Pi Mono guidance expects `compat.requiresReasoningContentOnAssistantMessages: true` and `compat.thinkingFormat: "deepseek"` alongside cache/session-affinity flags when the endpoint supports them.
|
|
114
114
|
- This extension's `doctor` and `compat` commands only advise; they do not modify `models.json`.
|
|
115
115
|
|
|
116
|
+
## Anthropic cache TTL compatibility
|
|
117
|
+
|
|
118
|
+
Anthropic processes cache breakpoints in `tools → system → messages` order and rejects a `ttl: "1h"` breakpoint that appears after a 5-minute breakpoint. An ephemeral `cache_control` without `ttl` uses the default 5-minute retention.
|
|
119
|
+
|
|
120
|
+
For `anthropic-messages` channels, the extension checks the final serialized payload before sending. If it detects an invalid short-to-long transition, it conservatively downgrades that request's `1h` breakpoints to the default 5-minute TTL. Legal long-only and `1h → 5m` payloads are left unchanged. This is based on payload structure, not provider names such as `pipi-cc`.
|
|
121
|
+
|
|
122
|
+
If a proxy adds its own conflicting cache controls after Pi's request hook, set `compat.supportsLongCacheRetention: false` for that provider/model so Pi does not emit `ttl: "1h"`.
|
|
123
|
+
|
|
116
124
|
## Adaptive thinking models
|
|
117
125
|
|
|
118
126
|
Claude models from opus-4.6 / sonnet-4.6 (including Sonnet 5) / fable-5 onwards require `forceAdaptiveThinking: true` in compat. Kimi Coding K3 (`k3`) and `kimi-for-coding` also use adaptive thinking and need `allowEmptySignature: true` so replayed empty-signature thinking blocks remain valid. Without the required compat, Pi may send a legacy thinking payload or replay thinking incorrectly.
|
package/README.zh-CN.md
CHANGED
|
@@ -113,6 +113,14 @@ Pi 0.81+ 也内置了使用 OpenAI-shaped transport 的 `llama.cpp` provider。P
|
|
|
113
113
|
- 对 DeepSeek 模型,Pi Mono 指南期望在支持时同时设置 `compat.requiresReasoningContentOnAssistantMessages: true` 和 `compat.thinkingFormat: "deepseek"`,再配合缓存 / session-affinity 相关 compat。
|
|
114
114
|
- 本扩展的 `doctor` 和 `compat` 命令只给建议,不会修改 `models.json`。
|
|
115
115
|
|
|
116
|
+
## Anthropic 缓存 TTL 兼容
|
|
117
|
+
|
|
118
|
+
Anthropic 按 `tools → system → messages` 顺序处理 cache breakpoint,并拒绝位于 5 分钟 breakpoint 之后的 `ttl: "1h"` breakpoint。省略 `ttl` 的 ephemeral `cache_control` 使用默认 5 分钟保留时间。
|
|
119
|
+
|
|
120
|
+
对于 `anthropic-messages` 渠道,本扩展会在发送前检查最终序列化 payload。如果检测到非法的短 TTL → 长 TTL 顺序,会保守地把该请求中的 `1h` breakpoint 降级为默认 5 分钟 TTL。合法的纯 `1h` 和 `1h → 5m` payload 保持不变。判断依据是 payload 结构,而不是 `pipi-cc` 等 provider 名称。
|
|
121
|
+
|
|
122
|
+
如果代理在 Pi request hook 之后自行添加冲突的 cache control,可为该 provider/model 设置 `compat.supportsLongCacheRetention: false`,使 Pi 不再发送 `ttl: "1h"`。
|
|
123
|
+
|
|
116
124
|
## Adaptive thinking 模型
|
|
117
125
|
|
|
118
126
|
Claude 从 opus-4.6 / sonnet-4.6(含 Sonnet 5)/ fable-5 开始需要在 compat 中设置 `forceAdaptiveThinking: true`。Kimi Coding K3(`k3`)和 `kimi-for-coding` 也使用 adaptive thinking,并需要 `allowEmptySignature: true`,以正确重放空 signature 的 thinking block。缺少这些 compat 时,Pi 可能发送旧版 thinking payload 或错误重放 thinking。
|
package/index.ts
CHANGED
|
@@ -14,6 +14,12 @@ type CacheRetentionEnvSnapshot = {
|
|
|
14
14
|
|
|
15
15
|
const PI_CACHE_RETENTION_ENV = "PI_CACHE_RETENTION";
|
|
16
16
|
const LONG_CACHE_RETENTION_VALUE = "long";
|
|
17
|
+
const PI_CACHE_RETENTION_BASELINE_SYMBOL = Symbol.for("pi.cache.optimizer.retention-baseline.v1");
|
|
18
|
+
|
|
19
|
+
type CacheRetentionBaselineV1 = {
|
|
20
|
+
version: 1;
|
|
21
|
+
snapshot: CacheRetentionEnvSnapshot;
|
|
22
|
+
};
|
|
17
23
|
|
|
18
24
|
function captureCacheRetentionEnv(env: MutableEnv = process.env): CacheRetentionEnvSnapshot {
|
|
19
25
|
return {
|
|
@@ -36,7 +42,28 @@ function restoreCacheRetentionEnv(snapshot: CacheRetentionEnvSnapshot, env: Muta
|
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
function isCacheRetentionBaselineV1(value: unknown): value is CacheRetentionBaselineV1 {
|
|
46
|
+
if (typeof value !== "object" || value === null) return false;
|
|
47
|
+
const record = value as { version?: unknown; snapshot?: unknown };
|
|
48
|
+
if (record.version !== 1 || typeof record.snapshot !== "object" || record.snapshot === null) return false;
|
|
49
|
+
const snapshot = record.snapshot as { wasSet?: unknown; value?: unknown };
|
|
50
|
+
return typeof snapshot.wasSet === "boolean" &&
|
|
51
|
+
(snapshot.value === undefined || typeof snapshot.value === "string");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getOrCaptureCacheRetentionBaseline(
|
|
55
|
+
env: MutableEnv = process.env,
|
|
56
|
+
globals: Record<symbol, unknown> = globalThis as Record<symbol, unknown>,
|
|
57
|
+
): CacheRetentionEnvSnapshot {
|
|
58
|
+
const existing = globals[PI_CACHE_RETENTION_BASELINE_SYMBOL];
|
|
59
|
+
if (isCacheRetentionBaselineV1(existing)) return { ...existing.snapshot };
|
|
60
|
+
|
|
61
|
+
const snapshot = captureCacheRetentionEnv(env);
|
|
62
|
+
globals[PI_CACHE_RETENTION_BASELINE_SYMBOL] = { version: 1, snapshot: { ...snapshot } };
|
|
63
|
+
return snapshot;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const STARTUP_CACHE_RETENTION_ENV = getOrCaptureCacheRetentionBaseline();
|
|
40
67
|
|
|
41
68
|
/**
|
|
42
69
|
* Pi Cache Optimizer (formerly pi-deepseek-cache-optimizer)
|
|
@@ -76,6 +103,7 @@ const NO_SKILL_COMPRESSION_ENV = "PI_CACHE_OPTIMIZER_NO_SKILL_COMPRESSION";
|
|
|
76
103
|
const NO_PROMPT_REWRITE_ENV = "PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE";
|
|
77
104
|
const PI_ROUTING_REGISTRY_SYMBOL = Symbol.for("pi.routing.registry.v1");
|
|
78
105
|
const PI_CACHE_HINTS_SYMBOL = Symbol.for("pi.cache.hints.v1");
|
|
106
|
+
const PI_CACHE_HINTS_OWNER_SYMBOL = Symbol.for("pi.cache.optimizer.hints-owner.v1");
|
|
79
107
|
|
|
80
108
|
let runtimeOptimizerEnabled = true;
|
|
81
109
|
|
|
@@ -921,16 +949,29 @@ function getCacheHintsService(): PiCacheHintsV1 | undefined {
|
|
|
921
949
|
return isCacheHintsServiceV1(candidate) ? candidate : undefined;
|
|
922
950
|
}
|
|
923
951
|
|
|
924
|
-
function
|
|
952
|
+
function markOptimizerOwnedCacheHintsService(service: PiCacheHintsV1): PiCacheHintsV1 {
|
|
953
|
+
(service as PiCacheHintsV1 & Record<symbol, unknown>)[PI_CACHE_HINTS_OWNER_SYMBOL] = true;
|
|
954
|
+
return service;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function isOptimizerOwnedCacheHintsService(value: unknown): boolean {
|
|
958
|
+
return typeof value === "object" && value !== null &&
|
|
959
|
+
(value as Record<symbol, unknown>)[PI_CACHE_HINTS_OWNER_SYMBOL] === true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function installCacheHintsService(
|
|
963
|
+
service: PiCacheHintsV1,
|
|
964
|
+
options: { discardPrevious?: (value: unknown) => boolean } = {},
|
|
965
|
+
): () => void {
|
|
925
966
|
const globals = getProtocolGlobal();
|
|
926
967
|
const previous = globals[PI_CACHE_HINTS_SYMBOL];
|
|
927
968
|
globals[PI_CACHE_HINTS_SYMBOL] = service;
|
|
928
969
|
return () => {
|
|
929
970
|
if (globals[PI_CACHE_HINTS_SYMBOL] !== service) return;
|
|
930
|
-
if (previous
|
|
931
|
-
delete globals[PI_CACHE_HINTS_SYMBOL];
|
|
932
|
-
} else {
|
|
971
|
+
if (previous !== undefined && !options.discardPrevious?.(previous)) {
|
|
933
972
|
globals[PI_CACHE_HINTS_SYMBOL] = previous;
|
|
973
|
+
} else {
|
|
974
|
+
delete globals[PI_CACHE_HINTS_SYMBOL];
|
|
934
975
|
}
|
|
935
976
|
};
|
|
936
977
|
}
|
|
@@ -1129,6 +1170,10 @@ function isOpenAICompatibleApi(api: unknown): boolean {
|
|
|
1129
1170
|
return value === "openai-completions" || value === "openai-responses";
|
|
1130
1171
|
}
|
|
1131
1172
|
|
|
1173
|
+
function isAnthropicMessagesApi(api: unknown): boolean {
|
|
1174
|
+
return lower(api) === "anthropic-messages";
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1132
1177
|
function isOpenAICompatibleProxyApi(api: unknown): boolean {
|
|
1133
1178
|
return lower(api) === "openai-completions";
|
|
1134
1179
|
}
|
|
@@ -1155,6 +1200,62 @@ function shouldInjectOpenAIPromptCacheKeyForModel(model: PiModel | undefined): b
|
|
|
1155
1200
|
return isOpenAICompatibleApi(model?.api);
|
|
1156
1201
|
}
|
|
1157
1202
|
|
|
1203
|
+
function collectAnthropicCacheControlsInWireOrder(payload: unknown): UnknownRecord[] {
|
|
1204
|
+
const record = asRecord(payload);
|
|
1205
|
+
if (!record) return [];
|
|
1206
|
+
|
|
1207
|
+
const controls: UnknownRecord[] = [];
|
|
1208
|
+
const collectFromBlock = (value: unknown): void => {
|
|
1209
|
+
const cacheControl = asRecord(asRecord(value)?.cache_control);
|
|
1210
|
+
if (cacheControl?.type === "ephemeral") controls.push(cacheControl);
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
// Anthropic processes cache breakpoints in tools → system → messages order.
|
|
1214
|
+
if (Array.isArray(record.tools)) {
|
|
1215
|
+
for (const tool of record.tools) collectFromBlock(tool);
|
|
1216
|
+
}
|
|
1217
|
+
if (Array.isArray(record.system)) {
|
|
1218
|
+
for (const block of record.system) collectFromBlock(block);
|
|
1219
|
+
}
|
|
1220
|
+
if (Array.isArray(record.messages)) {
|
|
1221
|
+
for (const message of record.messages) {
|
|
1222
|
+
const content = asRecord(message)?.content;
|
|
1223
|
+
if (!Array.isArray(content)) continue;
|
|
1224
|
+
for (const block of content) collectFromBlock(block);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return controls;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Anthropic requires every 1h cache breakpoint to precede every 5m breakpoint.
|
|
1233
|
+
* An ephemeral cache_control without ttl uses Anthropic's default 5m retention.
|
|
1234
|
+
* If the final serialized payload contains a short → long transition, downgrade
|
|
1235
|
+
* all 1h breakpoints to the default 5m so the request remains valid.
|
|
1236
|
+
*/
|
|
1237
|
+
function normalizeAnthropicCacheControlTtlOrder(payload: unknown): boolean {
|
|
1238
|
+
const controls = collectAnthropicCacheControlsInWireOrder(payload);
|
|
1239
|
+
let seenShort = false;
|
|
1240
|
+
let hasInvalidLongAfterShort = false;
|
|
1241
|
+
|
|
1242
|
+
for (const control of controls) {
|
|
1243
|
+
const ttl = control.ttl;
|
|
1244
|
+
if (ttl === undefined || ttl === "5m") {
|
|
1245
|
+
seenShort = true;
|
|
1246
|
+
} else if (ttl === "1h" && seenShort) {
|
|
1247
|
+
hasInvalidLongAfterShort = true;
|
|
1248
|
+
break;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (!hasInvalidLongAfterShort) return false;
|
|
1252
|
+
|
|
1253
|
+
for (const control of controls) {
|
|
1254
|
+
if (control.ttl === "1h") delete control.ttl;
|
|
1255
|
+
}
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1158
1259
|
function isResponsesPromptRewriteBypassApi(api: unknown): boolean {
|
|
1159
1260
|
const value = lower(api);
|
|
1160
1261
|
return value === "openai-codex-responses" || value === "openai-responses" || value === "azure-openai-responses";
|
|
@@ -3919,6 +4020,15 @@ function mergeCacheSessions(
|
|
|
3919
4020
|
return sessions;
|
|
3920
4021
|
}
|
|
3921
4022
|
|
|
4023
|
+
function createSerializedAsyncRunner(): <T>(operation: () => Promise<T>) => Promise<T> {
|
|
4024
|
+
let tail: Promise<unknown> = Promise.resolve();
|
|
4025
|
+
return <T>(operation: () => Promise<T>): Promise<T> => {
|
|
4026
|
+
const result = tail.then(operation);
|
|
4027
|
+
tail = result.catch(() => undefined);
|
|
4028
|
+
return result;
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
4031
|
+
|
|
3922
4032
|
function mergeCacheTotals(
|
|
3923
4033
|
existingTotalsByModel: Record<string, CacheStats>,
|
|
3924
4034
|
stateTotalsByModel: Record<string, CacheStats>,
|
|
@@ -5826,7 +5936,10 @@ export const __internals_for_tests = {
|
|
|
5826
5936
|
shouldInjectOpenAIPromptCacheKey,
|
|
5827
5937
|
shouldInjectOpenAIPromptCacheKeyForModel,
|
|
5828
5938
|
isOpenAICompatibleApi,
|
|
5939
|
+
isAnthropicMessagesApi,
|
|
5829
5940
|
isOpenAICompatibleProxyApi,
|
|
5941
|
+
collectAnthropicCacheControlsInWireOrder,
|
|
5942
|
+
normalizeAnthropicCacheControlTtlOrder,
|
|
5830
5943
|
isPiBuiltInLlamaCppModel,
|
|
5831
5944
|
isResponsesPromptRewriteBypassApi,
|
|
5832
5945
|
isMistralConversationsApi,
|
|
@@ -6025,10 +6138,15 @@ export const __internals_for_tests = {
|
|
|
6025
6138
|
isVirtualRoutingModel,
|
|
6026
6139
|
installCacheHintsService,
|
|
6027
6140
|
getCacheHintsService,
|
|
6141
|
+
markOptimizerOwnedCacheHintsService,
|
|
6142
|
+
isOptimizerOwnedCacheHintsService,
|
|
6143
|
+
getOrCaptureCacheRetentionBaseline,
|
|
6144
|
+
PI_CACHE_RETENTION_BASELINE_SYMBOL,
|
|
6028
6145
|
// Persistence helpers (for reload/reset tests)
|
|
6029
6146
|
mergeCacheSessions,
|
|
6030
6147
|
mergeCacheTotals,
|
|
6031
6148
|
mergeLastRoutedModels,
|
|
6149
|
+
createSerializedAsyncRunner,
|
|
6032
6150
|
writePersistedCacheStats,
|
|
6033
6151
|
readPersistedCacheStats,
|
|
6034
6152
|
STATE_FILE_PATH,
|
|
@@ -6078,7 +6196,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
6078
6196
|
let lastStatusText: string | undefined;
|
|
6079
6197
|
let persistenceWarningShown = false;
|
|
6080
6198
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
6199
|
+
const enqueuePersist = createSerializedAsyncRunner();
|
|
6081
6200
|
let replacePersistedTotalsOnNextWrite = false;
|
|
6201
|
+
let forceReplaceTotalsAfterFailure = false;
|
|
6082
6202
|
const pendingDeletedTotalModelKeys = new Set<string>();
|
|
6083
6203
|
let integrityNotificationShown = false;
|
|
6084
6204
|
let currentSessionId = "";
|
|
@@ -6100,7 +6220,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6100
6220
|
}
|
|
6101
6221
|
}
|
|
6102
6222
|
|
|
6103
|
-
const uninstallCacheHintsService = installCacheHintsService({
|
|
6223
|
+
const uninstallCacheHintsService = installCacheHintsService(markOptimizerOwnedCacheHintsService({
|
|
6104
6224
|
version: 1,
|
|
6105
6225
|
getHints(input: PiCacheHintsInput): PiCacheHintsOutput | undefined {
|
|
6106
6226
|
if (!runtimeOptimizerEnabled || isEnabledEnv(process.env[NO_PROMPT_REWRITE_ENV])) return undefined;
|
|
@@ -6119,8 +6239,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6119
6239
|
cacheRetention: hint.cacheRetention,
|
|
6120
6240
|
};
|
|
6121
6241
|
},
|
|
6122
|
-
});
|
|
6123
|
-
void uninstallCacheHintsService;
|
|
6242
|
+
}), { discardPrevious: isOptimizerOwnedCacheHintsService });
|
|
6124
6243
|
|
|
6125
6244
|
/**
|
|
6126
6245
|
* Build a session-scoped stats key from the current session hash + model key.
|
|
@@ -6243,28 +6362,59 @@ export default function (pi: ExtensionAPI) {
|
|
|
6243
6362
|
lastStatusText = undefined;
|
|
6244
6363
|
}
|
|
6245
6364
|
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
} catch (error) {
|
|
6259
|
-
console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
|
|
6260
|
-
if (!persistenceWarningShown) {
|
|
6261
|
-
persistenceWarningShown = true;
|
|
6262
|
-
ctx?.ui.notify(
|
|
6263
|
-
`${LOG_PREFIX}: failed to persist footer stats; using in-memory stats for this process.`,
|
|
6264
|
-
"warning",
|
|
6265
|
-
);
|
|
6266
|
-
}
|
|
6365
|
+
function snapshotCacheStatsState(): CacheStatsState {
|
|
6366
|
+
const state = getCacheStatsState();
|
|
6367
|
+
const statsByModel = Object.fromEntries(
|
|
6368
|
+
Object.entries(state.statsByModel).map(([key, stats]) => [key, { ...stats }]),
|
|
6369
|
+
);
|
|
6370
|
+
const totalsByModel = Object.fromEntries(
|
|
6371
|
+
Object.entries(state.totalsByModel).map(([key, stats]) => [key, { ...stats }]),
|
|
6372
|
+
);
|
|
6373
|
+
const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
|
|
6374
|
+
for (const id of CACHE_PROVIDER_IDS) {
|
|
6375
|
+
const stats = state.legacyFamily[id];
|
|
6376
|
+
if (stats) legacyFamily[id] = { ...stats };
|
|
6267
6377
|
}
|
|
6378
|
+
const lastRoutedModelBySession = state.lastRoutedModelBySession
|
|
6379
|
+
? Object.fromEntries(
|
|
6380
|
+
Object.entries(state.lastRoutedModelBySession).map(([key, model]) => [key, { ...model }]),
|
|
6381
|
+
)
|
|
6382
|
+
: undefined;
|
|
6383
|
+
return { statsByModel, totalsByModel, legacyFamily, lastRoutedModelBySession };
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
6387
|
+
const state = snapshotCacheStatsState();
|
|
6388
|
+
const sessionHash = currentSessionHashSet ? currentSessionHash : undefined;
|
|
6389
|
+
const deleteModelKeys = Array.from(pendingDeletedTotalModelKeys);
|
|
6390
|
+
const replaceTotals = replacePersistedTotalsOnNextWrite;
|
|
6391
|
+
// This queued write owns the current intents. Later mutations can enqueue
|
|
6392
|
+
// independent intents without an earlier completion clearing them.
|
|
6393
|
+
pendingDeletedTotalModelKeys.clear();
|
|
6394
|
+
replacePersistedTotalsOnNextWrite = false;
|
|
6395
|
+
|
|
6396
|
+
return enqueuePersist(async () => {
|
|
6397
|
+
const recoverFromEarlierFailure = forceReplaceTotalsAfterFailure;
|
|
6398
|
+
try {
|
|
6399
|
+
await writePersistedCacheStats(state, sessionHash, {
|
|
6400
|
+
deleteModelKeys,
|
|
6401
|
+
replaceTotals: replaceTotals || recoverFromEarlierFailure,
|
|
6402
|
+
});
|
|
6403
|
+
if (recoverFromEarlierFailure) forceReplaceTotalsAfterFailure = false;
|
|
6404
|
+
} catch (error) {
|
|
6405
|
+
// The next queued write must replace totals authoritatively so a failed
|
|
6406
|
+
// reset/delete cannot merge stale persisted counters back into state.
|
|
6407
|
+
forceReplaceTotalsAfterFailure = true;
|
|
6408
|
+
console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
|
|
6409
|
+
if (!persistenceWarningShown) {
|
|
6410
|
+
persistenceWarningShown = true;
|
|
6411
|
+
ctx?.ui.notify(
|
|
6412
|
+
`${LOG_PREFIX}: failed to persist footer stats; using in-memory stats for this process.`,
|
|
6413
|
+
"warning",
|
|
6414
|
+
);
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
});
|
|
6268
6418
|
}
|
|
6269
6419
|
|
|
6270
6420
|
/** Schedule a debounced persist. Coalesces rapid message_end writes
|
|
@@ -6554,10 +6704,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
6554
6704
|
}
|
|
6555
6705
|
|
|
6556
6706
|
pi.on("session_start", async (event, ctx) => {
|
|
6707
|
+
if (runtimeOptimizerEnabled) requestLongCacheRetention();
|
|
6557
6708
|
await restoreCacheStats(event.reason, ctx);
|
|
6558
6709
|
await publishStatus(ctx);
|
|
6559
6710
|
});
|
|
6560
6711
|
|
|
6712
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
6713
|
+
try {
|
|
6714
|
+
await flushPersistCacheStats(ctx);
|
|
6715
|
+
} finally {
|
|
6716
|
+
latestCacheHint = undefined;
|
|
6717
|
+
delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
|
|
6718
|
+
uninstallCacheHintsService();
|
|
6719
|
+
restoreCacheRetentionEnv(STARTUP_CACHE_RETENTION_ENV);
|
|
6720
|
+
}
|
|
6721
|
+
});
|
|
6722
|
+
|
|
6561
6723
|
pi.on("model_select", async (event, ctx) => {
|
|
6562
6724
|
if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(event.model, ctx) ?? event.model, ctx, warnedModels);
|
|
6563
6725
|
await publishStatus(ctx, event.model);
|
|
@@ -6684,6 +6846,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
6684
6846
|
});
|
|
6685
6847
|
|
|
6686
6848
|
pi.on("before_provider_request", (event, ctx) => {
|
|
6849
|
+
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
6850
|
+
|
|
6851
|
+
// Anthropic rejects mixed cache breakpoints when a 1h block appears after
|
|
6852
|
+
// a 5m/default block in wire order (tools → system → messages). Normalize
|
|
6853
|
+
// only provably-invalid final payloads; legal long-only and long→short
|
|
6854
|
+
// payloads keep their original retention.
|
|
6855
|
+
if (isAnthropicMessagesApi(requestModel?.api)) {
|
|
6856
|
+
normalizeAnthropicCacheControlTtlOrder(event.payload);
|
|
6857
|
+
}
|
|
6858
|
+
|
|
6687
6859
|
// ── Safety: strip prompt_cache_retention from payload for models that
|
|
6688
6860
|
// are not authorised to send it. Pi defaults supportsLongCacheRetention
|
|
6689
6861
|
// to true for all openai-completions models, but most third-party APIs
|
|
@@ -6700,14 +6872,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
6700
6872
|
if (runtimeOptimizerEnabled) {
|
|
6701
6873
|
const payload = event.payload as UnknownRecord;
|
|
6702
6874
|
if (payload && typeof payload.prompt_cache_retention === 'string') {
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
if (isOfficialOpenAIBaseUrl(rModel)) {
|
|
6875
|
+
if (requestModel) {
|
|
6876
|
+
if (isOfficialOpenAIBaseUrl(requestModel)) {
|
|
6706
6877
|
// Gate 1: Official OpenAI → keep
|
|
6707
|
-
} else if (promptCacheRetention400Models.has(modelKey(
|
|
6878
|
+
} else if (promptCacheRetention400Models.has(modelKey(requestModel))) {
|
|
6708
6879
|
// Gate 2: 400 history → strip (overrides user opt-in)
|
|
6709
6880
|
delete payload.prompt_cache_retention;
|
|
6710
|
-
} else if (hasExplicitLongRetentionOptIn(
|
|
6881
|
+
} else if (hasExplicitLongRetentionOptIn(requestModel)) {
|
|
6711
6882
|
// Gate 3: Explicit user opt-in → keep
|
|
6712
6883
|
} else {
|
|
6713
6884
|
// Gate 4: Safe default → strip
|
|
@@ -6718,7 +6889,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
6718
6889
|
}
|
|
6719
6890
|
|
|
6720
6891
|
if (!shouldInjectOpenAIPromptCacheKey()) return undefined;
|
|
6721
|
-
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
6722
6892
|
if (!shouldInjectOpenAIPromptCacheKeyForModel(requestModel)) return undefined;
|
|
6723
6893
|
|
|
6724
6894
|
return addOpenAIPromptCacheKey(event.payload, getSessionPromptCacheKey(ctx));
|
package/package.json
CHANGED