pi-cache-optimizer 2.6.24 → 2.6.25
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 +4 -2
- package/README.zh-CN.md +4 -2
- package/index.ts +500 -116
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Pi extension for improving provider-side KV / prompt cache hit rates. It keeps s
|
|
|
27
27
|
|
|
28
28
|
## What it does
|
|
29
29
|
|
|
30
|
-
- Reorders stable system-prompt content before dynamic context.
|
|
30
|
+
- Reorders uniquely identifiable stable system-prompt content before dynamic context. If the same candidate appears more than once (for example, quoted inside dynamic context), it is left unchanged to avoid removing the wrong occurrence.
|
|
31
31
|
- Compresses Pi skill listings and strips session-overview churn.
|
|
32
32
|
- Requests long cache retention when Pi/provider compat supports it.
|
|
33
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.
|
|
@@ -82,7 +82,7 @@ On Pi 0.79.7 and newer, `pi update` updates Pi itself only. To update installed
|
|
|
82
82
|
|
|
83
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
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.
|
|
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 effective `supportsLongCacheRetention: true` opt-in in `models.json`; otherwise strip it before sending. Effective values follow Pi's precedence: `modelOverrides[modelId].compat` first, then the matching `models[].compat`, then provider-level `compat`. An explicit `false` at a higher layer overrides `true` below it.
|
|
86
86
|
|
|
87
87
|
For real proxies, start with session affinity:
|
|
88
88
|
|
|
@@ -205,6 +205,8 @@ Pi 0.80.9+ already includes Kimi K3 in built-in Kimi Coding, Moonshot AI / China
|
|
|
205
205
|
5. Writes atomically (temp + rename); self-validates after write
|
|
206
206
|
6. Falls back to manual guidance if JSONC scanner cannot confidently locate the target
|
|
207
207
|
|
|
208
|
+
Existing `modelOverrides[modelId]` entries have Pi's highest precedence, so `fix` repairs them directly. For built-in or API-login models without a custom `models[]` entry, `fix` creates a compat-only `modelOverrides` entry instead of inventing a custom model definition. Self-validation checks the effective three-layer compat result, so a lower-level edit shadowed by an override is rejected.
|
|
209
|
+
|
|
208
210
|
**Non-interactive mode:** refuses to write; shows manual edit guidance instead.
|
|
209
211
|
|
|
210
212
|
**Run:** `/cache-optimizer fix` when the active model has detected compat issues. The command shows "nothing to fix" when compat is already complete.
|
package/README.zh-CN.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
## 功能
|
|
29
29
|
|
|
30
|
-
-
|
|
30
|
+
- 将能唯一定位的稳定 system prompt 内容移动到动态上下文之前。如果同一候选出现多次(例如动态上下文引用了它),则保持原样,避免删除错误的那一处。
|
|
31
31
|
- 压缩 Pi skill 列表,并移除 session-overview 中的易变字段。
|
|
32
32
|
- 在 Pi / provider compat 支持时请求长缓存保留。
|
|
33
33
|
- 对 `openai-completions` / `openai-responses` 请求,在没有有效 key 时使用 Pi session id 补 `prompt_cache_key`;Pi 0.82 core 对内置 `llama.cpp` 也使用这一语义。
|
|
@@ -82,7 +82,7 @@ Pi 0.79.7 及之后,`pi update` 默认只更新 Pi 本体。若要更新已安
|
|
|
82
82
|
|
|
83
83
|
LiteLLM / OneAPI / NewAPI / 类 OpenRouter 渠道等第三方 `openai-completions` 代理,常会把同一个 session 分散到多个上游后端,导致 provider 侧 prompt cache 被拆散。
|
|
84
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
|
|
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` 时保留,否则发送前移除。有效值遵循 Pi 的优先级:先看 `modelOverrides[modelId].compat`,再看匹配的 `models[].compat`,最后看 provider 级 `compat`;高层显式 `false` 会覆盖低层的 `true`。
|
|
86
86
|
|
|
87
87
|
对真正的代理,建议先启用 session affinity:
|
|
88
88
|
|
|
@@ -205,6 +205,8 @@ Pi 0.80.9+ 已在内置 Kimi Coding、Moonshot AI / 中国区、OpenRouter 和 V
|
|
|
205
205
|
5. 原子写入(temp + rename);写入后自我验证
|
|
206
206
|
6. 如果 JSONC 扫描器无法置信定位目标,回退到手动修改指引
|
|
207
207
|
|
|
208
|
+
已有的 `modelOverrides[modelId]` 具有 Pi 的最高优先级,因此 `fix` 会直接修复该 entry。对于没有自定义 `models[]` entry 的内置模型或 API-login 模型,`fix` 会创建仅含 compat 的 `modelOverrides` entry,而不会凭空添加自定义模型定义。自检会验证三层合并后的有效 compat,因此被更高层 override 遮蔽的低层修改会被拒绝。
|
|
209
|
+
|
|
208
210
|
**非交互模式:** 拒绝写入,显示手动编辑指引。
|
|
209
211
|
|
|
210
212
|
**运行:** 当 active model 检测到 compat 问题时执行 `/cache-optimizer fix`。compat 已完整时,命令显示"无需修复"。
|
package/index.ts
CHANGED
|
@@ -133,8 +133,8 @@ function getAnthropicTtlFallbackState(): AnthropicTtlFallbackStateV1 {
|
|
|
133
133
|
|
|
134
134
|
let runtimeOptimizerEnabled = true;
|
|
135
135
|
|
|
136
|
-
// WORM-flag: if optimizeSystemPrompt ever detects that
|
|
137
|
-
//
|
|
136
|
+
// WORM-flag: if optimizeSystemPrompt ever detects that candidate extraction
|
|
137
|
+
// has accidentally truncated a structural marker (any XML tag or
|
|
138
138
|
// HTML comment boundary marker present in the original prompt), we flip
|
|
139
139
|
// this. publishStatus reads it once, appends a footer warning, then
|
|
140
140
|
// resets it. The flag surface is kept separate from the regular
|
|
@@ -161,12 +161,10 @@ const SKILL_COMPRESSION_MIN_COUNT = 4;
|
|
|
161
161
|
|
|
162
162
|
// Minimum trimmed length for a candidate to qualify as a stable-prefix "part".
|
|
163
163
|
//
|
|
164
|
-
// `optimizeSystemPrompt` removes each accepted candidate
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// non-deterministic dynamic remainder per request. Both behaviors poison the
|
|
169
|
-
// provider's prompt-prefix cache.
|
|
164
|
+
// `optimizeSystemPrompt` removes each uniquely occurring accepted candidate
|
|
165
|
+
// from the dynamic remainder. Short or character-class candidates (think: `S`,
|
|
166
|
+
// `- u`, `- (`, `- }`) are too likely to match unrelated prompt text, so they
|
|
167
|
+
// are never eligible for lifting.
|
|
170
168
|
//
|
|
171
169
|
// The threshold also caps the upstream string-vs-array regression we saw with
|
|
172
170
|
// trellis 0.5.16 / 0.6.0-beta.17 (subagent tool registration passing
|
|
@@ -644,7 +642,7 @@ function stripSessionOverviewChurn(prompt: string): string {
|
|
|
644
642
|
* Extract structural markers from a prompt for the integrity guard.
|
|
645
643
|
*
|
|
646
644
|
* The guard runs in `optimizeSystemPrompt` to catch cases where the
|
|
647
|
-
*
|
|
645
|
+
* candidate extraction accidentally eats text inside
|
|
648
646
|
* an extension-injected structural block (e.g., trellis
|
|
649
647
|
* `<workflow-state>`, a hypothetical `<task-tracker>`, or AGENTS.md
|
|
650
648
|
* `<!-- TRELLIS:START -->` markers). When the original prompt contains
|
|
@@ -704,16 +702,43 @@ function optimizeSystemPrompt(
|
|
|
704
702
|
const seen = new Set<string>();
|
|
705
703
|
let rest = original;
|
|
706
704
|
|
|
707
|
-
//
|
|
708
|
-
//
|
|
705
|
+
// Classify candidate ambiguity against one immutable snapshot. Candidates
|
|
706
|
+
// can be nested (a full context-file block also contains its bare content),
|
|
707
|
+
// so recomputing occurrence counts after each removal is unsafe: deleting
|
|
708
|
+
// the full block can make the dynamic copy of its bare content appear unique.
|
|
709
|
+
const candidates: string[] = [];
|
|
709
710
|
for (const candidate of buildStableCandidates(opts)) {
|
|
710
711
|
const part = candidate.trim();
|
|
711
|
-
if (!part || part.length < MIN_STABLE_CANDIDATE_LENGTH) continue;
|
|
712
|
-
|
|
712
|
+
if (!part || part.length < MIN_STABLE_CANDIDATE_LENGTH || seen.has(part)) continue;
|
|
713
|
+
seen.add(part);
|
|
714
|
+
candidates.push(part);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const initialRemainder = rest;
|
|
718
|
+
const occurrenceCount = new Map<string, number>();
|
|
719
|
+
for (const part of candidates) {
|
|
720
|
+
let count = 0;
|
|
721
|
+
let searchFrom = 0;
|
|
722
|
+
while (searchFrom < initialRemainder.length) {
|
|
723
|
+
const occurrence = initialRemainder.indexOf(part, searchFrom);
|
|
724
|
+
if (occurrence < 0) break;
|
|
725
|
+
count++;
|
|
726
|
+
if (count > 1) break;
|
|
727
|
+
searchFrom = occurrence + 1;
|
|
728
|
+
}
|
|
729
|
+
occurrenceCount.set(part, count);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Stable layer: content likely to be identical across sessions/turns.
|
|
733
|
+
// Short / single-char candidates are dropped: see MIN_STABLE_CANDIDATE_LENGTH.
|
|
734
|
+
for (const part of candidates) {
|
|
735
|
+
if (occurrenceCount.get(part) !== 1) continue;
|
|
736
|
+
|
|
737
|
+
const firstOccurrence = rest.indexOf(part);
|
|
738
|
+
if (firstOccurrence < 0) continue;
|
|
713
739
|
|
|
714
740
|
stableParts.push(part);
|
|
715
|
-
|
|
716
|
-
rest = rest.replace(part, "");
|
|
741
|
+
rest = rest.slice(0, firstOccurrence) + rest.slice(firstOccurrence + part.length);
|
|
717
742
|
}
|
|
718
743
|
|
|
719
744
|
const stablePrefix = stableParts.join("\n\n");
|
|
@@ -731,8 +756,8 @@ function optimizeSystemPrompt(
|
|
|
731
756
|
|
|
732
757
|
// Sanity check: scan ALL structural markers (XML tags + HTML comment
|
|
733
758
|
// boundary markers) in the original and verify each one survives the
|
|
734
|
-
// reorder. If any marker drops,
|
|
735
|
-
//
|
|
759
|
+
// reorder. If any marker drops, candidate extraction ate something it
|
|
760
|
+
// shouldn't have — fall back to the original
|
|
736
761
|
// prompt and flag the footer warning. This is provider-agnostic and
|
|
737
762
|
// extension-agnostic: trellis `<workflow-state>`, a hypothetical
|
|
738
763
|
// `<task-tracker>`, AGENTS.md `<!-- TRELLIS:START -->`, etc., are all
|
|
@@ -5040,6 +5065,63 @@ function parseJsonc(text: string): unknown {
|
|
|
5040
5065
|
return JSON.parse(stripJsoncTrailingCommas(stripJsoncComments(text)));
|
|
5041
5066
|
}
|
|
5042
5067
|
|
|
5068
|
+
type ExplicitCompatSource = "modelOverride" | "model" | "provider";
|
|
5069
|
+
|
|
5070
|
+
interface ExplicitCompatValue {
|
|
5071
|
+
source: ExplicitCompatSource;
|
|
5072
|
+
value: unknown;
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
/**
|
|
5076
|
+
* Resolve one explicitly configured compat value using Pi's models.json
|
|
5077
|
+
* precedence. Built-in model overrides win over custom model definitions,
|
|
5078
|
+
* which in turn win over provider defaults.
|
|
5079
|
+
*/
|
|
5080
|
+
function resolveExplicitCompatValue(
|
|
5081
|
+
config: unknown,
|
|
5082
|
+
providerLabel: string,
|
|
5083
|
+
modelId: string,
|
|
5084
|
+
compatKey: string,
|
|
5085
|
+
): ExplicitCompatValue | undefined {
|
|
5086
|
+
const providers = asRecord(asRecord(config)?.providers);
|
|
5087
|
+
const provider = asRecord(providers?.[providerLabel]);
|
|
5088
|
+
if (!provider) return undefined;
|
|
5089
|
+
|
|
5090
|
+
const override = asRecord(asRecord(provider.modelOverrides)?.[modelId]);
|
|
5091
|
+
const overrideCompat = asRecord(override?.compat);
|
|
5092
|
+
if (overrideCompat && Object.prototype.hasOwnProperty.call(overrideCompat, compatKey)) {
|
|
5093
|
+
return { source: "modelOverride", value: overrideCompat[compatKey] };
|
|
5094
|
+
}
|
|
5095
|
+
|
|
5096
|
+
if (Array.isArray(provider.models)) {
|
|
5097
|
+
const model = provider.models.find((entry: unknown) => asRecord(entry)?.id === modelId);
|
|
5098
|
+
const modelCompat = asRecord(asRecord(model)?.compat);
|
|
5099
|
+
if (modelCompat && Object.prototype.hasOwnProperty.call(modelCompat, compatKey)) {
|
|
5100
|
+
return { source: "model", value: modelCompat[compatKey] };
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
const providerCompat = asRecord(provider.compat);
|
|
5105
|
+
if (providerCompat && Object.prototype.hasOwnProperty.call(providerCompat, compatKey)) {
|
|
5106
|
+
return { source: "provider", value: providerCompat[compatKey] };
|
|
5107
|
+
}
|
|
5108
|
+
|
|
5109
|
+
return undefined;
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
function hasExplicitLongRetentionOptInFromConfig(
|
|
5113
|
+
config: unknown,
|
|
5114
|
+
providerLabel: string,
|
|
5115
|
+
modelId: string,
|
|
5116
|
+
): boolean {
|
|
5117
|
+
return resolveExplicitCompatValue(
|
|
5118
|
+
config,
|
|
5119
|
+
providerLabel,
|
|
5120
|
+
modelId,
|
|
5121
|
+
"supportsLongCacheRetention",
|
|
5122
|
+
)?.value === true;
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5043
5125
|
/**
|
|
5044
5126
|
* JSONC scanner: locate the provider block and model entry in models.json text.
|
|
5045
5127
|
* Returns the byte offsets for surgical insertion, or undefined if ambiguous.
|
|
@@ -5065,10 +5147,99 @@ interface ModelNodeLocation {
|
|
|
5065
5147
|
providerCompatBrace: number;
|
|
5066
5148
|
/** Offset of the provider-level compat object's closing `}`, or -1 if absent */
|
|
5067
5149
|
providerCompatEnd: number;
|
|
5150
|
+
/** Offset of the target modelOverrides entry's opening `{`, or -1 if absent */
|
|
5151
|
+
modelOverrideObjectBrace: number;
|
|
5152
|
+
/** Offset of the target modelOverrides entry's closing `}`, or -1 if absent */
|
|
5153
|
+
modelOverrideObjectEnd: number;
|
|
5154
|
+
/** Offset of the target override compat object's opening `{`, or -1 if absent */
|
|
5155
|
+
modelOverrideCompatBrace: number;
|
|
5156
|
+
/** Offset of the target override compat object's closing `}`, or -1 if absent */
|
|
5157
|
+
modelOverrideCompatEnd: number;
|
|
5068
5158
|
/** All model ids found in this provider's models array (for placement safety analysis) */
|
|
5069
5159
|
allModelIds: string[];
|
|
5070
5160
|
}
|
|
5071
5161
|
|
|
5162
|
+
interface ModelOverrideNodeLocation {
|
|
5163
|
+
providerObjectBrace: number;
|
|
5164
|
+
providerObjectEnd: number;
|
|
5165
|
+
modelOverridesObjectBrace: number;
|
|
5166
|
+
modelOverridesObjectEnd: number;
|
|
5167
|
+
modelOverrideObjectBrace: number;
|
|
5168
|
+
modelOverrideObjectEnd: number;
|
|
5169
|
+
modelOverrideCompatBrace: number;
|
|
5170
|
+
modelOverrideCompatEnd: number;
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
/** Locate a provider and optional modelOverrides entry without requiring models[]. */
|
|
5174
|
+
function locateModelOverrideInJsonc(
|
|
5175
|
+
text: string,
|
|
5176
|
+
providerLabel: string,
|
|
5177
|
+
modelId: string,
|
|
5178
|
+
): ModelOverrideNodeLocation | undefined {
|
|
5179
|
+
const clean = stripJsoncComments(text);
|
|
5180
|
+
const rootBrace = skipJsonWhitespace(clean, 0);
|
|
5181
|
+
if (clean[rootBrace] !== "{") return undefined;
|
|
5182
|
+
const providersKey = findJsonObjectKey(clean, rootBrace, "providers");
|
|
5183
|
+
if (!providersKey) return undefined;
|
|
5184
|
+
const providersBrace = skipJsonWhitespace(clean, providersKey.valueStart);
|
|
5185
|
+
if (clean[providersBrace] !== "{") return undefined;
|
|
5186
|
+
const providersEnd = findMatchingBracket(clean, providersBrace);
|
|
5187
|
+
if (providersEnd === undefined) return undefined;
|
|
5188
|
+
const providerKey = findJsonObjectKey(clean, providersBrace, providerLabel);
|
|
5189
|
+
if (!providerKey || providerKey.keyStart > providersEnd) return undefined;
|
|
5190
|
+
const providerObjectBrace = skipJsonWhitespace(clean, providerKey.valueStart);
|
|
5191
|
+
if (clean[providerObjectBrace] !== "{") return undefined;
|
|
5192
|
+
const providerObjectEnd = findMatchingBracket(clean, providerObjectBrace);
|
|
5193
|
+
if (providerObjectEnd === undefined || providerObjectEnd > providersEnd) return undefined;
|
|
5194
|
+
|
|
5195
|
+
let modelOverridesObjectBrace = -1;
|
|
5196
|
+
let modelOverridesObjectEnd = -1;
|
|
5197
|
+
let modelOverrideObjectBrace = -1;
|
|
5198
|
+
let modelOverrideObjectEnd = -1;
|
|
5199
|
+
let modelOverrideCompatBrace = -1;
|
|
5200
|
+
let modelOverrideCompatEnd = -1;
|
|
5201
|
+
const overridesKey = findJsonObjectKey(clean, providerObjectBrace, "modelOverrides");
|
|
5202
|
+
if (overridesKey && overridesKey.keyStart < providerObjectEnd) {
|
|
5203
|
+
const brace = skipJsonWhitespace(clean, overridesKey.valueStart);
|
|
5204
|
+
if (clean[brace] !== "{") return undefined;
|
|
5205
|
+
const end = findMatchingBracket(clean, brace);
|
|
5206
|
+
if (end === undefined || end > providerObjectEnd) return undefined;
|
|
5207
|
+
modelOverridesObjectBrace = brace;
|
|
5208
|
+
modelOverridesObjectEnd = end;
|
|
5209
|
+
|
|
5210
|
+
const overrideKey = findJsonObjectKey(clean, brace, modelId);
|
|
5211
|
+
if (overrideKey && overrideKey.keyStart < end) {
|
|
5212
|
+
const entryBrace = skipJsonWhitespace(clean, overrideKey.valueStart);
|
|
5213
|
+
if (clean[entryBrace] !== "{") return undefined;
|
|
5214
|
+
const entryEnd = findMatchingBracket(clean, entryBrace);
|
|
5215
|
+
if (entryEnd === undefined || entryEnd > end) return undefined;
|
|
5216
|
+
modelOverrideObjectBrace = entryBrace;
|
|
5217
|
+
modelOverrideObjectEnd = entryEnd;
|
|
5218
|
+
|
|
5219
|
+
const compatKey = findJsonObjectKey(clean, entryBrace, "compat");
|
|
5220
|
+
if (compatKey && compatKey.keyStart < entryEnd) {
|
|
5221
|
+
const compatBrace = skipJsonWhitespace(clean, compatKey.valueStart);
|
|
5222
|
+
if (clean[compatBrace] !== "{") return undefined;
|
|
5223
|
+
const compatEnd = findMatchingBracket(clean, compatBrace);
|
|
5224
|
+
if (compatEnd === undefined || compatEnd > entryEnd) return undefined;
|
|
5225
|
+
modelOverrideCompatBrace = compatBrace;
|
|
5226
|
+
modelOverrideCompatEnd = compatEnd;
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
return {
|
|
5232
|
+
providerObjectBrace,
|
|
5233
|
+
providerObjectEnd,
|
|
5234
|
+
modelOverridesObjectBrace,
|
|
5235
|
+
modelOverridesObjectEnd,
|
|
5236
|
+
modelOverrideObjectBrace,
|
|
5237
|
+
modelOverrideObjectEnd,
|
|
5238
|
+
modelOverrideCompatBrace,
|
|
5239
|
+
modelOverrideCompatEnd,
|
|
5240
|
+
};
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5072
5243
|
/**
|
|
5073
5244
|
* Locate the provider + model entry in raw JSONC text.
|
|
5074
5245
|
* Returns the positions needed for surgical insertion, or undefined on failure.
|
|
@@ -5121,6 +5292,12 @@ function locateModelInJsonc(
|
|
|
5121
5292
|
}
|
|
5122
5293
|
}
|
|
5123
5294
|
|
|
5295
|
+
const overrideLocation = locateModelOverrideInJsonc(text, providerLabel, modelId);
|
|
5296
|
+
const modelOverrideObjectBrace = overrideLocation?.modelOverrideObjectBrace ?? -1;
|
|
5297
|
+
const modelOverrideObjectEnd = overrideLocation?.modelOverrideObjectEnd ?? -1;
|
|
5298
|
+
const modelOverrideCompatBrace = overrideLocation?.modelOverrideCompatBrace ?? -1;
|
|
5299
|
+
const modelOverrideCompatEnd = overrideLocation?.modelOverrideCompatEnd ?? -1;
|
|
5300
|
+
|
|
5124
5301
|
const modelsKey = findJsonObjectKey(clean, providerBrace, "models");
|
|
5125
5302
|
if (!modelsKey || modelsKey.keyStart > providerEndBrace) return undefined;
|
|
5126
5303
|
|
|
@@ -5208,6 +5385,10 @@ function locateModelInJsonc(
|
|
|
5208
5385
|
providerObjectEnd: providerEndBrace,
|
|
5209
5386
|
providerCompatBrace,
|
|
5210
5387
|
providerCompatEnd,
|
|
5388
|
+
modelOverrideObjectBrace,
|
|
5389
|
+
modelOverrideObjectEnd,
|
|
5390
|
+
modelOverrideCompatBrace,
|
|
5391
|
+
modelOverrideCompatEnd,
|
|
5211
5392
|
allModelIds,
|
|
5212
5393
|
};
|
|
5213
5394
|
}
|
|
@@ -5217,7 +5398,7 @@ function locateModelInJsonc(
|
|
|
5217
5398
|
* `locateModelInJsonc` cannot find the target provider/model.
|
|
5218
5399
|
*/
|
|
5219
5400
|
type MissingEntryDiagnosis =
|
|
5220
|
-
| { scenario: "provider_missing"; providersEnd: number }
|
|
5401
|
+
| { scenario: "provider_missing"; providersBrace: number; providersEnd: number }
|
|
5221
5402
|
| { scenario: "model_missing"; modelsEnd: number; providerBrace: number; providerEndBrace: number }
|
|
5222
5403
|
| { scenario: "provider_without_models"; providerBrace: number; providerEndBrace: number };
|
|
5223
5404
|
|
|
@@ -5248,7 +5429,7 @@ function analyzeModelsJsonForMissingEntry(
|
|
|
5248
5429
|
|
|
5249
5430
|
const providerKey = findJsonObjectKey(clean, providersBrace, providerLabel);
|
|
5250
5431
|
if (!providerKey || providerKey.keyStart > providersEnd) {
|
|
5251
|
-
return { scenario: "provider_missing", providersEnd };
|
|
5432
|
+
return { scenario: "provider_missing", providersBrace, providersEnd };
|
|
5252
5433
|
}
|
|
5253
5434
|
|
|
5254
5435
|
// Provider exists. Check for a models array so we know where to append.
|
|
@@ -5286,20 +5467,126 @@ function formatMissingEntryManualSnippet(
|
|
|
5286
5467
|
): string {
|
|
5287
5468
|
const lines: string[] = [];
|
|
5288
5469
|
const sorted = Object.entries(compatKeys).sort(([a], [b]) => a.localeCompare(b));
|
|
5289
|
-
const compatItems = sorted.map(([k, v]) => `
|
|
5470
|
+
const compatItems = sorted.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`);
|
|
5290
5471
|
lines.push(`"${providerLabel}": {`);
|
|
5291
|
-
lines.push(` "
|
|
5292
|
-
lines.push(` {`);
|
|
5293
|
-
lines.push(` "id": ${JSON.stringify(modelId)},`);
|
|
5472
|
+
lines.push(` "modelOverrides": {`);
|
|
5473
|
+
lines.push(` ${JSON.stringify(modelId)}: {`);
|
|
5294
5474
|
lines.push(` "compat": {`);
|
|
5295
5475
|
lines.push(compatItems.join(",\n"));
|
|
5296
5476
|
lines.push(` }`);
|
|
5297
5477
|
lines.push(` }`);
|
|
5298
|
-
lines.push(`
|
|
5478
|
+
lines.push(` }`);
|
|
5299
5479
|
lines.push(` }`);
|
|
5300
5480
|
return lines.join("\n");
|
|
5301
5481
|
}
|
|
5302
5482
|
|
|
5483
|
+
/**
|
|
5484
|
+
* Insert or repair a modelOverrides entry without creating a custom models[]
|
|
5485
|
+
* definition. This is the safest representation for built-in/API-login models
|
|
5486
|
+
* and has the same highest precedence Pi applies at runtime.
|
|
5487
|
+
*/
|
|
5488
|
+
function composeModelOverrideInsertion(
|
|
5489
|
+
originalText: string,
|
|
5490
|
+
providerLabel: string,
|
|
5491
|
+
modelId: string,
|
|
5492
|
+
compatKeys: Record<string, unknown>,
|
|
5493
|
+
): { modifiedText: string; placementLabel: string } | undefined {
|
|
5494
|
+
const location = locateModelOverrideInJsonc(originalText, providerLabel, modelId);
|
|
5495
|
+
|
|
5496
|
+
if (location?.modelOverrideObjectBrace !== undefined && location.modelOverrideObjectBrace >= 0) {
|
|
5497
|
+
const modelLocation: ModelNodeLocation = {
|
|
5498
|
+
modelObjectBrace: -1,
|
|
5499
|
+
modelObjectEnd: -1,
|
|
5500
|
+
compatKeyStart: -1,
|
|
5501
|
+
compatObjectBrace: -1,
|
|
5502
|
+
compatObjectEnd: -1,
|
|
5503
|
+
indent: "",
|
|
5504
|
+
providerObjectBrace: location.providerObjectBrace,
|
|
5505
|
+
providerObjectEnd: location.providerObjectEnd,
|
|
5506
|
+
providerCompatBrace: -1,
|
|
5507
|
+
providerCompatEnd: -1,
|
|
5508
|
+
modelOverrideObjectBrace: location.modelOverrideObjectBrace,
|
|
5509
|
+
modelOverrideObjectEnd: location.modelOverrideObjectEnd,
|
|
5510
|
+
modelOverrideCompatBrace: location.modelOverrideCompatBrace,
|
|
5511
|
+
modelOverrideCompatEnd: location.modelOverrideCompatEnd,
|
|
5512
|
+
allModelIds: [],
|
|
5513
|
+
};
|
|
5514
|
+
return {
|
|
5515
|
+
modifiedText: composeFixInsertion(originalText, modelLocation, compatKeys, "modelOverride"),
|
|
5516
|
+
placementLabel: `providers["${providerLabel}"] -> modelOverrides["${modelId}"] -> compat`,
|
|
5517
|
+
};
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
const sortedEntries = Object.entries(compatKeys).sort(([a], [b]) => a.localeCompare(b));
|
|
5521
|
+
const formatOverrideEntry = (keyIndent: string, unit: string): string => {
|
|
5522
|
+
const propertyIndent = keyIndent + unit;
|
|
5523
|
+
const compatIndent = propertyIndent + unit;
|
|
5524
|
+
const compatLines = sortedEntries
|
|
5525
|
+
.map(([key, value]) => `${compatIndent}${JSON.stringify(key)}: ${JSON.stringify(value)}`)
|
|
5526
|
+
.join(",\n");
|
|
5527
|
+
return `${keyIndent}${JSON.stringify(modelId)}: {\n` +
|
|
5528
|
+
`${propertyIndent}"compat": {\n${compatLines}\n${propertyIndent}}\n${keyIndent}}`;
|
|
5529
|
+
};
|
|
5530
|
+
const previousTokenNeedsComma = (clean: string, closeBrace: number): boolean => {
|
|
5531
|
+
let pos = closeBrace - 1;
|
|
5532
|
+
while (pos >= 0 && isJsonWhitespace(clean[pos])) pos--;
|
|
5533
|
+
return clean[pos] !== "{" && clean[pos] !== ",";
|
|
5534
|
+
};
|
|
5535
|
+
|
|
5536
|
+
if (location) {
|
|
5537
|
+
const clean = stripJsoncComments(originalText);
|
|
5538
|
+
if (location.modelOverridesObjectBrace >= 0) {
|
|
5539
|
+
const containerIndent = lineIndentOf(originalText, location.modelOverridesObjectBrace);
|
|
5540
|
+
const keyIndent = deriveInnerIndent(
|
|
5541
|
+
originalText,
|
|
5542
|
+
location.modelOverridesObjectBrace,
|
|
5543
|
+
location.modelOverridesObjectEnd,
|
|
5544
|
+
);
|
|
5545
|
+
const unit = keyIndent.length > containerIndent.length
|
|
5546
|
+
? keyIndent.slice(containerIndent.length)
|
|
5547
|
+
: " ";
|
|
5548
|
+
const comma = previousTokenNeedsComma(clean, location.modelOverridesObjectEnd) ? "," : "";
|
|
5549
|
+
const insertion = `${comma}\n${formatOverrideEntry(keyIndent, unit)}\n${containerIndent}`;
|
|
5550
|
+
return {
|
|
5551
|
+
modifiedText: originalText.slice(0, location.modelOverridesObjectEnd) + insertion + originalText.slice(location.modelOverridesObjectEnd),
|
|
5552
|
+
placementLabel: `providers["${providerLabel}"] -> modelOverrides -> (new entry "${modelId}")`,
|
|
5553
|
+
};
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
const providerIndent = lineIndentOf(originalText, location.providerObjectBrace);
|
|
5557
|
+
const propertyIndent = deriveInnerIndent(originalText, location.providerObjectBrace, location.providerObjectEnd);
|
|
5558
|
+
const unit = propertyIndent.length > providerIndent.length
|
|
5559
|
+
? propertyIndent.slice(providerIndent.length)
|
|
5560
|
+
: " ";
|
|
5561
|
+
const entryIndent = propertyIndent + unit;
|
|
5562
|
+
const block = `\n${propertyIndent}"modelOverrides": {\n` +
|
|
5563
|
+
`${formatOverrideEntry(entryIndent, unit)}\n${propertyIndent}},`;
|
|
5564
|
+
return {
|
|
5565
|
+
modifiedText: originalText.slice(0, location.providerObjectBrace + 1) + block + originalText.slice(location.providerObjectBrace + 1),
|
|
5566
|
+
placementLabel: `providers["${providerLabel}"] -> (new modelOverrides entry "${modelId}")`,
|
|
5567
|
+
};
|
|
5568
|
+
}
|
|
5569
|
+
|
|
5570
|
+
const diagnosis = analyzeModelsJsonForMissingEntry(originalText, providerLabel, modelId);
|
|
5571
|
+
if (!diagnosis || diagnosis.scenario !== "provider_missing") return undefined;
|
|
5572
|
+
const clean = stripJsoncComments(originalText);
|
|
5573
|
+
const providersIndent = lineIndentOf(originalText, diagnosis.providersEnd);
|
|
5574
|
+
const providerIndent = deriveInnerIndent(originalText, diagnosis.providersBrace, diagnosis.providersEnd);
|
|
5575
|
+
const unit = providerIndent.length > providersIndent.length
|
|
5576
|
+
? providerIndent.slice(providersIndent.length)
|
|
5577
|
+
: " ";
|
|
5578
|
+
const overridesIndent = providerIndent + unit;
|
|
5579
|
+
const entryIndent = overridesIndent + unit;
|
|
5580
|
+
const comma = previousTokenNeedsComma(clean, diagnosis.providersEnd) ? "," : "";
|
|
5581
|
+
const block = `${comma}\n${providerIndent}${JSON.stringify(providerLabel)}: {\n` +
|
|
5582
|
+
`${overridesIndent}"modelOverrides": {\n${formatOverrideEntry(entryIndent, unit)}\n` +
|
|
5583
|
+
`${overridesIndent}}\n${providerIndent}}\n${providersIndent}`;
|
|
5584
|
+
return {
|
|
5585
|
+
modifiedText: originalText.slice(0, diagnosis.providersEnd) + block + originalText.slice(diagnosis.providersEnd),
|
|
5586
|
+
placementLabel: `providers -> (new modelOverrides-only entry "${providerLabel}/${modelId}")`,
|
|
5587
|
+
};
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5303
5590
|
/**
|
|
5304
5591
|
* Surgically insert the missing provider/model entry into the original
|
|
5305
5592
|
* JSONC text. Returns the modified text and placement descriptor.
|
|
@@ -5458,10 +5745,10 @@ function composeMissingEntryInsertion(
|
|
|
5458
5745
|
}
|
|
5459
5746
|
|
|
5460
5747
|
/**
|
|
5461
|
-
* Lightweight self-check for a newly inserted entry.
|
|
5748
|
+
* Lightweight self-check for a newly inserted model or modelOverrides entry.
|
|
5462
5749
|
* Parses the modified text as JSONC and confirms:
|
|
5463
|
-
* 1. The target
|
|
5464
|
-
* 2. Every compat key has the expected
|
|
5750
|
+
* 1. The target exists under models[] or modelOverrides.
|
|
5751
|
+
* 2. Every compat key has the expected effective three-layer value.
|
|
5465
5752
|
* Returns null on success, an error string on failure.
|
|
5466
5753
|
*/
|
|
5467
5754
|
function selfCheckMissingEntryInsertion(
|
|
@@ -5472,34 +5759,88 @@ function selfCheckMissingEntryInsertion(
|
|
|
5472
5759
|
compatKeys: Record<string, unknown>,
|
|
5473
5760
|
): string | null {
|
|
5474
5761
|
try {
|
|
5762
|
+
const origParsed = parseJsonc(originalText);
|
|
5475
5763
|
const modParsed = parseJsonc(modifiedText);
|
|
5476
5764
|
const providers = asRecord(asRecord(modParsed)?.providers);
|
|
5477
5765
|
if (!providers) return "Modified file: providers object missing or invalid";
|
|
5478
5766
|
const provider = asRecord(providers[providerLabel]);
|
|
5479
5767
|
if (!provider) return `Modified file: provider "${providerLabel}" not found`;
|
|
5480
5768
|
const models = provider.models;
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5769
|
+
const targetModel = Array.isArray(models)
|
|
5770
|
+
? models.find((m: unknown) => asRecord(m)?.id === modelId)
|
|
5771
|
+
: undefined;
|
|
5772
|
+
const targetOverride = asRecord(asRecord(provider.modelOverrides)?.[modelId]);
|
|
5773
|
+
if (!targetModel && !targetOverride) {
|
|
5774
|
+
return `Modified file: model or modelOverrides entry "${modelId}" not found in provider after insertion`;
|
|
5775
|
+
}
|
|
5485
5776
|
|
|
5486
|
-
// Validate effective merged compat
|
|
5487
|
-
const provCompatRaw = (provider as Record<string, unknown>).compat;
|
|
5488
|
-
const provCompat = (provCompatRaw && typeof provCompatRaw === "object" && !Array.isArray(provCompatRaw))
|
|
5489
|
-
? provCompatRaw as Record<string, unknown>
|
|
5490
|
-
: {};
|
|
5491
|
-
const mdlCompatRaw = (targetModel as Record<string, unknown>).compat;
|
|
5492
|
-
const mdlCompat = (mdlCompatRaw && typeof mdlCompatRaw === "object" && !Array.isArray(mdlCompatRaw))
|
|
5493
|
-
? mdlCompatRaw as Record<string, unknown>
|
|
5494
|
-
: {};
|
|
5495
|
-
const merged = { ...provCompat, ...mdlCompat };
|
|
5496
5777
|
for (const [k, v] of Object.entries(compatKeys)) {
|
|
5497
|
-
|
|
5498
|
-
if (
|
|
5778
|
+
const effective = resolveExplicitCompatValue(modParsed, providerLabel, modelId, k);
|
|
5779
|
+
if (!effective) return `Modified file: effective compat.${k} not found`;
|
|
5780
|
+
if (effective.value !== v) {
|
|
5781
|
+
return `Modified file: effective compat.${k} wrong value: expected ${JSON.stringify(v)}, got ${JSON.stringify(effective.value)} from ${effective.source}`;
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
// Normalize only the intended override edit back to its original shape,
|
|
5786
|
+
// then require the complete parsed structure to match. This catches data
|
|
5787
|
+
// loss while allowing legitimate shorter repairs such as false -> true.
|
|
5788
|
+
const normalized = JSON.parse(JSON.stringify(modParsed)) as Record<string, unknown>;
|
|
5789
|
+
const origProviders = asRecord(asRecord(origParsed)?.providers);
|
|
5790
|
+
const origProvider = asRecord(origProviders?.[providerLabel]);
|
|
5791
|
+
const normalizedProviders = asRecord(normalized.providers);
|
|
5792
|
+
if (!normalizedProviders) return "Modified file: normalized providers object missing";
|
|
5793
|
+
|
|
5794
|
+
if (!origProvider) {
|
|
5795
|
+
delete normalizedProviders[providerLabel];
|
|
5796
|
+
} else if (targetOverride) {
|
|
5797
|
+
const normalizedProvider = asRecord(normalizedProviders[providerLabel]);
|
|
5798
|
+
const normalizedOverrides = asRecord(normalizedProvider?.modelOverrides);
|
|
5799
|
+
const origOverrides = asRecord(origProvider.modelOverrides);
|
|
5800
|
+
const origOverride = asRecord(origOverrides?.[modelId]);
|
|
5801
|
+
|
|
5802
|
+
if (!normalizedProvider || !normalizedOverrides) {
|
|
5803
|
+
return `Modified file: modelOverrides["${modelId}"] missing during structure validation`;
|
|
5804
|
+
}
|
|
5805
|
+
if (!origOverride) {
|
|
5806
|
+
delete normalizedOverrides[modelId];
|
|
5807
|
+
if (!origOverrides) delete normalizedProvider.modelOverrides;
|
|
5808
|
+
} else {
|
|
5809
|
+
const normalizedOverride = asRecord(normalizedOverrides[modelId]);
|
|
5810
|
+
if (!normalizedOverride) return `Modified file: modelOverrides["${modelId}"] invalid`;
|
|
5811
|
+
const origCompat = asRecord(origOverride.compat);
|
|
5812
|
+
const normalizedCompat = asRecord(normalizedOverride.compat);
|
|
5813
|
+
if (!origCompat) {
|
|
5814
|
+
delete normalizedOverride.compat;
|
|
5815
|
+
} else {
|
|
5816
|
+
if (!normalizedCompat) return `Modified file: modelOverrides["${modelId}"].compat invalid`;
|
|
5817
|
+
for (const key of Object.keys(compatKeys)) {
|
|
5818
|
+
if (Object.prototype.hasOwnProperty.call(origCompat, key)) {
|
|
5819
|
+
normalizedCompat[key] = origCompat[key];
|
|
5820
|
+
} else {
|
|
5821
|
+
delete normalizedCompat[key];
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
} else {
|
|
5827
|
+
// Backward-compatible validation for the legacy models[] insertion
|
|
5828
|
+
// helper retained in __internals_for_tests.
|
|
5829
|
+
const normalizedProvider = asRecord(normalizedProviders[providerLabel]);
|
|
5830
|
+
const normalizedModels = normalizedProvider?.models;
|
|
5831
|
+
const origModels = origProvider.models;
|
|
5832
|
+
if (!normalizedProvider || !Array.isArray(normalizedModels)) {
|
|
5833
|
+
return `Modified file: provider "${providerLabel}".models missing during structure validation`;
|
|
5834
|
+
}
|
|
5835
|
+
normalizedProvider.models = normalizedModels.filter(
|
|
5836
|
+
(entry: unknown) => asRecord(entry)?.id !== modelId,
|
|
5837
|
+
);
|
|
5838
|
+
if (!Array.isArray(origModels)) delete normalizedProvider.models;
|
|
5499
5839
|
}
|
|
5500
5840
|
|
|
5501
|
-
if (
|
|
5502
|
-
return "Modified file:
|
|
5841
|
+
if (!deepEqualIgnoringKeys(normalized, origParsed, [])) {
|
|
5842
|
+
return "Modified file: original structure was altered (data loss detected)";
|
|
5843
|
+
}
|
|
5503
5844
|
|
|
5504
5845
|
const modClean = stripJsoncComments(modifiedText);
|
|
5505
5846
|
const rootStart = skipJsonWhitespace(modClean, 0);
|
|
@@ -5655,7 +5996,14 @@ function chooseFixPlacement(
|
|
|
5655
5996
|
compatKeys: Record<string, unknown>,
|
|
5656
5997
|
providerLabel: string,
|
|
5657
5998
|
forceModelLevel = false,
|
|
5658
|
-
): { placement: "provider" | "model"; reason: string } {
|
|
5999
|
+
): { placement: "provider" | "model" | "modelOverride"; reason: string } {
|
|
6000
|
+
if (location.modelOverrideObjectBrace >= 0) {
|
|
6001
|
+
return {
|
|
6002
|
+
placement: "modelOverride",
|
|
6003
|
+
reason: "an existing modelOverrides entry has Pi's highest precedence — repairing it directly",
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
6006
|
+
|
|
5659
6007
|
if (forceModelLevel) {
|
|
5660
6008
|
return {
|
|
5661
6009
|
placement: "model",
|
|
@@ -5689,12 +6037,24 @@ function composeFixInsertion(
|
|
|
5689
6037
|
original: string,
|
|
5690
6038
|
location: ModelNodeLocation,
|
|
5691
6039
|
compatKeys: Record<string, unknown>,
|
|
5692
|
-
placement: "provider" | "model" = "model",
|
|
6040
|
+
placement: "provider" | "model" | "modelOverride" = "model",
|
|
5693
6041
|
): string {
|
|
5694
6042
|
// Resolve the target compat object and its container based on placement.
|
|
5695
|
-
const targetCompatBrace = placement === "provider"
|
|
5696
|
-
|
|
5697
|
-
|
|
6043
|
+
const targetCompatBrace = placement === "provider"
|
|
6044
|
+
? location.providerCompatBrace
|
|
6045
|
+
: placement === "modelOverride"
|
|
6046
|
+
? location.modelOverrideCompatBrace
|
|
6047
|
+
: location.compatObjectBrace;
|
|
6048
|
+
const targetCompatEnd = placement === "provider"
|
|
6049
|
+
? location.providerCompatEnd
|
|
6050
|
+
: placement === "modelOverride"
|
|
6051
|
+
? location.modelOverrideCompatEnd
|
|
6052
|
+
: location.compatObjectEnd;
|
|
6053
|
+
const containerBrace = placement === "provider"
|
|
6054
|
+
? location.providerObjectBrace
|
|
6055
|
+
: placement === "modelOverride"
|
|
6056
|
+
? location.modelOverrideObjectBrace
|
|
6057
|
+
: location.modelObjectBrace;
|
|
5698
6058
|
|
|
5699
6059
|
// Helper: format key/value pairs as lines with the given indent,
|
|
5700
6060
|
// alphabetically sorted for stable previews and deterministic edits.
|
|
@@ -5796,7 +6156,7 @@ function selfCheckFix(
|
|
|
5796
6156
|
providerLabel: string,
|
|
5797
6157
|
modelId: string,
|
|
5798
6158
|
compatKeys: Record<string, unknown>,
|
|
5799
|
-
placement: "provider" | "model" = "model",
|
|
6159
|
+
placement: "provider" | "model" | "modelOverride" = "model",
|
|
5800
6160
|
): string | null {
|
|
5801
6161
|
try {
|
|
5802
6162
|
// Step 1: Parse both versions as JSONC (comments + trailing commas allowed).
|
|
@@ -5840,9 +6200,9 @@ function selfCheckFix(
|
|
|
5840
6200
|
return `Original file: provider/model "${providerLabel}/${modelId}" not found`;
|
|
5841
6201
|
}
|
|
5842
6202
|
|
|
5843
|
-
// Step 5: Compute the EFFECTIVE merged compat
|
|
5844
|
-
//
|
|
5845
|
-
//
|
|
6203
|
+
// Step 5: Compute the EFFECTIVE merged compat using Pi's precedence:
|
|
6204
|
+
// provider, custom model, then modelOverrides. The fix may have written any
|
|
6205
|
+
// level, so validation must check what Pi will actually use.
|
|
5846
6206
|
const provCompatRaw = (provider as Record<string, unknown>).compat;
|
|
5847
6207
|
const provCompat = (provCompatRaw && typeof provCompatRaw === 'object' && !Array.isArray(provCompatRaw))
|
|
5848
6208
|
? provCompatRaw as Record<string, unknown>
|
|
@@ -5852,7 +6212,13 @@ function selfCheckFix(
|
|
|
5852
6212
|
return `Modified file: model "${modelId}" compat is not an object`;
|
|
5853
6213
|
}
|
|
5854
6214
|
const mdlCompat = (modelCompatRaw ?? {}) as Record<string, unknown>;
|
|
5855
|
-
const
|
|
6215
|
+
const override = asRecord(asRecord(provider.modelOverrides)?.[modelId]);
|
|
6216
|
+
const overrideCompatRaw = override?.compat;
|
|
6217
|
+
if (overrideCompatRaw !== undefined && !asRecord(overrideCompatRaw)) {
|
|
6218
|
+
return `Modified file: modelOverrides["${modelId}"].compat is not an object`;
|
|
6219
|
+
}
|
|
6220
|
+
const overrideCompat = asRecord(overrideCompatRaw) ?? {};
|
|
6221
|
+
const mergedCompat: Record<string, unknown> = { ...provCompat, ...mdlCompat, ...overrideCompat };
|
|
5856
6222
|
|
|
5857
6223
|
// Step 6: Validate all inserted keys are effective in the merged compat
|
|
5858
6224
|
for (const [k, v] of Object.entries(compatKeys)) {
|
|
@@ -5900,7 +6266,8 @@ function selfCheckFix(
|
|
|
5900
6266
|
// the placement-resolved object's own compat may be exempt.
|
|
5901
6267
|
const mayRepairThisCompat =
|
|
5902
6268
|
(placement === "provider" && origObj === origProvider) ||
|
|
5903
|
-
(placement === "model" && origObj === origTargetModelRecord)
|
|
6269
|
+
(placement === "model" && origObj === origTargetModelRecord) ||
|
|
6270
|
+
(placement === "modelOverride" && origObj === asRecord(asRecord(origProvider.modelOverrides)?.[modelId]));
|
|
5904
6271
|
for (const ck of Object.keys(origCompat)) {
|
|
5905
6272
|
if (!(ck in modCompat)) return false;
|
|
5906
6273
|
// The fix may repair an existing wrong compat value (for example
|
|
@@ -6222,11 +6589,15 @@ export const __internals_for_tests = {
|
|
|
6222
6589
|
stripJsoncComments,
|
|
6223
6590
|
stripJsoncTrailingCommas,
|
|
6224
6591
|
parseJsonc,
|
|
6592
|
+
resolveExplicitCompatValue,
|
|
6593
|
+
hasExplicitLongRetentionOptInFromConfig,
|
|
6225
6594
|
locateModelInJsonc,
|
|
6595
|
+
locateModelOverrideInJsonc,
|
|
6226
6596
|
composeFixInsertion,
|
|
6227
6597
|
selfCheckFix,
|
|
6228
6598
|
analyzeModelsJsonForMissingEntry,
|
|
6229
6599
|
composeMissingEntryInsertion,
|
|
6600
|
+
composeModelOverrideInsertion,
|
|
6230
6601
|
selfCheckMissingEntryInsertion,
|
|
6231
6602
|
decideFixPlacement,
|
|
6232
6603
|
chooseFixPlacement,
|
|
@@ -6760,9 +7131,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6760
7131
|
|
|
6761
7132
|
/**
|
|
6762
7133
|
* Check whether a model has an EXPLICIT supportsLongCacheRetention: true
|
|
6763
|
-
* opt-in in models.json
|
|
6764
|
-
*
|
|
6765
|
-
* mergeCompat behaviour: model wins on conflicts).
|
|
7134
|
+
* opt-in in models.json. Precedence mirrors Pi's effective model config:
|
|
7135
|
+
* modelOverrides[model.id].compat, then models[].compat, then provider.compat.
|
|
6766
7136
|
*
|
|
6767
7137
|
* Returns true ONLY when the user explicitly opted in. Returns false for:
|
|
6768
7138
|
* - Explicit false (opt-out)
|
|
@@ -6776,31 +7146,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6776
7146
|
try {
|
|
6777
7147
|
const text = readFileSync(MODELS_JSON_PATH, "utf8");
|
|
6778
7148
|
const parsed = parseJsonc(text);
|
|
6779
|
-
|
|
6780
|
-
if (!providers) return false;
|
|
6781
|
-
|
|
6782
|
-
const prov = asRecord(providers[model.provider]);
|
|
6783
|
-
if (!prov) return false;
|
|
6784
|
-
|
|
6785
|
-
// Check model-level first (higher priority in Pi's merge logic)
|
|
6786
|
-
const models = prov.models;
|
|
6787
|
-
if (Array.isArray(models)) {
|
|
6788
|
-
const modelEntry = models.find(m => asRecord(m)?.id === model.id);
|
|
6789
|
-
if (modelEntry) {
|
|
6790
|
-
const modelCompat = asRecord(asRecord(modelEntry)?.compat);
|
|
6791
|
-
if (modelCompat?.supportsLongCacheRetention !== undefined) {
|
|
6792
|
-
return modelCompat.supportsLongCacheRetention === true;
|
|
6793
|
-
}
|
|
6794
|
-
}
|
|
6795
|
-
}
|
|
6796
|
-
|
|
6797
|
-
// Check provider-level
|
|
6798
|
-
const provCompat = asRecord(prov.compat);
|
|
6799
|
-
if (provCompat?.supportsLongCacheRetention !== undefined) {
|
|
6800
|
-
return provCompat.supportsLongCacheRetention === true;
|
|
6801
|
-
}
|
|
6802
|
-
|
|
6803
|
-
return false;
|
|
7149
|
+
return hasExplicitLongRetentionOptInFromConfig(parsed, model.provider, model.id);
|
|
6804
7150
|
} catch {
|
|
6805
7151
|
return false;
|
|
6806
7152
|
}
|
|
@@ -7318,8 +7664,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7318
7664
|
}
|
|
7319
7665
|
manualLines.push(
|
|
7320
7666
|
"",
|
|
7321
|
-
"
|
|
7322
|
-
`providers["${suggestion.providerLabel}"] ->
|
|
7667
|
+
"Add these compat keys at Pi's highest-precedence model override path:",
|
|
7668
|
+
`providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat:`,
|
|
7323
7669
|
formatCompatKeysForInsertion(suggestion.compatKeys),
|
|
7324
7670
|
);
|
|
7325
7671
|
if (snippet.length > 0) {
|
|
@@ -7353,11 +7699,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
7353
7699
|
if (!location) {
|
|
7354
7700
|
const diagnosis = analyzeModelsJsonForMissingEntry(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
7355
7701
|
if (diagnosis && cmdCtx.hasUI) {
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7702
|
+
const overrideLocation = locateModelOverrideInJsonc(
|
|
7703
|
+
originalText, suggestion.providerLabel, suggestion.modelId,
|
|
7704
|
+
);
|
|
7705
|
+
const repairsExistingOverride = (overrideLocation?.modelOverrideObjectBrace ?? -1) >= 0;
|
|
7706
|
+
// Prefer a modelOverrides edit when models[] has no target entry.
|
|
7707
|
+
const plan = composeModelOverrideInsertion(
|
|
7708
|
+
originalText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
7360
7709
|
);
|
|
7710
|
+
if (!plan) {
|
|
7711
|
+
cmdCtx.ui.notify(
|
|
7712
|
+
`❌ Could not safely locate a modelOverrides insertion point.\n` +
|
|
7713
|
+
`Falling back to manual guidance. No changes were made.`,
|
|
7714
|
+
"error",
|
|
7715
|
+
);
|
|
7716
|
+
} else {
|
|
7361
7717
|
const checkError = selfCheckMissingEntryInsertion(
|
|
7362
7718
|
originalText, plan.modifiedText,
|
|
7363
7719
|
suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
@@ -7382,7 +7738,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7382
7738
|
keysPreview,
|
|
7383
7739
|
``,
|
|
7384
7740
|
`⚠️ Risk notice:`,
|
|
7385
|
-
|
|
7741
|
+
repairsExistingOverride
|
|
7742
|
+
? ` 1. This updates the existing modelOverrides entry for "${suggestion.modelId}". Existing auth is not affected.`
|
|
7743
|
+
: ` 1. This creates a modelOverrides entry in models.json. Existing auth (e.g. login API tokens) is not affected.`,
|
|
7386
7744
|
` 2. A timestamped backup will be written to: ${backupPath}`,
|
|
7387
7745
|
` 3. You must run /reload or restart Pi for the change to take effect.`,
|
|
7388
7746
|
` 4. If the file contains comments or unusual formatting, please verify the result after write.`,
|
|
@@ -7397,7 +7755,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
7397
7755
|
);
|
|
7398
7756
|
}
|
|
7399
7757
|
previewLines.push("", `Apply these changes?`);
|
|
7400
|
-
const confirmed = await cmdCtx.ui.confirm(
|
|
7758
|
+
const confirmed = await cmdCtx.ui.confirm(
|
|
7759
|
+
repairsExistingOverride ? "Cache Optimizer — Fix (model override)" : "Cache Optimizer — Fix (new override)",
|
|
7760
|
+
previewLines.join("\n"),
|
|
7761
|
+
);
|
|
7401
7762
|
if (confirmed) {
|
|
7402
7763
|
try {
|
|
7403
7764
|
await copyFile(MODELS_JSON_PATH, backupPath);
|
|
@@ -7436,6 +7797,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7436
7797
|
cmdCtx.ui.notify("No changes were made. Canceled by user.", "info");
|
|
7437
7798
|
return;
|
|
7438
7799
|
}
|
|
7800
|
+
}
|
|
7439
7801
|
}
|
|
7440
7802
|
|
|
7441
7803
|
// Non-interactive or no diagnosis: show manual guidance.
|
|
@@ -7448,7 +7810,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7448
7810
|
`❌ Could not locate model "${suggestion.modelId}" or provider "${suggestion.providerLabel}" in ${getModelsJsonDisplayPath()}.`,
|
|
7449
7811
|
"",
|
|
7450
7812
|
"Providers that were added via Pi /login API (e.g. opencode go) do not have",
|
|
7451
|
-
"entries in models.json. You can create a minimal
|
|
7813
|
+
"entries in models.json. You can create a minimal modelOverrides entry by hand:",
|
|
7452
7814
|
);
|
|
7453
7815
|
} else if (diagnosis.scenario === "provider_missing") {
|
|
7454
7816
|
adviceLines.push(
|
|
@@ -7463,7 +7825,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7463
7825
|
`ℹ️ Model "${suggestion.modelId}" was not found in ${getModelsJsonDisplayPath()}`,
|
|
7464
7826
|
`under providers["${suggestion.providerLabel}"].`,
|
|
7465
7827
|
"",
|
|
7466
|
-
"Add the following entry
|
|
7828
|
+
"Add the following modelOverrides entry (keep existing auth):",
|
|
7467
7829
|
);
|
|
7468
7830
|
}
|
|
7469
7831
|
adviceLines.push("", snippet, "", "Then save and run /reload.");
|
|
@@ -7496,11 +7858,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
7496
7858
|
// Build preview snippet as copyable JSON (the surgical editor will
|
|
7497
7859
|
// insert or repair these exact compat key/value pairs).
|
|
7498
7860
|
const keysPreview = JSON.stringify(suggestion.compatKeys, null, 2);
|
|
7499
|
-
const targetHasCompat = decision.placement === "provider"
|
|
7861
|
+
const targetHasCompat = decision.placement === "provider"
|
|
7862
|
+
? location.providerCompatBrace >= 0
|
|
7863
|
+
: decision.placement === "modelOverride"
|
|
7864
|
+
? location.modelOverrideCompatBrace >= 0
|
|
7865
|
+
: location.compatObjectBrace >= 0;
|
|
7500
7866
|
const placementDesc = targetHasCompat ? `existing "compat" object` : `new "compat" object`;
|
|
7501
7867
|
const locationDesc = decision.placement === "provider"
|
|
7502
7868
|
? `providers["${suggestion.providerLabel}"] -> compat (provider level, ${placementDesc})`
|
|
7503
|
-
:
|
|
7869
|
+
: decision.placement === "modelOverride"
|
|
7870
|
+
? `providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat (${placementDesc})`
|
|
7871
|
+
: `providers["${suggestion.providerLabel}"] -> models -> "${suggestion.modelId}" -> compat (model level, ${placementDesc})`;
|
|
7504
7872
|
|
|
7505
7873
|
const ts = backupTimestamp();
|
|
7506
7874
|
const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
|
|
@@ -7668,26 +8036,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
7668
8036
|
}
|
|
7669
8037
|
|
|
7670
8038
|
const location = locateModelInJsonc(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
7671
|
-
|
|
8039
|
+
const menuDecision = location
|
|
8040
|
+
? chooseFixPlacement(
|
|
8041
|
+
originalText,
|
|
8042
|
+
location,
|
|
8043
|
+
suggestion.compatKeys,
|
|
8044
|
+
suggestion.providerLabel,
|
|
8045
|
+
suggestion.forceModelLevel,
|
|
8046
|
+
)
|
|
8047
|
+
: undefined;
|
|
8048
|
+
const overridePlan = location
|
|
8049
|
+
? undefined
|
|
8050
|
+
: composeModelOverrideInsertion(
|
|
8051
|
+
originalText,
|
|
8052
|
+
suggestion.providerLabel,
|
|
8053
|
+
suggestion.modelId,
|
|
8054
|
+
suggestion.compatKeys,
|
|
8055
|
+
);
|
|
8056
|
+
if (!location && !overridePlan) {
|
|
7672
8057
|
cmdCtx.ui.notify(
|
|
7673
|
-
`❌ Could not locate
|
|
7674
|
-
`Manual edit required
|
|
7675
|
-
`${formatCompatKeysForInsertion(suggestion.compatKeys)}\n` +
|
|
8058
|
+
`❌ Could not safely locate a modelOverrides insertion point in ${getModelsJsonDisplayPath()}.\n` +
|
|
8059
|
+
`Manual edit required:\n${formatMissingEntryManualSnippet(suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys)}\n` +
|
|
7676
8060
|
`Then run /reload.`,
|
|
7677
8061
|
"warning",
|
|
7678
8062
|
);
|
|
7679
8063
|
return;
|
|
7680
8064
|
}
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
suggestion.compatKeys,
|
|
7686
|
-
suggestion.providerLabel,
|
|
7687
|
-
suggestion.forceModelLevel,
|
|
7688
|
-
);
|
|
7689
|
-
const modifiedText = composeFixInsertion(originalText, location, suggestion.compatKeys, menuDecision.placement);
|
|
7690
|
-
const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement);
|
|
8065
|
+
const modifiedText = location && menuDecision
|
|
8066
|
+
? composeFixInsertion(originalText, location, suggestion.compatKeys, menuDecision.placement)
|
|
8067
|
+
: overridePlan!.modifiedText;
|
|
8068
|
+
const checkError = location && menuDecision
|
|
8069
|
+
? selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement)
|
|
8070
|
+
: selfCheckMissingEntryInsertion(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
7691
8071
|
if (checkError !== null) {
|
|
7692
8072
|
cmdCtx.ui.notify(`❌ Self-check failed: ${checkError}\nNo changes made.`, "error");
|
|
7693
8073
|
return;
|
|
@@ -7697,17 +8077,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
7697
8077
|
const ts = backupTimestamp();
|
|
7698
8078
|
const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
|
|
7699
8079
|
|
|
7700
|
-
const menuLocationDesc = menuDecision
|
|
8080
|
+
const menuLocationDesc = overridePlan?.placementLabel ?? (menuDecision?.placement === "provider"
|
|
7701
8081
|
? `providers["${suggestion.providerLabel}"] -> compat (provider level)`
|
|
7702
|
-
:
|
|
7703
|
-
|
|
7704
|
-
|
|
8082
|
+
: menuDecision?.placement === "modelOverride"
|
|
8083
|
+
? `providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat`
|
|
8084
|
+
: `providers["${suggestion.providerLabel}"] -> models -> "${suggestion.modelId}" -> compat (model level)`);
|
|
8085
|
+
const menuScopeRiskLine = menuDecision?.placement === "provider"
|
|
8086
|
+
? ` 1. This change applies to ALL ${location?.allModelIds.length || 1} model(s) in the "${suggestion.providerLabel}" provider, across all sessions.`
|
|
7705
8087
|
: ` 1. This change affects ALL sessions using the "${suggestion.providerLabel}" provider/channel (scoped to model "${suggestion.modelId}").`;
|
|
7706
8088
|
|
|
7707
8089
|
const previewLines = [
|
|
7708
8090
|
`📝 Preview of changes to ${getModelsJsonDisplayPath()}:`,
|
|
7709
8091
|
`Location: ${menuLocationDesc}`,
|
|
7710
|
-
`Placement: ${menuDecision.placement} level — ${menuDecision.reason}`,
|
|
8092
|
+
`Placement: ${menuDecision ? `${menuDecision.placement} level — ${menuDecision.reason}` : "modelOverrides — built-in/API-login-safe model override"}`,
|
|
7711
8093
|
`Compat JSON to write:`,
|
|
7712
8094
|
keysPreview,
|
|
7713
8095
|
``,
|
|
@@ -7733,7 +8115,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7733
8115
|
await rename(tempPath, MODELS_JSON_PATH);
|
|
7734
8116
|
|
|
7735
8117
|
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
7736
|
-
const postCheck =
|
|
8118
|
+
const postCheck = location && menuDecision
|
|
8119
|
+
? selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement)
|
|
8120
|
+
: selfCheckMissingEntryInsertion(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
7737
8121
|
if (postCheck !== null) {
|
|
7738
8122
|
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
7739
8123
|
cmdCtx.ui.notify(`❌ Post-write check failed: ${postCheck}\nBackup restored.`, "error");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cache-optimizer",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.25",
|
|
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",
|
|
@@ -24,6 +24,21 @@
|
|
|
24
24
|
],
|
|
25
25
|
"image": "https://img.shields.io/badge/Pi-Cache%20Optimizer-4A90D9"
|
|
26
26
|
},
|
|
27
|
+
"imports": {
|
|
28
|
+
"#extension": "./index.ts"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --import jiti/register tests/review-findings.test.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit --pretty false",
|
|
33
|
+
"check:diff": "git diff --check",
|
|
34
|
+
"check:pack": "npm pack --dry-run --cache node_modules/.cache/npm",
|
|
35
|
+
"check": "npm run typecheck && npm test && npm run check:diff && npm run check:pack"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"jiti": "^2.7.0",
|
|
40
|
+
"typescript": "^5.8.3"
|
|
41
|
+
},
|
|
27
42
|
"peerDependencies": {
|
|
28
43
|
"@earendil-works/pi-coding-agent": "*"
|
|
29
44
|
},
|