pi-cache-optimizer 2.6.23 → 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 +6 -4
- package/README.zh-CN.md +6 -4
- package/index.ts +662 -160
- package/package.json +16 -1
package/index.ts
CHANGED
|
@@ -104,11 +104,37 @@ const NO_PROMPT_REWRITE_ENV = "PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE";
|
|
|
104
104
|
const PI_ROUTING_REGISTRY_SYMBOL = Symbol.for("pi.routing.registry.v1");
|
|
105
105
|
const PI_CACHE_HINTS_SYMBOL = Symbol.for("pi.cache.hints.v1");
|
|
106
106
|
const PI_CACHE_HINTS_OWNER_SYMBOL = Symbol.for("pi.cache.optimizer.hints-owner.v1");
|
|
107
|
+
const ANTHROPIC_TTL_FALLBACK_SYMBOL = Symbol.for("pi.cache.optimizer.anthropic-ttl-fallback.v1");
|
|
108
|
+
|
|
109
|
+
type AnthropicTtlFallbackStateV1 = {
|
|
110
|
+
version: 1;
|
|
111
|
+
modelKeys: Set<string>;
|
|
112
|
+
warnedModelKeys: Set<string>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function getAnthropicTtlFallbackState(): AnthropicTtlFallbackStateV1 {
|
|
116
|
+
const globals = globalThis as Record<symbol, unknown>;
|
|
117
|
+
const existing = globals[ANTHROPIC_TTL_FALLBACK_SYMBOL] as Partial<AnthropicTtlFallbackStateV1> | undefined;
|
|
118
|
+
if (
|
|
119
|
+
existing?.version === 1 &&
|
|
120
|
+
existing.modelKeys instanceof Set &&
|
|
121
|
+
existing.warnedModelKeys instanceof Set
|
|
122
|
+
) {
|
|
123
|
+
return existing as AnthropicTtlFallbackStateV1;
|
|
124
|
+
}
|
|
125
|
+
const state: AnthropicTtlFallbackStateV1 = {
|
|
126
|
+
version: 1,
|
|
127
|
+
modelKeys: new Set<string>(),
|
|
128
|
+
warnedModelKeys: new Set<string>(),
|
|
129
|
+
};
|
|
130
|
+
globals[ANTHROPIC_TTL_FALLBACK_SYMBOL] = state;
|
|
131
|
+
return state;
|
|
132
|
+
}
|
|
107
133
|
|
|
108
134
|
let runtimeOptimizerEnabled = true;
|
|
109
135
|
|
|
110
|
-
// WORM-flag: if optimizeSystemPrompt ever detects that
|
|
111
|
-
//
|
|
136
|
+
// WORM-flag: if optimizeSystemPrompt ever detects that candidate extraction
|
|
137
|
+
// has accidentally truncated a structural marker (any XML tag or
|
|
112
138
|
// HTML comment boundary marker present in the original prompt), we flip
|
|
113
139
|
// this. publishStatus reads it once, appends a footer warning, then
|
|
114
140
|
// resets it. The flag surface is kept separate from the regular
|
|
@@ -135,12 +161,10 @@ const SKILL_COMPRESSION_MIN_COUNT = 4;
|
|
|
135
161
|
|
|
136
162
|
// Minimum trimmed length for a candidate to qualify as a stable-prefix "part".
|
|
137
163
|
//
|
|
138
|
-
// `optimizeSystemPrompt` removes each accepted candidate
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// non-deterministic dynamic remainder per request. Both behaviors poison the
|
|
143
|
-
// 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.
|
|
144
168
|
//
|
|
145
169
|
// The threshold also caps the upstream string-vs-array regression we saw with
|
|
146
170
|
// trellis 0.5.16 / 0.6.0-beta.17 (subagent tool registration passing
|
|
@@ -618,7 +642,7 @@ function stripSessionOverviewChurn(prompt: string): string {
|
|
|
618
642
|
* Extract structural markers from a prompt for the integrity guard.
|
|
619
643
|
*
|
|
620
644
|
* The guard runs in `optimizeSystemPrompt` to catch cases where the
|
|
621
|
-
*
|
|
645
|
+
* candidate extraction accidentally eats text inside
|
|
622
646
|
* an extension-injected structural block (e.g., trellis
|
|
623
647
|
* `<workflow-state>`, a hypothetical `<task-tracker>`, or AGENTS.md
|
|
624
648
|
* `<!-- TRELLIS:START -->` markers). When the original prompt contains
|
|
@@ -678,16 +702,43 @@ function optimizeSystemPrompt(
|
|
|
678
702
|
const seen = new Set<string>();
|
|
679
703
|
let rest = original;
|
|
680
704
|
|
|
681
|
-
//
|
|
682
|
-
//
|
|
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[] = [];
|
|
683
710
|
for (const candidate of buildStableCandidates(opts)) {
|
|
684
711
|
const part = candidate.trim();
|
|
685
|
-
if (!part || part.length < MIN_STABLE_CANDIDATE_LENGTH) continue;
|
|
686
|
-
|
|
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;
|
|
687
739
|
|
|
688
740
|
stableParts.push(part);
|
|
689
|
-
|
|
690
|
-
rest = rest.replace(part, "");
|
|
741
|
+
rest = rest.slice(0, firstOccurrence) + rest.slice(firstOccurrence + part.length);
|
|
691
742
|
}
|
|
692
743
|
|
|
693
744
|
const stablePrefix = stableParts.join("\n\n");
|
|
@@ -705,8 +756,8 @@ function optimizeSystemPrompt(
|
|
|
705
756
|
|
|
706
757
|
// Sanity check: scan ALL structural markers (XML tags + HTML comment
|
|
707
758
|
// boundary markers) in the original and verify each one survives the
|
|
708
|
-
// reorder. If any marker drops,
|
|
709
|
-
//
|
|
759
|
+
// reorder. If any marker drops, candidate extraction ate something it
|
|
760
|
+
// shouldn't have — fall back to the original
|
|
710
761
|
// prompt and flag the footer warning. This is provider-agnostic and
|
|
711
762
|
// extension-agnostic: trellis `<workflow-state>`, a hypothetical
|
|
712
763
|
// `<task-tracker>`, AGENTS.md `<!-- TRELLIS:START -->`, etc., are all
|
|
@@ -1234,6 +1285,28 @@ function collectAnthropicCacheControlsInWireOrder(payload: unknown): UnknownReco
|
|
|
1234
1285
|
* If the final serialized payload contains a short → long transition, downgrade
|
|
1235
1286
|
* all 1h breakpoints to the default 5m so the request remains valid.
|
|
1236
1287
|
*/
|
|
1288
|
+
function downgradeAnthropicLongCacheControls(payload: unknown): boolean {
|
|
1289
|
+
let changed = false;
|
|
1290
|
+
for (const control of collectAnthropicCacheControlsInWireOrder(payload)) {
|
|
1291
|
+
if (control.ttl === "1h") {
|
|
1292
|
+
delete control.ttl;
|
|
1293
|
+
changed = true;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return changed;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function hasAnthropicCacheTtlOrderError(message: unknown): boolean {
|
|
1300
|
+
const record = getAssistantRecord(message);
|
|
1301
|
+
if (record?.stopReason !== "error" || typeof record.errorMessage !== "string") return false;
|
|
1302
|
+
|
|
1303
|
+
const error = lower(record.errorMessage);
|
|
1304
|
+
return error.includes("cache_control") &&
|
|
1305
|
+
error.includes("ttl='1h'") &&
|
|
1306
|
+
error.includes("ttl='5m'") &&
|
|
1307
|
+
error.includes("must not come after");
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1237
1310
|
function normalizeAnthropicCacheControlTtlOrder(payload: unknown): boolean {
|
|
1238
1311
|
const controls = collectAnthropicCacheControlsInWireOrder(payload);
|
|
1239
1312
|
let seenShort = false;
|
|
@@ -1250,10 +1323,7 @@ function normalizeAnthropicCacheControlTtlOrder(payload: unknown): boolean {
|
|
|
1250
1323
|
}
|
|
1251
1324
|
if (!hasInvalidLongAfterShort) return false;
|
|
1252
1325
|
|
|
1253
|
-
|
|
1254
|
-
if (control.ttl === "1h") delete control.ttl;
|
|
1255
|
-
}
|
|
1256
|
-
return true;
|
|
1326
|
+
return downgradeAnthropicLongCacheControls(payload);
|
|
1257
1327
|
}
|
|
1258
1328
|
|
|
1259
1329
|
function isResponsesPromptRewriteBypassApi(api: unknown): boolean {
|
|
@@ -4374,7 +4444,7 @@ function getCompatCheckNotApplicableLines(model: PiModel): string[] {
|
|
|
4374
4444
|
return ["ℹ️ Compat check not applicable for this model."];
|
|
4375
4445
|
}
|
|
4376
4446
|
|
|
4377
|
-
function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400?: boolean; sessionAffinity403?: boolean; openAISdkHeader403?: boolean } = {}): string {
|
|
4447
|
+
function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400?: boolean; anthropicTtlOrderError?: boolean; sessionAffinity403?: boolean; openAISdkHeader403?: boolean } = {}): string {
|
|
4378
4448
|
const lines: string[] = [];
|
|
4379
4449
|
lines.push(`Provider: ${model.provider}`);
|
|
4380
4450
|
lines.push(`Model: ${model.id}`);
|
|
@@ -4433,6 +4503,13 @@ function buildDoctorDiagnosis(model: PiModel, options: { promptCacheRetention400
|
|
|
4433
4503
|
}
|
|
4434
4504
|
}
|
|
4435
4505
|
|
|
4506
|
+
if (options.anthropicTtlOrderError) {
|
|
4507
|
+
lines.push("");
|
|
4508
|
+
lines.push("⚠️ An Anthropic cache-control TTL ordering error was observed for this model.");
|
|
4509
|
+
lines.push(" Runtime requests now fall back from 1h to the default 5-minute cache TTL.");
|
|
4510
|
+
lines.push(` Run /cache-optimizer fix to set model-level supportsLongCacheRetention: false in ${getModelsJsonDisplayPath()}.`);
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4436
4513
|
// ── Session affinity 403 diagnostics ──
|
|
4437
4514
|
// Advises the user when sendSessionAffinityHeaders is enabled, since some
|
|
4438
4515
|
// third-party proxies / CDNs / WAFs block Pi's custom session-affinity HTTP
|
|
@@ -4858,6 +4935,8 @@ interface FixSuggestion {
|
|
|
4858
4935
|
providerLabel: string;
|
|
4859
4936
|
modelId: string;
|
|
4860
4937
|
compatKeys: Record<string, unknown>;
|
|
4938
|
+
/** Runtime-observed failures must never broaden a model-specific fix. */
|
|
4939
|
+
forceModelLevel?: boolean;
|
|
4861
4940
|
}
|
|
4862
4941
|
|
|
4863
4942
|
/**
|
|
@@ -4986,6 +5065,63 @@ function parseJsonc(text: string): unknown {
|
|
|
4986
5065
|
return JSON.parse(stripJsoncTrailingCommas(stripJsoncComments(text)));
|
|
4987
5066
|
}
|
|
4988
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
|
+
|
|
4989
5125
|
/**
|
|
4990
5126
|
* JSONC scanner: locate the provider block and model entry in models.json text.
|
|
4991
5127
|
* Returns the byte offsets for surgical insertion, or undefined if ambiguous.
|
|
@@ -5011,10 +5147,99 @@ interface ModelNodeLocation {
|
|
|
5011
5147
|
providerCompatBrace: number;
|
|
5012
5148
|
/** Offset of the provider-level compat object's closing `}`, or -1 if absent */
|
|
5013
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;
|
|
5014
5158
|
/** All model ids found in this provider's models array (for placement safety analysis) */
|
|
5015
5159
|
allModelIds: string[];
|
|
5016
5160
|
}
|
|
5017
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
|
+
|
|
5018
5243
|
/**
|
|
5019
5244
|
* Locate the provider + model entry in raw JSONC text.
|
|
5020
5245
|
* Returns the positions needed for surgical insertion, or undefined on failure.
|
|
@@ -5067,6 +5292,12 @@ function locateModelInJsonc(
|
|
|
5067
5292
|
}
|
|
5068
5293
|
}
|
|
5069
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
|
+
|
|
5070
5301
|
const modelsKey = findJsonObjectKey(clean, providerBrace, "models");
|
|
5071
5302
|
if (!modelsKey || modelsKey.keyStart > providerEndBrace) return undefined;
|
|
5072
5303
|
|
|
@@ -5154,6 +5385,10 @@ function locateModelInJsonc(
|
|
|
5154
5385
|
providerObjectEnd: providerEndBrace,
|
|
5155
5386
|
providerCompatBrace,
|
|
5156
5387
|
providerCompatEnd,
|
|
5388
|
+
modelOverrideObjectBrace,
|
|
5389
|
+
modelOverrideObjectEnd,
|
|
5390
|
+
modelOverrideCompatBrace,
|
|
5391
|
+
modelOverrideCompatEnd,
|
|
5157
5392
|
allModelIds,
|
|
5158
5393
|
};
|
|
5159
5394
|
}
|
|
@@ -5163,7 +5398,7 @@ function locateModelInJsonc(
|
|
|
5163
5398
|
* `locateModelInJsonc` cannot find the target provider/model.
|
|
5164
5399
|
*/
|
|
5165
5400
|
type MissingEntryDiagnosis =
|
|
5166
|
-
| { scenario: "provider_missing"; providersEnd: number }
|
|
5401
|
+
| { scenario: "provider_missing"; providersBrace: number; providersEnd: number }
|
|
5167
5402
|
| { scenario: "model_missing"; modelsEnd: number; providerBrace: number; providerEndBrace: number }
|
|
5168
5403
|
| { scenario: "provider_without_models"; providerBrace: number; providerEndBrace: number };
|
|
5169
5404
|
|
|
@@ -5194,7 +5429,7 @@ function analyzeModelsJsonForMissingEntry(
|
|
|
5194
5429
|
|
|
5195
5430
|
const providerKey = findJsonObjectKey(clean, providersBrace, providerLabel);
|
|
5196
5431
|
if (!providerKey || providerKey.keyStart > providersEnd) {
|
|
5197
|
-
return { scenario: "provider_missing", providersEnd };
|
|
5432
|
+
return { scenario: "provider_missing", providersBrace, providersEnd };
|
|
5198
5433
|
}
|
|
5199
5434
|
|
|
5200
5435
|
// Provider exists. Check for a models array so we know where to append.
|
|
@@ -5232,20 +5467,126 @@ function formatMissingEntryManualSnippet(
|
|
|
5232
5467
|
): string {
|
|
5233
5468
|
const lines: string[] = [];
|
|
5234
5469
|
const sorted = Object.entries(compatKeys).sort(([a], [b]) => a.localeCompare(b));
|
|
5235
|
-
const compatItems = sorted.map(([k, v]) => `
|
|
5470
|
+
const compatItems = sorted.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`);
|
|
5236
5471
|
lines.push(`"${providerLabel}": {`);
|
|
5237
|
-
lines.push(` "
|
|
5238
|
-
lines.push(` {`);
|
|
5239
|
-
lines.push(` "id": ${JSON.stringify(modelId)},`);
|
|
5472
|
+
lines.push(` "modelOverrides": {`);
|
|
5473
|
+
lines.push(` ${JSON.stringify(modelId)}: {`);
|
|
5240
5474
|
lines.push(` "compat": {`);
|
|
5241
5475
|
lines.push(compatItems.join(",\n"));
|
|
5242
5476
|
lines.push(` }`);
|
|
5243
5477
|
lines.push(` }`);
|
|
5244
|
-
lines.push(`
|
|
5478
|
+
lines.push(` }`);
|
|
5245
5479
|
lines.push(` }`);
|
|
5246
5480
|
return lines.join("\n");
|
|
5247
5481
|
}
|
|
5248
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
|
+
|
|
5249
5590
|
/**
|
|
5250
5591
|
* Surgically insert the missing provider/model entry into the original
|
|
5251
5592
|
* JSONC text. Returns the modified text and placement descriptor.
|
|
@@ -5404,10 +5745,10 @@ function composeMissingEntryInsertion(
|
|
|
5404
5745
|
}
|
|
5405
5746
|
|
|
5406
5747
|
/**
|
|
5407
|
-
* Lightweight self-check for a newly inserted entry.
|
|
5748
|
+
* Lightweight self-check for a newly inserted model or modelOverrides entry.
|
|
5408
5749
|
* Parses the modified text as JSONC and confirms:
|
|
5409
|
-
* 1. The target
|
|
5410
|
-
* 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.
|
|
5411
5752
|
* Returns null on success, an error string on failure.
|
|
5412
5753
|
*/
|
|
5413
5754
|
function selfCheckMissingEntryInsertion(
|
|
@@ -5418,34 +5759,88 @@ function selfCheckMissingEntryInsertion(
|
|
|
5418
5759
|
compatKeys: Record<string, unknown>,
|
|
5419
5760
|
): string | null {
|
|
5420
5761
|
try {
|
|
5762
|
+
const origParsed = parseJsonc(originalText);
|
|
5421
5763
|
const modParsed = parseJsonc(modifiedText);
|
|
5422
5764
|
const providers = asRecord(asRecord(modParsed)?.providers);
|
|
5423
5765
|
if (!providers) return "Modified file: providers object missing or invalid";
|
|
5424
5766
|
const provider = asRecord(providers[providerLabel]);
|
|
5425
5767
|
if (!provider) return `Modified file: provider "${providerLabel}" not found`;
|
|
5426
5768
|
const models = provider.models;
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
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
|
+
}
|
|
5431
5776
|
|
|
5432
|
-
// Validate effective merged compat
|
|
5433
|
-
const provCompatRaw = (provider as Record<string, unknown>).compat;
|
|
5434
|
-
const provCompat = (provCompatRaw && typeof provCompatRaw === "object" && !Array.isArray(provCompatRaw))
|
|
5435
|
-
? provCompatRaw as Record<string, unknown>
|
|
5436
|
-
: {};
|
|
5437
|
-
const mdlCompatRaw = (targetModel as Record<string, unknown>).compat;
|
|
5438
|
-
const mdlCompat = (mdlCompatRaw && typeof mdlCompatRaw === "object" && !Array.isArray(mdlCompatRaw))
|
|
5439
|
-
? mdlCompatRaw as Record<string, unknown>
|
|
5440
|
-
: {};
|
|
5441
|
-
const merged = { ...provCompat, ...mdlCompat };
|
|
5442
5777
|
for (const [k, v] of Object.entries(compatKeys)) {
|
|
5443
|
-
|
|
5444
|
-
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;
|
|
5445
5839
|
}
|
|
5446
5840
|
|
|
5447
|
-
if (
|
|
5448
|
-
return "Modified file:
|
|
5841
|
+
if (!deepEqualIgnoringKeys(normalized, origParsed, [])) {
|
|
5842
|
+
return "Modified file: original structure was altered (data loss detected)";
|
|
5843
|
+
}
|
|
5449
5844
|
|
|
5450
5845
|
const modClean = stripJsoncComments(modifiedText);
|
|
5451
5846
|
const rootStart = skipJsonWhitespace(modClean, 0);
|
|
@@ -5519,8 +5914,9 @@ function syntheticModelForId(providerLabel: string, id: string): PiModel {
|
|
|
5519
5914
|
* Decide whether the fix should write provider-level or model-level compat.
|
|
5520
5915
|
*
|
|
5521
5916
|
* Strategy (auto-detect, prefer provider level when safe):
|
|
5522
|
-
* - Channel-capability keys (session affinity / long retention) are
|
|
5523
|
-
* provider-safe
|
|
5917
|
+
* - Channel-capability keys (session affinity / long retention) are normally
|
|
5918
|
+
* provider-safe; runtime-observed model failures explicitly override this
|
|
5919
|
+
* default through chooseFixPlacement(..., forceModelLevel=true).
|
|
5524
5920
|
* - Model-behavior keys (forceAdaptiveThinking, allowEmptySignature,
|
|
5525
5921
|
* thinkingFormat, ...) are provider-safe ONLY when every sibling model in
|
|
5526
5922
|
* the provider also matches the same detection (all adaptive-generation /
|
|
@@ -5599,7 +5995,22 @@ function chooseFixPlacement(
|
|
|
5599
5995
|
location: ModelNodeLocation,
|
|
5600
5996
|
compatKeys: Record<string, unknown>,
|
|
5601
5997
|
providerLabel: string,
|
|
5602
|
-
|
|
5998
|
+
forceModelLevel = false,
|
|
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
|
+
|
|
6007
|
+
if (forceModelLevel) {
|
|
6008
|
+
return {
|
|
6009
|
+
placement: "model",
|
|
6010
|
+
reason: "runtime-observed provider/model failure — scoping the repair to the affected model",
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
|
|
5603
6014
|
const decision = decideFixPlacement(compatKeys, providerLabel, location.allModelIds);
|
|
5604
6015
|
const existingModelKeys = findExistingCompatKeysInJsonc(
|
|
5605
6016
|
original,
|
|
@@ -5626,12 +6037,24 @@ function composeFixInsertion(
|
|
|
5626
6037
|
original: string,
|
|
5627
6038
|
location: ModelNodeLocation,
|
|
5628
6039
|
compatKeys: Record<string, unknown>,
|
|
5629
|
-
placement: "provider" | "model" = "model",
|
|
6040
|
+
placement: "provider" | "model" | "modelOverride" = "model",
|
|
5630
6041
|
): string {
|
|
5631
6042
|
// Resolve the target compat object and its container based on placement.
|
|
5632
|
-
const targetCompatBrace = placement === "provider"
|
|
5633
|
-
|
|
5634
|
-
|
|
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;
|
|
5635
6058
|
|
|
5636
6059
|
// Helper: format key/value pairs as lines with the given indent,
|
|
5637
6060
|
// alphabetically sorted for stable previews and deterministic edits.
|
|
@@ -5733,7 +6156,7 @@ function selfCheckFix(
|
|
|
5733
6156
|
providerLabel: string,
|
|
5734
6157
|
modelId: string,
|
|
5735
6158
|
compatKeys: Record<string, unknown>,
|
|
5736
|
-
placement: "provider" | "model" = "model",
|
|
6159
|
+
placement: "provider" | "model" | "modelOverride" = "model",
|
|
5737
6160
|
): string | null {
|
|
5738
6161
|
try {
|
|
5739
6162
|
// Step 1: Parse both versions as JSONC (comments + trailing commas allowed).
|
|
@@ -5777,9 +6200,9 @@ function selfCheckFix(
|
|
|
5777
6200
|
return `Original file: provider/model "${providerLabel}/${modelId}" not found`;
|
|
5778
6201
|
}
|
|
5779
6202
|
|
|
5780
|
-
// Step 5: Compute the EFFECTIVE merged compat
|
|
5781
|
-
//
|
|
5782
|
-
//
|
|
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.
|
|
5783
6206
|
const provCompatRaw = (provider as Record<string, unknown>).compat;
|
|
5784
6207
|
const provCompat = (provCompatRaw && typeof provCompatRaw === 'object' && !Array.isArray(provCompatRaw))
|
|
5785
6208
|
? provCompatRaw as Record<string, unknown>
|
|
@@ -5789,7 +6212,13 @@ function selfCheckFix(
|
|
|
5789
6212
|
return `Modified file: model "${modelId}" compat is not an object`;
|
|
5790
6213
|
}
|
|
5791
6214
|
const mdlCompat = (modelCompatRaw ?? {}) as Record<string, unknown>;
|
|
5792
|
-
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 };
|
|
5793
6222
|
|
|
5794
6223
|
// Step 6: Validate all inserted keys are effective in the merged compat
|
|
5795
6224
|
for (const [k, v] of Object.entries(compatKeys)) {
|
|
@@ -5837,7 +6266,8 @@ function selfCheckFix(
|
|
|
5837
6266
|
// the placement-resolved object's own compat may be exempt.
|
|
5838
6267
|
const mayRepairThisCompat =
|
|
5839
6268
|
(placement === "provider" && origObj === origProvider) ||
|
|
5840
|
-
(placement === "model" && origObj === origTargetModelRecord)
|
|
6269
|
+
(placement === "model" && origObj === origTargetModelRecord) ||
|
|
6270
|
+
(placement === "modelOverride" && origObj === asRecord(asRecord(origProvider.modelOverrides)?.[modelId]));
|
|
5841
6271
|
for (const ck of Object.keys(origCompat)) {
|
|
5842
6272
|
if (!(ck in modCompat)) return false;
|
|
5843
6273
|
// The fix may repair an existing wrong compat value (for example
|
|
@@ -5939,6 +6369,8 @@ export const __internals_for_tests = {
|
|
|
5939
6369
|
isAnthropicMessagesApi,
|
|
5940
6370
|
isOpenAICompatibleProxyApi,
|
|
5941
6371
|
collectAnthropicCacheControlsInWireOrder,
|
|
6372
|
+
downgradeAnthropicLongCacheControls,
|
|
6373
|
+
hasAnthropicCacheTtlOrderError,
|
|
5942
6374
|
normalizeAnthropicCacheControlTtlOrder,
|
|
5943
6375
|
isPiBuiltInLlamaCppModel,
|
|
5944
6376
|
isResponsesPromptRewriteBypassApi,
|
|
@@ -6157,11 +6589,15 @@ export const __internals_for_tests = {
|
|
|
6157
6589
|
stripJsoncComments,
|
|
6158
6590
|
stripJsoncTrailingCommas,
|
|
6159
6591
|
parseJsonc,
|
|
6592
|
+
resolveExplicitCompatValue,
|
|
6593
|
+
hasExplicitLongRetentionOptInFromConfig,
|
|
6160
6594
|
locateModelInJsonc,
|
|
6595
|
+
locateModelOverrideInJsonc,
|
|
6161
6596
|
composeFixInsertion,
|
|
6162
6597
|
selfCheckFix,
|
|
6163
6598
|
analyzeModelsJsonForMissingEntry,
|
|
6164
6599
|
composeMissingEntryInsertion,
|
|
6600
|
+
composeModelOverrideInsertion,
|
|
6165
6601
|
selfCheckMissingEntryInsertion,
|
|
6166
6602
|
decideFixPlacement,
|
|
6167
6603
|
chooseFixPlacement,
|
|
@@ -6186,6 +6622,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
6186
6622
|
const warnedModels = new Set<string>();
|
|
6187
6623
|
const promptCacheRetention400Models = new Set<string>();
|
|
6188
6624
|
const warnedPromptCacheRetention400Models = new Set<string>();
|
|
6625
|
+
const anthropicTtlFallbackState = getAnthropicTtlFallbackState();
|
|
6626
|
+
const anthropicTtlOrderErrorModels = anthropicTtlFallbackState.modelKeys;
|
|
6627
|
+
const warnedAnthropicTtlOrderErrorModels = anthropicTtlFallbackState.warnedModelKeys;
|
|
6189
6628
|
const sendSessionAffinityHeaders403Models = new Set<string>();
|
|
6190
6629
|
const warnedSendSessionAffinityHeaders403Models = new Set<string>();
|
|
6191
6630
|
const openAISdkHeader403Models = new Set<string>();
|
|
@@ -6207,6 +6646,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
6207
6646
|
let lastActualRoutedModel: PersistedRoutedModelRef | undefined;
|
|
6208
6647
|
let latestCacheHint: PiCacheHintSnapshot | undefined;
|
|
6209
6648
|
const PERSIST_DEBOUNCE_MS = 2000;
|
|
6649
|
+
|
|
6650
|
+
function buildObservedRuntimeFixSuggestion(model: PiModel): FixSuggestion | undefined {
|
|
6651
|
+
const key = modelKey(model);
|
|
6652
|
+
const slashIdx = key.indexOf("/");
|
|
6653
|
+
const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
|
|
6654
|
+
|
|
6655
|
+
if (
|
|
6656
|
+
anthropicTtlOrderErrorModels.has(key) ||
|
|
6657
|
+
(isPromptCacheRetention400Applicable(model) && promptCacheRetention400Models.has(key))
|
|
6658
|
+
) {
|
|
6659
|
+
return {
|
|
6660
|
+
providerLabel,
|
|
6661
|
+
modelId: model.id,
|
|
6662
|
+
compatKeys: { supportsLongCacheRetention: false },
|
|
6663
|
+
forceModelLevel: true,
|
|
6664
|
+
};
|
|
6665
|
+
}
|
|
6666
|
+
if (isSessionAffinity403Applicable(model) && sendSessionAffinityHeaders403Models.has(key)) {
|
|
6667
|
+
return { providerLabel, modelId: model.id, compatKeys: { sendSessionAffinityHeaders: false } };
|
|
6668
|
+
}
|
|
6669
|
+
return undefined;
|
|
6670
|
+
}
|
|
6671
|
+
|
|
6672
|
+
function buildCommandFixSuggestion(model: PiModel): FixSuggestion | undefined {
|
|
6673
|
+
const regular = buildFixSuggestion(model);
|
|
6674
|
+
const observed = buildObservedRuntimeFixSuggestion(model);
|
|
6675
|
+
if (!regular) return observed;
|
|
6676
|
+
if (!observed) return regular;
|
|
6677
|
+
return {
|
|
6678
|
+
providerLabel: regular.providerLabel,
|
|
6679
|
+
modelId: regular.modelId,
|
|
6680
|
+
compatKeys: { ...regular.compatKeys, ...observed.compatKeys },
|
|
6681
|
+
forceModelLevel: regular.forceModelLevel || observed.forceModelLevel,
|
|
6682
|
+
};
|
|
6683
|
+
}
|
|
6210
6684
|
/** In-memory recent usage samples per model key (not persisted, cleared on reload). */
|
|
6211
6685
|
const recentSamplesByModelKey = new Map<string, CacheUsageSample[]>();
|
|
6212
6686
|
|
|
@@ -6657,9 +7131,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6657
7131
|
|
|
6658
7132
|
/**
|
|
6659
7133
|
* Check whether a model has an EXPLICIT supportsLongCacheRetention: true
|
|
6660
|
-
* opt-in in models.json
|
|
6661
|
-
*
|
|
6662
|
-
* 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.
|
|
6663
7136
|
*
|
|
6664
7137
|
* Returns true ONLY when the user explicitly opted in. Returns false for:
|
|
6665
7138
|
* - Explicit false (opt-out)
|
|
@@ -6673,31 +7146,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6673
7146
|
try {
|
|
6674
7147
|
const text = readFileSync(MODELS_JSON_PATH, "utf8");
|
|
6675
7148
|
const parsed = parseJsonc(text);
|
|
6676
|
-
|
|
6677
|
-
if (!providers) return false;
|
|
6678
|
-
|
|
6679
|
-
const prov = asRecord(providers[model.provider]);
|
|
6680
|
-
if (!prov) return false;
|
|
6681
|
-
|
|
6682
|
-
// Check model-level first (higher priority in Pi's merge logic)
|
|
6683
|
-
const models = prov.models;
|
|
6684
|
-
if (Array.isArray(models)) {
|
|
6685
|
-
const modelEntry = models.find(m => asRecord(m)?.id === model.id);
|
|
6686
|
-
if (modelEntry) {
|
|
6687
|
-
const modelCompat = asRecord(asRecord(modelEntry)?.compat);
|
|
6688
|
-
if (modelCompat?.supportsLongCacheRetention !== undefined) {
|
|
6689
|
-
return modelCompat.supportsLongCacheRetention === true;
|
|
6690
|
-
}
|
|
6691
|
-
}
|
|
6692
|
-
}
|
|
6693
|
-
|
|
6694
|
-
// Check provider-level
|
|
6695
|
-
const provCompat = asRecord(prov.compat);
|
|
6696
|
-
if (provCompat?.supportsLongCacheRetention !== undefined) {
|
|
6697
|
-
return provCompat.supportsLongCacheRetention === true;
|
|
6698
|
-
}
|
|
6699
|
-
|
|
6700
|
-
return false;
|
|
7149
|
+
return hasExplicitLongRetentionOptInFromConfig(parsed, model.provider, model.id);
|
|
6701
7150
|
} catch {
|
|
6702
7151
|
return false;
|
|
6703
7152
|
}
|
|
@@ -6849,11 +7298,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
6849
7298
|
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
6850
7299
|
|
|
6851
7300
|
// Anthropic rejects mixed cache breakpoints when a 1h block appears after
|
|
6852
|
-
// a 5m/default block in wire order (tools → system → messages).
|
|
6853
|
-
//
|
|
6854
|
-
//
|
|
6855
|
-
|
|
6856
|
-
|
|
7301
|
+
// a 5m/default block in wire order (tools → system → messages). Repair any
|
|
7302
|
+
// conflict visible in Pi's final payload immediately. Some proxies inject
|
|
7303
|
+
// hidden short breakpoints after this hook; only models that have actually
|
|
7304
|
+
// returned the explicit TTL-order error receive the process-local 5m fallback.
|
|
7305
|
+
if (requestModel && isAnthropicMessagesApi(requestModel.api)) {
|
|
7306
|
+
const visibleConflictFixed = normalizeAnthropicCacheControlTtlOrder(event.payload);
|
|
7307
|
+
if (!visibleConflictFixed && anthropicTtlOrderErrorModels.has(modelKey(requestModel))) {
|
|
7308
|
+
downgradeAnthropicLongCacheControls(event.payload);
|
|
7309
|
+
}
|
|
6857
7310
|
}
|
|
6858
7311
|
|
|
6859
7312
|
// ── Safety: strip prompt_cache_retention from payload for models that
|
|
@@ -6957,6 +7410,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
6957
7410
|
|
|
6958
7411
|
pi.on("message_end", async (event, ctx) => {
|
|
6959
7412
|
syncSessionHash(ctx);
|
|
7413
|
+
const msgRecord = asRecord(event.message);
|
|
7414
|
+
|
|
7415
|
+
// Record only Anthropic's explicit mixed-TTL ordering error. This is a
|
|
7416
|
+
// non-retryable 400 in Pi 0.82.1, so the fallback applies to the next
|
|
7417
|
+
// subsequent request (and to a retry only if another layer initiates one).
|
|
7418
|
+
if (hasAnthropicCacheTtlOrderError(event.message)) {
|
|
7419
|
+
const fallbackModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
7420
|
+
const errorModel = modelFromAssistantMessage(event.message, fallbackModel) ?? fallbackModel;
|
|
7421
|
+
if (errorModel && isAnthropicMessagesApi(errorModel.api)) {
|
|
7422
|
+
const key = modelKey(errorModel);
|
|
7423
|
+
anthropicTtlOrderErrorModels.add(key);
|
|
7424
|
+
if (!warnedAnthropicTtlOrderErrorModels.has(key)) {
|
|
7425
|
+
warnedAnthropicTtlOrderErrorModels.add(key);
|
|
7426
|
+
ctx.ui.notify(
|
|
7427
|
+
`⚠️ ${LOG_PREFIX}: ${key} returned an Anthropic cache-control TTL ordering error. ` +
|
|
7428
|
+
`The next request will fall back to the default 5-minute cache TTL. ` +
|
|
7429
|
+
`Run /cache-optimizer fix to set model-level supportsLongCacheRetention: false persistently.`,
|
|
7430
|
+
"warning",
|
|
7431
|
+
);
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
}
|
|
7435
|
+
|
|
6960
7436
|
const adapter = selectAdapterForAssistantMessage(event.message, ctx.model);
|
|
6961
7437
|
if (!adapter) return;
|
|
6962
7438
|
|
|
@@ -6967,7 +7443,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
6967
7443
|
// returns as a valid (non-undefined) snapshot, which would inflate
|
|
6968
7444
|
// totalRequests and skew cache hit-rate accuracy. The final successful
|
|
6969
7445
|
// response is emitted as a separate message_end with real usage data.
|
|
6970
|
-
const msgRecord = asRecord(event.message);
|
|
6971
7446
|
if (msgRecord?.stopReason === "error" || msgRecord?.stopReason === "aborted") {
|
|
6972
7447
|
return;
|
|
6973
7448
|
}
|
|
@@ -7074,7 +7549,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7074
7549
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
7075
7550
|
return;
|
|
7076
7551
|
}
|
|
7077
|
-
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
|
|
7552
|
+
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), anthropicTtlOrderError: anthropicTtlOrderErrorModels.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
|
|
7078
7553
|
const adapter = selectAdapterForModel(model);
|
|
7079
7554
|
const sk = model ? sessionModelKey(model) : undefined;
|
|
7080
7555
|
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
@@ -7147,37 +7622,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7147
7622
|
return;
|
|
7148
7623
|
}
|
|
7149
7624
|
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
// If no regular missing compat flags but the model has a recorded
|
|
7153
|
-
// prompt_cache_retention 400 (Pi sent `prompt_cache_retention` and
|
|
7154
|
-
// the provider rejected it), offer to override
|
|
7155
|
-
// `supportsLongCacheRetention` to false in models.json.
|
|
7156
|
-
if (!suggestion && isPromptCacheRetention400Applicable(model) && promptCacheRetention400Models.has(modelKey(model))) {
|
|
7157
|
-
const key = modelKey(model);
|
|
7158
|
-
const slashIdx = key.indexOf("/");
|
|
7159
|
-
const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
|
|
7160
|
-
suggestion = {
|
|
7161
|
-
providerLabel,
|
|
7162
|
-
modelId: model.id,
|
|
7163
|
-
compatKeys: { supportsLongCacheRetention: false },
|
|
7164
|
-
};
|
|
7165
|
-
}
|
|
7166
|
-
|
|
7167
|
-
// If no regular missing compat flags but the model has a recorded
|
|
7168
|
-
// 403 while sendSessionAffinityHeaders was enabled (Pi sent custom
|
|
7169
|
-
// session-affinity HTTP headers and the proxy/CDN blocked them), offer
|
|
7170
|
-
// to override `sendSessionAffinityHeaders` to false in models.json.
|
|
7171
|
-
if (!suggestion && isSessionAffinity403Applicable(model) && sendSessionAffinityHeaders403Models.has(modelKey(model))) {
|
|
7172
|
-
const key = modelKey(model);
|
|
7173
|
-
const slashIdx = key.indexOf("/");
|
|
7174
|
-
const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
|
|
7175
|
-
suggestion = {
|
|
7176
|
-
providerLabel,
|
|
7177
|
-
modelId: model.id,
|
|
7178
|
-
compatKeys: { sendSessionAffinityHeaders: false },
|
|
7179
|
-
};
|
|
7180
|
-
}
|
|
7625
|
+
const suggestion = buildCommandFixSuggestion(model);
|
|
7181
7626
|
|
|
7182
7627
|
if (!suggestion) {
|
|
7183
7628
|
const key = modelKey(model);
|
|
@@ -7203,6 +7648,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
7203
7648
|
"Create or edit the entry below to override supportsLongCacheRetention to false.",
|
|
7204
7649
|
);
|
|
7205
7650
|
}
|
|
7651
|
+
if (anthropicTtlOrderErrorModels.has(modelKey(model))) {
|
|
7652
|
+
manualLines.push(
|
|
7653
|
+
"",
|
|
7654
|
+
"💡 This model returned an Anthropic cache-control TTL ordering error.",
|
|
7655
|
+
"Create or edit the entry below to override supportsLongCacheRetention to false.",
|
|
7656
|
+
);
|
|
7657
|
+
}
|
|
7206
7658
|
if (sendSessionAffinityHeaders403Models.has(modelKey(model))) {
|
|
7207
7659
|
manualLines.push(
|
|
7208
7660
|
"",
|
|
@@ -7212,8 +7664,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7212
7664
|
}
|
|
7213
7665
|
manualLines.push(
|
|
7214
7666
|
"",
|
|
7215
|
-
"
|
|
7216
|
-
`providers["${suggestion.providerLabel}"] ->
|
|
7667
|
+
"Add these compat keys at Pi's highest-precedence model override path:",
|
|
7668
|
+
`providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat:`,
|
|
7217
7669
|
formatCompatKeysForInsertion(suggestion.compatKeys),
|
|
7218
7670
|
);
|
|
7219
7671
|
if (snippet.length > 0) {
|
|
@@ -7247,11 +7699,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
7247
7699
|
if (!location) {
|
|
7248
7700
|
const diagnosis = analyzeModelsJsonForMissingEntry(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
7249
7701
|
if (diagnosis && cmdCtx.hasUI) {
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
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,
|
|
7254
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 {
|
|
7255
7717
|
const checkError = selfCheckMissingEntryInsertion(
|
|
7256
7718
|
originalText, plan.modifiedText,
|
|
7257
7719
|
suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
@@ -7276,7 +7738,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7276
7738
|
keysPreview,
|
|
7277
7739
|
``,
|
|
7278
7740
|
`⚠️ Risk notice:`,
|
|
7279
|
-
|
|
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.`,
|
|
7280
7744
|
` 2. A timestamped backup will be written to: ${backupPath}`,
|
|
7281
7745
|
` 3. You must run /reload or restart Pi for the change to take effect.`,
|
|
7282
7746
|
` 4. If the file contains comments or unusual formatting, please verify the result after write.`,
|
|
@@ -7291,7 +7755,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
7291
7755
|
);
|
|
7292
7756
|
}
|
|
7293
7757
|
previewLines.push("", `Apply these changes?`);
|
|
7294
|
-
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
|
+
);
|
|
7295
7762
|
if (confirmed) {
|
|
7296
7763
|
try {
|
|
7297
7764
|
await copyFile(MODELS_JSON_PATH, backupPath);
|
|
@@ -7330,6 +7797,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7330
7797
|
cmdCtx.ui.notify("No changes were made. Canceled by user.", "info");
|
|
7331
7798
|
return;
|
|
7332
7799
|
}
|
|
7800
|
+
}
|
|
7333
7801
|
}
|
|
7334
7802
|
|
|
7335
7803
|
// Non-interactive or no diagnosis: show manual guidance.
|
|
@@ -7342,7 +7810,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7342
7810
|
`❌ Could not locate model "${suggestion.modelId}" or provider "${suggestion.providerLabel}" in ${getModelsJsonDisplayPath()}.`,
|
|
7343
7811
|
"",
|
|
7344
7812
|
"Providers that were added via Pi /login API (e.g. opencode go) do not have",
|
|
7345
|
-
"entries in models.json. You can create a minimal
|
|
7813
|
+
"entries in models.json. You can create a minimal modelOverrides entry by hand:",
|
|
7346
7814
|
);
|
|
7347
7815
|
} else if (diagnosis.scenario === "provider_missing") {
|
|
7348
7816
|
adviceLines.push(
|
|
@@ -7357,7 +7825,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7357
7825
|
`ℹ️ Model "${suggestion.modelId}" was not found in ${getModelsJsonDisplayPath()}`,
|
|
7358
7826
|
`under providers["${suggestion.providerLabel}"].`,
|
|
7359
7827
|
"",
|
|
7360
|
-
"Add the following entry
|
|
7828
|
+
"Add the following modelOverrides entry (keep existing auth):",
|
|
7361
7829
|
);
|
|
7362
7830
|
}
|
|
7363
7831
|
adviceLines.push("", snippet, "", "Then save and run /reload.");
|
|
@@ -7365,9 +7833,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
7365
7833
|
return;
|
|
7366
7834
|
}
|
|
7367
7835
|
|
|
7368
|
-
// Compose the modified text —
|
|
7369
|
-
//
|
|
7370
|
-
const decision = chooseFixPlacement(
|
|
7836
|
+
// Compose the modified text — observed runtime failures are always
|
|
7837
|
+
// model-scoped; ordinary compat fixes use the safety-based placement.
|
|
7838
|
+
const decision = chooseFixPlacement(
|
|
7839
|
+
originalText,
|
|
7840
|
+
location,
|
|
7841
|
+
suggestion.compatKeys,
|
|
7842
|
+
suggestion.providerLabel,
|
|
7843
|
+
suggestion.forceModelLevel,
|
|
7844
|
+
);
|
|
7371
7845
|
const modifiedText = composeFixInsertion(originalText, location, suggestion.compatKeys, decision.placement);
|
|
7372
7846
|
|
|
7373
7847
|
// Self-check
|
|
@@ -7384,11 +7858,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
7384
7858
|
// Build preview snippet as copyable JSON (the surgical editor will
|
|
7385
7859
|
// insert or repair these exact compat key/value pairs).
|
|
7386
7860
|
const keysPreview = JSON.stringify(suggestion.compatKeys, null, 2);
|
|
7387
|
-
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;
|
|
7388
7866
|
const placementDesc = targetHasCompat ? `existing "compat" object` : `new "compat" object`;
|
|
7389
7867
|
const locationDesc = decision.placement === "provider"
|
|
7390
7868
|
? `providers["${suggestion.providerLabel}"] -> compat (provider level, ${placementDesc})`
|
|
7391
|
-
:
|
|
7869
|
+
: decision.placement === "modelOverride"
|
|
7870
|
+
? `providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat (${placementDesc})`
|
|
7871
|
+
: `providers["${suggestion.providerLabel}"] -> models -> "${suggestion.modelId}" -> compat (model level, ${placementDesc})`;
|
|
7392
7872
|
|
|
7393
7873
|
const ts = backupTimestamp();
|
|
7394
7874
|
const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
|
|
@@ -7495,7 +7975,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7495
7975
|
if (!model) {
|
|
7496
7976
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
7497
7977
|
} else {
|
|
7498
|
-
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
|
|
7978
|
+
const diagnosis = buildDoctorDiagnosis(model, { promptCacheRetention400: promptCacheRetention400Models.has(modelKey(model)), anthropicTtlOrderError: anthropicTtlOrderErrorModels.has(modelKey(model)), sessionAffinity403: sendSessionAffinityHeaders403Models.has(modelKey(model)), openAISdkHeader403: openAISdkHeader403Models.has(modelKey(model)) });
|
|
7499
7979
|
const adapter = selectAdapterForModel(model);
|
|
7500
7980
|
const sk = model ? sessionModelKey(model) : undefined;
|
|
7501
7981
|
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
@@ -7539,7 +8019,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7539
8019
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
7540
8020
|
return;
|
|
7541
8021
|
}
|
|
7542
|
-
const suggestion =
|
|
8022
|
+
const suggestion = buildCommandFixSuggestion(model);
|
|
7543
8023
|
if (!suggestion) {
|
|
7544
8024
|
const key = modelKey(model);
|
|
7545
8025
|
cmdCtx.ui.notify(`✅ Nothing to fix for "${key}". Compat already configured.`, "info");
|
|
@@ -7556,20 +8036,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
7556
8036
|
}
|
|
7557
8037
|
|
|
7558
8038
|
const location = locateModelInJsonc(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
7559
|
-
|
|
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) {
|
|
7560
8057
|
cmdCtx.ui.notify(
|
|
7561
|
-
`❌ Could not locate
|
|
7562
|
-
`Manual edit required
|
|
7563
|
-
`${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` +
|
|
7564
8060
|
`Then run /reload.`,
|
|
7565
8061
|
"warning",
|
|
7566
8062
|
);
|
|
7567
8063
|
return;
|
|
7568
8064
|
}
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
const checkError =
|
|
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);
|
|
7573
8071
|
if (checkError !== null) {
|
|
7574
8072
|
cmdCtx.ui.notify(`❌ Self-check failed: ${checkError}\nNo changes made.`, "error");
|
|
7575
8073
|
return;
|
|
@@ -7579,17 +8077,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
7579
8077
|
const ts = backupTimestamp();
|
|
7580
8078
|
const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
|
|
7581
8079
|
|
|
7582
|
-
const menuLocationDesc = menuDecision
|
|
8080
|
+
const menuLocationDesc = overridePlan?.placementLabel ?? (menuDecision?.placement === "provider"
|
|
7583
8081
|
? `providers["${suggestion.providerLabel}"] -> compat (provider level)`
|
|
7584
|
-
:
|
|
7585
|
-
|
|
7586
|
-
|
|
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.`
|
|
7587
8087
|
: ` 1. This change affects ALL sessions using the "${suggestion.providerLabel}" provider/channel (scoped to model "${suggestion.modelId}").`;
|
|
7588
8088
|
|
|
7589
8089
|
const previewLines = [
|
|
7590
8090
|
`📝 Preview of changes to ${getModelsJsonDisplayPath()}:`,
|
|
7591
8091
|
`Location: ${menuLocationDesc}`,
|
|
7592
|
-
`Placement: ${menuDecision.placement} level — ${menuDecision.reason}`,
|
|
8092
|
+
`Placement: ${menuDecision ? `${menuDecision.placement} level — ${menuDecision.reason}` : "modelOverrides — built-in/API-login-safe model override"}`,
|
|
7593
8093
|
`Compat JSON to write:`,
|
|
7594
8094
|
keysPreview,
|
|
7595
8095
|
``,
|
|
@@ -7615,7 +8115,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7615
8115
|
await rename(tempPath, MODELS_JSON_PATH);
|
|
7616
8116
|
|
|
7617
8117
|
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
7618
|
-
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);
|
|
7619
8121
|
if (postCheck !== null) {
|
|
7620
8122
|
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
7621
8123
|
cmdCtx.ui.notify(`❌ Post-write check failed: ${postCheck}\nBackup restored.`, "error");
|