pi-cache-optimizer 2.6.22 → 2.6.24

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 every `anthropic-messages` channel, the extension checks the final serialized payload and immediately downgrades a visibly invalid short-to-long transition. Legal long-only and `1h → 5m` payloads remain unchanged, including third-party endpoints that genuinely support 1-hour retention.
121
+
122
+ Some proxies rewrite or insert hidden 5-minute breakpoints after Pi's request hook. If a provider returns Anthropic's explicit TTL-ordering error, the extension records a process-local provider/model fallback so the next subsequent request uses the default 5-minute TTL. This error is a non-retryable HTTP 400 in Pi 0.82.1, so the extension does not claim that Pi's automatic retry will rerun the failed turn; if another layer retries it, that retry also uses the fallback. `/cache-optimizer doctor` reports the fallback and `/cache-optimizer fix` can persist a model-scoped `supportsLongCacheRetention: false` with the normal confirmation/backup flow. Other 400 errors and prompt-too-long failures do not activate this fallback. The runtime observation lasts until the current process exits and survives extension reloads within that process.
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` 和 `1h → 5m` payload 保持不变,包括真正支持 1 小时保留的第三方 endpoint。
121
+
122
+ 部分代理会在 Pi request hook 之后重写或插入隐藏的 5 分钟 breakpoint。如果 provider 返回 Anthropic 明确的 TTL 顺序错误,本扩展会记录当前进程内的 provider/model fallback,使下一次后续请求使用默认 5 分钟 TTL。Pi 0.82.1 将此错误视为不可自动重试的 HTTP 400,因此扩展不会声称 Pi 会自动重跑失败 turn;如果其它层发起重试,该重试也会使用 fallback。`/cache-optimizer doctor` 会显示该 fallback,`/cache-optimizer fix` 可通过现有确认/备份流程,以 model level 持久写入 `supportsLongCacheRetention: false`。其它 400 和 prompt-too-long 错误不会触发该 fallback。运行时观测会持续到当前进程退出,并会在同一进程内的扩展 reload 后保留。
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
@@ -104,6 +104,32 @@ const NO_PROMPT_REWRITE_ENV = "PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE";
104
104
  const PI_ROUTING_REGISTRY_SYMBOL = Symbol.for("pi.routing.registry.v1");
105
105
  const PI_CACHE_HINTS_SYMBOL = Symbol.for("pi.cache.hints.v1");
106
106
  const PI_CACHE_HINTS_OWNER_SYMBOL = Symbol.for("pi.cache.optimizer.hints-owner.v1");
107
+ const ANTHROPIC_TTL_FALLBACK_SYMBOL = Symbol.for("pi.cache.optimizer.anthropic-ttl-fallback.v1");
108
+
109
+ type AnthropicTtlFallbackStateV1 = {
110
+ version: 1;
111
+ modelKeys: Set<string>;
112
+ warnedModelKeys: Set<string>;
113
+ };
114
+
115
+ function getAnthropicTtlFallbackState(): AnthropicTtlFallbackStateV1 {
116
+ const globals = globalThis as Record<symbol, unknown>;
117
+ const existing = globals[ANTHROPIC_TTL_FALLBACK_SYMBOL] as Partial<AnthropicTtlFallbackStateV1> | undefined;
118
+ if (
119
+ existing?.version === 1 &&
120
+ existing.modelKeys instanceof Set &&
121
+ existing.warnedModelKeys instanceof Set
122
+ ) {
123
+ return existing as AnthropicTtlFallbackStateV1;
124
+ }
125
+ const state: AnthropicTtlFallbackStateV1 = {
126
+ version: 1,
127
+ modelKeys: new Set<string>(),
128
+ warnedModelKeys: new Set<string>(),
129
+ };
130
+ globals[ANTHROPIC_TTL_FALLBACK_SYMBOL] = state;
131
+ return state;
132
+ }
107
133
 
108
134
  let runtimeOptimizerEnabled = true;
109
135
 
@@ -1170,6 +1196,10 @@ function isOpenAICompatibleApi(api: unknown): boolean {
1170
1196
  return value === "openai-completions" || value === "openai-responses";
1171
1197
  }
1172
1198
 
1199
+ function isAnthropicMessagesApi(api: unknown): boolean {
1200
+ return lower(api) === "anthropic-messages";
1201
+ }
1202
+
1173
1203
  function isOpenAICompatibleProxyApi(api: unknown): boolean {
1174
1204
  return lower(api) === "openai-completions";
1175
1205
  }
@@ -1196,6 +1226,81 @@ function shouldInjectOpenAIPromptCacheKeyForModel(model: PiModel | undefined): b
1196
1226
  return isOpenAICompatibleApi(model?.api);
1197
1227
  }
1198
1228
 
1229
+ function collectAnthropicCacheControlsInWireOrder(payload: unknown): UnknownRecord[] {
1230
+ const record = asRecord(payload);
1231
+ if (!record) return [];
1232
+
1233
+ const controls: UnknownRecord[] = [];
1234
+ const collectFromBlock = (value: unknown): void => {
1235
+ const cacheControl = asRecord(asRecord(value)?.cache_control);
1236
+ if (cacheControl?.type === "ephemeral") controls.push(cacheControl);
1237
+ };
1238
+
1239
+ // Anthropic processes cache breakpoints in tools → system → messages order.
1240
+ if (Array.isArray(record.tools)) {
1241
+ for (const tool of record.tools) collectFromBlock(tool);
1242
+ }
1243
+ if (Array.isArray(record.system)) {
1244
+ for (const block of record.system) collectFromBlock(block);
1245
+ }
1246
+ if (Array.isArray(record.messages)) {
1247
+ for (const message of record.messages) {
1248
+ const content = asRecord(message)?.content;
1249
+ if (!Array.isArray(content)) continue;
1250
+ for (const block of content) collectFromBlock(block);
1251
+ }
1252
+ }
1253
+
1254
+ return controls;
1255
+ }
1256
+
1257
+ /**
1258
+ * Anthropic requires every 1h cache breakpoint to precede every 5m breakpoint.
1259
+ * An ephemeral cache_control without ttl uses Anthropic's default 5m retention.
1260
+ * If the final serialized payload contains a short → long transition, downgrade
1261
+ * all 1h breakpoints to the default 5m so the request remains valid.
1262
+ */
1263
+ function downgradeAnthropicLongCacheControls(payload: unknown): boolean {
1264
+ let changed = false;
1265
+ for (const control of collectAnthropicCacheControlsInWireOrder(payload)) {
1266
+ if (control.ttl === "1h") {
1267
+ delete control.ttl;
1268
+ changed = true;
1269
+ }
1270
+ }
1271
+ return changed;
1272
+ }
1273
+
1274
+ function hasAnthropicCacheTtlOrderError(message: unknown): boolean {
1275
+ const record = getAssistantRecord(message);
1276
+ if (record?.stopReason !== "error" || typeof record.errorMessage !== "string") return false;
1277
+
1278
+ const error = lower(record.errorMessage);
1279
+ return error.includes("cache_control") &&
1280
+ error.includes("ttl='1h'") &&
1281
+ error.includes("ttl='5m'") &&
1282
+ error.includes("must not come after");
1283
+ }
1284
+
1285
+ function normalizeAnthropicCacheControlTtlOrder(payload: unknown): boolean {
1286
+ const controls = collectAnthropicCacheControlsInWireOrder(payload);
1287
+ let seenShort = false;
1288
+ let hasInvalidLongAfterShort = false;
1289
+
1290
+ for (const control of controls) {
1291
+ const ttl = control.ttl;
1292
+ if (ttl === undefined || ttl === "5m") {
1293
+ seenShort = true;
1294
+ } else if (ttl === "1h" && seenShort) {
1295
+ hasInvalidLongAfterShort = true;
1296
+ break;
1297
+ }
1298
+ }
1299
+ if (!hasInvalidLongAfterShort) return false;
1300
+
1301
+ return downgradeAnthropicLongCacheControls(payload);
1302
+ }
1303
+
1199
1304
  function isResponsesPromptRewriteBypassApi(api: unknown): boolean {
1200
1305
  const value = lower(api);
1201
1306
  return value === "openai-codex-responses" || value === "openai-responses" || value === "azure-openai-responses";
@@ -4314,7 +4419,7 @@ function getCompatCheckNotApplicableLines(model: PiModel): string[] {
4314
4419
  return ["ℹ️ Compat check not applicable for this model."];
4315
4420
  }
4316
4421
 
4317
- function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400?: boolean; sessionAffinity403?: boolean; openAISdkHeader403?: boolean } = {}): string {
4422
+ function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400?: boolean; anthropicTtlOrderError?: boolean; sessionAffinity403?: boolean; openAISdkHeader403?: boolean } = {}): string {
4318
4423
  const lines: string[] = [];
4319
4424
  lines.push(`Provider: ${model.provider}`);
4320
4425
  lines.push(`Model: ${model.id}`);
@@ -4373,6 +4478,13 @@ function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400
4373
4478
  }
4374
4479
  }
4375
4480
 
4481
+ if (options.anthropicTtlOrderError) {
4482
+ lines.push("");
4483
+ lines.push("⚠️ An Anthropic cache-control TTL ordering error was observed for this model.");
4484
+ lines.push(" Runtime requests now fall back from 1h to the default 5-minute cache TTL.");
4485
+ lines.push(` Run /cache-optimizer fix to set model-level supportsLongCacheRetention: false in ${getModelsJsonDisplayPath()}.`);
4486
+ }
4487
+
4376
4488
  // ── Session affinity 403 diagnostics ──
4377
4489
  // Advises the user when sendSessionAffinityHeaders is enabled, since some
4378
4490
  // third-party proxies / CDNs / WAFs block Pi's custom session-affinity HTTP
@@ -4798,6 +4910,8 @@ interface FixSuggestion {
4798
4910
  providerLabel: string;
4799
4911
  modelId: string;
4800
4912
  compatKeys: Record<string, unknown>;
4913
+ /** Runtime-observed failures must never broaden a model-specific fix. */
4914
+ forceModelLevel?: boolean;
4801
4915
  }
4802
4916
 
4803
4917
  /**
@@ -5459,8 +5573,9 @@ function syntheticModelForId(providerLabel: string, id: string): PiModel {
5459
5573
  * Decide whether the fix should write provider-level or model-level compat.
5460
5574
  *
5461
5575
  * Strategy (auto-detect, prefer provider level when safe):
5462
- * - Channel-capability keys (session affinity / long retention) are always
5463
- * provider-safe.
5576
+ * - Channel-capability keys (session affinity / long retention) are normally
5577
+ * provider-safe; runtime-observed model failures explicitly override this
5578
+ * default through chooseFixPlacement(..., forceModelLevel=true).
5464
5579
  * - Model-behavior keys (forceAdaptiveThinking, allowEmptySignature,
5465
5580
  * thinkingFormat, ...) are provider-safe ONLY when every sibling model in
5466
5581
  * the provider also matches the same detection (all adaptive-generation /
@@ -5539,7 +5654,15 @@ function chooseFixPlacement(
5539
5654
  location: ModelNodeLocation,
5540
5655
  compatKeys: Record<string, unknown>,
5541
5656
  providerLabel: string,
5657
+ forceModelLevel = false,
5542
5658
  ): { placement: "provider" | "model"; reason: string } {
5659
+ if (forceModelLevel) {
5660
+ return {
5661
+ placement: "model",
5662
+ reason: "runtime-observed provider/model failure — scoping the repair to the affected model",
5663
+ };
5664
+ }
5665
+
5543
5666
  const decision = decideFixPlacement(compatKeys, providerLabel, location.allModelIds);
5544
5667
  const existingModelKeys = findExistingCompatKeysInJsonc(
5545
5668
  original,
@@ -5876,7 +5999,12 @@ export const __internals_for_tests = {
5876
5999
  shouldInjectOpenAIPromptCacheKey,
5877
6000
  shouldInjectOpenAIPromptCacheKeyForModel,
5878
6001
  isOpenAICompatibleApi,
6002
+ isAnthropicMessagesApi,
5879
6003
  isOpenAICompatibleProxyApi,
6004
+ collectAnthropicCacheControlsInWireOrder,
6005
+ downgradeAnthropicLongCacheControls,
6006
+ hasAnthropicCacheTtlOrderError,
6007
+ normalizeAnthropicCacheControlTtlOrder,
5880
6008
  isPiBuiltInLlamaCppModel,
5881
6009
  isResponsesPromptRewriteBypassApi,
5882
6010
  isMistralConversationsApi,
@@ -6123,6 +6251,9 @@ export default function (pi: ExtensionAPI) {
6123
6251
  const warnedModels = new Set<string>();
6124
6252
  const promptCacheRetention400Models = new Set<string>();
6125
6253
  const warnedPromptCacheRetention400Models = new Set<string>();
6254
+ const anthropicTtlFallbackState = getAnthropicTtlFallbackState();
6255
+ const anthropicTtlOrderErrorModels = anthropicTtlFallbackState.modelKeys;
6256
+ const warnedAnthropicTtlOrderErrorModels = anthropicTtlFallbackState.warnedModelKeys;
6126
6257
  const sendSessionAffinityHeaders403Models = new Set<string>();
6127
6258
  const warnedSendSessionAffinityHeaders403Models = new Set<string>();
6128
6259
  const openAISdkHeader403Models = new Set<string>();
@@ -6144,6 +6275,41 @@ export default function (pi: ExtensionAPI) {
6144
6275
  let lastActualRoutedModel: PersistedRoutedModelRef | undefined;
6145
6276
  let latestCacheHint: PiCacheHintSnapshot | undefined;
6146
6277
  const PERSIST_DEBOUNCE_MS = 2000;
6278
+
6279
+ function buildObservedRuntimeFixSuggestion(model: PiModel): FixSuggestion | undefined {
6280
+ const key = modelKey(model);
6281
+ const slashIdx = key.indexOf("/");
6282
+ const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
6283
+
6284
+ if (
6285
+ anthropicTtlOrderErrorModels.has(key) ||
6286
+ (isPromptCacheRetention400Applicable(model) && promptCacheRetention400Models.has(key))
6287
+ ) {
6288
+ return {
6289
+ providerLabel,
6290
+ modelId: model.id,
6291
+ compatKeys: { supportsLongCacheRetention: false },
6292
+ forceModelLevel: true,
6293
+ };
6294
+ }
6295
+ if (isSessionAffinity403Applicable(model) && sendSessionAffinityHeaders403Models.has(key)) {
6296
+ return { providerLabel, modelId: model.id, compatKeys: { sendSessionAffinityHeaders: false } };
6297
+ }
6298
+ return undefined;
6299
+ }
6300
+
6301
+ function buildCommandFixSuggestion(model: PiModel): FixSuggestion | undefined {
6302
+ const regular = buildFixSuggestion(model);
6303
+ const observed = buildObservedRuntimeFixSuggestion(model);
6304
+ if (!regular) return observed;
6305
+ if (!observed) return regular;
6306
+ return {
6307
+ providerLabel: regular.providerLabel,
6308
+ modelId: regular.modelId,
6309
+ compatKeys: { ...regular.compatKeys, ...observed.compatKeys },
6310
+ forceModelLevel: regular.forceModelLevel || observed.forceModelLevel,
6311
+ };
6312
+ }
6147
6313
  /** In-memory recent usage samples per model key (not persisted, cleared on reload). */
6148
6314
  const recentSamplesByModelKey = new Map<string, CacheUsageSample[]>();
6149
6315
 
@@ -6783,6 +6949,20 @@ export default function (pi: ExtensionAPI) {
6783
6949
  });
6784
6950
 
6785
6951
  pi.on("before_provider_request", (event, ctx) => {
6952
+ const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
6953
+
6954
+ // Anthropic rejects mixed cache breakpoints when a 1h block appears after
6955
+ // a 5m/default block in wire order (tools → system → messages). Repair any
6956
+ // conflict visible in Pi's final payload immediately. Some proxies inject
6957
+ // hidden short breakpoints after this hook; only models that have actually
6958
+ // returned the explicit TTL-order error receive the process-local 5m fallback.
6959
+ if (requestModel && isAnthropicMessagesApi(requestModel.api)) {
6960
+ const visibleConflictFixed = normalizeAnthropicCacheControlTtlOrder(event.payload);
6961
+ if (!visibleConflictFixed && anthropicTtlOrderErrorModels.has(modelKey(requestModel))) {
6962
+ downgradeAnthropicLongCacheControls(event.payload);
6963
+ }
6964
+ }
6965
+
6786
6966
  // ── Safety: strip prompt_cache_retention from payload for models that
6787
6967
  // are not authorised to send it. Pi defaults supportsLongCacheRetention
6788
6968
  // to true for all openai-completions models, but most third-party APIs
@@ -6799,14 +6979,13 @@ export default function (pi: ExtensionAPI) {
6799
6979
  if (runtimeOptimizerEnabled) {
6800
6980
  const payload = event.payload as UnknownRecord;
6801
6981
  if (payload && typeof payload.prompt_cache_retention === 'string') {
6802
- const rModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
6803
- if (rModel) {
6804
- if (isOfficialOpenAIBaseUrl(rModel)) {
6982
+ if (requestModel) {
6983
+ if (isOfficialOpenAIBaseUrl(requestModel)) {
6805
6984
  // Gate 1: Official OpenAI → keep
6806
- } else if (promptCacheRetention400Models.has(modelKey(rModel))) {
6985
+ } else if (promptCacheRetention400Models.has(modelKey(requestModel))) {
6807
6986
  // Gate 2: 400 history → strip (overrides user opt-in)
6808
6987
  delete payload.prompt_cache_retention;
6809
- } else if (hasExplicitLongRetentionOptIn(rModel)) {
6988
+ } else if (hasExplicitLongRetentionOptIn(requestModel)) {
6810
6989
  // Gate 3: Explicit user opt-in → keep
6811
6990
  } else {
6812
6991
  // Gate 4: Safe default → strip
@@ -6817,7 +6996,6 @@ export default function (pi: ExtensionAPI) {
6817
6996
  }
6818
6997
 
6819
6998
  if (!shouldInjectOpenAIPromptCacheKey()) return undefined;
6820
- const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
6821
6999
  if (!shouldInjectOpenAIPromptCacheKeyForModel(requestModel)) return undefined;
6822
7000
 
6823
7001
  return addOpenAIPromptCacheKey(event.payload, getSessionPromptCacheKey(ctx));
@@ -6886,6 +7064,29 @@ export default function (pi: ExtensionAPI) {
6886
7064
 
6887
7065
  pi.on("message_end", async (event, ctx) => {
6888
7066
  syncSessionHash(ctx);
7067
+ const msgRecord = asRecord(event.message);
7068
+
7069
+ // Record only Anthropic's explicit mixed-TTL ordering error. This is a
7070
+ // non-retryable 400 in Pi 0.82.1, so the fallback applies to the next
7071
+ // subsequent request (and to a retry only if another layer initiates one).
7072
+ if (hasAnthropicCacheTtlOrderError(event.message)) {
7073
+ const fallbackModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
7074
+ const errorModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
7075
+ if (errorModel && isAnthropicMessagesApi(errorModel.api)) {
7076
+ const key = modelKey(errorModel);
7077
+ anthropicTtlOrderErrorModels.add(key);
7078
+ if (!warnedAnthropicTtlOrderErrorModels.has(key)) {
7079
+ warnedAnthropicTtlOrderErrorModels.add(key);
7080
+ ctx.ui.notify(
7081
+ `⚠️ ${LOG_PREFIX}: ${key} returned an Anthropic cache-control TTL ordering error. ` +
7082
+ `The next request will fall back to the default 5-minute cache TTL. ` +
7083
+ `Run /cache-optimizer fix to set model-level supportsLongCacheRetention: false persistently.`,
7084
+ "warning",
7085
+ );
7086
+ }
7087
+ }
7088
+ }
7089
+
6889
7090
  const adapter = selectAdapterForAssistantMessage(event.message, ctx.model);
6890
7091
  if (!adapter) return;
6891
7092
 
@@ -6896,7 +7097,6 @@ export default function (pi: ExtensionAPI) {
6896
7097
  // returns as a valid (non-undefined) snapshot, which would inflate
6897
7098
  // totalRequests and skew cache hit-rate accuracy. The final successful
6898
7099
  // response is emitted as a separate message_end with real usage data.
6899
- const msgRecord = asRecord(event.message);
6900
7100
  if (msgRecord?.stopReason === "error" || msgRecord?.stopReason === "aborted") {
6901
7101
  return;
6902
7102
  }
@@ -7003,7 +7203,7 @@ export default function (pi: ExtensionAPI) {
7003
7203
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
7004
7204
  return;
7005
7205
  }
7006
- const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
7206
+ const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), anthropicTtlOrderError: anthropicTtlOrderErrorModels.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
7007
7207
  const adapter = selectAdapterForModel(model);
7008
7208
  const sk = model ? sessionModelKey(model) : undefined;
7009
7209
  const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
@@ -7076,37 +7276,7 @@ export default function (pi: ExtensionAPI) {
7076
7276
  return;
7077
7277
  }
7078
7278
 
7079
- let suggestion = buildFixSuggestion(model);
7080
-
7081
- // If no regular missing compat flags but the model has a recorded
7082
- // prompt_cache_retention 400 (Pi sent `prompt_cache_retention` and
7083
- // the provider rejected it), offer to override
7084
- // `supportsLongCacheRetention` to false in models.json.
7085
- if (!suggestion && isPromptCacheRetention400Applicable(model) && promptCacheRetention400Models.has(modelKey(model))) {
7086
- const key = modelKey(model);
7087
- const slashIdx = key.indexOf("/");
7088
- const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
7089
- suggestion = {
7090
- providerLabel,
7091
- modelId: model.id,
7092
- compatKeys: { supportsLongCacheRetention: false },
7093
- };
7094
- }
7095
-
7096
- // If no regular missing compat flags but the model has a recorded
7097
- // 403 while sendSessionAffinityHeaders was enabled (Pi sent custom
7098
- // session-affinity HTTP headers and the proxy/CDN blocked them), offer
7099
- // to override `sendSessionAffinityHeaders` to false in models.json.
7100
- if (!suggestion && isSessionAffinity403Applicable(model) && sendSessionAffinityHeaders403Models.has(modelKey(model))) {
7101
- const key = modelKey(model);
7102
- const slashIdx = key.indexOf("/");
7103
- const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
7104
- suggestion = {
7105
- providerLabel,
7106
- modelId: model.id,
7107
- compatKeys: { sendSessionAffinityHeaders: false },
7108
- };
7109
- }
7279
+ const suggestion = buildCommandFixSuggestion(model);
7110
7280
 
7111
7281
  if (!suggestion) {
7112
7282
  const key = modelKey(model);
@@ -7132,6 +7302,13 @@ export default function (pi: ExtensionAPI) {
7132
7302
  "Create or edit the entry below to override supportsLongCacheRetention to false.",
7133
7303
  );
7134
7304
  }
7305
+ if (anthropicTtlOrderErrorModels.has(modelKey(model))) {
7306
+ manualLines.push(
7307
+ "",
7308
+ "💡 This model returned an Anthropic cache-control TTL ordering error.",
7309
+ "Create or edit the entry below to override supportsLongCacheRetention to false.",
7310
+ );
7311
+ }
7135
7312
  if (sendSessionAffinityHeaders403Models.has(modelKey(model))) {
7136
7313
  manualLines.push(
7137
7314
  "",
@@ -7294,9 +7471,15 @@ export default function (pi: ExtensionAPI) {
7294
7471
  return;
7295
7472
  }
7296
7473
 
7297
- // Compose the modified text — auto-detect the best placement level:
7298
- // provider level (channel-wide) when safe for all sibling models, else model level.
7299
- const decision = chooseFixPlacement(originalText, location, suggestion.compatKeys, suggestion.providerLabel);
7474
+ // Compose the modified text — observed runtime failures are always
7475
+ // model-scoped; ordinary compat fixes use the safety-based placement.
7476
+ const decision = chooseFixPlacement(
7477
+ originalText,
7478
+ location,
7479
+ suggestion.compatKeys,
7480
+ suggestion.providerLabel,
7481
+ suggestion.forceModelLevel,
7482
+ );
7300
7483
  const modifiedText = composeFixInsertion(originalText, location, suggestion.compatKeys, decision.placement);
7301
7484
 
7302
7485
  // Self-check
@@ -7424,7 +7607,7 @@ export default function (pi: ExtensionAPI) {
7424
7607
  if (!model) {
7425
7608
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
7426
7609
  } else {
7427
- const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
7610
+ const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), anthropicTtlOrderError: anthropicTtlOrderErrorModels.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
7428
7611
  const adapter = selectAdapterForModel(model);
7429
7612
  const sk = model ? sessionModelKey(model) : undefined;
7430
7613
  const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
@@ -7468,7 +7651,7 @@ export default function (pi: ExtensionAPI) {
7468
7651
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
7469
7652
  return;
7470
7653
  }
7471
- const suggestion = buildFixSuggestion(model);
7654
+ const suggestion = buildCommandFixSuggestion(model);
7472
7655
  if (!suggestion) {
7473
7656
  const key = modelKey(model);
7474
7657
  cmdCtx.ui.notify(`✅ Nothing to fix for "${key}". Compat already configured.`, "info");
@@ -7496,7 +7679,13 @@ export default function (pi: ExtensionAPI) {
7496
7679
  return;
7497
7680
  }
7498
7681
 
7499
- const menuDecision = chooseFixPlacement(originalText, location, suggestion.compatKeys, suggestion.providerLabel);
7682
+ const menuDecision = chooseFixPlacement(
7683
+ originalText,
7684
+ location,
7685
+ suggestion.compatKeys,
7686
+ suggestion.providerLabel,
7687
+ suggestion.forceModelLevel,
7688
+ );
7500
7689
  const modifiedText = composeFixInsertion(originalText, location, suggestion.compatKeys, menuDecision.placement);
7501
7690
  const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement);
7502
7691
  if (checkError !== null) {
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.24",
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",