pi-cache-optimizer 2.6.12 → 2.6.14
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/index.ts +168 -17
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1717,6 +1717,51 @@ function keyForModelExt(model: { provider: string; id: string }): string {
|
|
|
1717
1717
|
return `${model.provider}/${model.id}`;
|
|
1718
1718
|
}
|
|
1719
1719
|
|
|
1720
|
+
/**
|
|
1721
|
+
* For direct (non-virtual-routing) providers, the upstream API may normalize or
|
|
1722
|
+
* rename the model id echoed in its response (e.g. a request to
|
|
1723
|
+
* `zai-org/GLM-5.2-FP8` returns a message whose `model` field is
|
|
1724
|
+
* `GLM5.2-FP8`). Writing stats under the echoed name fragments the bucket
|
|
1725
|
+
* away from the active-model key the footer reads (`totalsByModel[ctx.model]`),
|
|
1726
|
+
* so the footer shows 0% even when the backend is hitting cache.
|
|
1727
|
+
*
|
|
1728
|
+
* When the response-derived statsModel differs from the active context model
|
|
1729
|
+
* only in name (same provider + same cache adapter), consolidate stats back to
|
|
1730
|
+
* the active model identity. Virtual routing providers are excluded — their
|
|
1731
|
+
* message-local metadata is authoritative for router correctness (spec:
|
|
1732
|
+
* `message_end` MUST prefer assistant message metadata).
|
|
1733
|
+
*
|
|
1734
|
+
* Never merges across providers or across adapters, so genuinely different
|
|
1735
|
+
* models are never combined.
|
|
1736
|
+
*/
|
|
1737
|
+
function consolidateDirectProviderStatsModel(
|
|
1738
|
+
statsModel: PiModel | undefined,
|
|
1739
|
+
ctxModel: PiModel | undefined,
|
|
1740
|
+
ctx?: Pick<ExtensionContext, "sessionManager">,
|
|
1741
|
+
): PiModel | undefined {
|
|
1742
|
+
if (!statsModel || !ctxModel) return statsModel;
|
|
1743
|
+
// Virtual routing providers keep message-local stats identity.
|
|
1744
|
+
if (isVirtualRoutingModel(ctxModel, ctx)) return statsModel;
|
|
1745
|
+
// Only consolidate within the same provider.
|
|
1746
|
+
if (statsModel.provider !== ctxModel.provider) return statsModel;
|
|
1747
|
+
// Only consolidate when both resolve to the same cache adapter object, so
|
|
1748
|
+
// genuinely different models sharing a provider (or sharing the same adapter
|
|
1749
|
+
// family id, e.g. GPT and GLM both report family id "openai") are never
|
|
1750
|
+
// merged. `selectAdapterForModel` returns the precise adapter object, so
|
|
1751
|
+
// object identity is the correct criterion.
|
|
1752
|
+
const statsAdapter = selectAdapterForModel(statsModel);
|
|
1753
|
+
const ctxAdapter = selectAdapterForModel(ctxModel);
|
|
1754
|
+
if (!statsAdapter || !ctxAdapter || statsAdapter !== ctxAdapter) return statsModel;
|
|
1755
|
+
// No drift — nothing to consolidate.
|
|
1756
|
+
if (statsModel.id === ctxModel.id) return statsModel;
|
|
1757
|
+
// Consolidate: pin stats to the active-model identity the footer reads.
|
|
1758
|
+
return {
|
|
1759
|
+
...statsModel,
|
|
1760
|
+
id: ctxModel.id,
|
|
1761
|
+
name: ctxModel.name || ctxModel.id,
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1720
1765
|
function usageRecordFromAssistant(message: unknown): UnknownRecord | undefined {
|
|
1721
1766
|
return asRecord(getAssistantRecord(message)?.usage);
|
|
1722
1767
|
}
|
|
@@ -3876,6 +3921,22 @@ function isPromptCacheRetention400Applicable(model: PiModel): boolean {
|
|
|
3876
3921
|
getCompat(model).supportsLongCacheRetention === true;
|
|
3877
3922
|
}
|
|
3878
3923
|
|
|
3924
|
+
/**
|
|
3925
|
+
* Whether the 403 sendSessionAffinityHeaders diagnostic applies to a model.
|
|
3926
|
+
*
|
|
3927
|
+
* Pi's openai-completions / openai-responses adapters send three custom HTTP
|
|
3928
|
+
* headers (session_id, x-client-request-id, x-session-affinity) when
|
|
3929
|
+
* `sendSessionAffinityHeaders` is enabled. Some third-party proxies / CDNs /
|
|
3930
|
+
* WAFs block these custom headers and return HTTP 403 "Your request was
|
|
3931
|
+
* blocked". This guard returns true only for OpenAI-compatible APIs whose
|
|
3932
|
+
* merged compat currently enables session-affinity headers, so the 403
|
|
3933
|
+
* monitoring and doctor/fix paths can advise disabling the flag.
|
|
3934
|
+
*/
|
|
3935
|
+
function isSessionAffinity403Applicable(model: PiModel): boolean {
|
|
3936
|
+
if (!isOpenAICompatibleApi(model.api)) return false;
|
|
3937
|
+
return getCompat(model).sendSessionAffinityHeaders === true;
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3879
3940
|
/**
|
|
3880
3941
|
* Detect router / channel profiles from a PiModel and return diagnostic notes.
|
|
3881
3942
|
*
|
|
@@ -4073,7 +4134,7 @@ function getCompatCheckNotApplicableLines(model: PiModel): string[] {
|
|
|
4073
4134
|
return ["ℹ️ Compat check not applicable for this model."];
|
|
4074
4135
|
}
|
|
4075
4136
|
|
|
4076
|
-
function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400?: boolean } = {}): string {
|
|
4137
|
+
function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400?: boolean; sessionAffinity403?: boolean } = {}): string {
|
|
4077
4138
|
const lines: string[] = [];
|
|
4078
4139
|
lines.push(`Provider: ${model.provider}`);
|
|
4079
4140
|
lines.push(`Model: ${model.id}`);
|
|
@@ -4132,6 +4193,25 @@ function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400
|
|
|
4132
4193
|
}
|
|
4133
4194
|
}
|
|
4134
4195
|
|
|
4196
|
+
// ── Session affinity 403 diagnostics ──
|
|
4197
|
+
// Advises the user when sendSessionAffinityHeaders is enabled, since some
|
|
4198
|
+
// third-party proxies / CDNs / WAFs block Pi's custom session-affinity HTTP
|
|
4199
|
+
// headers (session_id, x-client-request-id, x-session-affinity) and return
|
|
4200
|
+
// 403. If a 403 was already observed for this model, show a stronger hint.
|
|
4201
|
+
if (isSessionAffinity403Applicable(model)) {
|
|
4202
|
+
lines.push("");
|
|
4203
|
+
if (options.sessionAffinity403) {
|
|
4204
|
+
lines.push("⚠️ A 403 response was observed while sendSessionAffinityHeaders is enabled.");
|
|
4205
|
+
lines.push(" The proxy/CDN likely blocks Pi's custom session-affinity headers (session_id,");
|
|
4206
|
+
lines.push(" x-client-request-id, x-session-affinity). Run /cache-optimizer fix to");
|
|
4207
|
+
lines.push(` set sendSessionAffinityHeaders: false in ${getModelsJsonDisplayPath()}.`);
|
|
4208
|
+
} else {
|
|
4209
|
+
lines.push("ℹ️ Session affinity headers are enabled. Some CDNs/WAFs block custom headers");
|
|
4210
|
+
lines.push(" (session_id, x-client-request-id, x-session-affinity) and return 403. If you");
|
|
4211
|
+
lines.push(" see 403 errors, run /cache-optimizer fix to set sendSessionAffinityHeaders: false.");
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4135
4215
|
// ── Router/channel diagnostics ──
|
|
4136
4216
|
const routerNotes = describeRouterChannelDiagnostics(model);
|
|
4137
4217
|
if (routerNotes.length > 0) {
|
|
@@ -4322,6 +4402,13 @@ function buildCompatDiagnosis(model: PiModel): string | undefined {
|
|
|
4322
4402
|
if (isPromptCacheRetention400Applicable(model)) {
|
|
4323
4403
|
lines.push(getPromptCacheRetentionUnsupportedHint());
|
|
4324
4404
|
}
|
|
4405
|
+
// Advisory for 403 session-affinity header blocking (only when enabled).
|
|
4406
|
+
if (isSessionAffinity403Applicable(model)) {
|
|
4407
|
+
lines.push(
|
|
4408
|
+
"ℹ️ Session affinity headers are enabled. If you see 403 \"blocked\" errors,",
|
|
4409
|
+
" the proxy/CDN may be blocking Pi's custom headers. Set sendSessionAffinityHeaders: false.",
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4325
4412
|
appendOptionalOpenAIProxyCompatAdviceLines(lines, optionalOpenAIProxyCompat);
|
|
4326
4413
|
} else {
|
|
4327
4414
|
lines.push(...getCompatCheckNotApplicableLines(model));
|
|
@@ -5598,6 +5685,7 @@ export const __internals_for_tests = {
|
|
|
5598
5685
|
isOfficialOpenAIBaseUrl,
|
|
5599
5686
|
isCompatCheckApplicable,
|
|
5600
5687
|
isPromptCacheRetention400Applicable,
|
|
5688
|
+
isSessionAffinity403Applicable,
|
|
5601
5689
|
hasPromptCacheRetentionUnsupportedSignal,
|
|
5602
5690
|
// Non-GPT OpenAI-compatible model detection
|
|
5603
5691
|
isKimiLikeModel,
|
|
@@ -5715,6 +5803,7 @@ export const __internals_for_tests = {
|
|
|
5715
5803
|
getAssistantMessageModelTokenValues,
|
|
5716
5804
|
getCompat,
|
|
5717
5805
|
modelKey,
|
|
5806
|
+
consolidateDirectProviderStatsModel,
|
|
5718
5807
|
// Platform-friendly path helpers
|
|
5719
5808
|
getModelsJsonDisplayPath,
|
|
5720
5809
|
buildProviderCompatOverride,
|
|
@@ -5813,6 +5902,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
5813
5902
|
const warnedModels = new Set<string>();
|
|
5814
5903
|
const promptCacheRetention400Models = new Set<string>();
|
|
5815
5904
|
const warnedPromptCacheRetention400Models = new Set<string>();
|
|
5905
|
+
const sendSessionAffinityHeaders403Models = new Set<string>();
|
|
5906
|
+
const warnedSendSessionAffinityHeaders403Models = new Set<string>();
|
|
5816
5907
|
let cacheStatsByModel: Record<string, CacheStats> = {};
|
|
5817
5908
|
let cacheStatsTotalsByModel: Record<string, CacheStats> = {};
|
|
5818
5909
|
let cacheStatsLegacyFamily: Partial<Record<CacheProviderId, CacheStats>> = emptyAllCacheStats();
|
|
@@ -6460,20 +6551,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
6460
6551
|
pi.on("after_provider_response", (event, ctx) => {
|
|
6461
6552
|
const model = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
6462
6553
|
if (!runtimeOptimizerEnabled || !model) return;
|
|
6463
|
-
if (event.status !== 400) return;
|
|
6464
|
-
if (!isPromptCacheRetention400Applicable(model)) return;
|
|
6465
|
-
if (!hasPromptCacheRetentionUnsupportedSignal(event.headers)) return;
|
|
6466
6554
|
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6555
|
+
// ── 400: prompt_cache_retention unsupported ──
|
|
6556
|
+
if (event.status === 400) {
|
|
6557
|
+
if (!isPromptCacheRetention400Applicable(model)) return;
|
|
6558
|
+
if (!hasPromptCacheRetentionUnsupportedSignal(event.headers)) return;
|
|
6559
|
+
|
|
6560
|
+
const key = modelKey(model);
|
|
6561
|
+
promptCacheRetention400Models.add(key);
|
|
6562
|
+
if (warnedPromptCacheRetention400Models.has(key)) return;
|
|
6563
|
+
warnedPromptCacheRetention400Models.add(key);
|
|
6564
|
+
ctx.ui.notify(
|
|
6565
|
+
`⚠️ ${LOG_PREFIX}: ${key} returned HTTP 400 while supportsLongCacheRetention is enabled. ` +
|
|
6566
|
+
getPromptCacheRetentionUnsupportedHint() +
|
|
6567
|
+
` Run /cache-optimizer doctor for the exact edit location.`,
|
|
6568
|
+
"warning",
|
|
6569
|
+
);
|
|
6570
|
+
return;
|
|
6571
|
+
}
|
|
6572
|
+
|
|
6573
|
+
// ── 403: sendSessionAffinityHeaders blocked by CDN/WAF ──
|
|
6574
|
+
//
|
|
6575
|
+
// Pi's openai-completions/responses adapters send three custom HTTP
|
|
6576
|
+
// headers (session_id, x-client-request-id, x-session-affinity) when
|
|
6577
|
+
// sendSessionAffinityHeaders is enabled. Some third-party proxies /
|
|
6578
|
+
// CDNs / WAFs block these custom headers and return 403. We record a
|
|
6579
|
+
// one-time per-session notification and surface it in doctor/fix so the
|
|
6580
|
+
// user can disable the flag for the blocked model.
|
|
6581
|
+
if (event.status === 403) {
|
|
6582
|
+
if (!isSessionAffinity403Applicable(model)) return;
|
|
6583
|
+
|
|
6584
|
+
const key403 = modelKey(model);
|
|
6585
|
+
sendSessionAffinityHeaders403Models.add(key403);
|
|
6586
|
+
if (warnedSendSessionAffinityHeaders403Models.has(key403)) return;
|
|
6587
|
+
warnedSendSessionAffinityHeaders403Models.add(key403);
|
|
6588
|
+
ctx.ui.notify(
|
|
6589
|
+
`⚠️ ${LOG_PREFIX}: ${key403} returned HTTP 403 while sendSessionAffinityHeaders is enabled. ` +
|
|
6590
|
+
`The proxy/CDN may be blocking Pi's custom session-affinity headers (session_id, x-client-request-id, x-session-affinity). ` +
|
|
6591
|
+
`Run /cache-optimizer doctor for details and /cache-optimizer fix to set sendSessionAffinityHeaders: false.`,
|
|
6592
|
+
"warning",
|
|
6593
|
+
);
|
|
6594
|
+
return;
|
|
6595
|
+
}
|
|
6477
6596
|
});
|
|
6478
6597
|
|
|
6479
6598
|
pi.on("message_end", async (event, ctx) => {
|
|
@@ -6498,7 +6617,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
6498
6617
|
// Completed message metadata is request-local and authoritative for virtual
|
|
6499
6618
|
// routing providers. Use it whenever it supplies provider/model identity;
|
|
6500
6619
|
// fall back to the active context model for direct providers.
|
|
6501
|
-
|
|
6620
|
+
let statsModel = modelFromAssistantMessage(event.message, ctx.model) ?? ctx.model;
|
|
6621
|
+
// For direct (non-virtual-routing) providers, the upstream API may echo a
|
|
6622
|
+
// normalized/renamed model id in its response (e.g. request
|
|
6623
|
+
// `zai-org/GLM-5.2-FP8` but the message carries `GLM5.2-FP8`). Writing
|
|
6624
|
+
// stats under the echoed name fragments the bucket away from the
|
|
6625
|
+
// active-model key the footer reads, showing 0% even when the backend is
|
|
6626
|
+
// hitting cache. When the response model drifts from the active model only
|
|
6627
|
+
// in name (same provider, same cache adapter), consolidate stats back to
|
|
6628
|
+
// the active model id. Virtual routing providers keep message-local
|
|
6629
|
+
// identity (router correctness).
|
|
6630
|
+
statsModel = consolidateDirectProviderStatsModel(statsModel, ctx.model, ctx);
|
|
6502
6631
|
let routedModelChanged = false;
|
|
6503
6632
|
if (isVirtualRoutingModel(ctx.model, ctx) && statsModel && !isVirtualRoutingModel(statsModel, ctx)) {
|
|
6504
6633
|
const nextRoutedModel: PersistedRoutedModelRef = {
|
|
@@ -6585,7 +6714,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6585
6714
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
6586
6715
|
return;
|
|
6587
6716
|
}
|
|
6588
|
-
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
|
|
6717
|
+
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)) });
|
|
6589
6718
|
const adapter = selectAdapterForModel(model);
|
|
6590
6719
|
const sk = model ? sessionModelKey(model) : undefined;
|
|
6591
6720
|
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
@@ -6675,6 +6804,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
6675
6804
|
};
|
|
6676
6805
|
}
|
|
6677
6806
|
|
|
6807
|
+
// If no regular missing compat flags but the model has a recorded
|
|
6808
|
+
// 403 while sendSessionAffinityHeaders was enabled (Pi sent custom
|
|
6809
|
+
// session-affinity HTTP headers and the proxy/CDN blocked them), offer
|
|
6810
|
+
// to override `sendSessionAffinityHeaders` to false in models.json.
|
|
6811
|
+
if (!suggestion && isSessionAffinity403Applicable(model) && sendSessionAffinityHeaders403Models.has(modelKey(model))) {
|
|
6812
|
+
const key = modelKey(model);
|
|
6813
|
+
const slashIdx = key.indexOf("/");
|
|
6814
|
+
const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
|
|
6815
|
+
suggestion = {
|
|
6816
|
+
providerLabel,
|
|
6817
|
+
modelId: model.id,
|
|
6818
|
+
compatKeys: { sendSessionAffinityHeaders: false },
|
|
6819
|
+
};
|
|
6820
|
+
}
|
|
6821
|
+
|
|
6678
6822
|
if (!suggestion) {
|
|
6679
6823
|
const key = modelKey(model);
|
|
6680
6824
|
cmdCtx.ui.notify(`✅ Nothing to fix for "${key}". Compat already configured.`, "info");
|
|
@@ -6699,6 +6843,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
6699
6843
|
"Create or edit the entry below to override supportsLongCacheRetention to false.",
|
|
6700
6844
|
);
|
|
6701
6845
|
}
|
|
6846
|
+
if (sendSessionAffinityHeaders403Models.has(modelKey(model))) {
|
|
6847
|
+
manualLines.push(
|
|
6848
|
+
"",
|
|
6849
|
+
"💡 This model returned HTTP 403 while sendSessionAffinityHeaders was enabled.",
|
|
6850
|
+
"Create or edit the entry below to override sendSessionAffinityHeaders to false.",
|
|
6851
|
+
);
|
|
6852
|
+
}
|
|
6702
6853
|
manualLines.push(
|
|
6703
6854
|
"",
|
|
6704
6855
|
"If the provider/model already exists in models.json, add these compat keys under",
|
|
@@ -6984,7 +7135,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6984
7135
|
if (!model) {
|
|
6985
7136
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
6986
7137
|
} else {
|
|
6987
|
-
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
|
|
7138
|
+
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)) });
|
|
6988
7139
|
const adapter = selectAdapterForModel(model);
|
|
6989
7140
|
const sk = model ? sessionModelKey(model) : undefined;
|
|
6990
7141
|
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
package/package.json
CHANGED