pi-cache-optimizer 2.8.4 → 2.8.5

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
@@ -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,27 @@ 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 = Array.isArray(provider?.models)
1061
+ ? asRecord(provider.models.find((entry: unknown) => asRecord(entry)?.id === model.id))
1062
+ : undefined;
1063
+ const configuredApi = customModel?.api ?? provider?.api;
1064
+ const configuredBaseUrl = customModel?.baseUrl ?? provider?.baseUrl;
1065
+ const api = isNonEmptyString(model.api)
1066
+ ? model.api
1067
+ : (isNonEmptyString(configuredApi) ? configuredApi : model.api);
1068
+ const baseUrl = isNonEmptyString(model.baseUrl)
1069
+ ? model.baseUrl
1070
+ : (isNonEmptyString(configuredBaseUrl) ? configuredBaseUrl : model.baseUrl);
1071
+ return api === model.api && baseUrl === model.baseUrl ? model : { ...model, api, baseUrl };
1072
+ }
1073
+
1044
1074
  function resolveRouteModel(
1045
1075
  model: PiModel | undefined,
1046
1076
  ctx?: ContextWithOptionalModelRegistry,
@@ -1048,8 +1078,9 @@ function resolveRouteModel(
1048
1078
  const snapshot = resolveActiveRouteSnapshot(model, ctx);
1049
1079
  if (!snapshot) return undefined;
1050
1080
 
1051
- return findModelInRegistry(ctx?.modelRegistry, snapshot.provider, snapshot.modelId)
1081
+ const resolved = findModelInRegistry(ctx?.modelRegistry, snapshot.provider, snapshot.modelId)
1052
1082
  ?? routeSnapshotToPiModel(snapshot, model);
1083
+ return applyConfiguredTransportToModel(resolved, readEffectiveCompatConfig());
1053
1084
  }
1054
1085
 
1055
1086
  function isVirtualRoutingModel(model: PiModel | undefined, ctx?: Pick<ExtensionContext, "sessionManager">): boolean {
@@ -1134,17 +1165,286 @@ function getNonNegativeNumber(record: UnknownRecord, key: string): number | unde
1134
1165
  * Model-level compat takes precedence over provider-level compat for overlapping keys.
1135
1166
  * This matches Pi's model-registry.js mergeCompat behavior.
1136
1167
  */
1168
+ const NESTED_COMPAT_KEYS = [
1169
+ "openRouterRouting",
1170
+ "vercelGatewayRouting",
1171
+ "chatTemplateKwargs",
1172
+ "chatTemplateArgs",
1173
+ ] as const;
1174
+
1175
+ function mergeCacheCompat(...sources: Array<UnknownRecord | undefined>): CacheCompat {
1176
+ const merged: CacheCompat = {};
1177
+ for (const source of sources) {
1178
+ if (!source) continue;
1179
+ const previousNested = Object.fromEntries(
1180
+ NESTED_COMPAT_KEYS.map((key) => [key, asRecord(merged[key])]),
1181
+ ) as Partial<Record<(typeof NESTED_COMPAT_KEYS)[number], UnknownRecord | undefined>>;
1182
+ Object.assign(merged, source);
1183
+ for (const key of NESTED_COMPAT_KEYS) {
1184
+ const baseValue = previousNested[key];
1185
+ const overrideValue = asRecord(source[key]);
1186
+ if (baseValue || overrideValue) {
1187
+ merged[key] = { ...baseValue, ...overrideValue };
1188
+ }
1189
+ }
1190
+ }
1191
+ return merged;
1192
+ }
1193
+
1194
+ function getEffectiveCompatSources(model: PiModel, config: unknown): Array<{ source: "provider" | "model" | "runtime" | "modelOverride"; compat: UnknownRecord }> {
1195
+ const root = asRecord(config);
1196
+ const providers = asRecord(root?.providers);
1197
+ const provider = asRecord(providers?.[model.provider]);
1198
+ const providerCompat = asRecord(provider?.compat);
1199
+ const customModel = Array.isArray(provider?.models)
1200
+ ? provider.models.find((entry: unknown) => asRecord(entry)?.id === model.id)
1201
+ : undefined;
1202
+ const customModelCompat = asRecord(asRecord(customModel)?.compat);
1203
+ const runtimeCompat = asRecord(model.compat);
1204
+ const modelOverride = asRecord(asRecord(provider?.modelOverrides)?.[model.id]);
1205
+ const modelOverrideCompat = asRecord(modelOverride?.compat);
1206
+
1207
+ return [
1208
+ ...(providerCompat ? [{ source: "provider" as const, compat: providerCompat }] : []),
1209
+ ...(customModelCompat ? [{ source: "model" as const, compat: customModelCompat }] : []),
1210
+ ...(runtimeCompat ? [{ source: "runtime" as const, compat: runtimeCompat }] : []),
1211
+ ...(modelOverrideCompat ? [{ source: "modelOverride" as const, compat: modelOverrideCompat }] : []),
1212
+ ];
1213
+ }
1214
+
1215
+ function resolveEffectiveCompatFromConfig(model: PiModel, config: unknown): CacheCompat {
1216
+ // Pi's effective precedence is provider → models[] → runtime model →
1217
+ // modelOverrides. The runtime layer is intentionally included between the
1218
+ // config model and modelOverride: extension providers can replace the model
1219
+ // object after lower config layers were applied, while the override remains
1220
+ // Pi's highest-precedence user layer.
1221
+ return mergeCacheCompat(...getEffectiveCompatSources(model, config).map(({ compat }) => compat));
1222
+ }
1223
+
1224
+ function getEffectiveCompatValueSource(
1225
+ model: PiModel,
1226
+ config: unknown,
1227
+ key: keyof CacheCompat,
1228
+ ): "provider" | "model" | "runtime" | "modelOverride" | undefined {
1229
+ let source: "provider" | "model" | "runtime" | "modelOverride" | undefined;
1230
+ for (const candidate of getEffectiveCompatSources(model, config)) {
1231
+ if (Object.prototype.hasOwnProperty.call(candidate.compat, key)) source = candidate.source;
1232
+ }
1233
+ return source;
1234
+ }
1235
+
1236
+ function isOptionalString(value: unknown): boolean {
1237
+ return value === undefined || (typeof value === "string" && value.length > 0);
1238
+ }
1239
+
1240
+ function isOptionalBoolean(value: unknown): boolean {
1241
+ return value === undefined || typeof value === "boolean";
1242
+ }
1243
+
1244
+ function isFiniteNumber(value: unknown): value is number {
1245
+ return typeof value === "number" && Number.isFinite(value);
1246
+ }
1247
+
1248
+ function isRecordOfStrings(value: unknown): boolean {
1249
+ const record = asRecord(value);
1250
+ return !!record && Object.values(record).every((entry) => typeof entry === "string");
1251
+ }
1252
+
1253
+ function isThinkingLevelMap(value: unknown): boolean {
1254
+ const map = asRecord(value);
1255
+ if (!map) return false;
1256
+ return ["off", "minimal", "low", "medium", "high", "xhigh", "max"].every((key) =>
1257
+ map[key] === undefined || map[key] === null || typeof map[key] === "string"
1258
+ );
1259
+ }
1260
+
1261
+ function isValidModelCostTier(value: unknown): boolean {
1262
+ const tier = asRecord(value);
1263
+ return !!tier
1264
+ && isFiniteNumber(tier.inputTokensAbove)
1265
+ && isFiniteNumber(tier.input)
1266
+ && isFiniteNumber(tier.output)
1267
+ && isFiniteNumber(tier.cacheRead)
1268
+ && isFiniteNumber(tier.cacheWrite);
1269
+ }
1270
+
1271
+ function isValidModelCost(value: unknown, partial: boolean): boolean {
1272
+ const cost = asRecord(value);
1273
+ if (!cost) return false;
1274
+ for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
1275
+ if ((!partial || cost[key] !== undefined) && !isFiniteNumber(cost[key])) return false;
1276
+ }
1277
+ return cost.tiers === undefined || (Array.isArray(cost.tiers) && cost.tiers.every(isValidModelCostTier));
1278
+ }
1279
+
1280
+ function isValidModelDefinition(value: unknown): boolean {
1281
+ const model = asRecord(value);
1282
+ if (!model || !isNonEmptyString(model.id)) return false;
1283
+ if (!isOptionalString(model.name) || !isOptionalString(model.api) || !isOptionalString(model.baseUrl)) return false;
1284
+ if (!isOptionalBoolean(model.reasoning) || !isValidCompatRecord(model.compat)) return false;
1285
+ if (model.thinkingLevelMap !== undefined && !isThinkingLevelMap(model.thinkingLevelMap)) return false;
1286
+ if (model.input !== undefined && (!Array.isArray(model.input) || !model.input.every((entry) => entry === "text" || entry === "image"))) return false;
1287
+ if (model.cost !== undefined && !isValidModelCost(model.cost, false)) return false;
1288
+ if (model.contextWindow !== undefined && !isFiniteNumber(model.contextWindow)) return false;
1289
+ if (model.maxTokens !== undefined && !isFiniteNumber(model.maxTokens)) return false;
1290
+ if (model.samplingParams !== undefined && !asRecord(model.samplingParams)) return false;
1291
+ if (model.headers !== undefined && !isRecordOfStrings(model.headers)) return false;
1292
+ return true;
1293
+ }
1294
+
1295
+ function isValidModelOverride(value: unknown): boolean {
1296
+ const override = asRecord(value);
1297
+ if (!override || !isValidCompatRecord(override.compat)) return false;
1298
+ if (!isOptionalString(override.name) || !isOptionalBoolean(override.reasoning)) return false;
1299
+ if (override.thinkingLevelMap !== undefined && !isThinkingLevelMap(override.thinkingLevelMap)) return false;
1300
+ if (override.input !== undefined && (!Array.isArray(override.input) || !override.input.every((entry) => entry === "text" || entry === "image"))) return false;
1301
+ if (override.cost !== undefined && !isValidModelCost(override.cost, true)) return false;
1302
+ if (override.contextWindow !== undefined && !isFiniteNumber(override.contextWindow)) return false;
1303
+ if (override.maxTokens !== undefined && !isFiniteNumber(override.maxTokens)) return false;
1304
+ if (override.samplingParams !== undefined && !asRecord(override.samplingParams)) return false;
1305
+ if (override.headers !== undefined && !isRecordOfStrings(override.headers)) return false;
1306
+ return true;
1307
+ }
1308
+
1309
+ function isValidOpenAICompletionsCompat(compat: UnknownRecord): boolean {
1310
+ const booleanKeys = [
1311
+ "supportsStore", "supportsDeveloperRole", "supportsReasoningEffort",
1312
+ "supportsUsageInStreaming", "requiresToolResultName", "requiresAssistantAfterToolResult",
1313
+ "requiresThinkingAsText", "requiresReasoningContentOnAssistantMessages",
1314
+ "supportsOpenAIGrammarTools", "supportsStrictMode", "sendSessionAffinityHeaders",
1315
+ "supportsLongCacheRetention",
1316
+ ];
1317
+ if (booleanKeys.some((key) => !isOptionalBoolean(compat[key]))) return false;
1318
+ if (compat.maxTokensField !== undefined && compat.maxTokensField !== "max_completion_tokens" && compat.maxTokensField !== "max_tokens") return false;
1319
+ if (compat.thinkingFormat !== undefined && ![
1320
+ "openai", "openrouter", "together", "baseten", "deepseek", "zai", "qwen",
1321
+ "chat-template", "qwen-chat-template", "string-thinking", "ant-ling",
1322
+ ].includes(String(compat.thinkingFormat))) return false;
1323
+ if (compat.cacheControlFormat !== undefined && compat.cacheControlFormat !== "anthropic") return false;
1324
+ if (compat.deferredToolsMode !== undefined && compat.deferredToolsMode !== "kimi") return false;
1325
+ if (compat.sessionAffinityFormat !== undefined && !["openai", "openai-nosession", "openrouter"].includes(String(compat.sessionAffinityFormat))) return false;
1326
+ for (const key of NESTED_COMPAT_KEYS) {
1327
+ if (compat[key] !== undefined && !asRecord(compat[key])) return false;
1328
+ }
1329
+ return true;
1330
+ }
1331
+
1332
+ function isValidOpenAIResponsesCompat(compat: UnknownRecord): boolean {
1333
+ const booleanKeys = [
1334
+ "supportsDeveloperRole", "supportsLongCacheRetention", "supportsStrictMode",
1335
+ "supportsOpenAIGrammarTools", "supportsAdditionalTools", "supportsToolSearch",
1336
+ ];
1337
+ if (booleanKeys.some((key) => !isOptionalBoolean(compat[key]))) return false;
1338
+ return compat.sessionAffinityFormat === undefined
1339
+ || ["openai", "openai-nosession", "openrouter"].includes(String(compat.sessionAffinityFormat));
1340
+ }
1341
+
1342
+ function isValidAnthropicMessagesCompat(compat: UnknownRecord): boolean {
1343
+ return [
1344
+ "supportsEagerToolInputStreaming", "supportsLongCacheRetention",
1345
+ "sendSessionAffinityHeaders", "supportsCacheControlOnTools", "supportsTemperature",
1346
+ "forceAdaptiveThinking", "allowEmptySignature", "supportsStrictTools", "supportsToolReferences",
1347
+ ].every((key) => isOptionalBoolean(compat[key]));
1348
+ }
1349
+
1350
+ function isValidCompatRecord(value: unknown): boolean {
1351
+ if (value === undefined) return true;
1352
+ const compat = asRecord(value);
1353
+ return !!compat && (
1354
+ isValidOpenAICompletionsCompat(compat)
1355
+ || isValidOpenAIResponsesCompat(compat)
1356
+ || isValidAnthropicMessagesCompat(compat)
1357
+ );
1358
+ }
1359
+
1360
+ function isValidModelsConfigForEffectiveCompat(value: unknown): boolean {
1361
+ const root = asRecord(value);
1362
+ const providers = asRecord(root?.providers);
1363
+ if (!root || !providers) return false;
1364
+
1365
+ for (const providerValue of Object.values(providers)) {
1366
+ const provider = asRecord(providerValue);
1367
+ if (!provider) return false;
1368
+ if (!isOptionalString(provider.name) || !isOptionalString(provider.baseUrl) || !isOptionalString(provider.apiKey) || !isOptionalString(provider.api)) return false;
1369
+ if (!isOptionalBoolean(provider.authHeader) || !isValidCompatRecord(provider.compat)) return false;
1370
+ if (provider.oauth !== undefined && provider.oauth !== "radius") return false;
1371
+ if (provider.headers !== undefined && !isRecordOfStrings(provider.headers)) return false;
1372
+ if (provider.models !== undefined && (!Array.isArray(provider.models) || !provider.models.every(isValidModelDefinition))) return false;
1373
+ const overrides = provider.modelOverrides === undefined ? undefined : asRecord(provider.modelOverrides);
1374
+ if (provider.modelOverrides !== undefined && (!overrides || !Object.values(overrides).every(isValidModelOverride))) return false;
1375
+ }
1376
+ return true;
1377
+ }
1378
+
1379
+ function readEffectiveCompatConfig(): unknown | undefined {
1380
+ try {
1381
+ const parsed = parseJsonc(readFileSync(MODELS_JSON_PATH, "utf8"));
1382
+ return isValidModelsConfigForEffectiveCompat(parsed) ? parsed : undefined;
1383
+ } catch {
1384
+ return undefined;
1385
+ }
1386
+ }
1387
+
1137
1388
  function getCompat(model: PiModel | undefined): CacheCompat {
1138
1389
  if (!model) return {} as CacheCompat;
1390
+ return resolveEffectiveCompatFromConfig(model, readEffectiveCompatConfig());
1391
+ }
1392
+
1393
+ function hasProviderHeader(headers: Record<string, unknown>, name: string): boolean {
1394
+ const normalized = name.toLowerCase();
1395
+ return Object.keys(headers).some((headerName) => headerName.toLowerCase() === normalized);
1396
+ }
1139
1397
 
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;
1398
+ function setProviderHeaderIfMissing(
1399
+ headers: Record<string, string | null | undefined>,
1400
+ name: string,
1401
+ value: string,
1402
+ ): boolean {
1403
+ if (hasProviderHeader(headers, name)) return false;
1404
+ headers[name] = value;
1405
+ return true;
1406
+ }
1144
1407
 
1145
- // Note: ctx.model from Pi should already contain merged compat,
1146
- // but we document the two-level structure for clarity
1147
- return modelCompat;
1408
+ function addEffectiveSessionAffinityHeaders(
1409
+ headers: Record<string, string | null | undefined>,
1410
+ model: PiModel | undefined,
1411
+ sessionId: string | undefined,
1412
+ effectiveCompat?: CacheCompat,
1413
+ optimizerEnabled: boolean = runtimeOptimizerEnabled,
1414
+ effectiveSource?: "provider" | "model" | "runtime" | "modelOverride",
1415
+ ): boolean {
1416
+ if (!optimizerEnabled || !model || !isNonEmptyString(sessionId)) return false;
1417
+ if (!isOpenAICompatibleProxyApi(model.api) || !isNonEmptyString(model.baseUrl) || isOfficialOpenAIBaseUrl(model)) return false;
1418
+ const config = effectiveCompat === undefined || effectiveSource === undefined
1419
+ ? readEffectiveCompatConfig()
1420
+ : undefined;
1421
+ const compat = effectiveCompat ?? resolveEffectiveCompatFromConfig(model, config);
1422
+ const source = effectiveSource
1423
+ ?? getEffectiveCompatValueSource(model, config, "sendSessionAffinityHeaders")
1424
+ ?? (Object.prototype.hasOwnProperty.call(asRecord(model.compat) ?? {}, "sendSessionAffinityHeaders") ? "runtime" : undefined);
1425
+ if (compat.sendSessionAffinityHeaders !== true) return false;
1426
+
1427
+ // Pi already handles an effective runtime-model true. The bridge is only for
1428
+ // effective values that live in models.json layers and were lost when an
1429
+ // extension provider rebuilt/replaced the runtime model object.
1430
+ if (source === "runtime") return false;
1431
+
1432
+ const format = compat.sessionAffinityFormat ?? (
1433
+ lower(model.provider).includes("openrouter") || lower(model.baseUrl).includes("openrouter.ai")
1434
+ ? "openrouter"
1435
+ : "openai"
1436
+ );
1437
+ const value = sessionId.trim();
1438
+ let changed = false;
1439
+ if (format === "openrouter") {
1440
+ return setProviderHeaderIfMissing(headers, "x-session-id", value);
1441
+ }
1442
+ if (format === "openai") {
1443
+ changed = setProviderHeaderIfMissing(headers, "session_id", value) || changed;
1444
+ }
1445
+ changed = setProviderHeaderIfMissing(headers, "x-client-request-id", value) || changed;
1446
+ changed = setProviderHeaderIfMissing(headers, "x-session-affinity", value) || changed;
1447
+ return changed;
1148
1448
  }
1149
1449
 
1150
1450
  /** Join display-only path fragments without resolving them for I/O. */
@@ -1626,6 +1926,14 @@ function isKimiCodingEmptySignatureModel(model: PiModel | undefined): boolean {
1626
1926
  }
1627
1927
 
1628
1928
  function isAdaptiveThinkingCompatApplicable(model: PiModel): boolean {
1929
+ // A routed registry miss may preserve the upstream API/model identity while
1930
+ // lacking a verified endpoint. Do not diagnose or suggest compat fixes for
1931
+ // that incomplete transport metadata; ordinary direct Anthropic models still
1932
+ // use their normal native adaptive-thinking check.
1933
+ if (isRoutedFallbackModel(model) && !isNonEmptyString(model.baseUrl)) {
1934
+ return false;
1935
+ }
1936
+
1629
1937
  return lower(model.api) === "anthropic-messages"
1630
1938
  && (isAdaptiveGenerationModel(model) || isKimiCodingAdaptiveModel(model));
1631
1939
  }
@@ -2451,13 +2759,17 @@ function isOfficialOpenAIBaseUrl(model: PiModel): boolean {
2451
2759
  }
2452
2760
  }
