pi-cache-optimizer 2.6.22 → 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 +77 -6
- 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
|
@@ -1170,6 +1170,10 @@ function isOpenAICompatibleApi(api: unknown): boolean {
|
|
|
1170
1170
|
return value === "openai-completions" || value === "openai-responses";
|
|
1171
1171
|
}
|
|
1172
1172
|
|
|
1173
|
+
function isAnthropicMessagesApi(api: unknown): boolean {
|
|
1174
|
+
return lower(api) === "anthropic-messages";
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1173
1177
|
function isOpenAICompatibleProxyApi(api: unknown): boolean {
|
|
1174
1178
|
return lower(api) === "openai-completions";
|
|
1175
1179
|
}
|
|
@@ -1196,6 +1200,62 @@ function shouldInjectOpenAIPromptCacheKeyForModel(model: PiModel | undefined): b
|
|
|
1196
1200
|
return isOpenAICompatibleApi(model?.api);
|
|
1197
1201
|
}
|
|
1198
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
|
+
|
|
1199
1259
|
function isResponsesPromptRewriteBypassApi(api: unknown): boolean {
|
|
1200
1260
|
const value = lower(api);
|
|
1201
1261
|
return value === "openai-codex-responses" || value === "openai-responses" || value === "azure-openai-responses";
|
|
@@ -5876,7 +5936,10 @@ export const __internals_for_tests = {
|
|
|
5876
5936
|
shouldInjectOpenAIPromptCacheKey,
|
|
5877
5937
|
shouldInjectOpenAIPromptCacheKeyForModel,
|
|
5878
5938
|
isOpenAICompatibleApi,
|
|
5939
|
+
isAnthropicMessagesApi,
|
|
5879
5940
|
isOpenAICompatibleProxyApi,
|
|
5941
|
+
collectAnthropicCacheControlsInWireOrder,
|
|
5942
|
+
normalizeAnthropicCacheControlTtlOrder,
|
|
5880
5943
|
isPiBuiltInLlamaCppModel,
|
|
5881
5944
|
isResponsesPromptRewriteBypassApi,
|
|
5882
5945
|
isMistralConversationsApi,
|
|
@@ -6783,6 +6846,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
6783
6846
|
});
|
|
6784
6847
|
|
|
6785
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
|
+
|
|
6786
6859
|
// ── Safety: strip prompt_cache_retention from payload for models that
|
|
6787
6860
|
// are not authorised to send it. Pi defaults supportsLongCacheRetention
|
|
6788
6861
|
// to true for all openai-completions models, but most third-party APIs
|
|
@@ -6799,14 +6872,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
6799
6872
|
if (runtimeOptimizerEnabled) {
|
|
6800
6873
|
const payload = event.payload as UnknownRecord;
|
|
6801
6874
|
if (payload && typeof payload.prompt_cache_retention === 'string') {
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
if (isOfficialOpenAIBaseUrl(rModel)) {
|
|
6875
|
+
if (requestModel) {
|
|
6876
|
+
if (isOfficialOpenAIBaseUrl(requestModel)) {
|
|
6805
6877
|
// Gate 1: Official OpenAI → keep
|
|
6806
|
-
} else if (promptCacheRetention400Models.has(modelKey(
|
|
6878
|
+
} else if (promptCacheRetention400Models.has(modelKey(requestModel))) {
|
|
6807
6879
|
// Gate 2: 400 history → strip (overrides user opt-in)
|
|
6808
6880
|
delete payload.prompt_cache_retention;
|
|
6809
|
-
} else if (hasExplicitLongRetentionOptIn(
|
|
6881
|
+
} else if (hasExplicitLongRetentionOptIn(requestModel)) {
|
|
6810
6882
|
// Gate 3: Explicit user opt-in → keep
|
|
6811
6883
|
} else {
|
|
6812
6884
|
// Gate 4: Safe default → strip
|
|
@@ -6817,7 +6889,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
6817
6889
|
}
|
|
6818
6890
|
|
|
6819
6891
|
if (!shouldInjectOpenAIPromptCacheKey()) return undefined;
|
|
6820
|
-
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
6821
6892
|
if (!shouldInjectOpenAIPromptCacheKeyForModel(requestModel)) return undefined;
|
|
6822
6893
|
|
|
6823
6894
|
return addOpenAIPromptCacheKey(event.payload, getSessionPromptCacheKey(ctx));
|
package/package.json
CHANGED