pi-cache-optimizer 2.8.4 → 2.8.6

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
@@ -114,7 +114,7 @@ The explicit setting is stored in `pi-cache-optimizer-config.json` under Pi's ag
114
114
 
115
115
  Third-party `openai-completions` proxies (LiteLLM / OneAPI / NewAPI / OpenRouter-like channels) often route one session across multiple upstream backends. That splits provider-side prompt caches.
116
116
 
117
- Pi 0.84.1 also fixes built-in Fireworks compatibility for models that reject `prompt_cache_retention`; keep the extension's diagnostics focused on the effective model compat exposed by Pi rather than adding a provider-name special case. Pi 0.81+ also has a built-in `llama.cpp` provider using an OpenAI-shaped transport. Pi 0.82+ core generates a session `prompt_cache_key` for it when cache retention is enabled, so this extension preserves that key and may add the same conservative fallback when missing. The built-in provider's explicit compat fingerprint is excluded from generic proxy routing/session-affinity advice, but a custom or overridden provider that merely reuses the id `llama.cpp` is treated like any other OpenAI-compatible channel. `prompt_cache_retention` remains subject to the normal safety rule: keep it only for official OpenAI or an explicit effective `supportsLongCacheRetention: true` opt-in in `models.json`; otherwise strip it before sending. Effective values follow Pi's precedence: `modelOverrides[modelId].compat` first, then the matching `models[].compat`, then provider-level `compat`. An explicit `false` at a higher layer overrides `true` below it.
117
+ Pi 0.84.1 also fixes built-in Fireworks compatibility for models that reject `prompt_cache_retention`; the extension avoids provider-name special cases and resolves exact provider/model compat from `models.json` plus the runtime model. Pi 0.81+ also has a built-in `llama.cpp` provider using an OpenAI-shaped transport. Pi 0.82+ core generates a session `prompt_cache_key` for it when cache retention is enabled, so this extension preserves that key and may add the same conservative fallback when missing. The built-in provider's explicit compat fingerprint is excluded from generic proxy routing/session-affinity advice, but a custom or overridden provider that merely reuses the id `llama.cpp` is treated like any other OpenAI-compatible channel. `prompt_cache_retention` remains subject to the normal safety rule: keep it only for official OpenAI or an explicit effective `supportsLongCacheRetention: true` opt-in in `models.json`; otherwise strip it before sending. Effective values follow Pi's precedence: `modelOverrides[modelId].compat` first, then the matching `models[].compat`, then provider-level `compat`. An explicit `false` at a higher layer overrides `true` below it.
118
118
 
119
119
  For real proxies, start with session affinity:
120
120
 
@@ -262,6 +262,8 @@ Provider-level minimal override:
262
262
  }
263
263
  ```
264
264
 
265
+ Pi Cache Optimizer resolves effective compat using Pi's precedence (`provider.compat` → matching `models[].compat` → runtime model compat → `modelOverrides[modelId].compat`). This also covers extension providers that replace their model list and accidentally omit lower-level compat from the runtime model object. For non-official `openai-completions` channels, when effective `sendSessionAffinityHeaders` is `true` but Pi's runtime model lost it, the extension restores Pi-compatible affinity headers at request time without overwriting existing headers. An explicit `false` remains a respected opt-out.
266
+
265
267
  If only one model should change, use `modelOverrides`:
266
268
 
267
269
  ```json
package/README.zh-CN.md CHANGED
@@ -114,7 +114,7 @@ Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 prov
114
114
 
115
115
  LiteLLM / OneAPI / NewAPI / 类 OpenRouter 渠道等第三方 `openai-completions` 代理,常会把同一个 session 分散到多个上游后端,导致 provider 侧 prompt cache 被拆散。
116
116
 
117
- Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention` 的模型兼容性;本扩展继续依据 Pi 暴露的有效 model compat 做诊断,不按 provider 名称增加特殊分支。Pi 0.81+ 也内置了使用 OpenAI-shaped transport 的 `llama.cpp` provider。Pi 0.82+ core 在启用 cache retention 时会为它生成 session `prompt_cache_key`,因此本扩展会保留该 key,并在缺失时使用同样的保守 fallback。只有符合 Pi 内置 provider 明确 compat 指纹的模型会跳过通用 proxy 路由 / session-affinity 建议;仅复用 `llama.cpp` id 的自定义或覆盖 provider 仍按普通 OpenAI-compatible 渠道处理。`prompt_cache_retention` 继续遵循统一安全规则:仅官方 OpenAI 或 `models.json` 中有效配置为 `supportsLongCacheRetention: true` 时保留,否则发送前移除。有效值遵循 Pi 的优先级:先看 `modelOverrides[modelId].compat`,再看匹配的 `models[].compat`,最后看 provider 级 `compat`;高层显式 `false` 会覆盖低层的 `true`。
117
+ Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention` 的模型兼容性;本扩展不按 provider 名称增加特殊分支,而是结合 `models.json` 与 runtime model,按精确 provider/model 解析有效 compat。Pi 0.81+ 也内置了使用 OpenAI-shaped transport 的 `llama.cpp` provider。Pi 0.82+ core 在启用 cache retention 时会为它生成 session `prompt_cache_key`,因此本扩展会保留该 key,并在缺失时使用同样的保守 fallback。只有符合 Pi 内置 provider 明确 compat 指纹的模型会跳过通用 proxy 路由 / session-affinity 建议;仅复用 `llama.cpp` id 的自定义或覆盖 provider 仍按普通 OpenAI-compatible 渠道处理。`prompt_cache_retention` 继续遵循统一安全规则:仅官方 OpenAI 或 `models.json` 中有效配置为 `supportsLongCacheRetention: true` 时保留,否则发送前移除。有效值遵循 Pi 的优先级:先看 `modelOverrides[modelId].compat`,再看匹配的 `models[].compat`,最后看 provider 级 `compat`;高层显式 `false` 会覆盖低层的 `true`。
118
118
 
