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 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
- const rModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
6803
- if (rModel) {
6804
- if (isOfficialOpenAIBaseUrl(rModel)) {
6875
+ if (requestModel) {
6876
+ if (isOfficialOpenAIBaseUrl(requestModel)) {
6805
6877
  // Gate 1: Official OpenAI → keep
6806
- } else if (promptCacheRetention400Models.has(modelKey(rModel))) {
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(rModel)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.6.22",
3
+ "version": "2.6.23",
4
4
  "description": "Improve Pi prompt/KV cache hit rates with stable prompts, OpenAI-compatible cache keys, proxy compat warnings, and footer cache stats.",
5
5
  "keywords": [
6
6
  "pi-package",