pi-cache-optimizer 2.6.13 → 2.6.15

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.
Files changed (2) hide show
  1. package/index.ts +118 -17
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1992,10 +1992,16 @@ function describeMissingOpenAICompatibleProxyCompat(model: PiModel): string[] {
1992
1992
  if (!isOpenAICompatibleProxyApi(model.api)) return missing;
1993
1993
  if (isOfficialOpenAIBaseUrl(model)) return missing;
1994
1994
 
1995
- if (compat.sendSessionAffinityHeaders !== true) {
1995
+ if (compat.sendSessionAffinityHeaders === undefined) {
1996
1996
  missing.push("sendSessionAffinityHeaders");
1997
1997
  }
1998
1998
 
1999
+ // Explicit `sendSessionAffinityHeaders: false` is a valid safe opt-out for
2000
+ // proxies/CDNs/WAFs that block Pi's custom affinity headers with HTTP 403.
2001
+ // Treat only a missing/undefined value as missing compat; do not mark an
2002
+ // intentional false override as ⚠️ compat or let /cache-optimizer fix turn it
2003
+ // back to true.
2004
+ //
1999
2005
  // NOTE: supportsLongCacheRetention is intentionally NOT checked here.
2000
2006
  // Per spec, it is optional/risky advisory text only and must NOT trigger
2001
2007
  // the ⚠️ compat marker. The before_provider_request hook proactively
@@ -3921,6 +3927,22 @@ function isPromptCacheRetention400Applicable(model: PiModel): boolean {
3921
3927
  getCompat(model).supportsLongCacheRetention === true;
3922
3928
  }
3923
3929
 
3930
+ /**
3931
+ * Whether the 403 sendSessionAffinityHeaders diagnostic applies to a model.
3932
+ *
3933
+ * Pi's openai-completions / openai-responses adapters send three custom HTTP
3934
+ * headers (session_id, x-client-request-id, x-session-affinity) when
3935
+ * `sendSessionAffinityHeaders` is enabled. Some third-party proxies / CDNs /
3936
+ * WAFs block these custom headers and return HTTP 403 "Your request was
3937
+ * blocked". This guard returns true only for OpenAI-compatible APIs whose
3938
+ * merged compat currently enables session-affinity headers, so the 403
3939
+ * monitoring and doctor/fix paths can advise disabling the flag.
3940
+ */
3941
+ function isSessionAffinity403Applicable(model: PiModel): boolean {
3942
+ if (!isOpenAICompatibleApi(model.api)) return false;
3943
+ return getCompat(model).sendSessionAffinityHeaders === true;
3944
+ }
3945
+
3924
3946
  /**
3925
3947
  * Detect router / channel profiles from a PiModel and return diagnostic notes.
3926
3948
  *
@@ -4118,7 +4140,7 @@ function getCompatCheckNotApplicableLines(model: PiModel): string[] {
4118
4140
  return ["ℹ️ Compat check not applicable for this model."];
4119
4141
  }
4120
4142
 
4121
- function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400?: boolean } = {}): string {
4143
+ function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400?: boolean; sessionAffinity403?: boolean } = {}): string {
4122
4144
  const lines: string[] = [];
4123
4145
  lines.push(`Provider: ${model.provider}`);
4124
4146
  lines.push(`Model: ${model.id}`);
@@ -4177,6 +4199,25 @@ function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400
4177
4199
  }
4178
4200
  }
4179
4201
 
4202
+ // ── Session affinity 403 diagnostics ──
4203
+ // Advises the user when sendSessionAffinityHeaders is enabled, since some
4204
+ // third-party proxies / CDNs / WAFs block Pi's custom session-affinity HTTP
4205
+ // headers (session_id, x-client-request-id, x-session-affinity) and return
4206
+ // 403. If a 403 was already observed for this model, show a stronger hint.
4207
+ if (isSessionAffinity403Applicable(model)) {
4208
+ lines.push("");
4209
+ if (options.sessionAffinity403) {
4210
+ lines.push("⚠️ A 403 response was observed while sendSessionAffinityHeaders is enabled.");
4211
+ lines.push(" The proxy/CDN likely blocks Pi's custom session-affinity headers (session_id,");
4212
+ lines.push(" x-client-request-id, x-session-affinity). Run /cache-optimizer fix to");
4213
+ lines.push(` set sendSessionAffinityHeaders: false in ${getModelsJsonDisplayPath()}.`);
4214
+ } else {
4215
+ lines.push("ℹ️ Session affinity headers are enabled. Some CDNs/WAFs block custom headers");
4216
+ lines.push(" (session_id, x-client-request-id, x-session-affinity) and return 403. If you");
4217
+ lines.push(" see 403 errors, run /cache-optimizer fix to set sendSessionAffinityHeaders: false.");
4218
+ }
4219
+ }
4220
+
4180
4221
  // ── Router/channel diagnostics ──
4181
4222
  const routerNotes = describeRouterChannelDiagnostics(model);
4182
4223
  if (routerNotes.length > 0) {
@@ -4367,6 +4408,13 @@ function buildCompatDiagnosis(model: PiModel): string | undefined {
4367
4408
  if (isPromptCacheRetention400Applicable(model)) {
4368
4409
  lines.push(getPromptCacheRetentionUnsupportedHint());
4369
4410
  }
4411
+ // Advisory for 403 session-affinity header blocking (only when enabled).
4412
+ if (isSessionAffinity403Applicable(model)) {
4413
+ lines.push(
4414
+ "ℹ️ Session affinity headers are enabled. If you see 403 \"blocked\" errors,",
4415
+ " the proxy/CDN may be blocking Pi's custom headers. Set sendSessionAffinityHeaders: false.",
4416
+ );
4417
+ }
4370
4418
  appendOptionalOpenAIProxyCompatAdviceLines(lines, optionalOpenAIProxyCompat);
4371
4419
  } else {
4372
4420
  lines.push(...getCompatCheckNotApplicableLines(model));
@@ -5643,6 +5691,7 @@ export const __internals_for_tests = {
5643
5691
  isOfficialOpenAIBaseUrl,
5644
5692
  isCompatCheckApplicable,
5645
5693
  isPromptCacheRetention400Applicable,
5694
+ isSessionAffinity403Applicable,
5646
5695
  hasPromptCacheRetentionUnsupportedSignal,
5647
5696
  // Non-GPT OpenAI-compatible model detection
5648
5697
  isKimiLikeModel,
@@ -5859,6 +5908,8 @@ export default function (pi: ExtensionAPI) {
5859
5908
  const warnedModels = new Set<string>();
5860
5909
  const promptCacheRetention400Models = new Set<string>();
5861
5910
  const warnedPromptCacheRetention400Models = new Set<string>();
5911
+ const sendSessionAffinityHeaders403Models = new Set<string>();
5912
+ const warnedSendSessionAffinityHeaders403Models = new Set<string>();
5862
5913
  let cacheStatsByModel: Record<string, CacheStats> = {};
5863
5914
  let cacheStatsTotalsByModel: Record<string, CacheStats> = {};
5864
5915
  let cacheStatsLegacyFamily: Partial<Record<CacheProviderId, CacheStats>> = emptyAllCacheStats();
@@ -6506,20 +6557,48 @@ export default function (pi: ExtensionAPI) {
6506
6557
  pi.on("after_provider_response", (event, ctx) => {
6507
6558
  const model = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
6508
6559
  if (!runtimeOptimizerEnabled || !model) return;
6509
- if (event.status !== 400) return;
6510
- if (!isPromptCacheRetention400Applicable(model)) return;
6511
- if (!hasPromptCacheRetentionUnsupportedSignal(event.headers)) return;
6512
6560
 
6513
- const key = modelKey(model);
6514
- promptCacheRetention400Models.add(key);
6515
- if (warnedPromptCacheRetention400Models.has(key)) return;
6516
- warnedPromptCacheRetention400Models.add(key);
6517
- ctx.ui.notify(
6518
- `⚠️ ${LOG_PREFIX}: ${key} returned HTTP 400 while supportsLongCacheRetention is enabled. ` +
6519
- getPromptCacheRetentionUnsupportedHint() +
6520
- ` Run /cache-optimizer doctor for the exact edit location.`,
6521
- "warning",
6522
- );
6561
+ // ── 400: prompt_cache_retention unsupported ──
6562
+ if (event.status === 400) {
6563
+ if (!isPromptCacheRetention400Applicable(model)) return;
6564
+ if (!hasPromptCacheRetentionUnsupportedSignal(event.headers)) return;
6565
+
6566
+ const key = modelKey(model);
6567
+ promptCacheRetention400Models.add(key);
6568
+ if (warnedPromptCacheRetention400Models.has(key)) return;
6569
+ warnedPromptCacheRetention400Models.add(key);
6570
+ ctx.ui.notify(
6571
+ `⚠️ ${LOG_PREFIX}: ${key} returned HTTP 400 while supportsLongCacheRetention is enabled. ` +
6572
+ getPromptCacheRetentionUnsupportedHint() +
6573
+ ` Run /cache-optimizer doctor for the exact edit location.`,
6574
+ "warning",
6575
+ );
6576
+ return;
6577
+ }
6578
+
6579
+ // ── 403: sendSessionAffinityHeaders blocked by CDN/WAF ──
6580
+ //
6581
+ // Pi's openai-completions/responses adapters send three custom HTTP
6582
+ // headers (session_id, x-client-request-id, x-session-affinity) when
6583
+ // sendSessionAffinityHeaders is enabled. Some third-party proxies /
6584
+ // CDNs / WAFs block these custom headers and return 403. We record a
6585
+ // one-time per-session notification and surface it in doctor/fix so the
6586
+ // user can disable the flag for the blocked model.
6587
+ if (event.status === 403) {
6588
+ if (!isSessionAffinity403Applicable(model)) return;
6589
+
6590
+ const key403 = modelKey(model);
6591
+ sendSessionAffinityHeaders403Models.add(key403);
6592
+ if (warnedSendSessionAffinityHeaders403Models.has(key403)) return;
6593
+ warnedSendSessionAffinityHeaders403Models.add(key403);
6594
+ ctx.ui.notify(
6595
+ `⚠️ ${LOG_PREFIX}: ${key403} returned HTTP 403 while sendSessionAffinityHeaders is enabled. ` +
6596
+ `The proxy/CDN may be blocking Pi's custom session-affinity headers (session_id, x-client-request-id, x-session-affinity). ` +
6597
+ `Run /cache-optimizer doctor for details and /cache-optimizer fix to set sendSessionAffinityHeaders: false.`,
6598
+ "warning",
6599
+ );
6600
+ return;
6601
+ }
6523
6602
  });
6524
6603
 
6525
6604
  pi.on("message_end", async (event, ctx) => {
@@ -6641,7 +6720,7 @@ export default function (pi: ExtensionAPI) {
6641
6720
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
6642
6721
  return;
6643
6722
  }
6644
- const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
6723
+ const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)) });
6645
6724
  const adapter = selectAdapterForModel(model);
6646
6725
  const sk = model ? sessionModelKey(model) : undefined;
6647
6726
  const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
@@ -6731,6 +6810,21 @@ export default function (pi: ExtensionAPI) {
6731
6810
  };
6732
6811
  }
6733
6812
 
6813
+ // If no regular missing compat flags but the model has a recorded
6814
+ // 403 while sendSessionAffinityHeaders was enabled (Pi sent custom
6815
+ // session-affinity HTTP headers and the proxy/CDN blocked them), offer
6816
+ // to override `sendSessionAffinityHeaders` to false in models.json.
6817
+ if (!suggestion && isSessionAffinity403Applicable(model) && sendSessionAffinityHeaders403Models.has(modelKey(model))) {
6818
+ const key = modelKey(model);
6819
+ const slashIdx = key.indexOf("/");
6820
+ const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
6821
+ suggestion = {
6822
+ providerLabel,
6823
+ modelId: model.id,
6824
+ compatKeys: { sendSessionAffinityHeaders: false },
6825
+ };
6826
+ }
6827
+
6734
6828
  if (!suggestion) {
6735
6829
  const key = modelKey(model);
6736
6830
  cmdCtx.ui.notify(`✅ Nothing to fix for "${key}". Compat already configured.`, "info");
@@ -6755,6 +6849,13 @@ export default function (pi: ExtensionAPI) {
6755
6849
  "Create or edit the entry below to override supportsLongCacheRetention to false.",
6756
6850
  );
6757
6851
  }
6852
+ if (sendSessionAffinityHeaders403Models.has(modelKey(model))) {
6853
+ manualLines.push(
6854
+ "",
6855
+ "💡 This model returned HTTP 403 while sendSessionAffinityHeaders was enabled.",
6856
+ "Create or edit the entry below to override sendSessionAffinityHeaders to false.",
6857
+ );
6858
+ }
6758
6859
  manualLines.push(
6759
6860
  "",
6760
6861
  "If the provider/model already exists in models.json, add these compat keys under",
@@ -7040,7 +7141,7 @@ export default function (pi: ExtensionAPI) {
7040
7141
  if (!model) {
7041
7142
  cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
7042
7143
  } else {
7043
- const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)) });
7144
+ const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)) });
7044
7145
  const adapter = selectAdapterForModel(model);
7045
7146
  const sk = model ? sessionModelKey(model) : undefined;
7046
7147
  const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.6.13",
3
+ "version": "2.6.15",
4
4
  "description": "Improve Pi prompt/KV cache hit rates with stable prompts, OpenAI-compatible cache keys, proxy compat warnings, and footer cache stats.",
5
5
  "keywords": [
6
6
  "pi-package",