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 +3 -1
- package/README.zh-CN.md +3 -1
- package/index.ts +452 -41
- package/package.json +1 -1
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`;
|
|
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`
|
|
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:
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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)
|
|
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) &&
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
4943
|
-
|
|
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
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
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
|
|
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
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
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