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 +16 -4
- package/README.zh-CN.md +16 -4
- package/index.ts +288 -16
- package/package.json +3 -3
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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:
|
|
11257
|
-
ambiguous
|
|
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
|
-
|
|
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
|
|
11490
|
-
|
|
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.
|
|
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.
|
|
39
|
-
"@earendil-works/pi-server": "0.
|
|
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"
|