2453
2761
 
2762
+ function isKnownThirdPartyOpenAIEndpoint(model: PiModel): boolean {
2763
+ return isNonEmptyString(model.baseUrl) && !isOfficialOpenAIBaseUrl(model);
2764
+ }
2765
+
2454
2766
  function describeMissingOpenAIFamilyProxyCompat(model: PiModel): string[] {
2455
2767
  const compat = getCompat(model);
2456
2768
  const missing: string[] = [];
2457
2769
 
2458
2770
  if (!isOpenAIFamilyModel(model)) return missing;
2459
2771
  if (!isOpenAICompatibleProxyApi(model.api)) return missing;
2460
- if (isOfficialOpenAIBaseUrl(model)) return missing;
2772
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return missing;
2461
2773
 
2462
2774
  if (compat.sendSessionAffinityHeaders !== true) {
2463
2775
  missing.push("sendSessionAffinityHeaders");
@@ -2477,7 +2789,7 @@ function describeMissingOpenAICompatibleProxyCompat(model: PiModel): string[] {
2477
2789
  const missing: string[] = [];
2478
2790
 
2479
2791
  if (!isOpenAICompatibleProxyApi(model.api)) return missing;
2480
- if (isOfficialOpenAIBaseUrl(model)) return missing;
2792
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return missing;
2481
2793
  if (isPiBuiltInLlamaCppModel(model)) return missing;
2482
2794
 
2483
2795
  if (compat.sendSessionAffinityHeaders === undefined) {
@@ -2505,7 +2817,7 @@ function describeOptionalOpenAICompatibleProxyCompat(model: PiModel): string[] {
2505
2817
  const optional: string[] = [];
2506
2818
 
2507
2819
  if (!isOpenAICompatibleProxyApi(model.api)) return optional;
2508
- if (isOfficialOpenAIBaseUrl(model)) return optional;
2820
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return optional;
2509
2821
  if (isPiBuiltInLlamaCppModel(model)) return optional;
2510
2822
 
2511
2823
  if (compat.supportsLongCacheRetention !== true) {
@@ -2690,7 +3002,10 @@ function describeMissingDeepSeekCompat(model: PiModel): string[] {
2690
3002
  }
2691
3003
 
2692
3004
  function isDeepSeekCompatCheckApplicable(model: PiModel): boolean {
2693
- return isDeepSeekLikeModel(model) && isOpenAICompatibleApi(model.api) && !isPiBuiltInLlamaCppModel(model);
3005
+ return isDeepSeekLikeModel(model)
3006
+ && isOpenAICompatibleApi(model.api)
3007
+ && isKnownThirdPartyOpenAIEndpoint(model)
3008
+ && !isPiBuiltInLlamaCppModel(model);
2694
3009
  }
2695
3010
 
2696
3011
  function describeMissingCacheCompatForModel(model: PiModel): string[] {
@@ -4852,12 +5167,12 @@ async function maybeCleanupStatsShardsV7(now = Date.now()): Promise<void> {
4852
5167
  }
4853
5168
 
4854
5169
  function isCompatCheckApplicable(model: PiModel): boolean {
4855
- return isOpenAICompatibleProxyApi(model.api) && !isOfficialOpenAIBaseUrl(model) && !isPiBuiltInLlamaCppModel(model);
5170
+ return isOpenAICompatibleProxyApi(model.api) && isKnownThirdPartyOpenAIEndpoint(model) && !isPiBuiltInLlamaCppModel(model);
4856
5171
  }
4857
5172
 
4858
5173
  function isPromptCacheRetention400Applicable(model: PiModel): boolean {
4859
5174
  return isOpenAICompatibleApi(model.api) &&
4860
- !isOfficialOpenAIBaseUrl(model) &&
5175
+ isKnownThirdPartyOpenAIEndpoint(model) &&
4861
5176
  !isPiBuiltInLlamaCppModel(model) &&
4862
5177
  getCompat(model).supportsLongCacheRetention === true;
4863
5178
  }
@@ -4885,6 +5200,7 @@ function isExplicitPromptCacheRetentionUnsupportedApplicable(model: PiModel): bo
4885
5200
  */
4886
5201
  function isSessionAffinity403Applicable(model: PiModel): boolean {
4887
5202
  if (!isOpenAICompatibleProxyApi(model.api)) return false;
5203
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return false;
4888
5204
  if (isPiBuiltInLlamaCppModel(model)) return false;
4889
5205
  return getCompat(model).sendSessionAffinityHeaders === true;
4890
5206
  }
@@ -4901,7 +5217,7 @@ function isSessionAffinity403Applicable(model: PiModel): boolean {
4901
5217
  */
4902
5218
  function isOpenAISdkHeader403Applicable(model: PiModel): boolean {
4903
5219
  if (!isOpenAICompatibleProxyApi(model.api)) return false;
4904
- if (isOfficialOpenAIBaseUrl(model)) return false;
5220
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) return false;
4905
5221
  if (isPiBuiltInLlamaCppModel(model)) return false;
4906
5222
  return getCompat(model).sendSessionAffinityHeaders !== true;
4907
5223
  }
@@ -4939,8 +5255,9 @@ function describeRouterChannelDiagnostics(model: PiModel): string[] {
4939
5255
  return notes;
4940
5256
  }
4941
5257
 
4942
- // Official OpenAI bypass — no notes needed.
4943
- if (isOfficialOpenAIBaseUrl(model)) {
5258
+ // Unknown/default endpoints and official OpenAI are not diagnosable as
5259
+ // third-party router channels. Request-header bridging also fails closed.
5260
+ if (!isKnownThirdPartyOpenAIEndpoint(model)) {
4944
5261
  return notes;
4945
5262
  }
4946
5263
 
@@ -5107,6 +5424,13 @@ function getCompatCheckNotApplicableLines(model: PiModel): string[] {
5107
5424
  ];
5108
5425
  }
5109
5426
 
5427
+ if (isOpenAICompatibleApi(api) && !isNonEmptyString(model.baseUrl)) {
5428
+ return [
5429
+ "ℹ️ Compat check not applicable for this model.",
5430
+ " Upstream endpoint metadata is unavailable; session-affinity header injection is disabled.",
5431
+ ];
5432
+ }
5433
+
5110
5434
  return ["ℹ️ Compat check not applicable for this model."];
5111
5435
  }
5112
5436
 
@@ -7158,6 +7482,7 @@ export const __internals_for_tests = {
7158
7482
  buildSafeOpenAIProxyCompatSuggestion,
7159
7483
  getPromptCacheRetentionUnsupportedHint,
7160
7484
  isOfficialOpenAIBaseUrl,
7485
+ isKnownThirdPartyOpenAIEndpoint,
7161
7486
  isCompatCheckApplicable,
7162
7487
  isPromptCacheRetention400Applicable,
7163
7488
  isSessionAffinity403Applicable,
@@ -7278,6 +7603,11 @@ export const __internals_for_tests = {
7278
7603
  buildOpenAIProxyCompatWarningText,
7279
7604
  getModelIdNameTokenValues,
7280
7605
  getAssistantMessageModelTokenValues,
7606
+ mergeCacheCompat,
7607
+ resolveEffectiveCompatFromConfig,
7608
+ getEffectiveCompatValueSource,
7609
+ isValidModelsConfigForEffectiveCompat,
7610
+ addEffectiveSessionAffinityHeaders,
7281
7611
  getCompat,
7282
7612
  modelKey,
7283
7613
  modelFromAssistantMessage,
@@ -7352,6 +7682,8 @@ export const __internals_for_tests = {
7352
7682
  parseRouteSnapshot,
7353
7683
  resolveActiveRouteSnapshot,
7354
7684
  routeSnapshotToPiModel,
7685
+ isRoutedFallbackModel,
7686
+ applyConfiguredTransportToModel,
7355
7687
  resolveRouteModel,
7356
7688
  isVirtualRoutingModel,
7357
7689
  installCacheHintsService,
@@ -7951,13 +8283,11 @@ export default function (pi: ExtensionAPI) {
7951
8283
  * The caller strips prompt_cache_retention when this returns false.
7952
8284
  */
7953
8285
  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
- }
8286
+ return hasExplicitLongRetentionOptInFromConfig(
8287
+ readEffectiveCompatConfig(),
8288
+ model.provider,
8289
+ model.id,
8290
+ );
7961
8291
  }
7962
8292
 
7963
8293
  pi.on("session_start", async (event, ctx) => {
@@ -8125,6 +8455,15 @@ export default function (pi: ExtensionAPI) {
8125
8455
  return {};
8126
8456
  });
8127
8457
 
8458
+ pi.on("before_provider_headers", (event, ctx) => {
8459
+ const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
8460
+ addEffectiveSessionAffinityHeaders(
8461
+ event.headers,
8462
+ requestModel,
8463
+ ctx.sessionManager.getSessionId(),
8464
+ );
8465
+ });
8466
+
8128
8467
  pi.on("before_provider_request", (event, ctx) => {
8129
8468
  const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
8130
8469
 
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.5",
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",