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 +3 -1
- package/README.zh-CN.md +3 -1
- package/index.ts +372 -33
- 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
|
@@ -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,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
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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)
|
|
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) &&
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
4943
|
-
|
|
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
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
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