119
119
  对真正的代理,建议先启用 session affinity:
120
120
 
@@ -262,6 +262,8 @@ Provider 级最小 override:
262
262
  }
263
263
  ```
264
264
 
265
+ Pi Cache Optimizer 按 Pi 的优先级解析有效 compat(`provider.compat` → 匹配的 `models[].compat` → runtime model compat → `modelOverrides[modelId].compat`)。这也覆盖了某些 extension provider 替换模型列表后,runtime model 意外丢失低层 compat 的情况。对于非官方 `openai-completions` 渠道,如果有效 `sendSessionAffinityHeaders` 为 `true`、但 Pi runtime model 丢失了该值,本扩展会在请求阶段恢复 Pi-compatible affinity headers,且不会覆盖已有 header;显式 `false` 仍作为有效 opt-out 尊重。
266
+
265
267
  如果只想影响单个模型,用 `modelOverrides`:
266
268
 
267
269
  ```json
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { chmod, copyFile, lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
3
- import { constants as fsConstants, readFileSync, watch } from "node:fs";
3
+ import { constants as fsConstants, readFileSync, statSync, watch } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import {
@@ -21,6 +21,7 @@ type CacheRetentionEnvSnapshot = {
21
21
  const PI_CACHE_RETENTION_ENV = "PI_CACHE_RETENTION";
22
22
  const LONG_CACHE_RETENTION_VALUE = "long";
23
23
  const PI_CACHE_RETENTION_BASELINE_SYMBOL = Symbol.for("pi.cache.optimizer.retention-baseline.v1");
24
+ const ROUTED_FALLBACK_MODEL_SYMBOL = Symbol("pi-cache-optimizer.routed-fallback-model");
24
25
 
25
26
  type CacheRetentionBaselineV1 = {
26
27
  version: 1;
@@ -236,6 +237,10 @@ type CacheCompat = {
236
237
  cacheControlFormat?: string;
237
238
  forceAdaptiveThinking?: boolean;
238
239
  allowEmptySignature?: boolean;
240
+ openRouterRouting?: UnknownRecord;
241
+ vercelGatewayRouting?: UnknownRecord;
242
+ chatTemplateKwargs?: UnknownRecord;
243
+ chatTemplateArgs?: UnknownRecord;
239
244
  };
240
245
 
241
246
  type CacheStats = {
@@ -1013,19 +1018,23 @@ function resolveActiveRouteSnapshot(
1013
1018
  }
1014
1019
 
1015
1020
  function routeSnapshotToPiModel(snapshot: PiRouteSnapshot, fallback?: PiModel): PiModel {
1021
+ const sameIdentity = fallback?.provider === snapshot.provider && fallback?.id === snapshot.modelId;
1016
1022
  return {
1017
- ...(fallback ?? {}),
1023
+ ...(sameIdentity ? fallback ?? {} : {}),
1018
1024
  id: snapshot.modelId,
1019
1025
  name: snapshot.canonicalModelId ?? snapshot.modelId,
1020
1026
  provider: snapshot.provider,
1021
- api: snapshot.api ?? fallback?.api ?? "",
1022
- baseUrl: fallback?.baseUrl ?? "",
1023
- reasoning: fallback?.reasoning ?? false,
1024
- input: fallback?.input ?? ["text"],
1025
- cost: fallback?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1026
- contextWindow: fallback?.contextWindow ?? 0,
1027
- maxTokens: fallback?.maxTokens ?? 0,
1028
- compat: fallback?.compat,
1027
+ api: snapshot.api ?? (sameIdentity ? fallback?.api : undefined) ?? "",
1028
+ baseUrl: sameIdentity ? fallback?.baseUrl ?? "" : "",
1029
+ reasoning: sameIdentity ? fallback?.reasoning ?? false : false,
1030
+ input: sameIdentity ? fallback?.input ?? ["text"] : ["text"],
1031
+ cost: sameIdentity
1032
+ ? fallback?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
1033
+ : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1034
+ contextWindow: sameIdentity ? fallback?.contextWindow ?? 0 : 0,
1035
+ maxTokens: sameIdentity ? fallback?.maxTokens ?? 0 : 0,
1036
+ compat: sameIdentity ? fallback?.compat : undefined,
1037
+ [ROUTED_FALLBACK_MODEL_SYMBOL]: true,
1029
1038
  } as PiModel;
1030
1039
  }
1031
1040
 
@@ -1041,6 +1050,28 @@ function findModelInRegistry(registry: ModelRegistryLike | undefined, provider:
1041
1050
  return all.find((candidate) => candidate.provider === provider && candidate.id === id);
1042
1051
  }
1043
1052
 
1053
+ function isRoutedFallbackModel(model: PiModel | undefined): boolean {
1054
+ return !!model && (model as PiModel & Record<symbol, unknown>)[ROUTED_FALLBACK_MODEL_SYMBOL] === true;
1055
+ }
1056
+
1057
+ function applyConfiguredTransportToModel(model: PiModel, config: unknown): PiModel {
1058
+ const providers = asRecord(asRecord(config)?.providers);
1059
+ const provider = asRecord(providers?.[model.provider]);
1060
+ const customModel = findLastExactModelDefinition(
1061
+ Array.isArray(provider?.models) ? provider.models : undefined,
1062
+ model.id,
1063
+ );
1064
+ const configuredApi = customModel?.api ?? provider?.api;
1065
+ const configuredBaseUrl = customModel?.baseUrl ?? provider?.baseUrl;
1066
+ const api = isNonEmptyString(model.api)
1067
+ ? model.api
1068
+ : (isNonEmptyString(configuredApi) ? configuredApi : model.api);
1069
+ const baseUrl = isNonEmptyString(model.baseUrl)
1070
+ ? model.baseUrl
1071
+ : (isNonEmptyString(configuredBaseUrl) ? configuredBaseUrl : model.baseUrl);
1072
+ return api === model.api && baseUrl === model.baseUrl ? model : { ...model, api, baseUrl };
1073
+ }
1074
+
1044
1075
  function resolveRouteModel(
1045
1076
  model: PiModel | undefined,
1046
1077
  ctx?: ContextWithOptionalModelRegistry,
@@ -1048,8 +1079,9 @@ function resolveRouteModel(
1048
1079
  const snapshot = resolveActiveRouteSnapshot(model, ctx);
1049
1080
  if (!snapshot) return undefined;
1050
1081
 
1051
- return findModelInRegistry(ctx?.modelRegistry, snapshot.provider, snapshot.modelId)
1082
+ const resolved = findModelInRegistry(ctx?.modelRegistry, snapshot.provider, snapshot.modelId)
1052
1083
  ?? routeSnapshotToPiModel(snapshot, model);
1084
+ return applyConfiguredTransportToModel(resolved, readEffectiveCompatConfig());
1053
1085
  }
1054
1086
 
1055
1087
  function isVirtualRoutingModel(model: PiModel | undefined, ctx?: Pick<ExtensionContext, "sessionManager">): boolean {
@@ -1134,17 +1166,335 @@ function getNonNegativeNumber(record: UnknownRecord, key: string): number | unde
1134
1166
  * Model-level compat takes precedence over provider-level compat for overlapping keys.
1135
1167
  * This matches Pi's model-registry.js mergeCompat behavior.
1136
1168
  */
1169
+ const NESTED_COMPAT_KEYS = [
1170
+ "openRouterRouting",
1171
+ "vercelGatewayRouting",
1172
+ "chatTemplateKwargs",
1173
+ "chatTemplateArgs",
1174
+ ] as const;
1175
+
1176
+ function findLastExactModelDefinition(
1177
+ models: unknown[] | undefined,
1178
+ modelId: string,
1179
+ ): UnknownRecord | undefined {
1180
+ if (!models) return undefined;
1181
+ // Pi's provider composer processes definitions in array order. If a malformed
1182
+ // or hand-edited config repeats an id, the last definition replaces earlier
1183
+ // definitions, so effective resolution must use the same deterministic rule.
1184
+ for (let index = models.length - 1; index >= 0; index--) {
1185
+ const candidate = asRecord(models[index]);
1186
+ if (candidate?.id === modelId) return candidate;
1187
+ }
1188
+ return undefined;
1189
+ }
1190
+
1191
+ function mergeCacheCompat(...sources: Array<UnknownRecord | undefined>): CacheCompat {
1192
+ const merged: CacheCompat = {};
1193
+ for (const source of sources) {
1194
+ if (!source) continue;
1195
+ const previousNested = Object.fromEntries(
1196
+ NESTED_COMPAT_KEYS.map((key) => [key, asRecord(merged[key])]),
1197
+ ) as Partial<Record<(typeof NESTED_COMPAT_KEYS)[number], UnknownRecord | undefined>>;
1198
+ Object.assign(merged, source);
1199
+ for (const key of NESTED_COMPAT_KEYS) {
1200
+ const baseValue = previousNested[key];
1201
+ const overrideValue = asRecord(source[key]);
1202
+ if (baseValue || overrideValue) {
1203
+ merged[key] = { ...baseValue, ...overrideValue };
1204
+ }
1205
+ }
1206
+ }
1207
+ return merged;
1208
+ }
1209
+
1210
+ function getEffectiveCompatSources(model: PiModel, config: unknown): Array<{ source: "provider" | "model" | "runtime" | "modelOverride"; compat: UnknownRecord }> {
1211
+ const root = asRecord(config);
1212
+ const providers = asRecord(root?.providers);
1213
+ const provider = asRecord(providers?.[model.provider]);
1214
+ const providerCompat = asRecord(provider?.compat);
1215
+ const customModel = findLastExactModelDefinition(
1216
+ Array.isArray(provider?.models) ? provider.models : undefined,
1217
+ model.id,
1218
+ );
1219
+ const customModelCompat = asRecord(asRecord(customModel)?.compat);
1220
+ const runtimeCompat = asRecord(model.compat);
1221
+ const modelOverride = asRecord(asRecord(provider?.modelOverrides)?.[model.id]);
1222
+ const modelOverrideCompat = asRecord(modelOverride?.compat);
1223
+
1224
+ return [
1225
+ ...(providerCompat ? [{ source: "provider" as const, compat: providerCompat }] : []),
1226
+ ...(customModelCompat ? [{ source: "model" as const, compat: customModelCompat }] : []),
1227
+ ...(runtimeCompat ? [{ source: "runtime" as const, compat: runtimeCompat }] : []),
1228
+ ...(modelOverrideCompat ? [{ source: "modelOverride" as const, compat: modelOverrideCompat }] : []),
1229
+ ];
1230
+ }
1231
+
1232
+ function resolveEffectiveCompatFromConfig(model: PiModel, config: unknown): CacheCompat {
1233
+ // Pi's effective precedence is provider → models[] → runtime model →
1234
+ // modelOverrides. The runtime layer is intentionally included between the
1235
+ // config model and modelOverride: extension providers can replace the model
1236
+ // object after lower config layers were applied, while the override remains
1237
+ // Pi's highest-precedence user layer.
1238
+ return mergeCacheCompat(...getEffectiveCompatSources(model, config).map(({ compat }) => compat));
1239
+ }
1240
+
1241
+ function getEffectiveCompatValueSource(
1242
+ model: PiModel,
1243
+ config: unknown,
1244
+ key: keyof CacheCompat,
1245
+ ): "provider" | "model" | "runtime" | "modelOverride" | undefined {
1246
+ let source: "provider" | "model" | "runtime" | "modelOverride" | undefined;
1247
+ for (const candidate of getEffectiveCompatSources(model, config)) {
1248
+ if (Object.prototype.hasOwnProperty.call(candidate.compat, key)) source = candidate.source;
1249
+ }
1250
+ return source;
1251
+ }
1252
+
1253
+ function isOptionalString(value: unknown): boolean {
1254
+ return value === undefined || (typeof value === "string" && value.length > 0);
1255
+ }
1256
+
1257
+ function isOptionalBoolean(value: unknown): boolean {
1258
+ return value === undefined || typeof value === "boolean";
1259
+ }
1260
+
1261
+ function isFiniteNumber(value: unknown): value is number {
1262
+ return typeof value === "number" && Number.isFinite(value);
1263
+ }
1264
+
1265
+ function isRecordOfStrings(value: unknown): boolean {
1266
+ const record = asRecord(value);
1267
+ return !!record && Object.values(record).every((entry) => typeof entry === "string");
1268
+ }
1269
+
1270
+ function isThinkingLevelMap(value: unknown): boolean {
1271
+ const map = asRecord(value);
1272
+ if (!map) return false;
1273
+ return ["off", "minimal", "low", "medium", "high", "xhigh", "max"].every((key) =>
1274
+ map[key] === undefined || map[key] === null || typeof map[key] === "string"
1275
+ );
1276
+ }
1277
+
1278
+ function isValidModelCostTier(value: unknown): boolean {
1279
+ const tier = asRecord(value);
1280
+ return !!tier
1281
+ && isFiniteNumber(tier.inputTokensAbove)
1282
+ && isFiniteNumber(tier.input)
1283
+ && isFiniteNumber(tier.output)
1284
+ && isFiniteNumber(tier.cacheRead)
1285
+ && isFiniteNumber(tier.cacheWrite);
1286
+ }
1287
+
1288
+ function isValidModelCost(value: unknown, partial: boolean): boolean {
1289
+ const cost = asRecord(value);
1290
+ if (!cost) return false;
1291
+ for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
1292
+ if ((!partial || cost[key] !== undefined) && !isFiniteNumber(cost[key])) return false;
1293
+ }
1294
+ return cost.tiers === undefined || (Array.isArray(cost.tiers) && cost.tiers.every(isValidModelCostTier));
1295
+ }
1296
+
1297
+ function isValidModelDefinition(value: unknown): boolean {
1298
+ const model = asRecord(value);
1299
+ if (!model || !isNonEmptyString(model.id)) return false;
1300
+ if (!isOptionalString(model.name) || !isOptionalString(model.api) || !isOptionalString(model.baseUrl)) return false;
1301
+ if (!isOptionalBoolean(model.reasoning) || !isValidCompatRecord(model.compat)) return false;
1302
+ if (model.thinkingLevelMap !== undefined && !isThinkingLevelMap(model.thinkingLevelMap)) return false;
1303
+ if (model.input !== undefined && (!Array.isArray(model.input) || !model.input.every((entry) => entry === "text" || entry === "image"))) return false;
1304
+ if (model.cost !== undefined && !isValidModelCost(model.cost, false)) return false;
1305
+ if (model.contextWindow !== undefined && !isFiniteNumber(model.contextWindow)) return false;
1306
+ if (model.maxTokens !== undefined && !isFiniteNumber(model.maxTokens)) return false;
1307
+ if (model.samplingParams !== undefined && !asRecord(model.samplingParams)) return false;
1308
+ if (model.headers !== undefined && !isRecordOfStrings(model.headers)) return false;
1309
+ return true;
1310
+ }
1311
+
1312
+ function isValidModelOverride(value: unknown): boolean {
1313
+ const override = asRecord(value);
1314
+ if (!override || !isValidCompatRecord(override.compat)) return false;
1315
+ if (!isOptionalString(override.name) || !isOptionalBoolean(override.reasoning)) return false;
1316
+ if (override.thinkingLevelMap !== undefined && !isThinkingLevelMap(override.thinkingLevelMap)) return false;
1317
+ if (override.input !== undefined && (!Array.isArray(override.input) || !override.input.every((entry) => entry === "text" || entry === "image"))) return false;
1318
+ if (override.cost !== undefined && !isValidModelCost(override.cost, true)) return false;
1319
+ if (override.contextWindow !== undefined && !isFiniteNumber(override.contextWindow)) return false;
1320
+ if (override.maxTokens !== undefined && !isFiniteNumber(override.maxTokens)) return false;
1321
+ if (override.samplingParams !== undefined && !asRecord(override.samplingParams)) return false;
1322
+ if (override.headers !== undefined && !isRecordOfStrings(override.headers)) return false;
1323
+ return true;
1324
+ }
1325
+
1326
+ function isValidOpenAICompletionsCompat(compat: UnknownRecord): boolean {
1327
+ const booleanKeys = [
1328
+ "supportsStore", "supportsDeveloperRole", "supportsReasoningEffort",
1329
+ "supportsUsageInStreaming", "requiresToolResultName", "requiresAssistantAfterToolResult",
1330
+ "requiresThinkingAsText", "requiresReasoningContentOnAssistantMessages",
1331
+ "supportsOpenAIGrammarTools", "supportsStrictMode", "sendSessionAffinityHeaders",
1332
+ "supportsLongCacheRetention",
1333
+ ];
1334
+ if (booleanKeys.some((key) => !isOptionalBoolean(compat[key]))) return false;
1335
+ if (compat.maxTokensField !== undefined && compat.maxTokensField !== "max_completion_tokens" && compat.maxTokensField !== "max_tokens") return false;
1336
+ if (compat.thinkingFormat !== undefined && ![
1337
+ "openai", "openrouter", "together", "baseten", "deepseek", "zai", "qwen",
1338
+ "chat-template", "qwen-chat-template", "string-thinking", "ant-ling",
1339
+ ].includes(String(compat.thinkingFormat))) return false;
1340
+ if (compat.cacheControlFormat !== undefined && compat.cacheControlFormat !== "anthropic") return false;
1341
+ if (compat.deferredToolsMode !== undefined && compat.deferredToolsMode !== "kimi") return false;
1342
+ if (compat.sessionAffinityFormat !== undefined && !["openai", "openai-nosession", "openrouter"].includes(String(compat.sessionAffinityFormat))) return false;
1343
+ for (const key of NESTED_COMPAT_KEYS) {
1344
+ if (compat[key] !== undefined && !asRecord(compat[key])) return false;
1345
+ }
1346
+ return true;
1347
+ }
1348
+
1349
+ function isValidOpenAIResponsesCompat(compat: UnknownRecord): boolean {
1350
+ const booleanKeys = [
1351
+ "supportsDeveloperRole", "supportsLongCacheRetention", "supportsStrictMode",
1352
+ "supportsOpenAIGrammarTools", "supportsAdditionalTools", "supportsToolSearch",
1353
+ ];
1354
+ if (booleanKeys.some((key) => !isOptionalBoolean(compat[key]))) return false;
1355
+ return compat.sessionAffinityFormat === undefined
1356
+ || ["openai", "openai-nosession", "openrouter"].includes(String(compat.sessionAffinityFormat));
1357
+ }
1358
+
1359
+ function isValidAnthropicMessagesCompat(compat: UnknownRecord): boolean {
1360
+ return [
1361
+ "supportsEagerToolInputStreaming", "supportsLongCacheRetention",
1362
+ "sendSessionAffinityHeaders", "supportsCacheControlOnTools", "supportsTemperature",
1363
+ "forceAdaptiveThinking", "allowEmptySignature", "supportsStrictTools", "supportsToolReferences",
1364
+ ].every((key) => isOptionalBoolean(compat[key]));
1365
+ }
1366
+
1367
+ function isValidCompatRecord(value: unknown): boolean {
1368
+ if (value === undefined) return true;
1369
+ const compat = asRecord(value);
1370
+ return !!compat && (
1371
+ isValidOpenAICompletionsCompat(compat)
1372
+ || isValidOpenAIResponsesCompat(compat)
1373
+ || isValidAnthropicMessagesCompat(compat)
1374
+ );
1375
+ }
1376
+
1377
+ // Credential-blind fail-closed subset of Pi's models.json validation. This
1378
+ // intentionally covers only fields consumed by compat diagnostics, transport
1379
+ // recovery, and exact route fallback; it is not a replacement for Pi's full
1380
+ // schema and must stay conservative when the file is malformed.
1381
+ function isValidModelsConfigForEffectiveCompat(value: unknown): boolean {
1382
+ const root = asRecord(value);
1383
+ const providers = asRecord(root?.providers);
1384
+ if (!root || !providers) return false;
1385
+
1386
+ for (const providerValue of Object.values(providers)) {
1387
+ const provider = asRecord(providerValue);
1388
+ if (!provider) return false;
1389
+ if (!isOptionalString(provider.name) || !isOptionalString(provider.baseUrl) || !isOptionalString(provider.apiKey) || !isOptionalString(provider.api)) return false;
1390
+ if (!isOptionalBoolean(provider.authHeader) || !isValidCompatRecord(provider.compat)) return false;
1391
+ if (provider.oauth !== undefined && provider.oauth !== "radius") return false;
1392
+ if (provider.headers !== undefined && !isRecordOfStrings(provider.headers)) return false;
1393
+ if (provider.models !== undefined && (!Array.isArray(provider.models) || !provider.models.every(isValidModelDefinition))) return false;
1394
+ const overrides = provider.modelOverrides === undefined ? undefined : asRecord(provider.modelOverrides);
1395
+ if (provider.modelOverrides !== undefined && (!overrides || !Object.values(overrides).every(isValidModelOverride))) return false;
1396
+ }
1397
+ return true;
1398
+ }
1399
+
1400
+ type ModelsConfigCache = {
1401
+ signature: string;
1402
+ value: unknown | undefined;
1403
+ };
1404
+
1405
+ let modelsConfigCache: ModelsConfigCache | undefined;
1406
+
1407
+ function invalidateModelsConfigCache(): void {
1408
+ modelsConfigCache = undefined;
1409
+ }
1410
+
1411
+ function getModelsConfigSignature(): string {
1412
+ try {
1413
+ const info = statSync(MODELS_JSON_PATH);
1414
+ return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
1415
+ } catch {
1416
+ return "missing";
1417
+ }
1418
+ }
1419
+
1420
+ function readEffectiveCompatConfig(): unknown | undefined {
1421
+ const signature = getModelsConfigSignature();
1422
+ if (modelsConfigCache?.signature === signature) return modelsConfigCache.value;
1423
+
1424
+ let value: unknown | undefined;
1425
+ if (signature !== "missing") {
1426
+ try {
1427
+ const parsed = parseJsonc(readFileSync(MODELS_JSON_PATH, "utf8"));
1428
+ value = isValidModelsConfigForEffectiveCompat(parsed) ? parsed : undefined;
1429
+ } catch {
1430
+ value = undefined;
1431
+ }
1432
+ }
1433
+
1434
+ modelsConfigCache = { signature, value };
1435
+ return value;
1436
+ }
1437
+
1137
1438
  function getCompat(model: PiModel | undefined): CacheCompat {
1138
1439
  if (!model) return {} as CacheCompat;
1440
+ return resolveEffectiveCompatFromConfig(model, readEffectiveCompatConfig());
1441
+ }
1139
1442
 
1140
- // Pi merges provider.compat with model.compat (model wins on conflicts)
1141
- // We approximate this by reading from ctx.model which should already have merged compat
1142
- // However, for safety, we check both levels if available
1143
- const modelCompat = (model.compat ?? {}) as CacheCompat;
1443
+ function hasProviderHeader(headers: Record<string, unknown>, name: string): boolean {
1444
+ const normalized = name.toLowerCase();
1445
+ return Object.keys(headers).some((headerName) => headerName.toLowerCase() === normalized);
1446
+ }
1144
1447
 
1145
- // Note: ctx.model from Pi should already contain merged compat,
1146
- // but we document the two-level structure for clarity
1147
- return modelCompat;
1448
+ function setProviderHeaderIfMissing(
1449
+ headers: Record<string, string | null | undefined>,
1450
+ name: string,
1451
+ value: string,
1452
+ ): boolean {
1453
+ if (hasProviderHeader(headers, name)) return false;
1454
+ headers[name] = value;
1455
+ return true;
1456
+ }
1457
+
1458
+ function addEffectiveSessionAffinityHeaders(
1459
+ headers: Record<string, string | null | undefined>,
1460
+ model: PiModel | undefined,
1461
+ sessionId: string | undefined,
1462
+ effectiveCompat?: CacheCompat,
1463
+ optimizerEnabled: boolean = runtimeOptimizerEnabled,
1464
+ effectiveSource?: "provider" | "model" | "runtime" | "modelOverride",
1465
+ ): boolean {
1466
+ if (!optimizerEnabled || !model || !isNonEmptyString(sessionId)) return false;
1467
+ if (!isOpenAICompatibleProxyApi(model.api) || !isNonEmptyString(model.baseUrl) || isOfficialOpenAIBaseUrl(model)) return false;
1468
+ const config = effectiveCompat === undefined || effectiveSource === undefined
1469
+ ? readEffectiveCompatConfig()
1470
+ : undefined;
1471
+ const compat = effectiveCompat ?? resolveEffectiveCompatFromConfig(model, config);
1472
+ const source = effectiveSource
1473
+ ?? getEffectiveCompatValueSource(model, config, "sendSessionAffinityHeaders")
1474
+ ?? (Object.prototype.hasOwnProperty.call(asRecord(model.compat) ?? {}, "sendSessionAffinityHeaders") ? "runtime" : undefined);
1475
+ if (compat.sendSessionAffinityHeaders !== true) return false;
1476
+
1477
+ // Pi already handles an effective runtime-model true. The bridge is only for
1478
+ // effective values that live in models.json layers and were lost when an
1479
+ // extension provider rebuilt/replaced the runtime model object.
1480
+ if (source === "runtime") return false;
1481
+
1482
+ const format = compat.sessionAffinityFormat ?? (
1483
+ lower(model.provider).includes("openrouter") || lower(model.baseUrl).includes("openrouter.ai")
1484
+ ? "openrouter"
1485
+ : "openai"
1486
+ );
1487
+ const value = sessionId.trim();
1488
+ let changed = false;
1489
+ if (format === "openrouter") {
1490
+ return setProviderHeaderIfMissing(headers, "x-session-id", value);
1491
+ }
1492
+ if (format === "openai") {
1493
+ changed = setProviderHeaderIfMissing(headers, "session_id", value) || changed;
1494
+ }
1495
+ changed = setProviderHeaderIfMissing(headers, "x-client-request-id", value) || changed;
1496
+ changed = setProviderHeaderIfMissing(headers, "x-session-affinity", value) || changed;
1497
+ return changed;
1148
1498
  }
1149
1499
 
1150
1500
  /** Join display-only path fragments without resolving them for I/O. */
@@ -1626,6 +1976,14 @@ function isKimiCodingEmptySignatureModel(model: PiModel | undefined): boolean {
1626
1976
  }
1627
1977
 
1628
1978
  function isAdaptiveThinkingCompatApplicable(model: PiModel): boolean {
1979
+ // A routed registry miss may preserve the upstream API/model identity while
1980
+ // lacking a verified endpoint. Do not diagnose or suggest compat fixes for
1981
+ // that incomplete transport metadata; ordinary direct Anthropic models still
1982
+ // use their normal native adaptive-thinking check.
1983
+ if (isRoutedFallbackModel(model) && !isNonEmptyString(model.baseUrl)) {
1984
+ return false;
1985
+ }
1986
+
1629
1987
  return lower(model.api) === "anthropic-messages"
1630
1988
  && (isAdaptiveGenerationModel(model) || isKimiCodingAdaptiveModel(model));
1631
1989
  }
@@ -2451,13 +2809,17 @@ function isOfficialOpenAIBaseUrl(model: PiModel): boolean {
2451
2809
  }
2452
2810
  }
2453
2811
 
2812
+ function isKnownThirdPartyOpenAIEndpoint(model: PiModel): boolean {
2813
+ return isNonEmptyString(model.baseUrl) && !isOfficialOpenAIBaseUrl(model);
2814
+ }
2815
+
2454
2816
  function describeMissingOpenAIFamilyProxyCompat(model: PiModel): string[] {
2455
2817
  const compat = getCompat(model);
2456
2818
  const missing: string[] = [];
2457
2819
 
2458
2820
  if (!isOpenAIFamilyModel(model)) return missing;
2459
2821
  if (!isOpenAICompatibleProxyApi(model.api)) return missing;
2460
- if (isOfficialOpenAIBaseUrl(model)) return missing;
2822
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return missing;
2461
2823
 
2462
2824
  if (compat.sendSessionAffinityHeaders !== true) {
2463
2825
  missing.push("sendSessionAffinityHeaders");
@@ -2477,7 +2839,7 @@ function describeMissingOpenAICompatibleProxyCompat(model: PiModel): string[] {
2477
2839
  const missing: string[] = [];
2478
2840
 
2479
2841
  if (!isOpenAICompatibleProxyApi(model.api)) return missing;
2480
- if (isOfficialOpenAIBaseUrl(model)) return missing;
2842
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return missing;
2481
2843
  if (isPiBuiltInLlamaCppModel(model)) return missing;
2482
2844
 
2483
2845
  if (compat.sendSessionAffinityHeaders === undefined) {
@@ -2505,7 +2867,7 @@ function describeOptionalOpenAICompatibleProxyCompat(model: PiModel): string[] {
2505
2867
  const optional: string[] = [];
2506
2868
 
2507
2869
  if (!isOpenAICompatibleProxyApi(model.api)) return optional;
2508
- if (isOfficialOpenAIBaseUrl(model)) return optional;
2870
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return optional;
2509
2871
  if (isPiBuiltInLlamaCppModel(model)) return optional;
2510
2872
 
2511
2873
  if (compat.supportsLongCacheRetention !== true) {
@@ -2690,7 +3052,10 @@ function describeMissingDeepSeekCompat(model: PiModel): string[] {
2690
3052
  }
2691
3053
 
2692
3054
  function isDeepSeekCompatCheckApplicable(model: PiModel): boolean {
2693
- return isDeepSeekLikeModel(model) && isOpenAICompatibleApi(model.api) && !isPiBuiltInLlamaCppModel(model);
3055
+ return isDeepSeekLikeModel(model)
3056
+ && isOpenAICompatibleApi(model.api)
3057
+ && isKnownThirdPartyOpenAIEndpoint(model)
3058
+ && !isPiBuiltInLlamaCppModel(model);
2694
3059
  }
2695
3060
 
2696
3061
  function describeMissingCacheCompatForModel(model: PiModel): string[] {
@@ -4852,12 +5217,12 @@ async function maybeCleanupStatsShardsV7(now = Date.now()): Promise<void> {
4852
5217
  }
4853
5218
 
4854
5219
  function isCompatCheckApplicable(model: PiModel): boolean {
4855
- return isOpenAICompatibleProxyApi(model.api) && !isOfficialOpenAIBaseUrl(model) && !isPiBuiltInLlamaCppModel(model);
5220
+ return isOpenAICompatibleProxyApi(model.api) && isKnownThirdPartyOpenAIEndpoint(model) && !isPiBuiltInLlamaCppModel(model);
4856
5221
  }
4857
5222
 
4858
5223
  function isPromptCacheRetention400Applicable(model: PiModel): boolean {
4859
5224
  return isOpenAICompatibleApi(model.api) &&
4860
- !isOfficialOpenAIBaseUrl(model) &&
5225
+ isKnownThirdPartyOpenAIEndpoint(model) &&
4861
5226
  !isPiBuiltInLlamaCppModel(model) &&
4862
5227
  getCompat(model).supportsLongCacheRetention === true;
4863
5228
  }
@@ -4885,6 +5250,7 @@ function isExplicitPromptCacheRetentionUnsupportedApplicable(model: PiModel): bo
4885
5250
  */
4886
5251
  function isSessionAffinity403Applicable(model: PiModel): boolean {
4887
5252
  if (!isOpenAICompatibleProxyApi(model.api)) return false;
5253
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return false;
4888
5254
  if (isPiBuiltInLlamaCppModel(model)) return false;
4889
5255
  return getCompat(model).sendSessionAffinityHeaders === true;
4890
5256
  }
@@ -4901,7 +5267,7 @@ function isSessionAffinity403Applicable(model: PiModel): boolean {
4901
5267
  */
4902
5268
  function isOpenAISdkHeader403Applicable(model: PiModel): boolean {
4903
5269
  if (!isOpenAICompatibleProxyApi(model.api)) return false;
4904
- if (isOfficialOpenAIBaseUrl(model)) return false;
5270
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return false;
4905
5271
  if (isPiBuiltInLlamaCppModel(model)) return false;
4906
5272
  return getCompat(model).sendSessionAffinityHeaders !== true;
4907
5273
  }
@@ -4939,8 +5305,9 @@ function describeRouterChannelDiagnostics(model: PiModel): string[] {
4939
5305
  return notes;
4940
5306
  }
4941
5307
 
4942
- // Official OpenAI bypass — no notes needed.
4943
- if (isOfficialOpenAIBaseUrl(model)) {
5308
+ // Unknown/default endpoints and official OpenAI are not diagnosable as
5309
+ // third-party router channels. Request-header bridging also fails closed.
5310
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) {
4944
5311
  return notes;
4945
5312
  }
4946
5313
 
@@ -5107,6 +5474,13 @@ function getCompatCheckNotApplicableLines(model: PiModel): string[] {
5107
5474
  ];
5108
5475
  }
5109
5476
 
5477
+ if (isOpenAICompatibleApi(api) && !isNonEmptyString(model.baseUrl)) {
5478
+ return [
5479
+ "ℹ️ Compat check not applicable for this model.",
5480
+ " Upstream endpoint metadata is unavailable; session-affinity header injection is disabled.",
5481
+ ];
5482
+ }
5483
+
5110
5484
  return ["ℹ️ Compat check not applicable for this model."];
5111
5485
  }
5112
5486
 
@@ -5759,12 +6133,13 @@ function resolveExplicitCompatValue(
5759
6133
  return { source: "modelOverride", value: overrideCompat[compatKey] };
5760
6134
  }
5761
6135
 
5762
- if (Array.isArray(provider.models)) {
5763
- const model = provider.models.find((entry: unknown) => asRecord(entry)?.id === modelId);
5764
- const modelCompat = asRecord(asRecord(model)?.compat);
5765
- if (modelCompat && Object.prototype.hasOwnProperty.call(modelCompat, compatKey)) {
5766
- return { source: "model", value: modelCompat[compatKey] };
5767
- }
6136
+ const model = findLastExactModelDefinition(
6137
+ Array.isArray(provider.models) ? provider.models : undefined,
6138
+ modelId,
6139
+ );
6140
+ const modelCompat = asRecord(model?.compat);
6141
+ if (modelCompat && Object.prototype.hasOwnProperty.call(modelCompat, compatKey)) {
6142
+ return { source: "model", value: modelCompat[compatKey] };
5768
6143
  }
5769
6144
 
5770
6145
  const providerCompat = asRecord(provider.compat);
@@ -6008,9 +6383,14 @@ function locateModelInJsonc(
6008
6383
  allModelIds.push(elementId);
6009
6384
  }
6010
6385
 
6011
- if (elementId === modelId && modelBrace < 0) {
6386
+ if (elementId === modelId) {
6387
+ // Match Pi's provider composer: later duplicate definitions replace
6388
+ // earlier ones. The effective/fix target is therefore the last exact id.
6012
6389
  modelBrace = elementBrace;
6013
6390
  modelEndBrace = elementEnd;
6391
+ compatKeyStartClean = -1;
6392
+ compatBrace = -1;
6393
+ compatEndBrace = -1;
6014
6394
 
6015
6395
  const compatKey = findJsonObjectKey(clean, modelBrace, "compat");
6016
6396
  if (compatKey && compatKey.keyStart < modelEndBrace) {
@@ -7158,6 +7538,7 @@ export const __internals_for_tests = {
7158
7538
  buildSafeOpenAIProxyCompatSuggestion,
7159
7539
  getPromptCacheRetentionUnsupportedHint,
7160
7540
  isOfficialOpenAIBaseUrl,
7541
+ isKnownThirdPartyOpenAIEndpoint,
7161
7542
  isCompatCheckApplicable,
7162
7543
  isPromptCacheRetention400Applicable,
7163
7544
  isSessionAffinity403Applicable,
@@ -7278,6 +7659,12 @@ export const __internals_for_tests = {
7278
7659
  buildOpenAIProxyCompatWarningText,
7279
7660
  getModelIdNameTokenValues,
7280
7661
  getAssistantMessageModelTokenValues,
7662
+ mergeCacheCompat,
7663
+ resolveEffectiveCompatFromConfig,
7664
+ getEffectiveCompatValueSource,
7665
+ findLastExactModelDefinition,
7666
+ isValidModelsConfigForEffectiveCompat,
7667
+ addEffectiveSessionAffinityHeaders,
7281
7668
  getCompat,
7282
7669
  modelKey,
7283
7670
  modelFromAssistantMessage,
@@ -7352,6 +7739,8 @@ export const __internals_for_tests = {
7352
7739
  parseRouteSnapshot,
7353
7740
  resolveActiveRouteSnapshot,
7354
7741
  routeSnapshotToPiModel,
7742
+ isRoutedFallbackModel,
7743
+ applyConfiguredTransportToModel,
7355
7744
  resolveRouteModel,
7356
7745
  isVirtualRoutingModel,
7357
7746
  installCacheHintsService,
@@ -7951,13 +8340,11 @@ export default function (pi: ExtensionAPI) {
7951
8340
  * The caller strips prompt_cache_retention when this returns false.
7952
8341
  */
7953
8342
  function hasExplicitLongRetentionOptIn(model: PiModel): boolean {
7954
- try {
7955
- const text = readFileSync(MODELS_JSON_PATH, "utf8");
7956
- const parsed = parseJsonc(text);
7957
- return hasExplicitLongRetentionOptInFromConfig(parsed, model.provider, model.id);
7958
- } catch {
7959
- return false;
7960
- }
8343
+ return hasExplicitLongRetentionOptInFromConfig(
8344
+ readEffectiveCompatConfig(),
8345
+ model.provider,
8346
+ model.id,
8347
+ );
7961
8348
  }
7962
8349
 
7963
8350
  pi.on("session_start", async (event, ctx) => {
@@ -8125,6 +8512,15 @@ export default function (pi: ExtensionAPI) {
8125
8512
  return {};
8126
8513
  });
8127
8514
 
8515
+ pi.on("before_provider_headers", (event, ctx) => {
8516
+ const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
8517
+ addEffectiveSessionAffinityHeaders(
8518
+ event.headers,
8519
+ requestModel,
8520
+ ctx.sessionManager.getSessionId(),
8521
+ );
8522
+ });
8523
+
8128
8524
  pi.on("before_provider_request", (event, ctx) => {
8129
8525
  const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
8130
8526
 
@@ -8742,6 +9138,20 @@ export default function (pi: ExtensionAPI) {
8742
9138
  return;
8743
9139
  }
8744
9140
 
9141
+ const duplicateTargetDefinitions = location.allModelIds.filter(
9142
+ (configuredModelId) => configuredModelId === suggestion.modelId,
9143
+ ).length;
9144
+ if (duplicateTargetDefinitions > 1) {
9145
+ cmdCtx.ui.notify(
9146
+ `❌ ${getModelsJsonDisplayPath()} contains ${duplicateTargetDefinitions} custom model definitions ` +
9147
+ `with the exact id "${suggestion.modelId}" under providers["${suggestion.providerLabel}"].\n` +
9148
+ `Pi uses the last definition, but /cache-optimizer fix refuses an ambiguous duplicate-id edit. ` +
9149
+ `Remove or consolidate the duplicates, then run the command again.`,
9150
+ "error",
9151
+ );
9152
+ return;
9153
+ }
9154
+
8745
9155
  // Compose the modified text — observed runtime failures are always
8746
9156
  // model-scoped; ordinary compat fixes use the safety-based placement.
8747
9157
  const decision = chooseFixPlacement(
@@ -8840,6 +9250,7 @@ export default function (pi: ExtensionAPI) {
8840
9250
  return;
8841
9251
  }
8842
9252
 
9253
+ invalidateModelsConfigCache();
8843
9254
  cmdCtx.ui.notify(
8844
9255
  `✅ Fix applied to ${getModelsJsonDisplayPath()}.\n` +
8845
9256
  `Backup saved to: ${backupPath}\n` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
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",