pi-cache-optimizer 2.6.17 → 2.6.20
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 +37 -12
- package/README.zh-CN.md +37 -12
- package/index.ts +174 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Pi extension for improving provider-side KV / prompt cache hit rates. It keeps stable prompt content near the front, adds a conservative OpenAI-compatible `prompt_cache_key` fallback, warns about common proxy cache-routing gaps, and shows read-only footer cache stats.
|
|
10
10
|
|
|
11
|
-
> Renamed from `pi-deepseek-cache-optimizer`. Existing footer counters migrate automatically. The extension does **not** touch `~/.pi/agent/models.json
|
|
11
|
+
> Renamed from `pi-deepseek-cache-optimizer`. Existing footer counters migrate automatically. The extension does **not** touch Pi's `models.json` during normal operation (default: `~/.pi/agent/models.json`; custom agent dirs use `PI_CODING_AGENT_DIR`); only `/cache-optimizer fix` can edit it, and only after an explicit interactive preview + confirmation with an automatic timestamped backup.
|
|
12
12
|
|
|
13
13
|
## Contents
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ Pi extension for improving provider-side KV / prompt cache hit rates. It keeps s
|
|
|
17
17
|
- [Commands](#commands)
|
|
18
18
|
- [Persistent opt-out](#persistent-opt-out)
|
|
19
19
|
- [OpenAI-compatible proxy setup](#openai-compatible-proxy-setup)
|
|
20
|
-
- [
|
|
20
|
+
- [Adaptive thinking models](#adaptive-thinking-models)
|
|
21
21
|
- [Auto-repair with `/cache-optimizer fix`](#auto-repair-with-cache-optimizer-fix)
|
|
22
22
|
- [Footer stats](#footer-stats)
|
|
23
23
|
- [For router / virtual-channel extension authors](#for-router--virtual-channel-extension-authors)
|
|
@@ -30,9 +30,9 @@ Pi extension for improving provider-side KV / prompt cache hit rates. It keeps s
|
|
|
30
30
|
- Reorders stable system-prompt content before dynamic context.
|
|
31
31
|
- Compresses Pi skill listings and strips session-overview churn.
|
|
32
32
|
- Requests long cache retention when Pi/provider compat supports it.
|
|
33
|
-
- Adds a session-id `prompt_cache_key` fallback for `openai-completions` / `openai-responses` payloads when no effective key exists.
|
|
33
|
+
- Adds a session-id `prompt_cache_key` fallback for `openai-completions` / `openai-responses` payloads when no effective key exists, including Pi's built-in `llama.cpp` provider as Pi 0.82 core does.
|
|
34
34
|
- Warns once for third-party OpenAI-compatible proxies missing cache/session-affinity compat flags.
|
|
35
|
-
- Detects
|
|
35
|
+
- Detects adaptive-thinking compat for Claude (opus-4.6+, sonnet-4.6+ including Sonnet 5, fable-5+) and Kimi Coding K3 / `kimi-for-coding` custom channels.
|
|
36
36
|
- Shows restart-persistent provider/model footer stats for supported model families.
|
|
37
37
|
- 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.
|
|
38
38
|
|
|
@@ -80,7 +80,11 @@ On Pi 0.79.7 and newer, `pi update` updates Pi itself only. To update installed
|
|
|
80
80
|
|
|
81
81
|
## OpenAI-compatible proxy setup
|
|
82
82
|
|
|
83
|
-
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.
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
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 `supportsLongCacheRetention: true` opt-in in `models.json`; otherwise strip it before sending.
|
|
86
|
+
|
|
87
|
+
For real proxies, start with session affinity:
|
|
84
88
|
|
|
85
89
|
```json
|
|
86
90
|
{
|
|
@@ -109,9 +113,9 @@ Notes:
|
|
|
109
113
|
- For DeepSeek models, the Pi Mono guidance expects `compat.requiresReasoningContentOnAssistantMessages: true` and `compat.thinkingFormat: "deepseek"` alongside cache/session-affinity flags when the endpoint supports them.
|
|
110
114
|
- This extension's `doctor` and `compat` commands only advise; they do not modify `models.json`.
|
|
111
115
|
|
|
112
|
-
##
|
|
116
|
+
## Adaptive thinking models
|
|
113
117
|
|
|
114
|
-
Claude models from opus-4.6 / sonnet-4.6 (including Sonnet 5) / fable-5 onwards require `forceAdaptiveThinking: true` in compat. Without
|
|
118
|
+
Claude models from opus-4.6 / sonnet-4.6 (including Sonnet 5) / fable-5 onwards require `forceAdaptiveThinking: true` in compat. Kimi Coding K3 (`k3`) and `kimi-for-coding` also use adaptive thinking and need `allowEmptySignature: true` so replayed empty-signature thinking blocks remain valid. Without the required compat, Pi may send a legacy thinking payload or replay thinking incorrectly.
|
|
115
119
|
|
|
116
120
|
Pi's built-in catalog already sets this flag for official models. Custom channels in `models.json` that override these models must include the flag:
|
|
117
121
|
|
|
@@ -151,15 +155,36 @@ Or use model-level override:
|
|
|
151
155
|
}
|
|
152
156
|
```
|
|
153
157
|
|
|
158
|
+
For Kimi Coding K3 custom channels, use model-level compat when the provider contains mixed models:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"providers": {
|
|
163
|
+
"your-kimi-coding-channel": {
|
|
164
|
+
"modelOverrides": {
|
|
165
|
+
"k3": {
|
|
166
|
+
"compat": {
|
|
167
|
+
"forceAdaptiveThinking": true,
|
|
168
|
+
"allowEmptySignature": true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Pi 0.80.9+ already includes Kimi K3 in built-in Kimi Coding, Moonshot AI / China, OpenRouter, and Vercel AI Gateway catalogs. The Moonshot/OpenRouter variants use their OpenAI-compatible transport and continue through the normal Kimi footer/proxy path; the adaptive compat above applies only to `anthropic-messages` Kimi Coding channels.
|
|
178
|
+
|
|
154
179
|
`/cache-optimizer doctor` and `/cache-optimizer compat` detect missing flags and show copyable JSON.
|
|
155
180
|
|
|
156
181
|
## Auto-repair with `/cache-optimizer fix`
|
|
157
182
|
|
|
158
183
|
**v2.6.0+** adds a `fix` subcommand that can auto-repair safe compat issues:
|
|
159
184
|
|
|
160
|
-
-
|
|
185
|
+
- Adaptive thinking (`forceAdaptiveThinking: true`; Kimi Coding K3 / `kimi-for-coding` also `allowEmptySignature: true`)
|
|
161
186
|
- DeepSeek Pi Mono reasoning compat (`thinkingFormat: "deepseek"`, `requiresReasoningContentOnAssistantMessages: true`)
|
|
162
|
-
- OpenAI-compatible proxy session affinity (`sendSessionAffinityHeaders: true` for `openai-completions
|
|
187
|
+
- OpenAI-compatible proxy session affinity (`sendSessionAffinityHeaders: true` for `openai-completions`). Pi 0.80.7+ controls `openai-responses` header shape with `sessionAffinityFormat` and auto-detects its default; this extension no longer writes the removed `sendSessionIdHeader` field.
|
|
163
188
|
|
|
164
189
|
**Scope:** only the currently active model. Other channels require switching models and running `fix` again.
|
|
165
190
|
|
|
@@ -178,7 +203,7 @@ Or use model-level override:
|
|
|
178
203
|
|
|
179
204
|
### Channels without a `models.json` provider entry
|
|
180
205
|
|
|
181
|
-
Some Pi channels may be available even when there is no provider block in `~/.pi/agent/models.json`
|
|
206
|
+
Some Pi channels may be available even when there is no provider block in Pi's agent `models.json` yet (default: `~/.pi/agent/models.json`; with `PI_CODING_AGENT_DIR`, use `$PI_CODING_AGENT_DIR/models.json`). Keep existing authentication as-is and do not copy credentials, tokens, or API keys. Add only cache/routing compatibility overrides in `models.json`.
|
|
182
207
|
|
|
183
208
|
Provider-level minimal override:
|
|
184
209
|
|
|
@@ -214,7 +239,7 @@ If only one model should change, use `modelOverrides`:
|
|
|
214
239
|
|
|
215
240
|
## Footer stats
|
|
216
241
|
|
|
217
|
-
Stats are read-only local counters stored
|
|
242
|
+
Stats are read-only local counters stored in Pi's agent directory (default: `~/.pi/agent/pi-cache-optimizer-stats.json`; custom agent dirs use `PI_CODING_AGENT_DIR`) and keyed by provider/model, so the same channel/model keeps today's footer counters after a Pi process or terminal restart. The file also keeps hashed session buckets for migration/reload bookkeeping. It contains only dates and numeric counters — no API keys, prompts, payloads, headers, responses, or model output.
|
|
218
243
|
|
|
219
244
|
Pi 0.79+ also includes a built-in footer `CH` marker for the latest prompt cache hit rate. This extension complements that marker with persisted provider/model counters plus proxy compat diagnostics.
|
|
220
245
|
|
|
@@ -343,7 +368,7 @@ When the query matches the current session/route, `hints` may contain `systemPro
|
|
|
343
368
|
pi remove npm:pi-cache-optimizer
|
|
344
369
|
```
|
|
345
370
|
|
|
346
|
-
Then run `/reload` or restart Pi. Optional local stats cleanup:
|
|
371
|
+
Then run `/reload` or restart Pi. Optional local stats cleanup (if you use `PI_CODING_AGENT_DIR`, delete the same files from that directory instead):
|
|
347
372
|
|
|
348
373
|
| Platform | Delete local stats files |
|
|
349
374
|
|---|---|
|
package/README.zh-CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
用于提升 Pi 中 provider 侧 KV Cache / Prompt Cache 命中率的扩展:把稳定 prompt 内容前置,给 OpenAI-compatible 请求补保守的 `prompt_cache_key`,提示代理渠道常见缓存路由兼容问题,并在底部显示只读缓存统计。
|
|
10
10
|
|
|
11
|
-
> 本包已从 `pi-deepseek-cache-optimizer`
|
|
11
|
+
> 本包已从 `pi-deepseek-cache-optimizer` 改名。已有底部统计会自动迁移。正常运行时扩展不会触碰 Pi 的 `models.json`(默认 `~/.pi/agent/models.json`;自定义 agent 目录使用 `PI_CODING_AGENT_DIR`);只有 `/cache-optimizer fix` 会在展示交互式预览、风险提示并得到明确确认后写入,且会先创建带时间戳的自动备份。
|
|
12
12
|
|
|
13
13
|
## 目录
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- [命令](#命令)
|
|
18
18
|
- [持久 Opt-out](#持久-opt-out)
|
|
19
19
|
- [OpenAI-compatible 代理配置](#openai-compatible-代理配置)
|
|
20
|
-
- [
|
|
20
|
+
- [Adaptive thinking 模型](#adaptive-thinking-模型)
|
|
21
21
|
- [使用 `/cache-optimizer fix` 自动修复](#使用-cache-optimizer-fix-自动修复)
|
|
22
22
|
- [Footer 统计](#footer-统计)
|
|
23
23
|
- [Router / Virtual-channel 扩展作者指南](#router--virtual-channel-扩展作者指南)
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
- 将稳定的 system prompt 内容移动到动态上下文之前。
|
|
31
31
|
- 压缩 Pi skill 列表,并移除 session-overview 中的易变字段。
|
|
32
32
|
- 在 Pi / provider compat 支持时请求长缓存保留。
|
|
33
|
-
- 对 `openai-completions` / `openai-responses` 请求,在没有有效 key 时使用 Pi session id 补 `prompt_cache_key
|
|
33
|
+
- 对 `openai-completions` / `openai-responses` 请求,在没有有效 key 时使用 Pi session id 补 `prompt_cache_key`;Pi 0.82 core 对内置 `llama.cpp` 也使用这一语义。
|
|
34
34
|
- 对缺少缓存 / session-affinity compat 的第三方 OpenAI-compatible 代理给出一次性提醒。
|
|
35
|
-
- 检测
|
|
35
|
+
- 检测 Claude(opus-4.6+、sonnet-4.6+ 含 Sonnet 5、fable-5+)以及 Kimi Coding K3 / `kimi-for-coding` 自定义渠道的 adaptive-thinking compat。
|
|
36
36
|
- 为支持的模型家族显示可跨 Pi 进程 / 终端重启延续的 provider/model footer 缓存统计。
|
|
37
37
|
- 通过版本化全局协议(`Symbol.for("pi.routing.registry.v1")` 与 `Symbol.for("pi.cache.hints.v1")`)支持可选的 router extension 集成,而不导入任何 router 包。
|
|
38
38
|
|
|
@@ -80,7 +80,11 @@ Pi 0.79.7 及之后,`pi update` 默认只更新 Pi 本体。若要更新已安
|
|
|
80
80
|
|
|
81
81
|
## OpenAI-compatible 代理配置
|
|
82
82
|
|
|
83
|
-
LiteLLM / OneAPI / NewAPI / 类 OpenRouter 渠道等第三方 `openai-completions` 代理,常会把同一个 session 分散到多个上游后端,导致 provider 侧 prompt cache
|
|
83
|
+
LiteLLM / OneAPI / NewAPI / 类 OpenRouter 渠道等第三方 `openai-completions` 代理,常会把同一个 session 分散到多个上游后端,导致 provider 侧 prompt cache 被拆散。
|
|
84
|
+
|
|
85
|
+
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` 时保留,否则发送前移除。
|
|
86
|
+
|
|
87
|
+
对真正的代理,建议先启用 session affinity:
|
|
84
88
|
|
|
85
89
|
```json
|
|
86
90
|
{
|
|
@@ -109,9 +113,9 @@ LiteLLM / OneAPI / NewAPI / 类 OpenRouter 渠道等第三方 `openai-completion
|
|
|
109
113
|
- 对 DeepSeek 模型,Pi Mono 指南期望在支持时同时设置 `compat.requiresReasoningContentOnAssistantMessages: true` 和 `compat.thinkingFormat: "deepseek"`,再配合缓存 / session-affinity 相关 compat。
|
|
110
114
|
- 本扩展的 `doctor` 和 `compat` 命令只给建议,不会修改 `models.json`。
|
|
111
115
|
|
|
112
|
-
##
|
|
116
|
+
## Adaptive thinking 模型
|
|
113
117
|
|
|
114
|
-
Claude 从 opus-4.6 / sonnet-4.6(含 Sonnet 5)/ fable-5 开始需要在 compat 中设置 `forceAdaptiveThinking: true
|
|
118
|
+
Claude 从 opus-4.6 / sonnet-4.6(含 Sonnet 5)/ fable-5 开始需要在 compat 中设置 `forceAdaptiveThinking: true`。Kimi Coding K3(`k3`)和 `kimi-for-coding` 也使用 adaptive thinking,并需要 `allowEmptySignature: true`,以正确重放空 signature 的 thinking block。缺少这些 compat 时,Pi 可能发送旧版 thinking payload 或错误重放 thinking。
|
|
115
119
|
|
|
116
120
|
Pi 内置 catalog 已为官方模型设置此 flag。`models.json` 中覆盖这些模型的自定义渠道必须包含该 flag:
|
|
117
121
|
|
|
@@ -151,15 +155,36 @@ Pi 内置 catalog 已为官方模型设置此 flag。`models.json` 中覆盖这
|
|
|
151
155
|
}
|
|
152
156
|
```
|
|
153
157
|
|
|
158
|
+
Kimi Coding K3 自定义渠道如果包含混合模型,建议使用模型级 compat:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"providers": {
|
|
163
|
+
"your-kimi-coding-channel": {
|
|
164
|
+
"modelOverrides": {
|
|
165
|
+
"k3": {
|
|
166
|
+
"compat": {
|
|
167
|
+
"forceAdaptiveThinking": true,
|
|
168
|
+
"allowEmptySignature": true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Pi 0.80.9+ 已在内置 Kimi Coding、Moonshot AI / 中国区、OpenRouter 和 Vercel AI Gateway catalog 中加入 Kimi K3。Moonshot/OpenRouter 变体使用 OpenAI-compatible transport,继续走普通 Kimi footer / proxy 路径;上面的 adaptive compat 只适用于 `anthropic-messages` Kimi Coding 渠道。
|
|
178
|
+
|
|
154
179
|
`/cache-optimizer doctor` 和 `/cache-optimizer compat` 会检测缺失的 flag 并显示可复制的 JSON。
|
|
155
180
|
|
|
156
181
|
## 使用 `/cache-optimizer fix` 自动修复
|
|
157
182
|
|
|
158
183
|
**v2.6.0+** 新增 `fix` 子命令,可自动修复安全的 compat 问题:
|
|
159
184
|
|
|
160
|
-
-
|
|
185
|
+
- Adaptive thinking(`forceAdaptiveThinking: true`;Kimi Coding K3 / `kimi-for-coding` 还包括 `allowEmptySignature: true`)
|
|
161
186
|
- DeepSeek Pi Mono reasoning compat(`thinkingFormat: "deepseek"`、`requiresReasoningContentOnAssistantMessages: true`)
|
|
162
|
-
- OpenAI-compatible proxy session affinity(`openai-completions`
|
|
187
|
+
- OpenAI-compatible proxy session affinity(`openai-completions` 使用 `sendSessionAffinityHeaders: true`)。Pi 0.80.7+ 使用 `sessionAffinityFormat` 控制 `openai-responses` header 形式并自动检测默认值;本扩展不再写入已移除的 `sendSessionIdHeader`。
|
|
163
188
|
|
|
164
189
|
**范围:** 仅当前 active model。其他渠道需切换模型后再次运行 `fix`。
|
|
165
190
|
|
|
@@ -178,7 +203,7 @@ Pi 内置 catalog 已为官方模型设置此 flag。`models.json` 中覆盖这
|
|
|
178
203
|
|
|
179
204
|
### 没有 `models.json` provider entry 的渠道
|
|
180
205
|
|
|
181
|
-
有些 Pi
|
|
206
|
+
有些 Pi 渠道可用时,Pi agent `models.json`(默认 `~/.pi/agent/models.json`;若设置了 `PI_CODING_AGENT_DIR`,则为 `$PI_CODING_AGENT_DIR/models.json`)里可能还没有对应 provider block。保留现有认证方式,不要复制 credential、token 或 API key。只在 `models.json` 里添加缓存 / 路由兼容覆盖。
|
|
182
207
|
|
|
183
208
|
Provider 级最小 override:
|
|
184
209
|
|
|
@@ -214,7 +239,7 @@ Provider 级最小 override:
|
|
|
214
239
|
|
|
215
240
|
## Footer 统计
|
|
216
241
|
|
|
217
|
-
统计是只读本地计数,保存在 `~/.pi/agent/pi-cache-optimizer-stats.json
|
|
242
|
+
统计是只读本地计数,保存在 Pi agent 目录(默认 `~/.pi/agent/pi-cache-optimizer-stats.json`;自定义 agent 目录使用 `PI_CODING_AGENT_DIR`),按 provider/model 作为 footer 展示维度,因此同一个渠道/模型在 Pi 进程或终端重启后会延续今天的统计。文件也保留 hashed session buckets,用于迁移和 reload 记录。文件只包含日期和数字计数,不包含 API key、prompt、payload、headers、响应或模型输出。
|
|
218
243
|
|
|
219
244
|
Pi 0.79+ 已内置 footer `CH` 标记,用于显示最近一次 prompt cache hit rate。本扩展在此基础上补充持久化的 provider/model 计数,以及代理 compat 诊断。
|
|
220
245
|
|
|
@@ -343,7 +368,7 @@ const hints = (globalThis as Record<symbol, any>)[CACHE_HINTS]?.getHints?.({
|
|
|
343
368
|
pi remove npm:pi-cache-optimizer
|
|
344
369
|
```
|
|
345
370
|
|
|
346
|
-
然后运行 `/reload` 或重启 Pi
|
|
371
|
+
然后运行 `/reload` 或重启 Pi。可选:删除本地统计文件(如果使用 `PI_CODING_AGENT_DIR`,请删除该目录中的同名文件):
|
|
347
372
|
|
|
348
373
|
| 平台 | 删除本地统计文件 |
|
|
349
374
|
|---|---|
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/pr
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import type
|
|
6
|
+
import { getAgentDir, type BuildSystemPromptOptions, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
8
8
|
type MutableEnv = Record<string, string | undefined>;
|
|
9
9
|
|
|
@@ -63,7 +63,9 @@ type CacheProviderId = "deepseek" | "openai" | "claude" | "gemini";
|
|
|
63
63
|
|
|
64
64
|
const LOG_PREFIX = "pi-cache-optimizer";
|
|
65
65
|
const STATUS_KEY = "pi-cache-stats";
|
|
66
|
-
|
|
66
|
+
|
|
67
|
+
/** Use Pi core's resolver so rebranded config names and env semantics stay aligned. */
|
|
68
|
+
const STATE_DIR = getAgentDir();
|
|
67
69
|
const STATE_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-stats.json");
|
|
68
70
|
const LEGACY_STATE_FILE_PATH = join(STATE_DIR, "deepseek-cache-optimizer-stats.json");
|
|
69
71
|
const CACHE_PROVIDER_IDS: CacheProviderId[] = ["deepseek", "openai", "claude", "gemini"];
|
|
@@ -141,13 +143,20 @@ const AYA_MODEL_PATTERN = /(^|[\/\s:_-])aya($|[\-_.:\/\s])/i;
|
|
|
141
143
|
const ORION_MODEL_PATTERN = /(^|[\/\s:_-])orion($|[\-_.:\/\s])/i;
|
|
142
144
|
|
|
143
145
|
type CacheCompat = {
|
|
146
|
+
supportsStore?: boolean;
|
|
147
|
+
supportsDeveloperRole?: boolean;
|
|
148
|
+
supportsReasoningEffort?: boolean;
|
|
149
|
+
supportsUsageInStreaming?: boolean;
|
|
150
|
+
supportsStrictMode?: boolean;
|
|
151
|
+
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
|
144
152
|
sendSessionAffinityHeaders?: boolean;
|
|
145
|
-
|
|
153
|
+
sessionAffinityFormat?: "openai" | "openai-nosession" | "openrouter";
|
|
146
154
|
supportsLongCacheRetention?: boolean;
|
|
147
155
|
thinkingFormat?: string;
|
|
148
156
|
requiresReasoningContentOnAssistantMessages?: boolean;
|
|
149
157
|
cacheControlFormat?: string;
|
|
150
158
|
forceAdaptiveThinking?: boolean;
|
|
159
|
+
allowEmptySignature?: boolean;
|
|
151
160
|
};
|
|
152
161
|
|
|
153
162
|
type CacheStats = {
|
|
@@ -979,24 +988,50 @@ function getCompat(model: PiModel | undefined): CacheCompat {
|
|
|
979
988
|
return modelCompat;
|
|
980
989
|
}
|
|
981
990
|
|
|
991
|
+
/** Join display-only path fragments without resolving them for I/O. */
|
|
992
|
+
function joinDisplayPath(base: string, child: string, platform: string = process.platform): string {
|
|
993
|
+
const sep = platform.startsWith("win") ? "\\" : "/";
|
|
994
|
+
return `${base.replace(/[\\/]+$/, "")}${sep}${child}`;
|
|
995
|
+
}
|
|
996
|
+
|
|
982
997
|
/**
|
|
983
|
-
* Return a platform-friendly display path for
|
|
998
|
+
* Return a platform-friendly display path for Pi's agent directory.
|
|
984
999
|
*
|
|
985
|
-
*
|
|
986
|
-
* `%USERPROFILE
|
|
987
|
-
*
|
|
988
|
-
* `~/.pi/agent/models.json` (the Unix-style tilde shorthand).
|
|
989
|
-
*
|
|
990
|
-
* This is a DISPLAY helper only. Actual path resolution is done by Pi
|
|
991
|
-
* (via Node `os.homedir()` + path.join), and this string is never used
|
|
992
|
-
* for I/O — only for warning/doctor/README text so that users on any
|
|
993
|
-
* platform see a copyable path they recognize.
|
|
1000
|
+
* Derives the display path from Pi core's `getAgentDir()` result. Home-relative
|
|
1001
|
+
* paths use `%USERPROFILE%` on Windows and `~` on Unix-like systems; custom
|
|
1002
|
+
* absolute or relative agent directories remain visible as configured.
|
|
994
1003
|
*/
|
|
995
|
-
function
|
|
996
|
-
|
|
997
|
-
|
|
1004
|
+
function getAgentDirDisplayPath(
|
|
1005
|
+
platform: string = process.platform,
|
|
1006
|
+
agentDir: string = getAgentDir(),
|
|
1007
|
+
homeDir: string = homedir(),
|
|
1008
|
+
): string {
|
|
1009
|
+
const sep = platform.startsWith("win") ? "\\" : "/";
|
|
1010
|
+
const normalizedAgentDir = agentDir.replace(/[\\/]+/g, sep);
|
|
1011
|
+
const normalizedHomeDir = homeDir.replace(/[\\/]+/g, sep).replace(/[\\/]+$/, "");
|
|
1012
|
+
const homePrefix = `${normalizedHomeDir}${sep}`;
|
|
1013
|
+
|
|
1014
|
+
if (normalizedAgentDir === normalizedHomeDir || normalizedAgentDir.startsWith(homePrefix)) {
|
|
1015
|
+
const relative = normalizedAgentDir.slice(normalizedHomeDir.length).replace(/^[\\/]+/, "");
|
|
1016
|
+
const homeLabel = platform.startsWith("win") ? "%USERPROFILE%" : "~";
|
|
1017
|
+
return relative ? `${homeLabel}${sep}${relative}` : homeLabel;
|
|
998
1018
|
}
|
|
999
|
-
|
|
1019
|
+
|
|
1020
|
+
return normalizedAgentDir;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Return a platform-friendly display path for Pi's `models.json`.
|
|
1025
|
+
*
|
|
1026
|
+
* This is a DISPLAY helper only. Actual I/O uses `MODELS_JSON_PATH`, resolved
|
|
1027
|
+
* from the same custom agent/config directory rules as `STATE_DIR`.
|
|
1028
|
+
*/
|
|
1029
|
+
function getModelsJsonDisplayPath(
|
|
1030
|
+
platform: string = process.platform,
|
|
1031
|
+
agentDir: string = getAgentDir(),
|
|
1032
|
+
homeDir: string = homedir(),
|
|
1033
|
+
): string {
|
|
1034
|
+
return joinDisplayPath(getAgentDirDisplayPath(platform, agentDir, homeDir), "models.json", platform);
|
|
1000
1035
|
}
|
|
1001
1036
|
|
|
1002
1037
|
function isEnabledEnv(value: string | undefined): boolean {
|
|
@@ -1098,6 +1133,28 @@ function isOpenAICompatibleProxyApi(api: unknown): boolean {
|
|
|
1098
1133
|
return lower(api) === "openai-completions";
|
|
1099
1134
|
}
|
|
1100
1135
|
|
|
1136
|
+
function isPiBuiltInLlamaCppModel(model: PiModel | undefined): boolean {
|
|
1137
|
+
if (lower(model?.provider) !== "llama.cpp" || !isOpenAICompatibleProxyApi(model?.api)) return false;
|
|
1138
|
+
|
|
1139
|
+
// Pi's built-in llama.cpp provider supplies this exact explicit compat
|
|
1140
|
+
// fingerprint. Provider ids are extension-overridable and models.json can add
|
|
1141
|
+
// cache/routing overrides, so provider id alone must never imply exemption.
|
|
1142
|
+
const compat = getCompat(model);
|
|
1143
|
+
return compat.supportsStore === false
|
|
1144
|
+
&& compat.supportsDeveloperRole === false
|
|
1145
|
+
&& compat.supportsReasoningEffort === false
|
|
1146
|
+
&& compat.supportsUsageInStreaming === false
|
|
1147
|
+
&& compat.supportsStrictMode === false
|
|
1148
|
+
&& compat.maxTokensField === "max_tokens"
|
|
1149
|
+
&& compat.sendSessionAffinityHeaders === undefined
|
|
1150
|
+
&& compat.sessionAffinityFormat === undefined
|
|
1151
|
+
&& compat.supportsLongCacheRetention === undefined;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function shouldInjectOpenAIPromptCacheKeyForModel(model: PiModel | undefined): boolean {
|
|
1155
|
+
return isOpenAICompatibleApi(model?.api);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1101
1158
|
function isResponsesPromptRewriteBypassApi(api: unknown): boolean {
|
|
1102
1159
|
const value = lower(api);
|
|
1103
1160
|
return value === "openai-codex-responses" || value === "openai-responses" || value === "azure-openai-responses";
|
|
@@ -1159,8 +1216,38 @@ function isAdaptiveGenerationModel(model: PiModel | undefined): boolean {
|
|
|
1159
1216
|
return tokens.some((t) => ADAPTIVE_OPUS_PATTERN.test(t) || ADAPTIVE_SONNET_PATTERN.test(t) || ADAPTIVE_FABLE_PATTERN.test(t));
|
|
1160
1217
|
}
|
|
1161
1218
|
|
|
1219
|
+
function isKimiCodingAdaptiveModel(model: PiModel | undefined): boolean {
|
|
1220
|
+
if (!model) return false;
|
|
1221
|
+
const provider = lower(model.provider);
|
|
1222
|
+
const baseUrl = lower(model.baseUrl);
|
|
1223
|
+
const isKimiCodingChannel = provider.includes("kimi-coding") || baseUrl.includes("api.kimi.com/coding");
|
|
1224
|
+
if (!isKimiCodingChannel) return false;
|
|
1225
|
+
|
|
1226
|
+
const tokens = getModelIdNameTokenValues(model);
|
|
1227
|
+
return tokens.some((token) =>
|
|
1228
|
+
token === "k3"
|
|
1229
|
+
|| token.includes("kimi-k3")
|
|
1230
|
+
|| token.includes("kimi k3")
|
|
1231
|
+
|| token.includes("kimi-for-coding")
|
|
1232
|
+
|| token.includes("kimi for coding")
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function isKimiCodingEmptySignatureModel(model: PiModel | undefined): boolean {
|
|
1237
|
+
if (!isKimiCodingAdaptiveModel(model)) return false;
|
|
1238
|
+
return getModelIdNameTokenValues(model).some((token) =>
|
|
1239
|
+
token === "k3"
|
|
1240
|
+
|| token === "kimi-k3"
|
|
1241
|
+
|| token.startsWith("kimi-k3-")
|
|
1242
|
+
|| token === "kimi k3"
|
|
1243
|
+
|| token === "kimi-for-coding"
|
|
1244
|
+
|| token === "kimi for coding"
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1162
1248
|
function isAdaptiveThinkingCompatApplicable(model: PiModel): boolean {
|
|
1163
|
-
return lower(model.api) === "anthropic-messages"
|
|
1249
|
+
return lower(model.api) === "anthropic-messages"
|
|
1250
|
+
&& (isAdaptiveGenerationModel(model) || isKimiCodingAdaptiveModel(model));
|
|
1164
1251
|
}
|
|
1165
1252
|
|
|
1166
1253
|
function describeMissingAdaptiveThinkingCompat(model: PiModel): string[] {
|
|
@@ -1169,6 +1256,9 @@ function describeMissingAdaptiveThinkingCompat(model: PiModel): string[] {
|
|
|
1169
1256
|
if (compat.forceAdaptiveThinking !== true) {
|
|
1170
1257
|
missing.push("forceAdaptiveThinking");
|
|
1171
1258
|
}
|
|
1259
|
+
if (isKimiCodingEmptySignatureModel(model) && compat.allowEmptySignature !== true) {
|
|
1260
|
+
missing.push("allowEmptySignature");
|
|
1261
|
+
}
|
|
1172
1262
|
return missing;
|
|
1173
1263
|
}
|
|
1174
1264
|
|
|
@@ -1177,6 +1267,9 @@ function buildAdaptiveThinkingCompatSuggestion(missing: string[]): Record<string
|
|
|
1177
1267
|
if (missing.includes("forceAdaptiveThinking")) {
|
|
1178
1268
|
suggestion.forceAdaptiveThinking = true;
|
|
1179
1269
|
}
|
|
1270
|
+
if (missing.includes("allowEmptySignature")) {
|
|
1271
|
+
suggestion.allowEmptySignature = true;
|
|
1272
|
+
}
|
|
1180
1273
|
return suggestion;
|
|
1181
1274
|
}
|
|
1182
1275
|
|
|
@@ -1189,6 +1282,9 @@ function appendAdaptiveThinkingCompatAdviceLines(lines: string[], missing: strin
|
|
|
1189
1282
|
lines.push("- forceAdaptiveThinking: true tells Pi to use adaptive thinking format");
|
|
1190
1283
|
lines.push(" (thinking: {type: 'adaptive'}) instead of legacy budget tokens format.");
|
|
1191
1284
|
lines.push(" Without this flag, Pi sends legacy thinking which adaptive-only upstreams reject.");
|
|
1285
|
+
if (missing.includes("allowEmptySignature")) {
|
|
1286
|
+
lines.push("- allowEmptySignature: true preserves Kimi Coding K3 thinking blocks whose replay signature is empty.");
|
|
1287
|
+
}
|
|
1192
1288
|
appendCredentialSafeProviderGuidance(lines, placement, suggestion);
|
|
1193
1289
|
}
|
|
1194
1290
|
|
|
@@ -1198,8 +1294,8 @@ function buildAdaptiveThinkingCompatWarningText(key: string, missing: string[]):
|
|
|
1198
1294
|
const modelId = slashIdx > 0 ? key.slice(slashIdx + 1) : undefined;
|
|
1199
1295
|
const modelsJsonPath = getModelsJsonDisplayPath();
|
|
1200
1296
|
const lines: string[] = [
|
|
1201
|
-
`💡 pi-cache-optimizer: ${key} is an adaptive-generation
|
|
1202
|
-
`Without
|
|
1297
|
+
`💡 pi-cache-optimizer: ${key} is an adaptive-generation model but merged compat lacks ${missing.join(" and ")}.`,
|
|
1298
|
+
`Without the required compat, Pi may send legacy thinking or replay thinking blocks incorrectly.`,
|
|
1203
1299
|
`Edit ${modelsJsonPath} -> providers["${providerLabel}"] -> compat (at the same level as baseUrl/api/apiKey/models).`,
|
|
1204
1300
|
"",
|
|
1205
1301
|
];
|
|
@@ -1991,6 +2087,7 @@ function describeMissingOpenAICompatibleProxyCompat(model: PiModel): string[] {
|
|
|
1991
2087
|
|
|
1992
2088
|
if (!isOpenAICompatibleProxyApi(model.api)) return missing;
|
|
1993
2089
|
if (isOfficialOpenAIBaseUrl(model)) return missing;
|
|
2090
|
+
if (isPiBuiltInLlamaCppModel(model)) return missing;
|
|
1994
2091
|
|
|
1995
2092
|
if (compat.sendSessionAffinityHeaders === undefined) {
|
|
1996
2093
|
missing.push("sendSessionAffinityHeaders");
|
|
@@ -2018,6 +2115,7 @@ function describeOptionalOpenAICompatibleProxyCompat(model: PiModel): string[] {
|
|
|
2018
2115
|
|
|
2019
2116
|
if (!isOpenAICompatibleProxyApi(model.api)) return optional;
|
|
2020
2117
|
if (isOfficialOpenAIBaseUrl(model)) return optional;
|
|
2118
|
+
if (isPiBuiltInLlamaCppModel(model)) return optional;
|
|
2021
2119
|
|
|
2022
2120
|
if (compat.supportsLongCacheRetention !== true) {
|
|
2023
2121
|
optional.push("supportsLongCacheRetention");
|
|
@@ -2177,11 +2275,7 @@ function describeMissingDeepSeekCompat(model: PiModel): string[] {
|
|
|
2177
2275
|
if (compat.supportsLongCacheRetention !== true) {
|
|
2178
2276
|
missing.push("supportsLongCacheRetention");
|
|
2179
2277
|
}
|
|
2180
|
-
if (model.api
|
|
2181
|
-
if (compat.sendSessionIdHeader !== true) {
|
|
2182
|
-
missing.push("sendSessionIdHeader");
|
|
2183
|
-
}
|
|
2184
|
-
} else if (compat.sendSessionAffinityHeaders !== true) {
|
|
2278
|
+
if (model.api !== "openai-responses" && compat.sendSessionAffinityHeaders !== true) {
|
|
2185
2279
|
missing.push("sendSessionAffinityHeaders");
|
|
2186
2280
|
}
|
|
2187
2281
|
if (compat.requiresReasoningContentOnAssistantMessages !== true) {
|
|
@@ -2195,7 +2289,7 @@ function describeMissingDeepSeekCompat(model: PiModel): string[] {
|
|
|
2195
2289
|
}
|
|
2196
2290
|
|
|
2197
2291
|
function isDeepSeekCompatCheckApplicable(model: PiModel): boolean {
|
|
2198
|
-
return isDeepSeekLikeModel(model) && isOpenAICompatibleApi(model.api);
|
|
2292
|
+
return isDeepSeekLikeModel(model) && isOpenAICompatibleApi(model.api) && !isPiBuiltInLlamaCppModel(model);
|
|
2199
2293
|
}
|
|
2200
2294
|
|
|
2201
2295
|
function describeMissingCacheCompatForModel(model: PiModel): string[] {
|
|
@@ -2214,9 +2308,6 @@ function buildDeepSeekCompatSuggestion(missing: string[]): Record<string, unknow
|
|
|
2214
2308
|
if (missing.includes("supportsLongCacheRetention")) {
|
|
2215
2309
|
suggestion.supportsLongCacheRetention = true;
|
|
2216
2310
|
}
|
|
2217
|
-
if (missing.includes("sendSessionIdHeader")) {
|
|
2218
|
-
suggestion.sendSessionIdHeader = true;
|
|
2219
|
-
}
|
|
2220
2311
|
if (missing.includes("sendSessionAffinityHeaders")) {
|
|
2221
2312
|
suggestion.sendSessionAffinityHeaders = true;
|
|
2222
2313
|
}
|
|
@@ -2246,9 +2337,6 @@ function appendDeepSeekCompatAdviceLines(lines: string[], missing: string[], pla
|
|
|
2246
2337
|
if (missing.includes("sendSessionAffinityHeaders")) {
|
|
2247
2338
|
lines.push("- sendSessionAffinityHeaders: recommended for OpenAI-compatible DeepSeek proxies when supported; it helps keep one Pi session on the same upstream/backend.");
|
|
2248
2339
|
}
|
|
2249
|
-
if (missing.includes("sendSessionIdHeader")) {
|
|
2250
|
-
lines.push("- sendSessionIdHeader: recommended for OpenAI Responses-compatible DeepSeek proxies when supported.");
|
|
2251
|
-
}
|
|
2252
2340
|
if (missing.includes("supportsLongCacheRetention")) {
|
|
2253
2341
|
lines.push("- supportsLongCacheRetention: enable for DeepSeek-compatible endpoints that support long cache retention.");
|
|
2254
2342
|
}
|
|
@@ -2285,7 +2373,7 @@ const CACHE_PROVIDER_ADAPTERS: CacheProviderAdapter[] = [
|
|
|
2285
2373
|
return normalizeWithFallback(message, getDeepSeekRawUsage, { allowInputOnlyPiUsage: true });
|
|
2286
2374
|
},
|
|
2287
2375
|
warningText(model) {
|
|
2288
|
-
if (!
|
|
2376
|
+
if (!isDeepSeekCompatCheckApplicable(model)) return undefined;
|
|
2289
2377
|
|
|
2290
2378
|
const missing = describeMissingDeepSeekCompat(model);
|
|
2291
2379
|
if (missing.length === 0) return undefined;
|
|
@@ -2307,7 +2395,7 @@ const CACHE_PROVIDER_ADAPTERS: CacheProviderAdapter[] = [
|
|
|
2307
2395
|
return normalizeWithFallback(message, getAnthropicRawUsage);
|
|
2308
2396
|
},
|
|
2309
2397
|
warningText(model) {
|
|
2310
|
-
if (!isClaudeLikeModel(model) || !isOpenAICompatibleApi(model.api)) return undefined;
|
|
2398
|
+
if (!isClaudeLikeModel(model) || !isOpenAICompatibleApi(model.api) || isPiBuiltInLlamaCppModel(model)) return undefined;
|
|
2311
2399
|
if (getCompat(model).cacheControlFormat === "anthropic") return undefined;
|
|
2312
2400
|
|
|
2313
2401
|
return (
|
|
@@ -3256,15 +3344,14 @@ function notifyCacheCompatIfNeeded(
|
|
|
3256
3344
|
if (!model) return;
|
|
3257
3345
|
|
|
3258
3346
|
// Native anthropic-messages adaptive thinking compat check.
|
|
3259
|
-
//
|
|
3260
|
-
//
|
|
3261
|
-
if (
|
|
3262
|
-
const
|
|
3263
|
-
if (
|
|
3347
|
+
// Adapter warningText only fires for OpenAI-compatible APIs, so native
|
|
3348
|
+
// Anthropic and Kimi Coding adaptive models need a separate check.
|
|
3349
|
+
if (isAdaptiveThinkingCompatApplicable(model)) {
|
|
3350
|
+
const missing = describeMissingAdaptiveThinkingCompat(model);
|
|
3351
|
+
if (missing.length > 0) {
|
|
3264
3352
|
const key = `adaptive-thinking:${modelKey(model)}`;
|
|
3265
3353
|
if (!warnedModels.has(key)) {
|
|
3266
3354
|
warnedModels.add(key);
|
|
3267
|
-
const missing = describeMissingAdaptiveThinkingCompat(model);
|
|
3268
3355
|
ctx.ui.notify(buildAdaptiveThinkingCompatWarningText(modelKey(model), missing), "warning");
|
|
3269
3356
|
}
|
|
3270
3357
|
}
|
|
@@ -3918,28 +4005,30 @@ async function writePersistedCacheStats(
|
|
|
3918
4005
|
|
|
3919
4006
|
|
|
3920
4007
|
function isCompatCheckApplicable(model: PiModel): boolean {
|
|
3921
|
-
return isOpenAICompatibleProxyApi(model.api) && !isOfficialOpenAIBaseUrl(model);
|
|
4008
|
+
return isOpenAICompatibleProxyApi(model.api) && !isOfficialOpenAIBaseUrl(model) && !isPiBuiltInLlamaCppModel(model);
|
|
3922
4009
|
}
|
|
3923
4010
|
|
|
3924
4011
|
function isPromptCacheRetention400Applicable(model: PiModel): boolean {
|
|
3925
4012
|
return isOpenAICompatibleApi(model.api) &&
|
|
3926
4013
|
!isOfficialOpenAIBaseUrl(model) &&
|
|
4014
|
+
!isPiBuiltInLlamaCppModel(model) &&
|
|
3927
4015
|
getCompat(model).supportsLongCacheRetention === true;
|
|
3928
4016
|
}
|
|
3929
4017
|
|
|
3930
4018
|
/**
|
|
3931
4019
|
* Whether the 403 sendSessionAffinityHeaders diagnostic applies to a model.
|
|
3932
4020
|
*
|
|
3933
|
-
* Pi's openai-completions
|
|
3934
|
-
*
|
|
3935
|
-
*
|
|
3936
|
-
*
|
|
3937
|
-
*
|
|
3938
|
-
*
|
|
3939
|
-
*
|
|
4021
|
+
* Pi's openai-completions adapter sends custom HTTP headers (session_id,
|
|
4022
|
+
* x-client-request-id, x-session-affinity) when `sendSessionAffinityHeaders`
|
|
4023
|
+
* is enabled. Some third-party proxies / CDNs / WAFs block these headers and
|
|
4024
|
+
* return HTTP 403 "Your request was blocked". Pi 0.80.7+ no longer uses this
|
|
4025
|
+
* flag for openai-responses; Responses header shape is controlled by
|
|
4026
|
+
* `sessionAffinityFormat`. This guard therefore applies only to
|
|
4027
|
+
* openai-completions, where doctor/fix can safely advise disabling the flag.
|
|
3940
4028
|
*/
|
|
3941
4029
|
function isSessionAffinity403Applicable(model: PiModel): boolean {
|
|
3942
|
-
if (!
|
|
4030
|
+
if (!isOpenAICompatibleProxyApi(model.api)) return false;
|
|
4031
|
+
if (isPiBuiltInLlamaCppModel(model)) return false;
|
|
3943
4032
|
return getCompat(model).sendSessionAffinityHeaders === true;
|
|
3944
4033
|
}
|
|
3945
4034
|
|
|
@@ -3956,6 +4045,7 @@ function isSessionAffinity403Applicable(model: PiModel): boolean {
|
|
|
3956
4045
|
function isOpenAISdkHeader403Applicable(model: PiModel): boolean {
|
|
3957
4046
|
if (!isOpenAICompatibleProxyApi(model.api)) return false;
|
|
3958
4047
|
if (isOfficialOpenAIBaseUrl(model)) return false;
|
|
4048
|
+
if (isPiBuiltInLlamaCppModel(model)) return false;
|
|
3959
4049
|
return getCompat(model).sendSessionAffinityHeaders !== true;
|
|
3960
4050
|
}
|
|
3961
4051
|
|
|
@@ -3997,6 +4087,13 @@ function describeRouterChannelDiagnostics(model: PiModel): string[] {
|
|
|
3997
4087
|
return notes;
|
|
3998
4088
|
}
|
|
3999
4089
|
|
|
4090
|
+
// Pi 0.81+ built-in llama.cpp uses an OpenAI-shaped transport, but its
|
|
4091
|
+
// untouched explicit compat fingerprint does not expose proxy-routing or
|
|
4092
|
+
// session-affinity configuration. Same-id overrides are not exempt.
|
|
4093
|
+
if (isPiBuiltInLlamaCppModel(model)) {
|
|
4094
|
+
return notes;
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4000
4097
|
// ── 1. OpenRouter ────────────────────────────────────────────────
|
|
4001
4098
|
if (
|
|
4002
4099
|
baseUrl.includes("openrouter.ai") ||
|
|
@@ -5290,7 +5387,6 @@ function deepEqualIgnoringKeys(a: unknown, b: unknown, extraKeys: string[]): boo
|
|
|
5290
5387
|
*/
|
|
5291
5388
|
const PROVIDER_LEVEL_SAFE_COMPAT_KEYS = new Set<string>([
|
|
5292
5389
|
"sendSessionAffinityHeaders",
|
|
5293
|
-
"sendSessionIdHeader",
|
|
5294
5390
|
"supportsLongCacheRetention",
|
|
5295
5391
|
]);
|
|
5296
5392
|
|
|
@@ -5304,9 +5400,10 @@ function syntheticModelForId(providerLabel: string, id: string): PiModel {
|
|
|
5304
5400
|
* Strategy (auto-detect, prefer provider level when safe):
|
|
5305
5401
|
* - Channel-capability keys (session affinity / long retention) are always
|
|
5306
5402
|
* provider-safe.
|
|
5307
|
-
* - Model-behavior keys (forceAdaptiveThinking,
|
|
5308
|
-
* provider-safe ONLY when every sibling model in
|
|
5309
|
-
* the same detection (all adaptive-generation /
|
|
5403
|
+
* - Model-behavior keys (forceAdaptiveThinking, allowEmptySignature,
|
|
5404
|
+
* thinkingFormat, ...) are provider-safe ONLY when every sibling model in
|
|
5405
|
+
* the provider also matches the same detection (all adaptive-generation /
|
|
5406
|
+
* Kimi Coding adaptive / DeepSeek-like).
|
|
5310
5407
|
* - Single-model providers: provider level is equivalent — prefer it.
|
|
5311
5408
|
* - Any unsafe key → fall back to model level (single write, smallest blast radius).
|
|
5312
5409
|
*/
|
|
@@ -5329,10 +5426,18 @@ function decideFixPlacement(
|
|
|
5329
5426
|
if (PROVIDER_LEVEL_SAFE_COMPAT_KEYS.has(key)) continue;
|
|
5330
5427
|
|
|
5331
5428
|
if (key === "forceAdaptiveThinking") {
|
|
5332
|
-
const allAdaptive = siblings.every((id) =>
|
|
5429
|
+
const allAdaptive = siblings.every((id) => {
|
|
5430
|
+
const sibling = syntheticModelForId(providerLabel, id);
|
|
5431
|
+
return isAdaptiveGenerationModel(sibling) || isKimiCodingAdaptiveModel(sibling);
|
|
5432
|
+
});
|
|
5333
5433
|
if (!allAdaptive) unsafeKeys.push(key);
|
|
5334
5434
|
continue;
|
|
5335
5435
|
}
|
|
5436
|
+
if (key === "allowEmptySignature") {
|
|
5437
|
+
const allKimiCodingAdaptive = siblings.every((id) => isKimiCodingAdaptiveModel(syntheticModelForId(providerLabel, id)));
|
|
5438
|
+
if (!allKimiCodingAdaptive) unsafeKeys.push(key);
|
|
5439
|
+
continue;
|
|
5440
|
+
}
|
|
5336
5441
|
if (key === "thinkingFormat" || key === "requiresReasoningContentOnAssistantMessages") {
|
|
5337
5442
|
const allDeepSeek = siblings.every((id) => isDeepSeekLikeModel(syntheticModelForId(providerLabel, id)));
|
|
5338
5443
|
if (!allDeepSeek) unsafeKeys.push(key);
|
|
@@ -5708,8 +5813,10 @@ export const __internals_for_tests = {
|
|
|
5708
5813
|
hasEffectivePromptCacheKey,
|
|
5709
5814
|
isNonEmptyString,
|
|
5710
5815
|
shouldInjectOpenAIPromptCacheKey,
|
|
5816
|
+
shouldInjectOpenAIPromptCacheKeyForModel,
|
|
5711
5817
|
isOpenAICompatibleApi,
|
|
5712
5818
|
isOpenAICompatibleProxyApi,
|
|
5819
|
+
isPiBuiltInLlamaCppModel,
|
|
5713
5820
|
isResponsesPromptRewriteBypassApi,
|
|
5714
5821
|
isMistralConversationsApi,
|
|
5715
5822
|
isOpenAIFamilyModel,
|
|
@@ -5849,6 +5956,7 @@ export const __internals_for_tests = {
|
|
|
5849
5956
|
modelKey,
|
|
5850
5957
|
consolidateDirectProviderStatsModel,
|
|
5851
5958
|
// Platform-friendly path helpers
|
|
5959
|
+
getAgentDirDisplayPath,
|
|
5852
5960
|
getModelsJsonDisplayPath,
|
|
5853
5961
|
buildProviderCompatOverride,
|
|
5854
5962
|
buildModelCompatOverride,
|
|
@@ -5935,6 +6043,8 @@ export const __internals_for_tests = {
|
|
|
5935
6043
|
buildFixSuggestion,
|
|
5936
6044
|
// Adaptive thinking compat helpers
|
|
5937
6045
|
isAdaptiveGenerationModel,
|
|
6046
|
+
isKimiCodingAdaptiveModel,
|
|
6047
|
+
isKimiCodingEmptySignatureModel,
|
|
5938
6048
|
isAdaptiveThinkingCompatApplicable,
|
|
5939
6049
|
describeMissingAdaptiveThinkingCompat,
|
|
5940
6050
|
buildAdaptiveThinkingCompatSuggestion,
|
|
@@ -6443,6 +6553,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
6443
6553
|
|
|
6444
6554
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
6445
6555
|
latestCacheHint = undefined;
|
|
6556
|
+
// Clear the legacy global before any bypass/disable early return. A valid
|
|
6557
|
+
// rewrite path republishes the current session key below; otherwise callers
|
|
6558
|
+
// must not observe a stale key from the previously selected model/route.
|
|
6559
|
+
delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
|
|
6446
6560
|
const routeSnapshot = resolveActiveRouteSnapshot(_ctx.model, _ctx);
|
|
6447
6561
|
const routedModel = routeSnapshot
|
|
6448
6562
|
? findModelInRegistry(_ctx.modelRegistry, routeSnapshot.provider, routeSnapshot.modelId) ?? routeSnapshotToPiModel(routeSnapshot, _ctx.model)
|
|
@@ -6528,7 +6642,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
6528
6642
|
timestamp: Date.now(),
|
|
6529
6643
|
};
|
|
6530
6644
|
const globals = getProtocolGlobal();
|
|
6531
|
-
|
|
6645
|
+
if (promptCacheKey) {
|
|
6646
|
+
globals.__piCacheOptimizerCacheKey__ = promptCacheKey;
|
|
6647
|
+
} else {
|
|
6648
|
+
delete globals.__piCacheOptimizerCacheKey__;
|
|
6649
|
+
}
|
|
6532
6650
|
};
|
|
6533
6651
|
|
|
6534
6652
|
if (optimized.changed && optimized.systemPrompt.trim().length > 0) {
|
|
@@ -6589,7 +6707,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6589
6707
|
|
|
6590
6708
|
if (!shouldInjectOpenAIPromptCacheKey()) return undefined;
|
|
6591
6709
|
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
6592
|
-
if (!
|
|
6710
|
+
if (!shouldInjectOpenAIPromptCacheKeyForModel(requestModel)) return undefined;
|
|
6593
6711
|
|
|
6594
6712
|
return addOpenAIPromptCacheKey(event.payload, getSessionPromptCacheKey(ctx));
|
|
6595
6713
|
});
|
package/package.json
CHANGED