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 +8 -0
- package/README.zh-CN.md +8 -0
- package/index.ts +237 -48
- 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 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
|
|
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
|
-
|
|
6803
|
-
|
|
6804
|
-
if (isOfficialOpenAIBaseUrl(rModel)) {
|
|
6982
|
+
if (requestModel) {
|
|
6983
|
+
if (isOfficialOpenAIBaseUrl(requestModel)) {
|
|
6805
6984
|
// Gate 1: Official OpenAI → keep
|
|
6806
|
-
} else if (promptCacheRetention400Models.has(modelKey(
|
|
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(
|
|
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
|
-
|
|
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 —
|
|
7298
|
-
//
|
|
7299
|
-
const decision = chooseFixPlacement(
|
|
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 =
|
|
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(
|
|
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