pi-cache-optimizer 2.8.11 → 2.8.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,7 @@ Pi extension for improving provider-side KV / prompt cache hit rates. It keeps s
25
25
  - [Auto-repair with `/cache-optimizer fix`](#auto-repair-with-cache-optimizer-fix)
26
26
  - [DeepSeek protocol safety and rollback](#deepseek-protocol-safety-and-rollback)
27
27
  - [Footer stats](#footer-stats)
28
+ - [Native virtual models (Pi 0.99+)](#native-virtual-models-pi-099)
28
29
  - [For router / virtual-channel extension authors](#for-router--virtual-channel-extension-authors)
29
30
  - [Uninstall](#uninstall)
30
31
  - [Verify effect](#verify-effect)
@@ -42,6 +43,7 @@ Pi extension for improving provider-side KV / prompt cache hit rates. It keeps s
42
43
  - Shows current conversation-session provider/model footer stats by default; `total` aggregates all valid local shards for the exact provider/model.
43
44
  - Supports optional router-extension integration through versioned global protocols (`Symbol.for("pi.routing.registry.v1")` and `Symbol.for("pi.cache.hints.v1")`) without importing router packages.
44
45
  - Includes disabled-by-default deterministic ordering for verified built-in tool payloads.
46
+ - Supports Pi 0.99+ native virtual models (`pi.registerVirtualModel()`): request hooks, footer stats, and diagnostics act on the physical model each request is routed to.
45
47
 
46
48
  Caching is provider-side and best-effort. Third-party proxies and router extensions can still hide cache usage, reject unsupported parameters, or route requests across multiple upstreams.
47
49
 
@@ -61,7 +63,7 @@ Run `/reload` in Pi after install/update/remove so extension hooks refresh.
61
63
 
62
64
  On Pi 0.79.7 and newer, `pi update` updates Pi itself only. To update installed Pi packages such as this extension, run `pi update --extensions` (packages only) or `pi update --all` (Pi + packages).
63
65
 
64
- This extension requires Pi 0.82+ and is validated against Pi 0.87.1. It uses the official Pi package types directly for type-checking, along with extension hooks, `getAgentDir()`, and prompt options shared by those versions; it does not depend on Pi 0.83+ APIs such as `ctx.scopedModels` or the bundled TypeBox 1.3 aliases.
66
+ This extension requires Pi 0.82+ and is validated against Pi 0.99.2. It uses the official Pi package types directly for type-checking, along with extension hooks, `getAgentDir()`, and prompt options shared by those versions; it does not depend on Pi 0.83+ APIs such as `ctx.scopedModels` or the bundled TypeBox 1.3 aliases. Native virtual model support and codemode nested-call coalescing activate only on Pi 0.99+ hosts that produce them; older hosts keep the previous behavior.
65
67
 
66
68
  ## Commands
67
69
 
@@ -134,7 +136,7 @@ The explicit setting is stored in `pi-cache-optimizer-config.json` under Pi's ag
134
136
 
135
137
  ## Per-model `prompt_cache_key` opt-out
136
138
 
137
- Some OpenAI-compatible endpoints reject `prompt_cache_key` with HTTP 400 even though the same field is valid for other providers. Pi 0.87.1 has no native `supportsPromptCacheKey` compat field; do **not** add that unknown field to `models.json`. `supportsLongCacheRetention` is not an equivalent switch and should not be used for this purpose.
139
+ Some OpenAI-compatible endpoints reject `prompt_cache_key` with HTTP 400 even though the same field is valid for other providers. Pi 0.99.2 has no native `supportsPromptCacheKey` compat field; do **not** add that unknown field to `models.json`. `supportsLongCacheRetention` is not an equivalent switch and should not be used for this purpose.
138
140
 
139
141
  When the extension observes an explicit field-level `prompt_cache_key` unsupported error for the exact provider/model, ordinary `/cache-optimizer fix` offers a confirmed model-scoped repair. Value-validation failures and conditional restrictions such as “not allowed when temperature is set” do not qualify. If concurrent responses from different models cannot be correlated because Pi provides no request ID, header-only evidence is ignored unless the finalized assistant message supplies exact provider/model identity. If you already know that the endpoint rejects the field, use the explicit command:
140
142
 
@@ -148,7 +150,7 @@ The preview explains that the setting is stored in the extension-owned `pi-cache
148
150
 
149
151
  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.
150
152
 
151
- 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. Pi 0.87.1 has no native `supportsPromptCacheKey` compat field, so the per-model key opt-out is stored in this extension's `pi-cache-optimizer-config.json` instead of `models.json`. The extension config is independent of Pi's compat precedence and only affects the exact provider/model listed there.
153
+ 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. Pi 0.99.2 has no native `supportsPromptCacheKey` compat field, so the per-model key opt-out is stored in this extension's `pi-cache-optimizer-config.json` instead of `models.json`. The extension config is independent of Pi's compat precedence and only affects the exact provider/model listed there.
152
154
 
153
155
  For real proxies, start with session affinity:
154
156
 
@@ -174,7 +176,7 @@ Notes:
174
176
 
175
177
  - `sendSessionAffinityHeaders: true` is the safe default when your proxy supports sticky routing.
176
178
  - `supportsLongCacheRetention: true` is optional. Add it only when the endpoint explicitly supports OpenAI long prompt cache retention.
177
- - Do not add `supportsPromptCacheKey` to `models.json`: Pi 0.87.1 does not define that compat field. Use `/cache-optimizer fix prompt-cache-key` to store an exact provider/model omit rule in the extension-owned config; it removes both key spellings, including a key supplied by Pi.
179
+ - Do not add `supportsPromptCacheKey` to `models.json`: Pi 0.99.2 does not define that compat field. Use `/cache-optimizer fix prompt-cache-key` to store an exact provider/model omit rule in the extension-owned config; it removes both key spellings, including a key supplied by Pi.
178
180
  - If you see `400 Unsupported parameter: prompt_cache_retention`, remove/avoid `supportsLongCacheRetention` for that channel. Keep `sendSessionAffinityHeaders` if supported. The extension detects the explicit error from response headers or the finalized assistant error message and strips the parameter from subsequent requests in the current process.
179
181
  - Use `/cache-optimizer compat` or `/cache-optimizer doctor` to see model-specific advice.
180
182
  - DeepSeek model names select the `DS cache` adapter only; they do not prove a reasoning wire protocol. Generic cache/routing advice remains active for absent or non-DeepSeek formats. DeepSeek replay advice is shown only when effective `compat.thinkingFormat: "deepseek"` is explicitly configured; it never treats `thinkingFormat` as a missing fix key.
@@ -358,6 +360,16 @@ Supported footer labels include: DS, Claude, OpenAI, Gemini, Kimi, Qwen, GLM, Mi
358
360
 
359
361
  Adapter selection uses only model id/name (plus assistant message model/name on message end). Generic OpenAI-shaped APIs are not treated as OpenAI-family unless the model id/name matches a supported family.
360
362
 
363
+ ## Native virtual models (Pi 0.99+)
364
+
365
+ Pi 0.99 lets extensions register virtual models with `pi.registerVirtualModel()`. While one is selected, `ctx.model` stays the virtual model (`api: "pi-virtual"`) and Pi routes every request to a physical model. This extension follows the physical model automatically; router authors do not need the protocol below.
366
+
367
+ - Request hooks read the dispatched model id from the provider payload and match it against physical models with configured credentials. The `prompt_cache_key` fallback, `prompt_cache_retention` safety, Anthropic TTL repair, and per-model `prompt_cache_key` omit rules then apply to that physical model. If several credentialed providers share the id and would be treated differently, the extension does not guess; identity-dependent request changes are skipped.
368
+ - Footer stats and `/cache-optimizer doctor`, `compat`, `stats`, `reset`, and `fix` use the physical model that answered last on the current session branch, matching Pi's own routed-model display and context limits. Doctor and compat name both the virtual selection and that physical model.
369
+ - Before Pi routes the first request, the footer stays empty and diagnostics ask you to send a prompt first.
370
+ - Prompt rewriting is skipped for virtual selections: Pi picks the physical model after the system prompt is built, and a reordered prompt must not reach a safety-filtered Codex route.
371
+ - The session-affinity header bridge is skipped as well, because Pi builds request headers before the payload exists. Pi still sends the physical model's own configured affinity headers.
372
+
361
373
  ## For router / virtual-channel extension authors
362
374
 
363
375
  If your Pi extension provides a virtual routing provider (for example `router/auto`, `router/smart`, or a profile/channel that forwards to a real upstream), this extension can show cache stats for the real upstream provider/model instead of the virtual shell. Integration is optional, versioned, and does **not** require importing this package.
package/README.zh-CN.md CHANGED
@@ -25,6 +25,7 @@
25
25
  - [使用 `/cache-optimizer fix` 自动修复](#使用-cache-optimizer-fix-自动修复)
26
26
  - [DeepSeek 协议安全与回滚](#deepseek-协议安全与回滚)
27
27
  - [Footer 统计](#footer-统计)
28
+ - [原生虚拟模型(Pi 0.99+)](#原生虚拟模型pi-099)
28
29
  - [Router / Virtual-channel 扩展作者指南](#router--virtual-channel-扩展作者指南)
29
30
  - [卸载](#卸载)
30
31
  - [验证效果](#验证效果)
@@ -42,6 +43,7 @@
42
43
  - Footer 默认显示当前 conversation session 的 provider/model 统计;`total` 可聚合同一精确 provider/model 的所有有效本地 shard。
43
44
  - 通过版本化全局协议(`Symbol.for("pi.routing.registry.v1")` 与 `Symbol.for("pi.cache.hints.v1")`)支持可选的 router extension 集成,而不导入任何 router 包。
44
45
  - 提供默认关闭的确定性排序,用于已验证的 Pi 内置工具 payload。
46
+ - 支持 Pi 0.99+ 原生虚拟模型(`pi.registerVirtualModel()`):请求 hook、footer 统计和诊断都作用于每次请求实际路由到的物理模型。
45
47
 
46
48
  缓存是 provider 侧的 best-effort 行为。第三方代理和 router extension 仍可能隐藏缓存 usage、拒绝不支持的参数,或把请求路由到多个上游。
47
49
 
@@ -61,7 +63,7 @@ pi remove npm:pi-deepseek-cache-optimizer && pi install npm:pi-cache-optimizer
61
63
 
62
64
  Pi 0.79.7 及之后,`pi update` 默认只更新 Pi 本体。若要更新已安装的 Pi package(包括本扩展),请运行 `pi update --extensions`(只更新 packages)或 `pi update --all`(Pi 与 packages 一起更新)。
63
65
 
64
- 本扩展要求 Pi 0.82+,并已使用 Pi 0.87.1 验证。TypeScript 校验直接使用官方 Pi package 类型,同时只使用这些版本共有的 extension hooks、`getAgentDir()` 和 prompt options;不依赖 Pi 0.83+ 专有 API(例如 `ctx.scopedModels` 或 bundled TypeBox 1.3 aliases)。
66
+ 本扩展要求 Pi 0.82+,并已使用 Pi 0.99.2 验证。TypeScript 校验直接使用官方 Pi package 类型,同时只使用这些版本共有的 extension hooks、`getAgentDir()` 和 prompt options;不依赖 Pi 0.83+ 专有 API(例如 `ctx.scopedModels` 或 bundled TypeBox 1.3 aliases)。原生虚拟模型支持与 codemode 嵌套工具调用合并只在会产生它们的 Pi 0.99+ 上生效,较早版本保持原有行为。
65
67
 
66
68
  ## 命令
67
69
 
@@ -134,7 +136,7 @@ Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 prov
134
136
 
135
137
  ## 按模型关闭 `prompt_cache_key`
136
138
 
137
- 某些 OpenAI-compatible endpoint 会因 `prompt_cache_key` 返回 HTTP 400,但同一个字段对其他 provider 可能有效。Pi 0.87.1 没有原生的 `supportsPromptCacheKey` compat 字段,**不要**把这个未知字段加入 `models.json`。`supportsLongCacheRetention` 也不是等价开关,不应借用来实现此目的。
139
+ 某些 OpenAI-compatible endpoint 会因 `prompt_cache_key` 返回 HTTP 400,但同一个字段对其他 provider 可能有效。Pi 0.99.2 没有原生的 `supportsPromptCacheKey` compat 字段,**不要**把这个未知字段加入 `models.json`。`supportsLongCacheRetention` 也不是等价开关,不应借用来实现此目的。
138
140
 
139
141
  当扩展观察到精确 provider/model 对 `prompt_cache_key` 的明确字段级拒绝后,普通 `/cache-optimizer fix` 会提供经过确认的模型级修复。参数值校验失败,以及“设置 temperature 时不允许”这类条件限制都不构成证据。若不同模型的响应并发交错,而 Pi 又没有提供 request ID,扩展会忽略无法安全关联的 response-header 证据;只有最终 assistant message 提供精确 provider/model 身份时才恢复归因。如果你已经确定 endpoint 不支持该字段,可以主动执行:
140
142
 
@@ -148,7 +150,7 @@ Footer 默认使用 `session`,避免另一个并行 Pi 终端使用相同 prov
148
150
 
149
151
  LiteLLM / OneAPI / NewAPI / 类 OpenRouter 渠道等第三方 `openai-completions` 代理,常会把同一个 session 分散到多个上游后端,导致 provider 侧 prompt cache 被拆散。
150
152
 
151
- 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 0.87.1 没有原生的 `supportsPromptCacheKey` compat 字段,因此按模型关闭 key 的策略保存在本扩展的 `pi-cache-optimizer-config.json` 中,而不是 `models.json`。扩展配置独立于 Pi 的 compat 优先级,只影响其中列出的精确 provider/model。
153
+ 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 0.99.2 没有原生的 `supportsPromptCacheKey` compat 字段,因此按模型关闭 key 的策略保存在本扩展的 `pi-cache-optimizer-config.json` 中,而不是 `models.json`。扩展配置独立于 Pi 的 compat 优先级,只影响其中列出的精确 provider/model。
152
154
 
153
155
  对真正的代理,建议先启用 session affinity:
154
156
 
@@ -174,7 +176,7 @@ Pi 0.84.1 还修复了内置 Fireworks 渠道对拒绝 `prompt_cache_retention`
174
176
 
175
177
  - `sendSessionAffinityHeaders: true` 是安全默认项,前提是你的代理支持 sticky routing。
176
178
  - `supportsLongCacheRetention: true` 是可选项。只有 endpoint 明确支持 OpenAI long prompt cache retention 时才添加。
177
- - 不要把 `supportsPromptCacheKey` 加入 `models.json`:Pi 0.87.1 没有定义这个 compat 字段。请使用 `/cache-optimizer fix prompt-cache-key` 在扩展自有配置中保存精确 provider/model 的 omit 规则;它会移除两种 key 写法,包括 Pi 提供的 key。
179
+ - 不要把 `supportsPromptCacheKey` 加入 `models.json`:Pi 0.99.2 没有定义这个 compat 字段。请使用 `/cache-optimizer fix prompt-cache-key` 在扩展自有配置中保存精确 provider/model 的 omit 规则;它会移除两种 key 写法,包括 Pi 提供的 key。
178
180
  - 如果出现 `400 Unsupported parameter: prompt_cache_retention`,请为该渠道移除 / 避免 `supportsLongCacheRetention`;如支持,可保留 `sendSessionAffinityHeaders`。扩展会从响应头或最终 assistant error message 中识别这条明确错误,并在当前进程的后续请求中移除该参数。
179
181
  - 使用 `/cache-optimizer compat` 或 `/cache-optimizer doctor` 查看当前模型的具体建议。
180
182
  - DeepSeek 模型名只用于选择 `DS cache` adapter,不能证明 reasoning wire protocol。缺少或使用非 DeepSeek format 时仍保留通用缓存 / 路由建议;只有 effective `compat.thinkingFormat: "deepseek"` 被明确配置时,才显示 DeepSeek replay 建议,且不会把 `thinkingFormat` 列为缺失修复项。
@@ -357,6 +359,16 @@ Pi 0.79+ 已内置 footer `CH` 标记,用于显示最近一次 prompt cache hi
357
359
 
358
360
  Adapter 选择只看模型 id/name(以及 message_end 时 assistant message 的 model/name)。仅使用 OpenAI-shaped API 不会被当作 OpenAI-family,除非模型 id/name 匹配受支持的家族。
359
361
 
362
+ ## 原生虚拟模型(Pi 0.99+)
363
+
364
+ Pi 0.99 允许扩展通过 `pi.registerVirtualModel()` 注册虚拟模型。选中虚拟模型时,`ctx.model` 保持为虚拟模型(`api: "pi-virtual"`),而 Pi 会把每次请求路由到某个物理模型。本扩展会自动跟随该物理模型,router 作者无需接入下文的协议。
365
+
366
+ - 请求 hook 从 provider payload 中读取实际派发的模型 id,并在已配置凭证的物理模型中匹配。`prompt_cache_key` fallback、`prompt_cache_retention` 安全规则、Anthropic TTL 修复以及按模型关闭 `prompt_cache_key` 的规则都按该物理模型生效。如果多个已配置凭证的 provider 共用同一个 id 且处理方式不同,扩展不会猜测,而是跳过依赖模型身份的请求修改。
367
+ - Footer 统计以及 `/cache-optimizer doctor`、`compat`、`stats`、`reset`、`fix` 使用当前会话分支上最近一次应答的物理模型,与 Pi 自身显示的路由模型和 context 上限一致。doctor 与 compat 会同时标出虚拟选择和该物理模型。
368
+ - 在 Pi 路由第一个请求之前,footer 保持为空,诊断会提示先发送一个 prompt。
369
+ - 虚拟选择不做 prompt 改写:Pi 在构建 system prompt 之后才决定物理模型,重排后的 prompt 不应被送到有安全过滤的 Codex 路由。
370
+ - Session-affinity header 桥接同样跳过,因为 Pi 在 payload 生成之前就构造请求 header。Pi 仍会发送物理模型自身配置的 affinity header。
371
+
360
372
  ## Router / Virtual-channel 扩展作者指南
361
373
 
362
374
  如果你的 Pi 扩展提供虚拟 routing provider(例如 `router/auto`、`router/smart`,或会转发到真实上游的 profile/channel),本扩展可以为真实上游 provider/model 显示缓存统计,而不是把统计记到虚拟外壳上。集成是可选、版本化的,并且**不需要导入本包**。
package/index.ts CHANGED
@@ -212,6 +212,8 @@ type PromptCacheKeyConfigReceiptSnapshot = {
212
212
  };
213
213
  const PI_ROUTING_REGISTRY_SYMBOL = Symbol.for("pi.routing.registry.v1");
214
214
  const PI_CACHE_HINTS_SYMBOL = Symbol.for("pi.cache.hints.v1");
215
+ // Model API that Pi 0.99+ assigns to native virtual models (VIRTUAL_MODEL_API).
216
+ const PI_VIRTUAL_MODEL_API = "pi-virtual";
215
217
  const PI_CACHE_HINTS_OWNER_SYMBOL = Symbol.for("pi.cache.optimizer.hints-owner.v1");
216
218
  const ANTHROPIC_TTL_FALLBACK_SYMBOL = Symbol.for("pi.cache.optimizer.anthropic-ttl-fallback.v1");
217
219
  const REASONING_PROTOCOL_FALLBACK_SYMBOL = Symbol.for("pi.cache.optimizer.reasoning-protocol-fallback.v1");
@@ -1397,6 +1399,9 @@ function resolveRouteModel(
1397
1399
  model: PiModel | undefined,
1398
1400
  ctx?: ContextWithOptionalModelRegistry,
1399
1401
  ): PiModel | undefined {
1402
+ const nativeVirtualRoute = resolveNativeVirtualRouteModel(model, ctx);
1403
+ if (nativeVirtualRoute) return nativeVirtualRoute;
1404
+
1400
1405
  const snapshot = resolveActiveRouteSnapshot(model, ctx);
1401
1406
  if (!snapshot) return undefined;
1402
1407
 
@@ -1407,7 +1412,171 @@ function resolveRouteModel(
1407
1412
 
1408
1413
  function isVirtualRoutingModel(model: PiModel | undefined, ctx?: Pick<ExtensionContext, "sessionManager">): boolean {
1409
1414
  if (!model) return false;
1410
- return isRouterModel(model) || !!getRoutingRegistry()?.getRouter(model.provider) || !!resolveActiveRouteSnapshot(model, ctx);
1415
+ return isNativeVirtualModel(model) || isRouterModel(model) || !!getRoutingRegistry()?.getRouter(model.provider) || !!resolveActiveRouteSnapshot(model, ctx);
1416
+ }
1417
+
1418
+ // Pi 0.99+ native virtual models (`pi.registerVirtualModel()`). `ctx.model`
1419
+ // stays the virtual selection while Pi dispatches every request to a physical
1420
+ // model; only assistant messages and the provider payload name that model.
1421
+ // Older Pi hosts never produce this API, so every native-virtual path is inert.
1422
+ type NativeVirtualDispatch = { provider: string; id: string; api: string };
1423
+
1424
+ function isNativeVirtualModel(model: { api?: unknown } | undefined): boolean {
1425
+ return model?.api === PI_VIRTUAL_MODEL_API;
1426
+ }
1427
+
1428
+ function readSessionBranch(ctx: Pick<ExtensionContext, "sessionManager"> | undefined): unknown[] {
1429
+ try {
1430
+ const manager = ctx?.sessionManager as { getBranch?: () => unknown } | undefined;
1431
+ if (typeof manager?.getBranch !== "function") return [];
1432
+ const branch = manager.getBranch();
1433
+ return Array.isArray(branch) ? branch : [];
1434
+ } catch {
1435
+ return [];
1436
+ }
1437
+ }
1438
+
1439
+ /**
1440
+ * Physical models that answered on the current session branch, newest first:
1441
+ * the latest successful response (Pi's `request.previous`) and the latest
1442
+ * response of any outcome (a failed attempt that a retry may reuse). The
1443
+ * catalog id in `message.model` is preferred over the echoed `responseModel`
1444
+ * because it is the id Pi dispatched and the registry knows.
1445
+ */
1446
+ function findNativeVirtualDispatches(ctx: Pick<ExtensionContext, "sessionManager"> | undefined): {
1447
+ latestSuccessful?: NativeVirtualDispatch;
1448
+ latestAny?: NativeVirtualDispatch;
1449
+ } {
1450
+ const branch = readSessionBranch(ctx);
1451
+ let latestSuccessful: NativeVirtualDispatch | undefined;
1452
+ let latestAny: NativeVirtualDispatch | undefined;
1453
+ for (let index = branch.length - 1; index >= 0 && !latestSuccessful; index--) {
1454
+ const entry = asRecord(branch[index]);
1455
+ if (entry?.type !== "message") continue;
1456
+ const message = getAssistantRecord(entry.message);
1457
+ if (!message || isNativeVirtualModel(message)) continue;
1458
+ const dispatch = nativeVirtualDispatchFromMessage(message);
1459
+ if (!dispatch) continue;
1460
+ latestAny ??= dispatch;
1461
+ if (message.stopReason !== "error" && message.stopReason !== "aborted") latestSuccessful = dispatch;
1462
+ }
1463
+ return { latestSuccessful, latestAny };
1464
+ }
1465
+
1466
+ function nativeVirtualDispatchFromMessage(message: unknown): NativeVirtualDispatch | undefined {
1467
+ const record = getAssistantRecord(message);
1468
+ if (!record || isNativeVirtualModel(record)) return undefined;
1469
+ const provider = firstNonEmptyString(record.provider);
1470
+ const id = firstNonEmptyString(record.model, record.responseModel);
1471
+ return provider && id ? { provider, id, api: firstNonEmptyString(record.api) ?? "" } : undefined;
1472
+ }
1473
+
1474
+ function nativeVirtualDispatchToModel(
1475
+ dispatch: NativeVirtualDispatch,
1476
+ ctx?: ContextWithOptionalModelRegistry,
1477
+ ): PiModel {
1478
+ // A virtual model hides a same-id physical model in the catalog, so a
1479
+ // registry hit that is itself virtual is not physical metadata.
1480
+ const registered = findModelInRegistry(ctx?.modelRegistry, dispatch.provider, dispatch.id);
1481
+ const model = registered && !isNativeVirtualModel(registered)
1482
+ ? registered
1483
+ : ({
1484
+ provider: dispatch.provider,
1485
+ id: dispatch.id,
1486
+ name: dispatch.id,
1487
+ api: dispatch.api,
1488
+ baseUrl: "",
1489
+ reasoning: false,
1490
+ input: ["text"],
1491
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1492
+ contextWindow: 0,
1493
+ maxTokens: 0,
1494
+ [ROUTED_FALLBACK_MODEL_SYMBOL]: true,
1495
+ } as PiModel);
1496
+ return applyConfiguredTransportToModel(model, readEffectiveCompatConfig());
1497
+ }
1498
+
1499
+ /**
1500
+ * Physical model behind a native virtual selection for pre-request UX (footer,
1501
+ * doctor, compat, stats, reset, fix): the model that answered last on this
1502
+ * session branch, matching Pi's own routed-model display and context limits.
1503
+ */
1504
+ function resolveNativeVirtualRouteModel(
1505
+ model: PiModel | undefined,
1506
+ ctx?: ContextWithOptionalModelRegistry,
1507
+ ): PiModel | undefined {
1508
+ if (!isNativeVirtualModel(model)) return undefined;
1509
+ const { latestSuccessful, latestAny } = findNativeVirtualDispatches(ctx);
1510
+ const dispatch = latestSuccessful ?? latestAny;
1511
+ return dispatch ? nativeVirtualDispatchToModel(dispatch, ctx) : undefined;
1512
+ }
1513
+
1514
+ function getProviderPayloadModelId(payload: unknown): string | undefined {
1515
+ const record = asRecord(payload);
1516
+ // Pi's built-in transports put the dispatched catalog id in `model`
1517
+ // (OpenAI, Anthropic, Google, Mistral) or `modelId` (Bedrock Converse).
1518
+ return firstNonEmptyString(record?.model, record?.modelId);
1519
+ }
1520
+
1521
+ function physicalModelsWithId(candidates: readonly PiModel[] | undefined, id: string): PiModel[] {
1522
+ return (candidates ?? []).filter((candidate) => candidate.id === id && !isNativeVirtualModel(candidate));
1523
+ }
1524
+
1525
+ /**
1526
+ * Physical model of one native-virtual request. `before_provider_request`
1527
+ * carries only the payload and `ctx.model` stays virtual, so the payload's
1528
+ * dispatched model id is matched against credentialed physical models (a
1529
+ * router may only route to those). A unique provider match wins. An id shared
1530
+ * by several credentialed providers is used only when every candidate gets the
1531
+ * same request treatment (`requestPolicyKey`); the identity then stays
1532
+ * ambiguous so response evidence is never pinned to a guessed provider.
1533
+ * Differing policies return undefined so identity-dependent request mutations
1534
+ * fail closed. Without a credentialed match, the sticky branch candidates and
1535
+ * then the full catalog are consulted.
1536
+ */
1537
+ function resolveNativeVirtualRequestModel(
1538
+ model: PiModel | undefined,
1539
+ payload: unknown,
1540
+ ctx?: ContextWithOptionalModelRegistry,
1541
+ requestPolicyKey?: (candidate: PiModel) => string,
1542
+ ): { model: PiModel; identityAmbiguous: boolean } | undefined {
1543
+ if (!isNativeVirtualModel(model)) return undefined;
1544
+ const payloadModelId = getProviderPayloadModelId(payload);
1545
+ if (!payloadModelId) return undefined;
1546
+ const config = readEffectiveCompatConfig();
1547
+ // null: no candidate; undefined: ambiguous with differing request policies.
1548
+ const pick = (matches: PiModel[]): { model: PiModel; identityAmbiguous: boolean } | undefined | null => {
1549
+ if (matches.length === 0) return null;
1550
+ const configured = matches.map((candidate) => applyConfiguredTransportToModel(candidate, config));
1551
+ if (new Set(configured.map((candidate) => candidate.provider)).size === 1) {
1552
+ return { model: configured[0], identityAmbiguous: false };
1553
+ }
1554
+ const policies = requestPolicyKey ? new Set(configured.map(requestPolicyKey)) : undefined;
1555
+ return policies?.size === 1 ? { model: configured[0], identityAmbiguous: true } : undefined;
1556
+ };
1557
+ try {
1558
+ const credentialed = pick(physicalModelsWithId(ctx?.modelRegistry?.getAvailable?.(), payloadModelId));
1559
+ if (credentialed !== null) return credentialed;
1560
+
1561
+ const { latestSuccessful, latestAny } = findNativeVirtualDispatches(ctx);
1562
+ for (const dispatch of [latestSuccessful, latestAny]) {
1563
+ if (dispatch?.id === payloadModelId) {
1564
+ return { model: nativeVirtualDispatchToModel(dispatch, ctx), identityAmbiguous: false };
1565
+ }
1566
+ }
1567
+
1568
+ return pick(physicalModelsWithId(ctx?.modelRegistry?.getAll?.(), payloadModelId)) ?? undefined;
1569
+ } catch {
1570
+ // Registry access is optional input; never fail the provider request hook.
1571
+ return undefined;
1572
+ }
1573
+ }
1574
+
1575
+ function describeNativeVirtualRouteNote(selected: PiModel | undefined, effective: PiModel | undefined): string | undefined {
1576
+ if (!selected || !isNativeVirtualModel(selected)) return undefined;
1577
+ // Without a routed response, the not-applicable text explains the state.
1578
+ if (!effective || isNativeVirtualModel(effective)) return undefined;
1579
+ return `🔀 Native virtual model ${modelKey(selected)} → latest routed physical model ${modelKey(effective)}. The diagnostics below apply to that physical model; Pi may route later requests elsewhere.`;
1411
1580
  }
1412
1581
 
1413
1582
  function isCacheHintsServiceV1(value: unknown): value is PiCacheHintsV1 {
@@ -2589,11 +2758,13 @@ function isPiBuiltInLlamaCppModel(model: PiModel | undefined): boolean {
2589
2758
  // Pi's built-in llama.cpp provider supplies this exact explicit compat
2590
2759
  // fingerprint. Provider ids are extension-overridable and models.json can add
2591
2760
  // cache/routing overrides, so provider id alone must never imply exemption.
2761
+ // supportsUsageInStreaming is deliberately not part of the fingerprint: Pi
2762
+ // 0.82.x sets it false and Pi 0.83+ sets it true after fixing streamed usage,
2763
+ // and it carries no routing or cache configuration.
2592
2764
  const compat = getCompat(model);
2593
2765
  return compat.supportsStore === false
2594
2766
  && compat.supportsDeveloperRole === false
2595
2767
  && compat.supportsReasoningEffort === false
2596
- && compat.supportsUsageInStreaming === false
2597
2768
  && compat.supportsStrictMode === false
2598
2769
  && compat.maxTokensField === "max_tokens"
2599
2770
  && compat.sendSessionAffinityHeaders === undefined
@@ -2602,7 +2773,7 @@ function isPiBuiltInLlamaCppModel(model: PiModel | undefined): boolean {
2602
2773
  }
2603
2774
 
2604
2775
  function shouldInjectOpenAIPromptCacheKeyForModel(model: PiModel | undefined): boolean {
2605
- // Pi 0.87.1 has no native supportsPromptCacheKey compat field. Per-model
2776
+ // Pi 0.99.2 has no native supportsPromptCacheKey compat field. Per-model
2606
2777
  // opt-out is owned by this extension's promptCacheKey.omit configuration;
2607
2778
  // this helper only exposes the transport API gate for fixture consumers.
2608
2779
  return isOpenAICompatibleApi(model?.api);
@@ -3445,6 +3616,22 @@ function snapshotProviderRequestModel(model: PiModel | undefined): PiModel | und
3445
3616
  } as PiModel;
3446
3617
  }
3447
3618
 
3619
+ // Some provider requests never produce a finalized assistant message: Pi's
3620
+ // prompt-cache warming (0.86+) replays a request with maxTokens: 1 outside the
3621
+ // agent loop, so message_end never consumes its lifecycle record. Cap the
3622
+ // process-local list and drop the oldest completed record first.
3623
+ const MAX_PROVIDER_REQUEST_STATES = 32;
3624
+
3625
+ function pruneProviderRequestStates<T extends { responseReceived: boolean }>(
3626
+ states: T[],
3627
+ max = MAX_PROVIDER_REQUEST_STATES,
3628
+ ): void {
3629
+ while (states.length > max) {
3630
+ const completed = states.findIndex((state) => state.responseReceived);
3631
+ states.splice(completed >= 0 ? completed : 0, 1);
3632
+ }
3633
+ }
3634
+
3448
3635
  function keyForModelExt(model: { provider: string; id: string }): string {
3449
3636
  return `${model.provider}/${model.id}`;
3450
3637
  }
@@ -5271,6 +5458,9 @@ const CACHE_PROVIDER_ADAPTERS: CacheProviderAdapter[] = [
5271
5458
  ];
5272
5459
 
5273
5460
  function selectAdapterForModel(model: PiModel | undefined): CacheProviderAdapter | undefined {
5461
+ // A native virtual selection has no cache identity of its own; stats always
5462
+ // belong to the physical model it routed to.
5463
+ if (isNativeVirtualModel(model)) return undefined;
5274
5464
  return CACHE_PROVIDER_ADAPTERS.find((adapter) => adapter.matchesModel(model));
5275
5465
  }
5276
5466
 
@@ -6642,6 +6832,13 @@ function describeRouterChannelDiagnostics(model: PiModel): string[] {
6642
6832
  function getCompatCheckNotApplicableLines(model: PiModel): string[] {
6643
6833
  const api = lower(model.api);
6644
6834
 
6835
+ if (isNativeVirtualModel(model)) {
6836
+ return [
6837
+ "ℹ️ Compat check not applicable for this model.",
6838
+ " Native virtual model: Pi routes each request to a physical model and none has answered on this session branch yet. Send a prompt, then rerun to diagnose the physical model that answered.",
6839
+ ];
6840
+ }
6841
+
6645
6842
  if (isMistralConversationsApi(api)) {
6646
6843
  return [
6647
6844
  "ℹ️ Compat check not applicable for this model.",
@@ -10212,6 +10409,16 @@ export const __internals_for_tests = {
10212
10409
  applyConfiguredTransportToModel,
10213
10410
  resolveRouteModel,
10214
10411
  isVirtualRoutingModel,
10412
+ PI_VIRTUAL_MODEL_API,
10413
+ isNativeVirtualModel,
10414
+ findNativeVirtualDispatches,
10415
+ resolveNativeVirtualRouteModel,
10416
+ resolveNativeVirtualRequestModel,
10417
+ getProviderPayloadModelId,
10418
+ getCompatCheckNotApplicableLines,
10419
+ describeNativeVirtualRouteNote,
10420
+ pruneProviderRequestStates,
10421
+ MAX_PROVIDER_REQUEST_STATES,
10215
10422
  installCacheHintsService,
10216
10423
  getCacheHintsService,
10217
10424
  markOptimizerOwnedCacheHintsService,
@@ -10347,6 +10554,8 @@ export default function (pi: ExtensionAPI) {
10347
10554
  model: PiModel;
10348
10555
  responseReceived: boolean;
10349
10556
  correlationAmbiguous: boolean;
10557
+ // A native virtual request whose dispatched id matched several providers.
10558
+ identityAmbiguous?: boolean;
10350
10559
  };
10351
10560
  const providerRequestStates: ProviderRequestState[] = [];
10352
10561
  let shardCreatedAt = Date.now();
@@ -10878,6 +11087,24 @@ export default function (pi: ExtensionAPI) {
10878
11087
  );
10879
11088
  }
10880
11089
 
11090
+ /**
11091
+ * Request treatment that before_provider_request derives from a model's
11092
+ * identity. A native virtual request whose dispatched id matches several
11093
+ * credentialed providers is resolved only when this key agrees for all of
11094
+ * them, so no provider-specific decision is applied on a guess.
11095
+ */
11096
+ function requestPolicyKey(model: PiModel): string {
11097
+ const key = modelKey(model);
11098
+ return JSON.stringify([
11099
+ model.api ?? "",
11100
+ isOfficialOpenAIBaseUrl(model),
11101
+ hasExplicitLongRetentionOptIn(model),
11102
+ promptCacheRetention400Models.has(key),
11103
+ anthropicTtlOrderErrorModels.has(key),
11104
+ isPromptCacheKeyOmittedForModel(model),
11105
+ ]);
11106
+ }
11107
+
10881
11108
  pi.on("session_start", async (event, ctx) => {
10882
11109
  if (runtimeOptimizerEnabled) requestLongCacheRetention();
10883
11110
  await restoreCacheStats(event.reason, ctx);
@@ -10892,7 +11119,10 @@ export default function (pi: ExtensionAPI) {
10892
11119
  await publishStatus(ctx);
10893
11120
  });
10894
11121
 
10895
- pi.on("tool_execution_end", async (_event, ctx) => {
11122
+ pi.on("tool_execution_end", async (event, ctx) => {
11123
+ // Pi 0.99+ codemode scripts run nested tool calls in parallel; the calling
11124
+ // tool's own end event follows them, so one refresh covers the batch.
11125
+ if (isNonEmptyString((event as { parentToolCallId?: unknown } | undefined)?.parentToolCallId)) return;
10896
11126
  await refreshShardAggregate();
10897
11127
  await publishStatus(ctx);
10898
11128
  });
@@ -10965,6 +11195,13 @@ export default function (pi: ExtensionAPI) {
10965
11195
  if (model && isResponsesPromptRewriteBypassApi(model.api)) {
10966
11196
  return {};
10967
11197
  }
11198
+ // Pi 0.99+ native virtual selections pick the physical model per request,
11199
+ // after this system prompt is built, so the bypass above cannot be decided
11200
+ // here. A route may reach the safety-filtered Codex backend; keep Pi's
11201
+ // prompt byte-for-byte instead of reordering it.
11202
+ if (isNativeVirtualModel(_ctx.model)) {
11203
+ return {};
11204
+ }
10968
11205
 
10969
11206
  if (!runtimeOptimizerEnabled) return {};
10970
11207
 
@@ -11043,6 +11280,10 @@ export default function (pi: ExtensionAPI) {
11043
11280
  });
11044
11281
 
11045
11282
  pi.on("before_provider_headers", (event, ctx) => {
11283
+ // Pi transforms headers before the payload exists and passes no model, so a
11284
+ // native virtual request's physical model is unknown here. Fail closed; Pi
11285
+ // core still sends the physical model's own configured affinity headers.
11286
+ if (isNativeVirtualModel(ctx.model)) return;
11046
11287
  const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
11047
11288
  addEffectiveSessionAffinityHeaders(
11048
11289
  event.headers,
@@ -11052,12 +11293,24 @@ export default function (pi: ExtensionAPI) {
11052
11293
  });
11053
11294
 
11054
11295
  pi.on("before_provider_request", (event, ctx) => {
11055
- const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
11296
+ // A native virtual selection keeps ctx.model virtual; resolve the physical
11297
+ // model from the dispatched payload. Unresolved requests keep the virtual
11298
+ // model, which fails every identity-dependent mutation closed.
11299
+ const nativeVirtualRequest = resolveNativeVirtualRequestModel(ctx.model, event.payload, ctx, requestPolicyKey);
11300
+ const requestModel = nativeVirtualRequest?.model ?? resolveRouteModel(ctx.model, ctx) ?? ctx.model;
11056
11301
  // Request-local identity is also needed by the always-on Anthropic TTL
11057
11302
  // validity repair, so retain the credential-blind snapshot even while the
11058
11303
  // optional runtime optimizer features are disabled.
11059
11304
  const snapshot = snapshotProviderRequestModel(requestModel);
11060
- if (snapshot) providerRequestStates.push({ model: snapshot, responseReceived: false, correlationAmbiguous: false });
11305
+ if (snapshot) {
11306
+ providerRequestStates.push({
11307
+ model: snapshot,
11308
+ responseReceived: false,
11309
+ correlationAmbiguous: false,
11310
+ identityAmbiguous: isNativeVirtualModel(ctx.model) && (!nativeVirtualRequest || nativeVirtualRequest.identityAmbiguous),
11311
+ });
11312
+ pruneProviderRequestStates(providerRequestStates);
11313
+ }
11061
11314
  let requestPayload: unknown = event.payload;
11062
11315
  let toolOrderChanged = false;
11063
11316
 
@@ -11143,7 +11396,12 @@ export default function (pi: ExtensionAPI) {
11143
11396
  }
11144
11397
  }
11145
11398
  if (responseState) responseState.responseReceived = true;
11146
- const model = responseState?.model ?? (pendingStates.length === 0 ? (resolveRouteModel(ctx.model, ctx) ?? ctx.model) : undefined);
11399
+ // A native virtual request whose physical provider could not be pinned
11400
+ // records no model-scoped header evidence; finalized assistant messages
11401
+ // still carry exact provider/model identity.
11402
+ const model = responseState
11403
+ ? (responseState.identityAmbiguous ? undefined : responseState.model)
11404
+ : (pendingStates.length === 0 && !isNativeVirtualModel(ctx.model) ? (resolveRouteModel(ctx.model, ctx) ?? ctx.model) : undefined);
11147
11405
  if (!runtimeOptimizerEnabled || !model) return;
11148
11406
 
11149
11407
  // Keep only the category, never the provider's complete error text. This
@@ -11252,9 +11510,10 @@ export default function (pi: ExtensionAPI) {
11252
11510
  : (completedIndex >= 0 ? completedIndex : (contextIndex >= 0 ? contextIndex : 0));
11253
11511
  const state = providerRequestStates.splice(index, 1)[0];
11254
11512
  if (!state) return { model: undefined, ambiguous: false };
11513
+ const ambiguous = (state.correlationAmbiguous || state.identityAmbiguous === true) && explicitIndex < 0;
11255
11514
  return {
11256
- model: state.correlationAmbiguous && explicitIndex < 0 ? undefined : state.model,
11257
- ambiguous: state.correlationAmbiguous && explicitIndex < 0,
11515
+ model: ambiguous ? undefined : state.model,
11516
+ ambiguous,
11258
11517
  };
11259
11518
  })()
11260
11519
  : { model: undefined, ambiguous: false };
@@ -11342,7 +11601,18 @@ export default function (pi: ExtensionAPI) {
11342
11601
  }
11343
11602
  }
11344
11603
 
11345
- const adapter = selectAdapterForAssistantMessage(event.message, ctx.model);
11604
+ // A native virtual selection keeps ctx.model virtual, while the finalized
11605
+ // message names the physical catalog model Pi dispatched. Resolve that
11606
+ // model from the registry so adapter tokens, the stats key, and the footer
11607
+ // compat marker use the same physical identity as the pre-request UX.
11608
+ const nativeVirtualDispatch = isNativeVirtualModel(ctx.model)
11609
+ ? nativeVirtualDispatchFromMessage(event.message)
11610
+ : undefined;
11611
+ const nativeVirtualMessageModel = nativeVirtualDispatch
11612
+ ? nativeVirtualDispatchToModel(nativeVirtualDispatch, ctx)
11613
+ : undefined;
11614
+
11615
+ const adapter = selectAdapterForAssistantMessage(event.message, nativeVirtualMessageModel ?? ctx.model);
11346
11616
  if (!adapter) return;
11347
11617
 
11348
11618
  // Skip stats for error/aborted messages (network retries, user aborts).
@@ -11372,6 +11642,7 @@ export default function (pi: ExtensionAPI) {
11372
11642
  // the active model id. Virtual routing providers keep message-local
11373
11643
  // identity (router correctness).
11374
11644
  statsModel = consolidateDirectProviderStatsModel(statsModel, ctx.model, ctx);
11645
+ if (nativeVirtualMessageModel) statsModel = nativeVirtualMessageModel;
11375
11646
  let routedModelChanged = false;
11376
11647
  if (isVirtualRoutingModel(ctx.model, ctx) && statsModel && !isVirtualRoutingModel(statsModel, ctx)) {
11377
11648
  const nextRoutedModel: PersistedRoutedModelRef = {
@@ -11486,9 +11757,8 @@ export default function (pi: ExtensionAPI) {
11486
11757
  const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
11487
11758
  const samples = sk ? getRecentSamples(sk) : [];
11488
11759
  const lowHitLines = buildLowHitDiagnosis(model, adapter, statsState, samples);
11489
- const fullDiagnosis = lowHitLines.length > 0
11490
- ? diagnosis + "\n" + lowHitLines.join("\n")
11491
- : diagnosis;
11760
+ const routeNote = describeNativeVirtualRouteNote(selectedModel, model);
11761
+ const fullDiagnosis = [routeNote, diagnosis, ...lowHitLines].filter((line) => line !== undefined).join("\n");
11492
11762
  cmdCtx.ui.notify(fullDiagnosis, "info");
11493
11763
  } else if (subcommand === "stats") {
11494
11764
  const aggregate = await refreshShardAggregate();
@@ -11539,13 +11809,15 @@ export default function (pi: ExtensionAPI) {
11539
11809
  return;
11540
11810
  }
11541
11811
  const compatResult = buildCompatDiagnosis(model);
11812
+ const compatRouteNote = describeNativeVirtualRouteNote(selectedModel, model);
11813
+ const withRouteNote = (text: string): string => compatRouteNote ? `${compatRouteNote}\n${text}` : text;
11542
11814
  if (compatResult) {
11543
- cmdCtx.ui.notify(compatResult, "warning");
11815
+ cmdCtx.ui.notify(withRouteNote(compatResult), "warning");
11544
11816
  } else {
11545
11817
  cmdCtx.ui.notify(
11546
- isAdaptiveThinkingCompatApplicable(model) || isDeepSeekCompatCheckApplicable(model) || isCompatCheckApplicable(model)
11818
+ withRouteNote(isAdaptiveThinkingCompatApplicable(model) || isDeepSeekCompatCheckApplicable(model) || isCompatCheckApplicable(model)
11547
11819
  ? "✅ Compat fully configured."
11548
- : getCompatCheckNotApplicableLines(model).join("\n"),
11820
+ : getCompatCheckNotApplicableLines(model).join("\n")),
11549
11821
  "info",
11550
11822
  );
11551
11823
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cache-optimizer",
3
- "version": "2.8.11",
3
+ "version": "2.8.12",
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",
@@ -35,8 +35,8 @@
35
35
  "check": "npm run typecheck && npm test && npm run check:diff && npm run check:pack"
36
36
  },
37
37
  "devDependencies": {
38
- "@earendil-works/pi-coding-agent": "^0.87.1",
39
- "@earendil-works/pi-server": "0.87.1",
38
+ "@earendil-works/pi-coding-agent": "^0.99.2",
39
+ "@earendil-works/pi-server": "0.99.2",
40
40
  "@types/node": "^22.0.0",
41
41
  "jiti": "^2.7.0",
42
42
  "typescript": "^5.8.3"