pi-cache-optimizer 2.6.24 → 2.7.0
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 +36 -12
- package/README.zh-CN.md +36 -12
- package/index.ts +676 -170
- package/package.json +16 -1
package/index.ts
CHANGED
|
@@ -95,12 +95,20 @@ const STATUS_KEY = "pi-cache-stats";
|
|
|
95
95
|
const STATE_DIR = getAgentDir();
|
|
96
96
|
const STATE_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-stats.json");
|
|
97
97
|
const LEGACY_STATE_FILE_PATH = join(STATE_DIR, "deepseek-cache-optimizer-stats.json");
|
|
98
|
+
const CONFIG_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-config.json");
|
|
98
99
|
const CACHE_PROVIDER_IDS: CacheProviderId[] = ["deepseek", "openai", "claude", "gemini"];
|
|
99
100
|
const OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_OPENAI_CACHE_KEY";
|
|
100
101
|
const NO_OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_NO_OPENAI_CACHE_KEY";
|
|
101
102
|
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
102
103
|
const NO_SKILL_COMPRESSION_ENV = "PI_CACHE_OPTIMIZER_NO_SKILL_COMPRESSION";
|
|
103
104
|
const NO_PROMPT_REWRITE_ENV = "PI_CACHE_OPTIMIZER_NO_PROMPT_REWRITE";
|
|
105
|
+
const FOOTER_MODE_ENV = "PI_CACHE_OPTIMIZER_FOOTER_MODE";
|
|
106
|
+
type FooterStatsMode = "session" | "total";
|
|
107
|
+
type FooterStatsModeSource = "config" | "env" | "default";
|
|
108
|
+
type PersistedCacheOptimizerConfigV1 = {
|
|
109
|
+
version: 1;
|
|
110
|
+
footerMode?: FooterStatsMode;
|
|
111
|
+
};
|
|
104
112
|
const PI_ROUTING_REGISTRY_SYMBOL = Symbol.for("pi.routing.registry.v1");
|
|
105
113
|
const PI_CACHE_HINTS_SYMBOL = Symbol.for("pi.cache.hints.v1");
|
|
106
114
|
const PI_CACHE_HINTS_OWNER_SYMBOL = Symbol.for("pi.cache.optimizer.hints-owner.v1");
|
|
@@ -133,8 +141,8 @@ function getAnthropicTtlFallbackState(): AnthropicTtlFallbackStateV1 {
|
|
|
133
141
|
|
|
134
142
|
let runtimeOptimizerEnabled = true;
|
|
135
143
|
|
|
136
|
-
// WORM-flag: if optimizeSystemPrompt ever detects that
|
|
137
|
-
//
|
|
144
|
+
// WORM-flag: if optimizeSystemPrompt ever detects that candidate extraction
|
|
145
|
+
// has accidentally truncated a structural marker (any XML tag or
|
|
138
146
|
// HTML comment boundary marker present in the original prompt), we flip
|
|
139
147
|
// this. publishStatus reads it once, appends a footer warning, then
|
|
140
148
|
// resets it. The flag surface is kept separate from the regular
|
|
@@ -161,12 +169,10 @@ const SKILL_COMPRESSION_MIN_COUNT = 4;
|
|
|
161
169
|
|
|
162
170
|
// Minimum trimmed length for a candidate to qualify as a stable-prefix "part".
|
|
163
171
|
//
|
|
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.
|
|
172
|
+
// `optimizeSystemPrompt` removes each uniquely occurring accepted candidate
|
|
173
|
+
// from the dynamic remainder. Short or character-class candidates (think: `S`,
|
|
174
|
+
// `- u`, `- (`, `- }`) are too likely to match unrelated prompt text, so they
|
|
175
|
+
// are never eligible for lifting.
|
|
170
176
|
//
|
|
171
177
|
// The threshold also caps the upstream string-vs-array regression we saw with
|
|
172
178
|
// trellis 0.5.16 / 0.6.0-beta.17 (subagent tool registration passing
|
|
@@ -644,7 +650,7 @@ function stripSessionOverviewChurn(prompt: string): string {
|
|
|
644
650
|
* Extract structural markers from a prompt for the integrity guard.
|
|
645
651
|
*
|
|
646
652
|
* The guard runs in `optimizeSystemPrompt` to catch cases where the
|
|
647
|
-
*
|
|
653
|
+
* candidate extraction accidentally eats text inside
|
|
648
654
|
* an extension-injected structural block (e.g., trellis
|
|
649
655
|
* `<workflow-state>`, a hypothetical `<task-tracker>`, or AGENTS.md
|
|
650
656
|
* `<!-- TRELLIS:START -->` markers). When the original prompt contains
|
|
@@ -704,16 +710,43 @@ function optimizeSystemPrompt(
|
|
|
704
710
|
const seen = new Set<string>();
|
|
705
711
|
let rest = original;
|
|
706
712
|
|
|
707
|
-
//
|
|
708
|
-
//
|
|
713
|
+
// Classify candidate ambiguity against one immutable snapshot. Candidates
|
|
714
|
+
// can be nested (a full context-file block also contains its bare content),
|
|
715
|
+
// so recomputing occurrence counts after each removal is unsafe: deleting
|
|
716
|
+
// the full block can make the dynamic copy of its bare content appear unique.
|
|
717
|
+
const candidates: string[] = [];
|
|
709
718
|
for (const candidate of buildStableCandidates(opts)) {
|
|
710
719
|
const part = candidate.trim();
|
|
711
|
-
if (!part || part.length < MIN_STABLE_CANDIDATE_LENGTH) continue;
|
|
712
|
-
|
|
720
|
+
if (!part || part.length < MIN_STABLE_CANDIDATE_LENGTH || seen.has(part)) continue;
|
|
721
|
+
seen.add(part);
|
|
722
|
+
candidates.push(part);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const initialRemainder = rest;
|
|
726
|
+
const occurrenceCount = new Map<string, number>();
|
|
727
|
+
for (const part of candidates) {
|
|
728
|
+
let count = 0;
|
|
729
|
+
let searchFrom = 0;
|
|
730
|
+
while (searchFrom < initialRemainder.length) {
|
|
731
|
+
const occurrence = initialRemainder.indexOf(part, searchFrom);
|
|
732
|
+
if (occurrence < 0) break;
|
|
733
|
+
count++;
|
|
734
|
+
if (count > 1) break;
|
|
735
|
+
searchFrom = occurrence + 1;
|
|
736
|
+
}
|
|
737
|
+
occurrenceCount.set(part, count);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Stable layer: content likely to be identical across sessions/turns.
|
|
741
|
+
// Short / single-char candidates are dropped: see MIN_STABLE_CANDIDATE_LENGTH.
|
|
742
|
+
for (const part of candidates) {
|
|
743
|
+
if (occurrenceCount.get(part) !== 1) continue;
|
|
744
|
+
|
|
745
|
+
const firstOccurrence = rest.indexOf(part);
|
|
746
|
+
if (firstOccurrence < 0) continue;
|
|
713
747
|
|
|
714
748
|
stableParts.push(part);
|
|
715
|
-
|
|
716
|
-
rest = rest.replace(part, "");
|
|
749
|
+
rest = rest.slice(0, firstOccurrence) + rest.slice(firstOccurrence + part.length);
|
|
717
750
|
}
|
|
718
751
|
|
|
719
752
|
const stablePrefix = stableParts.join("\n\n");
|
|
@@ -731,8 +764,8 @@ function optimizeSystemPrompt(
|
|
|
731
764
|
|
|
732
765
|
// Sanity check: scan ALL structural markers (XML tags + HTML comment
|
|
733
766
|
// boundary markers) in the original and verify each one survives the
|
|
734
|
-
// reorder. If any marker drops,
|
|
735
|
-
//
|
|
767
|
+
// reorder. If any marker drops, candidate extraction ate something it
|
|
768
|
+
// shouldn't have — fall back to the original
|
|
736
769
|
// prompt and flag the footer warning. This is provider-agnostic and
|
|
737
770
|
// extension-agnostic: trellis `<workflow-state>`, a hypothetical
|
|
738
771
|
// `<task-tracker>`, AGENTS.md `<!-- TRELLIS:START -->`, etc., are all
|
|
@@ -1107,6 +1140,62 @@ function isEnabledEnv(value: string | undefined): boolean {
|
|
|
1107
1140
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
1108
1141
|
}
|
|
1109
1142
|
|
|
1143
|
+
function parseFooterStatsMode(value: unknown): FooterStatsMode | undefined {
|
|
1144
|
+
if (typeof value !== "string") return undefined;
|
|
1145
|
+
const normalized = value.trim().toLowerCase();
|
|
1146
|
+
return normalized === "session" || normalized === "total" ? normalized : undefined;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function parsePersistedCacheOptimizerConfig(value: unknown): PersistedCacheOptimizerConfigV1 | undefined {
|
|
1150
|
+
const record = asRecord(value);
|
|
1151
|
+
if (!record || record.version !== 1) return undefined;
|
|
1152
|
+
const footerMode = parseFooterStatsMode(record.footerMode);
|
|
1153
|
+
if (record.footerMode !== undefined && !footerMode) return undefined;
|
|
1154
|
+
return { version: 1, ...(footerMode ? { footerMode } : {}) };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function readPersistedFooterMode(configPath: string = CONFIG_FILE_PATH): FooterStatsMode | undefined {
|
|
1158
|
+
try {
|
|
1159
|
+
const parsed = parsePersistedCacheOptimizerConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
1160
|
+
if (!parsed) throw new Error("invalid footer config schema");
|
|
1161
|
+
return parsed.footerMode;
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
if (getErrorCode(error) !== "ENOENT") {
|
|
1164
|
+
console.warn(`${LOG_PREFIX}: failed to read footer config; using environment/default mode`, error);
|
|
1165
|
+
}
|
|
1166
|
+
return undefined;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async function writePersistedFooterMode(
|
|
1171
|
+
mode: FooterStatsMode,
|
|
1172
|
+
configPath: string = CONFIG_FILE_PATH,
|
|
1173
|
+
): Promise<void> {
|
|
1174
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1175
|
+
const payload: PersistedCacheOptimizerConfigV1 = { version: 1, footerMode: mode };
|
|
1176
|
+
const tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1177
|
+
await writeFile(tempPath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
1178
|
+
await rename(tempPath, configPath);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function resolveFooterStatsMode(
|
|
1182
|
+
configuredMode: FooterStatsMode | undefined,
|
|
1183
|
+
env: MutableEnv = process.env,
|
|
1184
|
+
): { mode: FooterStatsMode; source: FooterStatsModeSource } {
|
|
1185
|
+
if (configuredMode) return { mode: configuredMode, source: "config" };
|
|
1186
|
+
const envMode = parseFooterStatsMode(env[FOOTER_MODE_ENV]);
|
|
1187
|
+
return envMode ? { mode: envMode, source: "env" } : { mode: "total", source: "default" };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function footerStatsMode(
|
|
1191
|
+
env: MutableEnv = process.env,
|
|
1192
|
+
configuredMode: FooterStatsMode | undefined = persistedFooterStatsMode,
|
|
1193
|
+
): FooterStatsMode {
|
|
1194
|
+
return resolveFooterStatsMode(configuredMode, env).mode;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
let persistedFooterStatsMode = readPersistedFooterMode();
|
|
1198
|
+
|
|
1110
1199
|
function isDisabledEnv(value: string | undefined): boolean {
|
|
1111
1200
|
if (!value) return false;
|
|
1112
1201
|
const normalized = value.trim().toLowerCase();
|
|
@@ -3802,11 +3891,24 @@ function routedModelRefToPiModel(ref: PersistedRoutedModelRef): PiModel {
|
|
|
3802
3891
|
} as PiModel;
|
|
3803
3892
|
}
|
|
3804
3893
|
|
|
3894
|
+
function selectFooterStatsForModel(
|
|
3895
|
+
mode: FooterStatsMode,
|
|
3896
|
+
sessionHash: string | undefined,
|
|
3897
|
+
statsByModel: Record<string, CacheStats>,
|
|
3898
|
+
totalsByModel: Record<string, CacheStats>,
|
|
3899
|
+
model: { provider: string; id: string },
|
|
3900
|
+
): CacheStats | undefined {
|
|
3901
|
+
if (mode === "total") return totalsByModel[modelKey(model)];
|
|
3902
|
+
if (!sessionHash) return undefined;
|
|
3903
|
+
return statsByModel[makeSessionModelKey(sessionHash, model.provider, model.id)];
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3805
3906
|
function buildExactRouterStatusEntry(
|
|
3806
3907
|
sessionHash: string | undefined,
|
|
3807
3908
|
statsByModel: Record<string, CacheStats>,
|
|
3808
3909
|
lastRoutedModel: PersistedRoutedModelRef | undefined,
|
|
3809
3910
|
totalsByModel: Record<string, CacheStats> = {},
|
|
3911
|
+
mode: FooterStatsMode = "total",
|
|
3810
3912
|
): { model: PiModel; adapter: CacheProviderAdapter; stats: CacheStats } | undefined {
|
|
3811
3913
|
if (!sessionHash || !lastRoutedModel) return undefined;
|
|
3812
3914
|
|
|
@@ -3814,8 +3916,43 @@ function buildExactRouterStatusEntry(
|
|
|
3814
3916
|
const adapter = selectAdapterForModel(model);
|
|
3815
3917
|
if (!adapter) return undefined;
|
|
3816
3918
|
|
|
3817
|
-
|
|
3818
|
-
|
|
3919
|
+
return {
|
|
3920
|
+
model,
|
|
3921
|
+
adapter,
|
|
3922
|
+
stats: selectFooterStatsForModel(mode, sessionHash, statsByModel, totalsByModel, model) ?? emptyCacheStats(),
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
function findBestRouterModelStats(
|
|
3927
|
+
mode: FooterStatsMode,
|
|
3928
|
+
sessionHash: string | undefined,
|
|
3929
|
+
statsByModel: Record<string, CacheStats>,
|
|
3930
|
+
totalsByModel: Record<string, CacheStats>,
|
|
3931
|
+
): { model: PiModel; adapter: CacheProviderAdapter; stats: CacheStats } | undefined {
|
|
3932
|
+
const entries = mode === "total"
|
|
3933
|
+
? Object.entries(totalsByModel)
|
|
3934
|
+
: sessionHash
|
|
3935
|
+
? Object.entries(statsByModel)
|
|
3936
|
+
.filter(([key]) => key.startsWith(`${sessionHash}:`))
|
|
3937
|
+
.map(([key, stats]) => [key.slice(sessionHash.length + 1), stats] as const)
|
|
3938
|
+
: [];
|
|
3939
|
+
let best: { model: PiModel; adapter: CacheProviderAdapter; stats: CacheStats; total: number } | undefined;
|
|
3940
|
+
|
|
3941
|
+
for (const [modelKeyPart, stats] of entries) {
|
|
3942
|
+
const slashIdx = modelKeyPart.indexOf("/");
|
|
3943
|
+
if (slashIdx < 1 || slashIdx >= modelKeyPart.length - 1) continue;
|
|
3944
|
+
const model = routedModelRefToPiModel({
|
|
3945
|
+
provider: modelKeyPart.slice(0, slashIdx),
|
|
3946
|
+
id: modelKeyPart.slice(slashIdx + 1),
|
|
3947
|
+
});
|
|
3948
|
+
const adapter = selectAdapterForModel(model);
|
|
3949
|
+
if (!adapter) continue;
|
|
3950
|
+
if (!best || stats.totalRequests > best.total) {
|
|
3951
|
+
best = { model, adapter, stats, total: stats.totalRequests };
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
return best ? { model: best.model, adapter: best.adapter, stats: best.stats } : undefined;
|
|
3819
3956
|
}
|
|
3820
3957
|
|
|
3821
3958
|
function parsePersistedCacheStats(value: unknown): CacheStatsState | undefined {
|
|
@@ -5040,6 +5177,63 @@ function parseJsonc(text: string): unknown {
|
|
|
5040
5177
|
return JSON.parse(stripJsoncTrailingCommas(stripJsoncComments(text)));
|
|
5041
5178
|
}
|
|
5042
5179
|
|
|
5180
|
+
type ExplicitCompatSource = "modelOverride" | "model" | "provider";
|
|
5181
|
+
|
|
5182
|
+
interface ExplicitCompatValue {
|
|
5183
|
+
source: ExplicitCompatSource;
|
|
5184
|
+
value: unknown;
|
|
5185
|
+
}
|
|
5186
|
+
|
|
5187
|
+
/**
|
|
5188
|
+
* Resolve one explicitly configured compat value using Pi's models.json
|
|
5189
|
+
* precedence. Built-in model overrides win over custom model definitions,
|
|
5190
|
+
* which in turn win over provider defaults.
|
|
5191
|
+
*/
|
|
5192
|
+
function resolveExplicitCompatValue(
|
|
5193
|
+
config: unknown,
|
|
5194
|
+
providerLabel: string,
|
|
5195
|
+
modelId: string,
|
|
5196
|
+
compatKey: string,
|
|
5197
|
+
): ExplicitCompatValue | undefined {
|
|
5198
|
+
const providers = asRecord(asRecord(config)?.providers);
|
|
5199
|
+
const provider = asRecord(providers?.[providerLabel]);
|
|
5200
|
+
if (!provider) return undefined;
|
|
5201
|
+
|
|
5202
|
+
const override = asRecord(asRecord(provider.modelOverrides)?.[modelId]);
|
|
5203
|
+
const overrideCompat = asRecord(override?.compat);
|
|
5204
|
+
if (overrideCompat && Object.prototype.hasOwnProperty.call(overrideCompat, compatKey)) {
|
|
5205
|
+
return { source: "modelOverride", value: overrideCompat[compatKey] };
|
|
5206
|
+
}
|
|
5207
|
+
|
|
5208
|
+
if (Array.isArray(provider.models)) {
|
|
5209
|
+
const model = provider.models.find((entry: unknown) => asRecord(entry)?.id === modelId);
|
|
5210
|
+
const modelCompat = asRecord(asRecord(model)?.compat);
|
|
5211
|
+
if (modelCompat && Object.prototype.hasOwnProperty.call(modelCompat, compatKey)) {
|
|
5212
|
+
return { source: "model", value: modelCompat[compatKey] };
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
const providerCompat = asRecord(provider.compat);
|
|
5217
|
+
if (providerCompat && Object.prototype.hasOwnProperty.call(providerCompat, compatKey)) {
|
|
5218
|
+
return { source: "provider", value: providerCompat[compatKey] };
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
return undefined;
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
function hasExplicitLongRetentionOptInFromConfig(
|
|
5225
|
+
config: unknown,
|
|
5226
|
+
providerLabel: string,
|
|
5227
|
+
modelId: string,
|
|
5228
|
+
): boolean {
|
|
5229
|
+
return resolveExplicitCompatValue(
|
|
5230
|
+
config,
|
|
5231
|
+
providerLabel,
|
|
5232
|
+
modelId,
|
|
5233
|
+
"supportsLongCacheRetention",
|
|
5234
|
+
)?.value === true;
|
|
5235
|
+
}
|
|
5236
|
+
|
|
5043
5237
|
/**
|
|
5044
5238
|
* JSONC scanner: locate the provider block and model entry in models.json text.
|
|
5045
5239
|
* Returns the byte offsets for surgical insertion, or undefined if ambiguous.
|
|
@@ -5065,10 +5259,99 @@ interface ModelNodeLocation {
|
|
|
5065
5259
|
providerCompatBrace: number;
|
|
5066
5260
|
/** Offset of the provider-level compat object's closing `}`, or -1 if absent */
|
|
5067
5261
|
providerCompatEnd: number;
|
|
5262
|
+
/** Offset of the target modelOverrides entry's opening `{`, or -1 if absent */
|
|
5263
|
+
modelOverrideObjectBrace: number;
|
|
5264
|
+
/** Offset of the target modelOverrides entry's closing `}`, or -1 if absent */
|
|
5265
|
+
modelOverrideObjectEnd: number;
|
|
5266
|
+
/** Offset of the target override compat object's opening `{`, or -1 if absent */
|
|
5267
|
+
modelOverrideCompatBrace: number;
|
|
5268
|
+
/** Offset of the target override compat object's closing `}`, or -1 if absent */
|
|
5269
|
+
modelOverrideCompatEnd: number;
|
|
5068
5270
|
/** All model ids found in this provider's models array (for placement safety analysis) */
|
|
5069
5271
|
allModelIds: string[];
|
|
5070
5272
|
}
|
|
5071
5273
|
|
|
5274
|
+
interface ModelOverrideNodeLocation {
|
|
5275
|
+
providerObjectBrace: number;
|
|
5276
|
+
providerObjectEnd: number;
|
|
5277
|
+
modelOverridesObjectBrace: number;
|
|
5278
|
+
modelOverridesObjectEnd: number;
|
|
5279
|
+
modelOverrideObjectBrace: number;
|
|
5280
|
+
modelOverrideObjectEnd: number;
|
|
5281
|
+
modelOverrideCompatBrace: number;
|
|
5282
|
+
modelOverrideCompatEnd: number;
|
|
5283
|
+
}
|
|
5284
|
+
|
|
5285
|
+
/** Locate a provider and optional modelOverrides entry without requiring models[]. */
|
|
5286
|
+
function locateModelOverrideInJsonc(
|
|
5287
|
+
text: string,
|
|
5288
|
+
providerLabel: string,
|
|
5289
|
+
modelId: string,
|
|
5290
|
+
): ModelOverrideNodeLocation | undefined {
|
|
5291
|
+
const clean = stripJsoncComments(text);
|
|
5292
|
+
const rootBrace = skipJsonWhitespace(clean, 0);
|
|
5293
|
+
if (clean[rootBrace] !== "{") return undefined;
|
|
5294
|
+
const providersKey = findJsonObjectKey(clean, rootBrace, "providers");
|
|
5295
|
+
if (!providersKey) return undefined;
|
|
5296
|
+
const providersBrace = skipJsonWhitespace(clean, providersKey.valueStart);
|
|
5297
|
+
if (clean[providersBrace] !== "{") return undefined;
|
|
5298
|
+
const providersEnd = findMatchingBracket(clean, providersBrace);
|
|
5299
|
+
if (providersEnd === undefined) return undefined;
|
|
5300
|
+
const providerKey = findJsonObjectKey(clean, providersBrace, providerLabel);
|
|
5301
|
+
if (!providerKey || providerKey.keyStart > providersEnd) return undefined;
|
|
5302
|
+
const providerObjectBrace = skipJsonWhitespace(clean, providerKey.valueStart);
|
|
5303
|
+
if (clean[providerObjectBrace] !== "{") return undefined;
|
|
5304
|
+
const providerObjectEnd = findMatchingBracket(clean, providerObjectBrace);
|
|
5305
|
+
if (providerObjectEnd === undefined || providerObjectEnd > providersEnd) return undefined;
|
|
5306
|
+
|
|
5307
|
+
let modelOverridesObjectBrace = -1;
|
|
5308
|
+
let modelOverridesObjectEnd = -1;
|
|
5309
|
+
let modelOverrideObjectBrace = -1;
|
|
5310
|
+
let modelOverrideObjectEnd = -1;
|
|
5311
|
+
let modelOverrideCompatBrace = -1;
|
|
5312
|
+
let modelOverrideCompatEnd = -1;
|
|
5313
|
+
const overridesKey = findJsonObjectKey(clean, providerObjectBrace, "modelOverrides");
|
|
5314
|
+
if (overridesKey && overridesKey.keyStart < providerObjectEnd) {
|
|
5315
|
+
const brace = skipJsonWhitespace(clean, overridesKey.valueStart);
|
|
5316
|
+
if (clean[brace] !== "{") return undefined;
|
|
5317
|
+
const end = findMatchingBracket(clean, brace);
|
|
5318
|
+
if (end === undefined || end > providerObjectEnd) return undefined;
|
|
5319
|
+
modelOverridesObjectBrace = brace;
|
|
5320
|
+
modelOverridesObjectEnd = end;
|
|
5321
|
+
|
|
5322
|
+
const overrideKey = findJsonObjectKey(clean, brace, modelId);
|
|
5323
|
+
if (overrideKey && overrideKey.keyStart < end) {
|
|
5324
|
+
const entryBrace = skipJsonWhitespace(clean, overrideKey.valueStart);
|
|
5325
|
+
if (clean[entryBrace] !== "{") return undefined;
|
|
5326
|
+
const entryEnd = findMatchingBracket(clean, entryBrace);
|
|
5327
|
+
if (entryEnd === undefined || entryEnd > end) return undefined;
|
|
5328
|
+
modelOverrideObjectBrace = entryBrace;
|
|
5329
|
+
modelOverrideObjectEnd = entryEnd;
|
|
5330
|
+
|
|
5331
|
+
const compatKey = findJsonObjectKey(clean, entryBrace, "compat");
|
|
5332
|
+
if (compatKey && compatKey.keyStart < entryEnd) {
|
|
5333
|
+
const compatBrace = skipJsonWhitespace(clean, compatKey.valueStart);
|
|
5334
|
+
if (clean[compatBrace] !== "{") return undefined;
|
|
5335
|
+
const compatEnd = findMatchingBracket(clean, compatBrace);
|
|
5336
|
+
if (compatEnd === undefined || compatEnd > entryEnd) return undefined;
|
|
5337
|
+
modelOverrideCompatBrace = compatBrace;
|
|
5338
|
+
modelOverrideCompatEnd = compatEnd;
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
|
|
5343
|
+
return {
|
|
5344
|
+
providerObjectBrace,
|
|
5345
|
+
providerObjectEnd,
|
|
5346
|
+
modelOverridesObjectBrace,
|
|
5347
|
+
modelOverridesObjectEnd,
|
|
5348
|
+
modelOverrideObjectBrace,
|
|
5349
|
+
modelOverrideObjectEnd,
|
|
5350
|
+
modelOverrideCompatBrace,
|
|
5351
|
+
modelOverrideCompatEnd,
|
|
5352
|
+
};
|
|
5353
|
+
}
|
|
5354
|
+
|
|
5072
5355
|
/**
|
|
5073
5356
|
* Locate the provider + model entry in raw JSONC text.
|
|
5074
5357
|
* Returns the positions needed for surgical insertion, or undefined on failure.
|
|
@@ -5121,6 +5404,12 @@ function locateModelInJsonc(
|
|
|
5121
5404
|
}
|
|
5122
5405
|
}
|
|
5123
5406
|
|
|
5407
|
+
const overrideLocation = locateModelOverrideInJsonc(text, providerLabel, modelId);
|
|
5408
|
+
const modelOverrideObjectBrace = overrideLocation?.modelOverrideObjectBrace ?? -1;
|
|
5409
|
+
const modelOverrideObjectEnd = overrideLocation?.modelOverrideObjectEnd ?? -1;
|
|
5410
|
+
const modelOverrideCompatBrace = overrideLocation?.modelOverrideCompatBrace ?? -1;
|
|
5411
|
+
const modelOverrideCompatEnd = overrideLocation?.modelOverrideCompatEnd ?? -1;
|
|
5412
|
+
|
|
5124
5413
|
const modelsKey = findJsonObjectKey(clean, providerBrace, "models");
|
|
5125
5414
|
if (!modelsKey || modelsKey.keyStart > providerEndBrace) return undefined;
|
|
5126
5415
|
|
|
@@ -5208,6 +5497,10 @@ function locateModelInJsonc(
|
|
|
5208
5497
|
providerObjectEnd: providerEndBrace,
|
|
5209
5498
|
providerCompatBrace,
|
|
5210
5499
|
providerCompatEnd,
|
|
5500
|
+
modelOverrideObjectBrace,
|
|
5501
|
+
modelOverrideObjectEnd,
|
|
5502
|
+
modelOverrideCompatBrace,
|
|
5503
|
+
modelOverrideCompatEnd,
|
|
5211
5504
|
allModelIds,
|
|
5212
5505
|
};
|
|
5213
5506
|
}
|
|
@@ -5217,7 +5510,7 @@ function locateModelInJsonc(
|
|
|
5217
5510
|
* `locateModelInJsonc` cannot find the target provider/model.
|
|
5218
5511
|
*/
|
|
5219
5512
|
type MissingEntryDiagnosis =
|
|
5220
|
-
| { scenario: "provider_missing"; providersEnd: number }
|
|
5513
|
+
| { scenario: "provider_missing"; providersBrace: number; providersEnd: number }
|
|
5221
5514
|
| { scenario: "model_missing"; modelsEnd: number; providerBrace: number; providerEndBrace: number }
|
|
5222
5515
|
| { scenario: "provider_without_models"; providerBrace: number; providerEndBrace: number };
|
|
5223
5516
|
|
|
@@ -5248,7 +5541,7 @@ function analyzeModelsJsonForMissingEntry(
|
|
|
5248
5541
|
|
|
5249
5542
|
const providerKey = findJsonObjectKey(clean, providersBrace, providerLabel);
|
|
5250
5543
|
if (!providerKey || providerKey.keyStart > providersEnd) {
|
|
5251
|
-
return { scenario: "provider_missing", providersEnd };
|
|
5544
|
+
return { scenario: "provider_missing", providersBrace, providersEnd };
|
|
5252
5545
|
}
|
|
5253
5546
|
|
|
5254
5547
|
// Provider exists. Check for a models array so we know where to append.
|
|
@@ -5286,20 +5579,126 @@ function formatMissingEntryManualSnippet(
|
|
|
5286
5579
|
): string {
|
|
5287
5580
|
const lines: string[] = [];
|
|
5288
5581
|
const sorted = Object.entries(compatKeys).sort(([a], [b]) => a.localeCompare(b));
|
|
5289
|
-
const compatItems = sorted.map(([k, v]) => `
|
|
5582
|
+
const compatItems = sorted.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`);
|
|
5290
5583
|
lines.push(`"${providerLabel}": {`);
|
|
5291
|
-
lines.push(` "
|
|
5292
|
-
lines.push(` {`);
|
|
5293
|
-
lines.push(` "id": ${JSON.stringify(modelId)},`);
|
|
5584
|
+
lines.push(` "modelOverrides": {`);
|
|
5585
|
+
lines.push(` ${JSON.stringify(modelId)}: {`);
|
|
5294
5586
|
lines.push(` "compat": {`);
|
|
5295
5587
|
lines.push(compatItems.join(",\n"));
|
|
5296
5588
|
lines.push(` }`);
|
|
5297
5589
|
lines.push(` }`);
|
|
5298
|
-
lines.push(`
|
|
5590
|
+
lines.push(` }`);
|
|
5299
5591
|
lines.push(` }`);
|
|
5300
5592
|
return lines.join("\n");
|
|
5301
5593
|
}
|
|
5302
5594
|
|
|
5595
|
+
/**
|
|
5596
|
+
* Insert or repair a modelOverrides entry without creating a custom models[]
|
|
5597
|
+
* definition. This is the safest representation for built-in/API-login models
|
|
5598
|
+
* and has the same highest precedence Pi applies at runtime.
|
|
5599
|
+
*/
|
|
5600
|
+
function composeModelOverrideInsertion(
|
|
5601
|
+
originalText: string,
|
|
5602
|
+
providerLabel: string,
|
|
5603
|
+
modelId: string,
|
|
5604
|
+
compatKeys: Record<string, unknown>,
|
|
5605
|
+
): { modifiedText: string; placementLabel: string } | undefined {
|
|
5606
|
+
const location = locateModelOverrideInJsonc(originalText, providerLabel, modelId);
|
|
5607
|
+
|
|
5608
|
+
if (location?.modelOverrideObjectBrace !== undefined && location.modelOverrideObjectBrace >= 0) {
|
|
5609
|
+
const modelLocation: ModelNodeLocation = {
|
|
5610
|
+
modelObjectBrace: -1,
|
|
5611
|
+
modelObjectEnd: -1,
|
|
5612
|
+
compatKeyStart: -1,
|
|
5613
|
+
compatObjectBrace: -1,
|
|
5614
|
+
compatObjectEnd: -1,
|
|
5615
|
+
indent: "",
|
|
5616
|
+
providerObjectBrace: location.providerObjectBrace,
|
|
5617
|
+
providerObjectEnd: location.providerObjectEnd,
|
|
5618
|
+
providerCompatBrace: -1,
|
|
5619
|
+
providerCompatEnd: -1,
|
|
5620
|
+
modelOverrideObjectBrace: location.modelOverrideObjectBrace,
|
|
5621
|
+
modelOverrideObjectEnd: location.modelOverrideObjectEnd,
|
|
5622
|
+
modelOverrideCompatBrace: location.modelOverrideCompatBrace,
|
|
5623
|
+
modelOverrideCompatEnd: location.modelOverrideCompatEnd,
|
|
5624
|
+
allModelIds: [],
|
|
5625
|
+
};
|
|
5626
|
+
return {
|
|
5627
|
+
modifiedText: composeFixInsertion(originalText, modelLocation, compatKeys, "modelOverride"),
|
|
5628
|
+
placementLabel: `providers["${providerLabel}"] -> modelOverrides["${modelId}"] -> compat`,
|
|
5629
|
+
};
|
|
5630
|
+
}
|
|
5631
|
+
|
|
5632
|
+
const sortedEntries = Object.entries(compatKeys).sort(([a], [b]) => a.localeCompare(b));
|
|
5633
|
+
const formatOverrideEntry = (keyIndent: string, unit: string): string => {
|
|
5634
|
+
const propertyIndent = keyIndent + unit;
|
|
5635
|
+
const compatIndent = propertyIndent + unit;
|
|
5636
|
+
const compatLines = sortedEntries
|
|
5637
|
+
.map(([key, value]) => `${compatIndent}${JSON.stringify(key)}: ${JSON.stringify(value)}`)
|
|
5638
|
+
.join(",\n");
|
|
5639
|
+
return `${keyIndent}${JSON.stringify(modelId)}: {\n` +
|
|
5640
|
+
`${propertyIndent}"compat": {\n${compatLines}\n${propertyIndent}}\n${keyIndent}}`;
|
|
5641
|
+
};
|
|
5642
|
+
const previousTokenNeedsComma = (clean: string, closeBrace: number): boolean => {
|
|
5643
|
+
let pos = closeBrace - 1;
|
|
5644
|
+
while (pos >= 0 && isJsonWhitespace(clean[pos])) pos--;
|
|
5645
|
+
return clean[pos] !== "{" && clean[pos] !== ",";
|
|
5646
|
+
};
|
|
5647
|
+
|
|
5648
|
+
if (location) {
|
|
5649
|
+
const clean = stripJsoncComments(originalText);
|
|
5650
|
+
if (location.modelOverridesObjectBrace >= 0) {
|
|
5651
|
+
const containerIndent = lineIndentOf(originalText, location.modelOverridesObjectBrace);
|
|
5652
|
+
const keyIndent = deriveInnerIndent(
|
|
5653
|
+
originalText,
|
|
5654
|
+
location.modelOverridesObjectBrace,
|
|
5655
|
+
location.modelOverridesObjectEnd,
|
|
5656
|
+
);
|
|
5657
|
+
const unit = keyIndent.length > containerIndent.length
|
|
5658
|
+
? keyIndent.slice(containerIndent.length)
|
|
5659
|
+
: " ";
|
|
5660
|
+
const comma = previousTokenNeedsComma(clean, location.modelOverridesObjectEnd) ? "," : "";
|
|
5661
|
+
const insertion = `${comma}\n${formatOverrideEntry(keyIndent, unit)}\n${containerIndent}`;
|
|
5662
|
+
return {
|
|
5663
|
+
modifiedText: originalText.slice(0, location.modelOverridesObjectEnd) + insertion + originalText.slice(location.modelOverridesObjectEnd),
|
|
5664
|
+
placementLabel: `providers["${providerLabel}"] -> modelOverrides -> (new entry "${modelId}")`,
|
|
5665
|
+
};
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
const providerIndent = lineIndentOf(originalText, location.providerObjectBrace);
|
|
5669
|
+
const propertyIndent = deriveInnerIndent(originalText, location.providerObjectBrace, location.providerObjectEnd);
|
|
5670
|
+
const unit = propertyIndent.length > providerIndent.length
|
|
5671
|
+
? propertyIndent.slice(providerIndent.length)
|
|
5672
|
+
: " ";
|
|
5673
|
+
const entryIndent = propertyIndent + unit;
|
|
5674
|
+
const block = `\n${propertyIndent}"modelOverrides": {\n` +
|
|
5675
|
+
`${formatOverrideEntry(entryIndent, unit)}\n${propertyIndent}},`;
|
|
5676
|
+
return {
|
|
5677
|
+
modifiedText: originalText.slice(0, location.providerObjectBrace + 1) + block + originalText.slice(location.providerObjectBrace + 1),
|
|
5678
|
+
placementLabel: `providers["${providerLabel}"] -> (new modelOverrides entry "${modelId}")`,
|
|
5679
|
+
};
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5682
|
+
const diagnosis = analyzeModelsJsonForMissingEntry(originalText, providerLabel, modelId);
|
|
5683
|
+
if (!diagnosis || diagnosis.scenario !== "provider_missing") return undefined;
|
|
5684
|
+
const clean = stripJsoncComments(originalText);
|
|
5685
|
+
const providersIndent = lineIndentOf(originalText, diagnosis.providersEnd);
|
|
5686
|
+
const providerIndent = deriveInnerIndent(originalText, diagnosis.providersBrace, diagnosis.providersEnd);
|
|
5687
|
+
const unit = providerIndent.length > providersIndent.length
|
|
5688
|
+
? providerIndent.slice(providersIndent.length)
|
|
5689
|
+
: " ";
|
|
5690
|
+
const overridesIndent = providerIndent + unit;
|
|
5691
|
+
const entryIndent = overridesIndent + unit;
|
|
5692
|
+
const comma = previousTokenNeedsComma(clean, diagnosis.providersEnd) ? "," : "";
|
|
5693
|
+
const block = `${comma}\n${providerIndent}${JSON.stringify(providerLabel)}: {\n` +
|
|
5694
|
+
`${overridesIndent}"modelOverrides": {\n${formatOverrideEntry(entryIndent, unit)}\n` +
|
|
5695
|
+
`${overridesIndent}}\n${providerIndent}}\n${providersIndent}`;
|
|
5696
|
+
return {
|
|
5697
|
+
modifiedText: originalText.slice(0, diagnosis.providersEnd) + block + originalText.slice(diagnosis.providersEnd),
|
|
5698
|
+
placementLabel: `providers -> (new modelOverrides-only entry "${providerLabel}/${modelId}")`,
|
|
5699
|
+
};
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5303
5702
|
/**
|
|
5304
5703
|
* Surgically insert the missing provider/model entry into the original
|
|
5305
5704
|
* JSONC text. Returns the modified text and placement descriptor.
|
|
@@ -5458,10 +5857,10 @@ function composeMissingEntryInsertion(
|
|
|
5458
5857
|
}
|
|
5459
5858
|
|
|
5460
5859
|
/**
|
|
5461
|
-
* Lightweight self-check for a newly inserted entry.
|
|
5860
|
+
* Lightweight self-check for a newly inserted model or modelOverrides entry.
|
|
5462
5861
|
* Parses the modified text as JSONC and confirms:
|
|
5463
|
-
* 1. The target
|
|
5464
|
-
* 2. Every compat key has the expected
|
|
5862
|
+
* 1. The target exists under models[] or modelOverrides.
|
|
5863
|
+
* 2. Every compat key has the expected effective three-layer value.
|
|
5465
5864
|
* Returns null on success, an error string on failure.
|
|
5466
5865
|
*/
|
|
5467
5866
|
function selfCheckMissingEntryInsertion(
|
|
@@ -5472,34 +5871,88 @@ function selfCheckMissingEntryInsertion(
|
|
|
5472
5871
|
compatKeys: Record<string, unknown>,
|
|
5473
5872
|
): string | null {
|
|
5474
5873
|
try {
|
|
5874
|
+
const origParsed = parseJsonc(originalText);
|
|
5475
5875
|
const modParsed = parseJsonc(modifiedText);
|
|
5476
5876
|
const providers = asRecord(asRecord(modParsed)?.providers);
|
|
5477
5877
|
if (!providers) return "Modified file: providers object missing or invalid";
|
|
5478
5878
|
const provider = asRecord(providers[providerLabel]);
|
|
5479
5879
|
if (!provider) return `Modified file: provider "${providerLabel}" not found`;
|
|
5480
5880
|
const models = provider.models;
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5881
|
+
const targetModel = Array.isArray(models)
|
|
5882
|
+
? models.find((m: unknown) => asRecord(m)?.id === modelId)
|
|
5883
|
+
: undefined;
|
|
5884
|
+
const targetOverride = asRecord(asRecord(provider.modelOverrides)?.[modelId]);
|
|
5885
|
+
if (!targetModel && !targetOverride) {
|
|
5886
|
+
return `Modified file: model or modelOverrides entry "${modelId}" not found in provider after insertion`;
|
|
5887
|
+
}
|
|
5485
5888
|
|
|
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
5889
|
for (const [k, v] of Object.entries(compatKeys)) {
|
|
5497
|
-
|
|
5498
|
-
if (
|
|
5890
|
+
const effective = resolveExplicitCompatValue(modParsed, providerLabel, modelId, k);
|
|
5891
|
+
if (!effective) return `Modified file: effective compat.${k} not found`;
|
|
5892
|
+
if (effective.value !== v) {
|
|
5893
|
+
return `Modified file: effective compat.${k} wrong value: expected ${JSON.stringify(v)}, got ${JSON.stringify(effective.value)} from ${effective.source}`;
|
|
5894
|
+
}
|
|
5499
5895
|
}
|
|
5500
5896
|
|
|
5501
|
-
|
|
5502
|
-
|
|
5897
|
+
// Normalize only the intended override edit back to its original shape,
|
|
5898
|
+
// then require the complete parsed structure to match. This catches data
|
|
5899
|
+
// loss while allowing legitimate shorter repairs such as false -> true.
|
|
5900
|
+
const normalized = JSON.parse(JSON.stringify(modParsed)) as Record<string, unknown>;
|
|
5901
|
+
const origProviders = asRecord(asRecord(origParsed)?.providers);
|
|
5902
|
+
const origProvider = asRecord(origProviders?.[providerLabel]);
|
|
5903
|
+
const normalizedProviders = asRecord(normalized.providers);
|
|
5904
|
+
if (!normalizedProviders) return "Modified file: normalized providers object missing";
|
|
5905
|
+
|
|
5906
|
+
if (!origProvider) {
|
|
5907
|
+
delete normalizedProviders[providerLabel];
|
|
5908
|
+
} else if (targetOverride) {
|
|
5909
|
+
const normalizedProvider = asRecord(normalizedProviders[providerLabel]);
|
|
5910
|
+
const normalizedOverrides = asRecord(normalizedProvider?.modelOverrides);
|
|
5911
|
+
const origOverrides = asRecord(origProvider.modelOverrides);
|
|
5912
|
+
const origOverride = asRecord(origOverrides?.[modelId]);
|
|
5913
|
+
|
|
5914
|
+
if (!normalizedProvider || !normalizedOverrides) {
|
|
5915
|
+
return `Modified file: modelOverrides["${modelId}"] missing during structure validation`;
|
|
5916
|
+
}
|
|
5917
|
+
if (!origOverride) {
|
|
5918
|
+
delete normalizedOverrides[modelId];
|
|
5919
|
+
if (!origOverrides) delete normalizedProvider.modelOverrides;
|
|
5920
|
+
} else {
|
|
5921
|
+
const normalizedOverride = asRecord(normalizedOverrides[modelId]);
|
|
5922
|
+
if (!normalizedOverride) return `Modified file: modelOverrides["${modelId}"] invalid`;
|
|
5923
|
+
const origCompat = asRecord(origOverride.compat);
|
|
5924
|
+
const normalizedCompat = asRecord(normalizedOverride.compat);
|
|
5925
|
+
if (!origCompat) {
|
|
5926
|
+
delete normalizedOverride.compat;
|
|
5927
|
+
} else {
|
|
5928
|
+
if (!normalizedCompat) return `Modified file: modelOverrides["${modelId}"].compat invalid`;
|
|
5929
|
+
for (const key of Object.keys(compatKeys)) {
|
|
5930
|
+
if (Object.prototype.hasOwnProperty.call(origCompat, key)) {
|
|
5931
|
+
normalizedCompat[key] = origCompat[key];
|
|
5932
|
+
} else {
|
|
5933
|
+
delete normalizedCompat[key];
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
} else {
|
|
5939
|
+
// Backward-compatible validation for the legacy models[] insertion
|
|
5940
|
+
// helper retained in __internals_for_tests.
|
|
5941
|
+
const normalizedProvider = asRecord(normalizedProviders[providerLabel]);
|
|
5942
|
+
const normalizedModels = normalizedProvider?.models;
|
|
5943
|
+
const origModels = origProvider.models;
|
|
5944
|
+
if (!normalizedProvider || !Array.isArray(normalizedModels)) {
|
|
5945
|
+
return `Modified file: provider "${providerLabel}".models missing during structure validation`;
|
|
5946
|
+
}
|
|
5947
|
+
normalizedProvider.models = normalizedModels.filter(
|
|
5948
|
+
(entry: unknown) => asRecord(entry)?.id !== modelId,
|
|
5949
|
+
);
|
|
5950
|
+
if (!Array.isArray(origModels)) delete normalizedProvider.models;
|
|
5951
|
+
}
|
|
5952
|
+
|
|
5953
|
+
if (!deepEqualIgnoringKeys(normalized, origParsed, [])) {
|
|
5954
|
+
return "Modified file: original structure was altered (data loss detected)";
|
|
5955
|
+
}
|
|
5503
5956
|
|
|
5504
5957
|
const modClean = stripJsoncComments(modifiedText);
|
|
5505
5958
|
const rootStart = skipJsonWhitespace(modClean, 0);
|
|
@@ -5655,7 +6108,14 @@ function chooseFixPlacement(
|
|
|
5655
6108
|
compatKeys: Record<string, unknown>,
|
|
5656
6109
|
providerLabel: string,
|
|
5657
6110
|
forceModelLevel = false,
|
|
5658
|
-
): { placement: "provider" | "model"; reason: string } {
|
|
6111
|
+
): { placement: "provider" | "model" | "modelOverride"; reason: string } {
|
|
6112
|
+
if (location.modelOverrideObjectBrace >= 0) {
|
|
6113
|
+
return {
|
|
6114
|
+
placement: "modelOverride",
|
|
6115
|
+
reason: "an existing modelOverrides entry has Pi's highest precedence — repairing it directly",
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
|
|
5659
6119
|
if (forceModelLevel) {
|
|
5660
6120
|
return {
|
|
5661
6121
|
placement: "model",
|
|
@@ -5689,12 +6149,24 @@ function composeFixInsertion(
|
|
|
5689
6149
|
original: string,
|
|
5690
6150
|
location: ModelNodeLocation,
|
|
5691
6151
|
compatKeys: Record<string, unknown>,
|
|
5692
|
-
placement: "provider" | "model" = "model",
|
|
6152
|
+
placement: "provider" | "model" | "modelOverride" = "model",
|
|
5693
6153
|
): string {
|
|
5694
6154
|
// Resolve the target compat object and its container based on placement.
|
|
5695
|
-
const targetCompatBrace = placement === "provider"
|
|
5696
|
-
|
|
5697
|
-
|
|
6155
|
+
const targetCompatBrace = placement === "provider"
|
|
6156
|
+
? location.providerCompatBrace
|
|
6157
|
+
: placement === "modelOverride"
|
|
6158
|
+
? location.modelOverrideCompatBrace
|
|
6159
|
+
: location.compatObjectBrace;
|
|
6160
|
+
const targetCompatEnd = placement === "provider"
|
|
6161
|
+
? location.providerCompatEnd
|
|
6162
|
+
: placement === "modelOverride"
|
|
6163
|
+
? location.modelOverrideCompatEnd
|
|
6164
|
+
: location.compatObjectEnd;
|
|
6165
|
+
const containerBrace = placement === "provider"
|
|
6166
|
+
? location.providerObjectBrace
|
|
6167
|
+
: placement === "modelOverride"
|
|
6168
|
+
? location.modelOverrideObjectBrace
|
|
6169
|
+
: location.modelObjectBrace;
|
|
5698
6170
|
|
|
5699
6171
|
// Helper: format key/value pairs as lines with the given indent,
|
|
5700
6172
|
// alphabetically sorted for stable previews and deterministic edits.
|
|
@@ -5796,7 +6268,7 @@ function selfCheckFix(
|
|
|
5796
6268
|
providerLabel: string,
|
|
5797
6269
|
modelId: string,
|
|
5798
6270
|
compatKeys: Record<string, unknown>,
|
|
5799
|
-
placement: "provider" | "model" = "model",
|
|
6271
|
+
placement: "provider" | "model" | "modelOverride" = "model",
|
|
5800
6272
|
): string | null {
|
|
5801
6273
|
try {
|
|
5802
6274
|
// Step 1: Parse both versions as JSONC (comments + trailing commas allowed).
|
|
@@ -5840,9 +6312,9 @@ function selfCheckFix(
|
|
|
5840
6312
|
return `Original file: provider/model "${providerLabel}/${modelId}" not found`;
|
|
5841
6313
|
}
|
|
5842
6314
|
|
|
5843
|
-
// Step 5: Compute the EFFECTIVE merged compat
|
|
5844
|
-
//
|
|
5845
|
-
//
|
|
6315
|
+
// Step 5: Compute the EFFECTIVE merged compat using Pi's precedence:
|
|
6316
|
+
// provider, custom model, then modelOverrides. The fix may have written any
|
|
6317
|
+
// level, so validation must check what Pi will actually use.
|
|
5846
6318
|
const provCompatRaw = (provider as Record<string, unknown>).compat;
|
|
5847
6319
|
const provCompat = (provCompatRaw && typeof provCompatRaw === 'object' && !Array.isArray(provCompatRaw))
|
|
5848
6320
|
? provCompatRaw as Record<string, unknown>
|
|
@@ -5852,7 +6324,13 @@ function selfCheckFix(
|
|
|
5852
6324
|
return `Modified file: model "${modelId}" compat is not an object`;
|
|
5853
6325
|
}
|
|
5854
6326
|
const mdlCompat = (modelCompatRaw ?? {}) as Record<string, unknown>;
|
|
5855
|
-
const
|
|
6327
|
+
const override = asRecord(asRecord(provider.modelOverrides)?.[modelId]);
|
|
6328
|
+
const overrideCompatRaw = override?.compat;
|
|
6329
|
+
if (overrideCompatRaw !== undefined && !asRecord(overrideCompatRaw)) {
|
|
6330
|
+
return `Modified file: modelOverrides["${modelId}"].compat is not an object`;
|
|
6331
|
+
}
|
|
6332
|
+
const overrideCompat = asRecord(overrideCompatRaw) ?? {};
|
|
6333
|
+
const mergedCompat: Record<string, unknown> = { ...provCompat, ...mdlCompat, ...overrideCompat };
|
|
5856
6334
|
|
|
5857
6335
|
// Step 6: Validate all inserted keys are effective in the merged compat
|
|
5858
6336
|
for (const [k, v] of Object.entries(compatKeys)) {
|
|
@@ -5900,7 +6378,8 @@ function selfCheckFix(
|
|
|
5900
6378
|
// the placement-resolved object's own compat may be exempt.
|
|
5901
6379
|
const mayRepairThisCompat =
|
|
5902
6380
|
(placement === "provider" && origObj === origProvider) ||
|
|
5903
|
-
(placement === "model" && origObj === origTargetModelRecord)
|
|
6381
|
+
(placement === "model" && origObj === origTargetModelRecord) ||
|
|
6382
|
+
(placement === "modelOverride" && origObj === asRecord(asRecord(origProvider.modelOverrides)?.[modelId]));
|
|
5904
6383
|
for (const ck of Object.keys(origCompat)) {
|
|
5905
6384
|
if (!(ck in modCompat)) return false;
|
|
5906
6385
|
// The fix may repair an existing wrong compat value (for example
|
|
@@ -6191,6 +6670,16 @@ export const __internals_for_tests = {
|
|
|
6191
6670
|
parsePersistedRoutedModelRef,
|
|
6192
6671
|
routedModelRefToPiModel,
|
|
6193
6672
|
buildExactRouterStatusEntry,
|
|
6673
|
+
findBestRouterModelStats,
|
|
6674
|
+
selectFooterStatsForModel,
|
|
6675
|
+
parseFooterStatsMode,
|
|
6676
|
+
parsePersistedCacheOptimizerConfig,
|
|
6677
|
+
readPersistedFooterMode,
|
|
6678
|
+
writePersistedFooterMode,
|
|
6679
|
+
resolveFooterStatsMode,
|
|
6680
|
+
footerStatsMode,
|
|
6681
|
+
CONFIG_FILE_PATH,
|
|
6682
|
+
FOOTER_MODE_ENV,
|
|
6194
6683
|
// Routing-provider protocol helpers
|
|
6195
6684
|
PI_ROUTING_REGISTRY_SYMBOL,
|
|
6196
6685
|
PI_CACHE_HINTS_SYMBOL,
|
|
@@ -6222,11 +6711,15 @@ export const __internals_for_tests = {
|
|
|
6222
6711
|
stripJsoncComments,
|
|
6223
6712
|
stripJsoncTrailingCommas,
|
|
6224
6713
|
parseJsonc,
|
|
6714
|
+
resolveExplicitCompatValue,
|
|
6715
|
+
hasExplicitLongRetentionOptInFromConfig,
|
|
6225
6716
|
locateModelInJsonc,
|
|
6717
|
+
locateModelOverrideInJsonc,
|
|
6226
6718
|
composeFixInsertion,
|
|
6227
6719
|
selfCheckFix,
|
|
6228
6720
|
analyzeModelsJsonForMissingEntry,
|
|
6229
6721
|
composeMissingEntryInsertion,
|
|
6722
|
+
composeModelOverrideInsertion,
|
|
6230
6723
|
selfCheckMissingEntryInsertion,
|
|
6231
6724
|
decideFixPlacement,
|
|
6232
6725
|
chooseFixPlacement,
|
|
@@ -6625,50 +7118,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
6625
7118
|
await rollOverStatsIfNeeded(ctx);
|
|
6626
7119
|
}
|
|
6627
7120
|
|
|
6628
|
-
/**
|
|
6629
|
-
* Fallback for older persisted files that do not yet carry exact
|
|
6630
|
-
* last-routed-model metadata. When the current model is a router channel
|
|
6631
|
-
* (e.g. router/auto), restorable stats are stored under the real upstream
|
|
6632
|
-
* model's provider/id key, not under router/auto. Find the best valid entry
|
|
6633
|
-
* (highest totalRequests among adapter-detectable model keys) so we can show
|
|
6634
|
-
* meaningful footer content on session_start after reload.
|
|
6635
|
-
*/
|
|
6636
|
-
function findBestRouterModelStats(): { adapter: CacheProviderAdapter; stats: CacheStats } | undefined {
|
|
6637
|
-
let best: { adapter: CacheProviderAdapter; stats: CacheStats; total: number } | undefined;
|
|
6638
|
-
|
|
6639
|
-
for (const [modelKeyPart, stats] of Object.entries(cacheStatsTotalsByModel)) {
|
|
6640
|
-
const slashIdx = modelKeyPart.indexOf("/");
|
|
6641
|
-
if (slashIdx < 0 || slashIdx >= modelKeyPart.length - 1) continue;
|
|
6642
|
-
const modelId = modelKeyPart.slice(slashIdx + 1);
|
|
6643
|
-
const providerName = modelKeyPart.slice(0, slashIdx);
|
|
6644
|
-
|
|
6645
|
-
// Construct a minimal model for adapter detection.
|
|
6646
|
-
// Every is*LikeModel function only accesses model.id and model.name
|
|
6647
|
-
// via getModelIdNameTokenValues, so { id, name } is sufficient.
|
|
6648
|
-
const mockModel = {
|
|
6649
|
-
id: modelId,
|
|
6650
|
-
name: modelId,
|
|
6651
|
-
provider: providerName,
|
|
6652
|
-
api: "",
|
|
6653
|
-
baseUrl: "",
|
|
6654
|
-
reasoning: false,
|
|
6655
|
-
input: ["text"],
|
|
6656
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
6657
|
-
contextWindow: 0,
|
|
6658
|
-
maxTokens: 0,
|
|
6659
|
-
} as PiModel;
|
|
6660
|
-
|
|
6661
|
-
const adapter = selectAdapterForModel(mockModel);
|
|
6662
|
-
if (!adapter) continue;
|
|
6663
|
-
|
|
6664
|
-
if (!best || stats.totalRequests > best.total) {
|
|
6665
|
-
best = { adapter, stats, total: stats.totalRequests };
|
|
6666
|
-
}
|
|
6667
|
-
}
|
|
6668
|
-
|
|
6669
|
-
return best ? { adapter: best.adapter, stats: best.stats } : undefined;
|
|
6670
|
-
}
|
|
6671
|
-
|
|
6672
7121
|
async function publishStatus(ctx: ExtensionContext, model: PiModel | undefined = ctx.model): Promise<void> {
|
|
6673
7122
|
syncSessionHash(ctx);
|
|
6674
7123
|
await rollOverStatsIfNeeded(ctx);
|
|
@@ -6678,6 +7127,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6678
7127
|
const adapter = selectAdapterForModel(displayModel);
|
|
6679
7128
|
const activeIsVirtualRoute = !!routedModel || isVirtualRoutingModel(model, ctx);
|
|
6680
7129
|
let statusText: string | undefined;
|
|
7130
|
+
const mode = footerStatsMode();
|
|
7131
|
+
const sessionHash = currentSessionHashSet ? currentSessionHash : undefined;
|
|
6681
7132
|
|
|
6682
7133
|
if (!adapter && !routedModel && activeIsVirtualRoute) {
|
|
6683
7134
|
// On model_select (existing footer), keep the existing cache footer
|
|
@@ -6687,11 +7138,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
6687
7138
|
// heuristics only when no exact metadata exists.
|
|
6688
7139
|
if (lastStatusText !== undefined) return;
|
|
6689
7140
|
const realEntry = buildExactRouterStatusEntry(
|
|
6690
|
-
|
|
7141
|
+
sessionHash,
|
|
6691
7142
|
cacheStatsByModel,
|
|
6692
7143
|
lastActualRoutedModel,
|
|
6693
7144
|
cacheStatsTotalsByModel,
|
|
6694
|
-
|
|
7145
|
+
mode,
|
|
7146
|
+
) ?? findBestRouterModelStats(
|
|
7147
|
+
mode,
|
|
7148
|
+
sessionHash,
|
|
7149
|
+
cacheStatsByModel,
|
|
7150
|
+
cacheStatsTotalsByModel,
|
|
7151
|
+
);
|
|
6695
7152
|
if (realEntry) {
|
|
6696
7153
|
const statsText = formatCacheStats(realEntry.adapter, realEntry.stats);
|
|
6697
7154
|
statusText = runtimeOptimizerEnabled
|
|
@@ -6701,10 +7158,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
6701
7158
|
}
|
|
6702
7159
|
|
|
6703
7160
|
if (adapter) {
|
|
6704
|
-
//
|
|
6705
|
-
//
|
|
6706
|
-
|
|
6707
|
-
|
|
7161
|
+
// Footer mode defaults to daily provider/model totals. Users may select
|
|
7162
|
+
// current-session counters through persistent command config or the env var.
|
|
7163
|
+
const stats = displayModel
|
|
7164
|
+
? selectFooterStatsForModel(mode, sessionHash, cacheStatsByModel, cacheStatsTotalsByModel, displayModel)
|
|
7165
|
+
: undefined;
|
|
6708
7166
|
const statsText = formatCacheStats(adapter, stats ?? emptyCacheStats());
|
|
6709
7167
|
statusText = runtimeOptimizerEnabled ? statsText : `Cache Optimizer disabled · ${statsText}`;
|
|
6710
7168
|
}
|
|
@@ -6760,9 +7218,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6760
7218
|
|
|
6761
7219
|
/**
|
|
6762
7220
|
* Check whether a model has an EXPLICIT supportsLongCacheRetention: true
|
|
6763
|
-
* opt-in in models.json
|
|
6764
|
-
*
|
|
6765
|
-
* mergeCompat behaviour: model wins on conflicts).
|
|
7221
|
+
* opt-in in models.json. Precedence mirrors Pi's effective model config:
|
|
7222
|
+
* modelOverrides[model.id].compat, then models[].compat, then provider.compat.
|
|
6766
7223
|
*
|
|
6767
7224
|
* Returns true ONLY when the user explicitly opted in. Returns false for:
|
|
6768
7225
|
* - Explicit false (opt-out)
|
|
@@ -6776,31 +7233,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6776
7233
|
try {
|
|
6777
7234
|
const text = readFileSync(MODELS_JSON_PATH, "utf8");
|
|
6778
7235
|
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;
|
|
7236
|
+
return hasExplicitLongRetentionOptInFromConfig(parsed, model.provider, model.id);
|
|
6804
7237
|
} catch {
|
|
6805
7238
|
return false;
|
|
6806
7239
|
}
|
|
@@ -7174,17 +7607,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
7174
7607
|
// with low-hit diagnosis
|
|
7175
7608
|
// stats — show active model stats bucket, recent trend, usage
|
|
7176
7609
|
// compat — show compat suggestion with file path
|
|
7610
|
+
// config footer-mode session|total — persist footer mode override
|
|
7177
7611
|
// fix — auto-fix compat issues (writes models.json, requires UI)
|
|
7178
7612
|
// reset — reset current provider/model footer stats bucket (local only)
|
|
7179
7613
|
// (no args) — interactive menu (with UI) or help summary
|
|
7180
7614
|
// ────────────────────────────────────────────────────────────────
|
|
7181
7615
|
pi.registerCommand("cache-optimizer", {
|
|
7182
|
-
description: "
|
|
7616
|
+
description: "Configure and diagnose Pi cache behavior",
|
|
7183
7617
|
handler: async (args: string, cmdCtx) => {
|
|
7184
7618
|
syncSessionHash(cmdCtx);
|
|
7185
7619
|
const selectedModel = cmdCtx.model;
|
|
7186
7620
|
const model = resolveRouteModel(selectedModel, cmdCtx as unknown as ExtensionContext) ?? selectedModel;
|
|
7187
|
-
const
|
|
7621
|
+
const commandParts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
7622
|
+
const subcommand = commandParts[0] || "help";
|
|
7188
7623
|
|
|
7189
7624
|
if (subcommand === "enable") {
|
|
7190
7625
|
setRuntimeOptimizerEnabled(true);
|
|
@@ -7224,6 +7659,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
7224
7659
|
const samples = sk ? getRecentSamples(sk) : [];
|
|
7225
7660
|
const output = buildStatsOutput(model, adapter, statsState, samples);
|
|
7226
7661
|
cmdCtx.ui.notify(output, "info");
|
|
7662
|
+
} else if (subcommand === "config") {
|
|
7663
|
+
const configKey = commandParts[1];
|
|
7664
|
+
const requestedMode = commandParts[2];
|
|
7665
|
+
if (configKey !== "footer-mode" || !requestedMode || !["session", "total"].includes(requestedMode)) {
|
|
7666
|
+
const resolved = resolveFooterStatsMode(persistedFooterStatsMode);
|
|
7667
|
+
cmdCtx.ui.notify(
|
|
7668
|
+
`Usage: /cache-optimizer config footer-mode session|total\n` +
|
|
7669
|
+
`Current footer mode: ${resolved.mode} (${resolved.source})`,
|
|
7670
|
+
"info",
|
|
7671
|
+
);
|
|
7672
|
+
return;
|
|
7673
|
+
}
|
|
7674
|
+
|
|
7675
|
+
const nextMode = requestedMode as FooterStatsMode;
|
|
7676
|
+
try {
|
|
7677
|
+
await writePersistedFooterMode(nextMode);
|
|
7678
|
+
persistedFooterStatsMode = nextMode;
|
|
7679
|
+
lastStatusText = undefined;
|
|
7680
|
+
await publishStatus(cmdCtx as unknown as ExtensionContext, model);
|
|
7681
|
+
const resolved = resolveFooterStatsMode(persistedFooterStatsMode);
|
|
7682
|
+
cmdCtx.ui.notify(
|
|
7683
|
+
`✅ Footer mode set to ${resolved.mode}. Persistent config overrides ${FOOTER_MODE_ENV}.`,
|
|
7684
|
+
"info",
|
|
7685
|
+
);
|
|
7686
|
+
} catch (error) {
|
|
7687
|
+
cmdCtx.ui.notify(
|
|
7688
|
+
`❌ Could not update footer mode config: ${error instanceof Error ? error.message : String(error)}`,
|
|
7689
|
+
"error",
|
|
7690
|
+
);
|
|
7691
|
+
}
|
|
7227
7692
|
} else if (subcommand === "compat") {
|
|
7228
7693
|
if (!model) {
|
|
7229
7694
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
@@ -7318,8 +7783,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7318
7783
|
}
|
|
7319
7784
|
manualLines.push(
|
|
7320
7785
|
"",
|
|
7321
|
-
"
|
|
7322
|
-
`providers["${suggestion.providerLabel}"] ->
|
|
7786
|
+
"Add these compat keys at Pi's highest-precedence model override path:",
|
|
7787
|
+
`providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat:`,
|
|
7323
7788
|
formatCompatKeysForInsertion(suggestion.compatKeys),
|
|
7324
7789
|
);
|
|
7325
7790
|
if (snippet.length > 0) {
|
|
@@ -7353,11 +7818,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
7353
7818
|
if (!location) {
|
|
7354
7819
|
const diagnosis = analyzeModelsJsonForMissingEntry(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
7355
7820
|
if (diagnosis && cmdCtx.hasUI) {
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7821
|
+
const overrideLocation = locateModelOverrideInJsonc(
|
|
7822
|
+
originalText, suggestion.providerLabel, suggestion.modelId,
|
|
7823
|
+
);
|
|
7824
|
+
const repairsExistingOverride = (overrideLocation?.modelOverrideObjectBrace ?? -1) >= 0;
|
|
7825
|
+
// Prefer a modelOverrides edit when models[] has no target entry.
|
|
7826
|
+
const plan = composeModelOverrideInsertion(
|
|
7827
|
+
originalText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
7360
7828
|
);
|
|
7829
|
+
if (!plan) {
|
|
7830
|
+
cmdCtx.ui.notify(
|
|
7831
|
+
`❌ Could not safely locate a modelOverrides insertion point.\n` +
|
|
7832
|
+
`Falling back to manual guidance. No changes were made.`,
|
|
7833
|
+
"error",
|
|
7834
|
+
);
|
|
7835
|
+
} else {
|
|
7361
7836
|
const checkError = selfCheckMissingEntryInsertion(
|
|
7362
7837
|
originalText, plan.modifiedText,
|
|
7363
7838
|
suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
@@ -7382,7 +7857,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7382
7857
|
keysPreview,
|
|
7383
7858
|
``,
|
|
7384
7859
|
`⚠️ Risk notice:`,
|
|
7385
|
-
|
|
7860
|
+
repairsExistingOverride
|
|
7861
|
+
? ` 1. This updates the existing modelOverrides entry for "${suggestion.modelId}". Existing auth is not affected.`
|
|
7862
|
+
: ` 1. This creates a modelOverrides entry in models.json. Existing auth (e.g. login API tokens) is not affected.`,
|
|
7386
7863
|
` 2. A timestamped backup will be written to: ${backupPath}`,
|
|
7387
7864
|
` 3. You must run /reload or restart Pi for the change to take effect.`,
|
|
7388
7865
|
` 4. If the file contains comments or unusual formatting, please verify the result after write.`,
|
|
@@ -7397,7 +7874,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
7397
7874
|
);
|
|
7398
7875
|
}
|
|
7399
7876
|
previewLines.push("", `Apply these changes?`);
|
|
7400
|
-
const confirmed = await cmdCtx.ui.confirm(
|
|
7877
|
+
const confirmed = await cmdCtx.ui.confirm(
|
|
7878
|
+
repairsExistingOverride ? "Cache Optimizer — Fix (model override)" : "Cache Optimizer — Fix (new override)",
|
|
7879
|
+
previewLines.join("\n"),
|
|
7880
|
+
);
|
|
7401
7881
|
if (confirmed) {
|
|
7402
7882
|
try {
|
|
7403
7883
|
await copyFile(MODELS_JSON_PATH, backupPath);
|
|
@@ -7436,6 +7916,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7436
7916
|
cmdCtx.ui.notify("No changes were made. Canceled by user.", "info");
|
|
7437
7917
|
return;
|
|
7438
7918
|
}
|
|
7919
|
+
}
|
|
7439
7920
|
}
|
|
7440
7921
|
|
|
7441
7922
|
// Non-interactive or no diagnosis: show manual guidance.
|
|
@@ -7448,7 +7929,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7448
7929
|
`❌ Could not locate model "${suggestion.modelId}" or provider "${suggestion.providerLabel}" in ${getModelsJsonDisplayPath()}.`,
|
|
7449
7930
|
"",
|
|
7450
7931
|
"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
|
|
7932
|
+
"entries in models.json. You can create a minimal modelOverrides entry by hand:",
|
|
7452
7933
|
);
|
|
7453
7934
|
} else if (diagnosis.scenario === "provider_missing") {
|
|
7454
7935
|
adviceLines.push(
|
|
@@ -7463,7 +7944,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7463
7944
|
`ℹ️ Model "${suggestion.modelId}" was not found in ${getModelsJsonDisplayPath()}`,
|
|
7464
7945
|
`under providers["${suggestion.providerLabel}"].`,
|
|
7465
7946
|
"",
|
|
7466
|
-
"Add the following entry
|
|
7947
|
+
"Add the following modelOverrides entry (keep existing auth):",
|
|
7467
7948
|
);
|
|
7468
7949
|
}
|
|
7469
7950
|
adviceLines.push("", snippet, "", "Then save and run /reload.");
|
|
@@ -7496,11 +7977,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
7496
7977
|
// Build preview snippet as copyable JSON (the surgical editor will
|
|
7497
7978
|
// insert or repair these exact compat key/value pairs).
|
|
7498
7979
|
const keysPreview = JSON.stringify(suggestion.compatKeys, null, 2);
|
|
7499
|
-
const targetHasCompat = decision.placement === "provider"
|
|
7980
|
+
const targetHasCompat = decision.placement === "provider"
|
|
7981
|
+
? location.providerCompatBrace >= 0
|
|
7982
|
+
: decision.placement === "modelOverride"
|
|
7983
|
+
? location.modelOverrideCompatBrace >= 0
|
|
7984
|
+
: location.compatObjectBrace >= 0;
|
|
7500
7985
|
const placementDesc = targetHasCompat ? `existing "compat" object` : `new "compat" object`;
|
|
7501
7986
|
const locationDesc = decision.placement === "provider"
|
|
7502
7987
|
? `providers["${suggestion.providerLabel}"] -> compat (provider level, ${placementDesc})`
|
|
7503
|
-
:
|
|
7988
|
+
: decision.placement === "modelOverride"
|
|
7989
|
+
? `providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat (${placementDesc})`
|
|
7990
|
+
: `providers["${suggestion.providerLabel}"] -> models -> "${suggestion.modelId}" -> compat (model level, ${placementDesc})`;
|
|
7504
7991
|
|
|
7505
7992
|
const ts = backupTimestamp();
|
|
7506
7993
|
const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
|
|
@@ -7668,26 +8155,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
7668
8155
|
}
|
|
7669
8156
|
|
|
7670
8157
|
const location = locateModelInJsonc(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
7671
|
-
|
|
8158
|
+
const menuDecision = location
|
|
8159
|
+
? chooseFixPlacement(
|
|
8160
|
+
originalText,
|
|
8161
|
+
location,
|
|
8162
|
+
suggestion.compatKeys,
|
|
8163
|
+
suggestion.providerLabel,
|
|
8164
|
+
suggestion.forceModelLevel,
|
|
8165
|
+
)
|
|
8166
|
+
: undefined;
|
|
8167
|
+
const overridePlan = location
|
|
8168
|
+
? undefined
|
|
8169
|
+
: composeModelOverrideInsertion(
|
|
8170
|
+
originalText,
|
|
8171
|
+
suggestion.providerLabel,
|
|
8172
|
+
suggestion.modelId,
|
|
8173
|
+
suggestion.compatKeys,
|
|
8174
|
+
);
|
|
8175
|
+
if (!location && !overridePlan) {
|
|
7672
8176
|
cmdCtx.ui.notify(
|
|
7673
|
-
`❌ Could not locate
|
|
7674
|
-
`Manual edit required
|
|
7675
|
-
`${formatCompatKeysForInsertion(suggestion.compatKeys)}\n` +
|
|
8177
|
+
`❌ Could not safely locate a modelOverrides insertion point in ${getModelsJsonDisplayPath()}.\n` +
|
|
8178
|
+
`Manual edit required:\n${formatMissingEntryManualSnippet(suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys)}\n` +
|
|
7676
8179
|
`Then run /reload.`,
|
|
7677
8180
|
"warning",
|
|
7678
8181
|
);
|
|
7679
8182
|
return;
|
|
7680
8183
|
}
|
|
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);
|
|
8184
|
+
const modifiedText = location && menuDecision
|
|
8185
|
+
? composeFixInsertion(originalText, location, suggestion.compatKeys, menuDecision.placement)
|
|
8186
|
+
: overridePlan!.modifiedText;
|
|
8187
|
+
const checkError = location && menuDecision
|
|
8188
|
+
? selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement)
|
|
8189
|
+
: selfCheckMissingEntryInsertion(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
7691
8190
|
if (checkError !== null) {
|
|
7692
8191
|
cmdCtx.ui.notify(`❌ Self-check failed: ${checkError}\nNo changes made.`, "error");
|
|
7693
8192
|
return;
|
|
@@ -7697,17 +8196,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
7697
8196
|
const ts = backupTimestamp();
|
|
7698
8197
|
const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
|
|
7699
8198
|
|
|
7700
|
-
const menuLocationDesc = menuDecision
|
|
8199
|
+
const menuLocationDesc = overridePlan?.placementLabel ?? (menuDecision?.placement === "provider"
|
|
7701
8200
|
? `providers["${suggestion.providerLabel}"] -> compat (provider level)`
|
|
7702
|
-
:
|
|
7703
|
-
|
|
7704
|
-
|
|
8201
|
+
: menuDecision?.placement === "modelOverride"
|
|
8202
|
+
? `providers["${suggestion.providerLabel}"] -> modelOverrides -> "${suggestion.modelId}" -> compat`
|
|
8203
|
+
: `providers["${suggestion.providerLabel}"] -> models -> "${suggestion.modelId}" -> compat (model level)`);
|
|
8204
|
+
const menuScopeRiskLine = menuDecision?.placement === "provider"
|
|
8205
|
+
? ` 1. This change applies to ALL ${location?.allModelIds.length || 1} model(s) in the "${suggestion.providerLabel}" provider, across all sessions.`
|
|
7705
8206
|
: ` 1. This change affects ALL sessions using the "${suggestion.providerLabel}" provider/channel (scoped to model "${suggestion.modelId}").`;
|
|
7706
8207
|
|
|
7707
8208
|
const previewLines = [
|
|
7708
8209
|
`📝 Preview of changes to ${getModelsJsonDisplayPath()}:`,
|
|
7709
8210
|
`Location: ${menuLocationDesc}`,
|
|
7710
|
-
`Placement: ${menuDecision.placement} level — ${menuDecision.reason}`,
|
|
8211
|
+
`Placement: ${menuDecision ? `${menuDecision.placement} level — ${menuDecision.reason}` : "modelOverrides — built-in/API-login-safe model override"}`,
|
|
7711
8212
|
`Compat JSON to write:`,
|
|
7712
8213
|
keysPreview,
|
|
7713
8214
|
``,
|
|
@@ -7733,7 +8234,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7733
8234
|
await rename(tempPath, MODELS_JSON_PATH);
|
|
7734
8235
|
|
|
7735
8236
|
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
7736
|
-
const postCheck =
|
|
8237
|
+
const postCheck = location && menuDecision
|
|
8238
|
+
? selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement)
|
|
8239
|
+
: selfCheckMissingEntryInsertion(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
7737
8240
|
if (postCheck !== null) {
|
|
7738
8241
|
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
7739
8242
|
cmdCtx.ui.notify(`❌ Post-write check failed: ${postCheck}\nBackup restored.`, "error");
|
|
@@ -7784,10 +8287,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
7784
8287
|
diagnosis.push(" doctor — Show current model/provider/api/baseUrl/compat and low-hit diagnosis");
|
|
7785
8288
|
diagnosis.push(" stats — Show active model stats bucket and recent trend");
|
|
7786
8289
|
diagnosis.push(" compat — Show compat suggestion with edit location");
|
|
8290
|
+
diagnosis.push(" config footer-mode session|total — Persist the footer stats mode");
|
|
7787
8291
|
diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
|
|
7788
8292
|
diagnosis.push(" reset — Reset local provider/model stats for current model (does not affect upstream)");
|
|
7789
8293
|
diagnosis.push("");
|
|
7790
8294
|
diagnosis.push(formatOptimizerRuntimeMode());
|
|
8295
|
+
const resolvedFooterMode = resolveFooterStatsMode(persistedFooterStatsMode);
|
|
8296
|
+
diagnosis.push(`Footer stats mode: ${resolvedFooterMode.mode} (${resolvedFooterMode.source})`);
|
|
7791
8297
|
diagnosis.push("");
|
|
7792
8298
|
if (model) {
|
|
7793
8299
|
const displayKey = modelKey(model);
|