pi-cache-optimizer 2.6.7 → 2.6.10
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/index.ts +613 -48
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import type { BuildSystemPromptOptions, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -958,12 +959,12 @@ function getNonNegativeNumber(record: UnknownRecord, key: string): number | unde
|
|
|
958
959
|
*/
|
|
959
960
|
function getCompat(model: PiModel | undefined): CacheCompat {
|
|
960
961
|
if (!model) return {} as CacheCompat;
|
|
961
|
-
|
|
962
|
+
|
|
962
963
|
// Pi merges provider.compat with model.compat (model wins on conflicts)
|
|
963
964
|
// We approximate this by reading from ctx.model which should already have merged compat
|
|
964
965
|
// However, for safety, we check both levels if available
|
|
965
966
|
const modelCompat = (model.compat ?? {}) as CacheCompat;
|
|
966
|
-
|
|
967
|
+
|
|
967
968
|
// Note: ctx.model from Pi should already contain merged compat,
|
|
968
969
|
// but we document the two-level structure for clarity
|
|
969
970
|
return modelCompat;
|
|
@@ -1941,6 +1942,13 @@ function describeMissingOpenAICompatibleProxyCompat(model: PiModel): string[] {
|
|
|
1941
1942
|
missing.push("sendSessionAffinityHeaders");
|
|
1942
1943
|
}
|
|
1943
1944
|
|
|
1945
|
+
// NOTE: supportsLongCacheRetention is intentionally NOT checked here.
|
|
1946
|
+
// Per spec, it is optional/risky advisory text only and must NOT trigger
|
|
1947
|
+
// the ⚠️ compat marker. The before_provider_request hook proactively
|
|
1948
|
+
// strips prompt_cache_retention for models without explicit opt-in,
|
|
1949
|
+
// so 400 errors are prevented regardless of this compat flag.
|
|
1950
|
+
// Doctor/compat may mention it as optional guidance separately.
|
|
1951
|
+
|
|
1944
1952
|
return missing;
|
|
1945
1953
|
}
|
|
1946
1954
|
|
|
@@ -1963,6 +1971,9 @@ function buildSafeOpenAIProxyCompatSuggestion(missing: string[]): Record<string,
|
|
|
1963
1971
|
if (missing.includes("sendSessionAffinityHeaders")) {
|
|
1964
1972
|
suggestion.sendSessionAffinityHeaders = true;
|
|
1965
1973
|
}
|
|
1974
|
+
// supportsLongCacheRetention is NOT suggested here — per spec it is
|
|
1975
|
+
// optional/risky and must not appear in the copyable safe snippet.
|
|
1976
|
+
// The proactive stripping in before_provider_request handles 400 prevention.
|
|
1966
1977
|
return suggestion;
|
|
1967
1978
|
}
|
|
1968
1979
|
|
|
@@ -1984,6 +1995,10 @@ function hasPromptCacheRetentionUnsupportedSignal(headers: Record<string, string
|
|
|
1984
1995
|
"unknown parameter",
|
|
1985
1996
|
"not supported",
|
|
1986
1997
|
"unsupported field",
|
|
1998
|
+
"extra inputs",
|
|
1999
|
+
"not permitted",
|
|
2000
|
+
"unrecognized",
|
|
2001
|
+
"bad request",
|
|
1987
2002
|
].some((needle) => normalized.includes(needle));
|
|
1988
2003
|
}
|
|
1989
2004
|
|
|
@@ -4714,6 +4729,308 @@ function locateModelInJsonc(
|
|
|
4714
4729
|
};
|
|
4715
4730
|
}
|
|
4716
4731
|
|
|
4732
|
+
/**
|
|
4733
|
+
* Scan produced by `analyzeModelsJsonForMissingEntry` when
|
|
4734
|
+
* `locateModelInJsonc` cannot find the target provider/model.
|
|
4735
|
+
*/
|
|
4736
|
+
type MissingEntryDiagnosis =
|
|
4737
|
+
| { scenario: "provider_missing"; providersEnd: number }
|
|
4738
|
+
| { scenario: "model_missing"; modelsEnd: number; providerBrace: number; providerEndBrace: number }
|
|
4739
|
+
| { scenario: "provider_without_models"; providerBrace: number; providerEndBrace: number };
|
|
4740
|
+
|
|
4741
|
+
/**
|
|
4742
|
+
* Light second-pass scan that determines *why* `locateModelInJsonc` failed.
|
|
4743
|
+
* Returns structured diagnostic so the fix handler can compose targeted
|
|
4744
|
+
* guidance and an optional surgical insertion for API-logged-in models
|
|
4745
|
+
* (e.g. opencode go) that never appear in `models.json`.
|
|
4746
|
+
*/
|
|
4747
|
+
function analyzeModelsJsonForMissingEntry(
|
|
4748
|
+
text: string,
|
|
4749
|
+
providerLabel: string,
|
|
4750
|
+
modelId: string,
|
|
4751
|
+
): MissingEntryDiagnosis | undefined {
|
|
4752
|
+
const clean = stripJsoncComments(text);
|
|
4753
|
+
const rootBrace = skipJsonWhitespace(clean, 0);
|
|
4754
|
+
if (clean[rootBrace] !== "{") return undefined;
|
|
4755
|
+
|
|
4756
|
+
const providersKey = findJsonObjectKey(clean, rootBrace, "providers");
|
|
4757
|
+
if (!providersKey) {
|
|
4758
|
+
// Root has no "providers" key at all — we don't auto-create one.
|
|
4759
|
+
return undefined;
|
|
4760
|
+
}
|
|
4761
|
+
const providersBrace = skipJsonWhitespace(clean, providersKey.valueStart);
|
|
4762
|
+
if (clean[providersBrace] !== "{") return undefined;
|
|
4763
|
+
const providersEnd = findMatchingBracket(clean, providersBrace);
|
|
4764
|
+
if (providersEnd === undefined) return undefined;
|
|
4765
|
+
|
|
4766
|
+
const providerKey = findJsonObjectKey(clean, providersBrace, providerLabel);
|
|
4767
|
+
if (!providerKey || providerKey.keyStart > providersEnd) {
|
|
4768
|
+
return { scenario: "provider_missing", providersEnd };
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4771
|
+
// Provider exists. Check for a models array so we know where to append.
|
|
4772
|
+
const providerBrace = skipJsonWhitespace(clean, providerKey.valueStart);
|
|
4773
|
+
if (clean[providerBrace] !== "{") return undefined;
|
|
4774
|
+
const providerEndBrace = findMatchingBracket(clean, providerBrace);
|
|
4775
|
+
if (providerEndBrace === undefined || providerEndBrace > providersEnd) return undefined;
|
|
4776
|
+
|
|
4777
|
+
const modelsKey = findJsonObjectKey(clean, providerBrace, "models");
|
|
4778
|
+
if (modelsKey && modelsKey.keyStart < providerEndBrace) {
|
|
4779
|
+
let mScan = skipJsonWhitespace(clean, modelsKey.valueStart);
|
|
4780
|
+
if (clean[mScan] === "[") {
|
|
4781
|
+
const modelsEnd = findMatchingBracket(clean, mScan);
|
|
4782
|
+
if (modelsEnd !== undefined && modelsEnd <= providerEndBrace) {
|
|
4783
|
+
return { scenario: "model_missing", modelsEnd, providerBrace, providerEndBrace };
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
// Provider exists, but there's no discoverable models array — treat as
|
|
4789
|
+
// a provider that needs one.
|
|
4790
|
+
return { scenario: "provider_without_models", providerBrace, providerEndBrace };
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4793
|
+
/**
|
|
4794
|
+
* Build a copyable manual-edit snippet for the missing entry. Used when the
|
|
4795
|
+
* terminal is non-interactive or the user chooses to edit by hand.
|
|
4796
|
+
* Returns a complete provider→model→compat JSON block that the user can
|
|
4797
|
+
* paste into `models.json` under `providers`.
|
|
4798
|
+
*/
|
|
4799
|
+
function formatMissingEntryManualSnippet(
|
|
4800
|
+
providerLabel: string,
|
|
4801
|
+
modelId: string,
|
|
4802
|
+
compatKeys: Record<string, unknown>,
|
|
4803
|
+
): string {
|
|
4804
|
+
const lines: string[] = [];
|
|
4805
|
+
const sorted = Object.entries(compatKeys).sort(([a], [b]) => a.localeCompare(b));
|
|
4806
|
+
const compatItems = sorted.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`);
|
|
4807
|
+
lines.push(`"${providerLabel}": {`);
|
|
4808
|
+
lines.push(` "models": [`);
|
|
4809
|
+
lines.push(` {`);
|
|
4810
|
+
lines.push(` "id": ${JSON.stringify(modelId)},`);
|
|
4811
|
+
lines.push(` "compat": {`);
|
|
4812
|
+
lines.push(compatItems.join(",\n"));
|
|
4813
|
+
lines.push(` }`);
|
|
4814
|
+
lines.push(` }`);
|
|
4815
|
+
lines.push(` ]`);
|
|
4816
|
+
lines.push(` }`);
|
|
4817
|
+
return lines.join("\n");
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
/**
|
|
4821
|
+
* Surgically insert the missing provider/model entry into the original
|
|
4822
|
+
* JSONC text. Returns the modified text and placement descriptor.
|
|
4823
|
+
*
|
|
4824
|
+
* Handles three scenarios:
|
|
4825
|
+
* - `model_missing`: append a new model object to the provider's `models` array.
|
|
4826
|
+
* - `provider_missing`: append a new provider block to the root `providers` object.
|
|
4827
|
+
* - `provider_without_models`: inject a `"models": [...]` key into the existing provider.
|
|
4828
|
+
*/
|
|
4829
|
+
function composeMissingEntryInsertion(
|
|
4830
|
+
originalText: string,
|
|
4831
|
+
diagnosis: MissingEntryDiagnosis,
|
|
4832
|
+
providerLabel: string,
|
|
4833
|
+
modelId: string,
|
|
4834
|
+
compatKeys: Record<string, unknown>,
|
|
4835
|
+
): { modifiedText: string; placementLabel: string } {
|
|
4836
|
+
// Comments preserve length when stripped (`stripJsoncComments` replaces
|
|
4837
|
+
// comment bytes 1-for-1 with spaces), so offsets derived from the
|
|
4838
|
+
// comment-stripped text map cleanly back to the original. However,
|
|
4839
|
+
// `lastIndexOf("{", pos)` / `lastIndexOf("[", pos)` must NOT be run
|
|
4840
|
+
// against the raw original: a comment like `// add [more] here with a {
|
|
4841
|
+
// brace` would surface `[` / `{` bytes that have no structural meaning,
|
|
4842
|
+
// contaminating the `hasExisting`/`hasExistingElements` decision below
|
|
4843
|
+
// and producing a stray leading comma. Run the structural searches
|
|
4844
|
+
// against the comment-stripped version; keep the indentation lookups
|
|
4845
|
+
// (which only care about newlines + leading whitespace) on the
|
|
4846
|
+
// original since comments never contain forward-scan-relevant bytes.
|
|
4847
|
+
const cleanText = stripJsoncComments(originalText);
|
|
4848
|
+
|
|
4849
|
+
// Resolve a sensible indentation step from an arbitrary byte offset in
|
|
4850
|
+
// the original file.
|
|
4851
|
+
const indentUnitAt = (offset: number): string => {
|
|
4852
|
+
const ls = originalText.lastIndexOf("\n", offset);
|
|
4853
|
+
const line = originalText.slice(ls < 0 ? 0 : ls + 1, offset);
|
|
4854
|
+
const m = line.match(/^(\s+)/);
|
|
4855
|
+
return m ? m[1] : " ";
|
|
4856
|
+
};
|
|
4857
|
+
|
|
4858
|
+
// Figure out the base indent from the insertion point's own line.
|
|
4859
|
+
// Then derive inner indents (+1 and +2 levels).
|
|
4860
|
+
const sorted = Object.entries(compatKeys).sort(([a], [b]) => a.localeCompare(b));
|
|
4861
|
+
const formatCompactCompat = (indent: string): string => {
|
|
4862
|
+
// Single-line compact when there's only one key, multi-line otherwise.
|
|
4863
|
+
if (sorted.length === 1) {
|
|
4864
|
+
const [k, v] = sorted[0];
|
|
4865
|
+
return `{ ${JSON.stringify(k)}: ${JSON.stringify(v)} }`;
|
|
4866
|
+
}
|
|
4867
|
+
return (
|
|
4868
|
+
"{\n" +
|
|
4869
|
+
sorted.map(([k, v]) => `${indent}${JSON.stringify(k)}: ${JSON.stringify(v)}`).join(",\n") +
|
|
4870
|
+
"\n" +
|
|
4871
|
+
indent.slice(0, -2) +
|
|
4872
|
+
"}"
|
|
4873
|
+
);
|
|
4874
|
+
};
|
|
4875
|
+
|
|
4876
|
+
if (diagnosis.scenario === "model_missing") {
|
|
4877
|
+
// Append to the provider's models array, right before `]`.
|
|
4878
|
+
const unit = indentUnitAt(diagnosis.modelsEnd);
|
|
4879
|
+
const inner0 = unit + unit; // indent of model object's own keys
|
|
4880
|
+
const inner1 = inner0 + unit; // indent of compat keys inside the model
|
|
4881
|
+
const inner2 = inner1 + unit; // indent of compat values
|
|
4882
|
+
|
|
4883
|
+
// Determine whether the array is empty (need to skip the leading comma).
|
|
4884
|
+
// Search for the models `[` on the comment-stripped text so a `[` inside
|
|
4885
|
+
// a comment cannot be mistaken for the array opener.
|
|
4886
|
+
const arrayInterior = cleanText.slice(
|
|
4887
|
+
cleanText.lastIndexOf("[", diagnosis.modelsEnd) + 1,
|
|
4888
|
+
diagnosis.modelsEnd,
|
|
4889
|
+
).trim();
|
|
4890
|
+
const hasExistingElements = arrayInterior.length > 0;
|
|
4891
|
+
|
|
4892
|
+
const compatBlock = formatCompactCompat(inner2);
|
|
4893
|
+
const modelBlock = [
|
|
4894
|
+
hasExistingElements ? "," : "",
|
|
4895
|
+
inner0 + "{",
|
|
4896
|
+
inner1 + `"id": ${JSON.stringify(modelId)},`,
|
|
4897
|
+
inner1 + `"compat": ` + compatBlock,
|
|
4898
|
+
inner0 + "}",
|
|
4899
|
+
unit,
|
|
4900
|
+
].filter(Boolean).join("\n");
|
|
4901
|
+
|
|
4902
|
+
const insertionPoint = diagnosis.modelsEnd;
|
|
4903
|
+
const prefix = originalText.slice(0, insertionPoint);
|
|
4904
|
+
const suffix = originalText.slice(insertionPoint); // starts with `]`
|
|
4905
|
+
return {
|
|
4906
|
+
modifiedText: prefix + modelBlock + suffix,
|
|
4907
|
+
placementLabel: `providers["${providerLabel}"] -> models -> (new entry for "${modelId}")`,
|
|
4908
|
+
};
|
|
4909
|
+
}
|
|
4910
|
+
|
|
4911
|
+
if (diagnosis.scenario === "provider_missing") {
|
|
4912
|
+
// Append a new provider entry to the root `providers` object, right
|
|
4913
|
+
// before its closing `}`.
|
|
4914
|
+
const unit = indentUnitAt(diagnosis.providersEnd);
|
|
4915
|
+
const inner0 = unit + unit;
|
|
4916
|
+
const inner1 = inner0 + unit;
|
|
4917
|
+
const inner2 = inner1 + unit;
|
|
4918
|
+
const inner3 = inner2 + unit;
|
|
4919
|
+
|
|
4920
|
+
const compatBlock = formatCompactCompat(inner3);
|
|
4921
|
+
// Search for the providers `{` on the comment-stripped text so a `{`
|
|
4922
|
+
// inside a comment cannot be mistaken for the providers object opener.
|
|
4923
|
+
const providersInterior = cleanText.slice(
|
|
4924
|
+
cleanText.lastIndexOf("{", diagnosis.providersEnd) + 1,
|
|
4925
|
+
diagnosis.providersEnd,
|
|
4926
|
+
).trim();
|
|
4927
|
+
const hasExisting = providersInterior.length > 0;
|
|
4928
|
+
|
|
4929
|
+
const providerBlock = [
|
|
4930
|
+
hasExisting ? "," : "",
|
|
4931
|
+
inner0 + `"${providerLabel}": {`,
|
|
4932
|
+
inner1 + `"models": [`,
|
|
4933
|
+
inner2 + "{",
|
|
4934
|
+
inner3 + `"id": ${JSON.stringify(modelId)},`,
|
|
4935
|
+
inner3 + `"compat": ` + compatBlock,
|
|
4936
|
+
inner2 + "}",
|
|
4937
|
+
inner1 + "]",
|
|
4938
|
+
inner0 + "}",
|
|
4939
|
+
unit,
|
|
4940
|
+
].filter(Boolean).join("\n");
|
|
4941
|
+
|
|
4942
|
+
const insertionPoint = diagnosis.providersEnd;
|
|
4943
|
+
const prefix = originalText.slice(0, insertionPoint);
|
|
4944
|
+
const suffix = originalText.slice(insertionPoint);
|
|
4945
|
+
return {
|
|
4946
|
+
modifiedText: prefix + providerBlock + suffix,
|
|
4947
|
+
placementLabel: `providers -> (new entry "${providerLabel}")`,
|
|
4948
|
+
};
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
// `provider_without_models`: inject a models array key into the
|
|
4952
|
+
// existing provider block, right after the provider's opening `{`.
|
|
4953
|
+
const unit = indentUnitAt(diagnosis.providerBrace);
|
|
4954
|
+
const inner0 = unit + unit;
|
|
4955
|
+
const inner1 = inner0 + unit;
|
|
4956
|
+
const inner2 = inner1 + unit;
|
|
4957
|
+
|
|
4958
|
+
const compatBlock = formatCompactCompat(inner2);
|
|
4959
|
+
const afterBrace = diagnosis.providerBrace + 1;
|
|
4960
|
+
const modelsBlock = [
|
|
4961
|
+
"",
|
|
4962
|
+
inner0 + `"models": [`,
|
|
4963
|
+
inner1 + "{",
|
|
4964
|
+
inner2 + `"id": ${JSON.stringify(modelId)},`,
|
|
4965
|
+
inner2 + `"compat": ` + compatBlock,
|
|
4966
|
+
inner1 + "}",
|
|
4967
|
+
inner0 + "],",
|
|
4968
|
+
unit,
|
|
4969
|
+
].join("\n");
|
|
4970
|
+
|
|
4971
|
+
return {
|
|
4972
|
+
modifiedText: originalText.slice(0, afterBrace) + modelsBlock + originalText.slice(afterBrace),
|
|
4973
|
+
placementLabel: `providers["${providerLabel}"] -> (new "models" array with "${modelId}")`,
|
|
4974
|
+
};
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
/**
|
|
4978
|
+
* Lightweight self-check for a newly inserted entry.
|
|
4979
|
+
* Parses the modified text as JSONC and confirms:
|
|
4980
|
+
* 1. The target model exists under the provider.
|
|
4981
|
+
* 2. Every compat key has the expected value (merged provider+model).
|
|
4982
|
+
* Returns null on success, an error string on failure.
|
|
4983
|
+
*/
|
|
4984
|
+
function selfCheckMissingEntryInsertion(
|
|
4985
|
+
originalText: string,
|
|
4986
|
+
modifiedText: string,
|
|
4987
|
+
providerLabel: string,
|
|
4988
|
+
modelId: string,
|
|
4989
|
+
compatKeys: Record<string, unknown>,
|
|
4990
|
+
): string | null {
|
|
4991
|
+
try {
|
|
4992
|
+
const modParsed = parseJsonc(modifiedText);
|
|
4993
|
+
const providers = asRecord(asRecord(modParsed)?.providers);
|
|
4994
|
+
if (!providers) return "Modified file: providers object missing or invalid";
|
|
4995
|
+
const provider = asRecord(providers[providerLabel]);
|
|
4996
|
+
if (!provider) return `Modified file: provider "${providerLabel}" not found`;
|
|
4997
|
+
const models = provider.models;
|
|
4998
|
+
if (!Array.isArray(models)) return `Modified file: provider "${providerLabel}".models is not an array`;
|
|
4999
|
+
const targetModel = models.find((m: unknown) => asRecord(m)?.id === modelId);
|
|
5000
|
+
if (!targetModel || typeof targetModel !== "object")
|
|
5001
|
+
return `Modified file: model "${modelId}" not found in provider after insertion`;
|
|
5002
|
+
|
|
5003
|
+
// Validate effective merged compat
|
|
5004
|
+
const provCompatRaw = (provider as Record<string, unknown>).compat;
|
|
5005
|
+
const provCompat = (provCompatRaw && typeof provCompatRaw === "object" && !Array.isArray(provCompatRaw))
|
|
5006
|
+
? provCompatRaw as Record<string, unknown>
|
|
5007
|
+
: {};
|
|
5008
|
+
const mdlCompatRaw = (targetModel as Record<string, unknown>).compat;
|
|
5009
|
+
const mdlCompat = (mdlCompatRaw && typeof mdlCompatRaw === "object" && !Array.isArray(mdlCompatRaw))
|
|
5010
|
+
? mdlCompatRaw as Record<string, unknown>
|
|
5011
|
+
: {};
|
|
5012
|
+
const merged = { ...provCompat, ...mdlCompat };
|
|
5013
|
+
for (const [k, v] of Object.entries(compatKeys)) {
|
|
5014
|
+
if (!(k in merged)) return `Modified file: effective compat.${k} not found`;
|
|
5015
|
+
if (merged[k] !== v) return `Modified file: effective compat.${k} wrong value`;
|
|
5016
|
+
}
|
|
5017
|
+
|
|
5018
|
+
if (modifiedText.length < originalText.length)
|
|
5019
|
+
return "Modified file: content is shorter than original (possible truncation)";
|
|
5020
|
+
|
|
5021
|
+
const modClean = stripJsoncComments(modifiedText);
|
|
5022
|
+
const rootStart = skipJsonWhitespace(modClean, 0);
|
|
5023
|
+
const rootEnd = findMatchingBracket(modClean, rootStart);
|
|
5024
|
+
if (rootEnd === undefined) return "Modified file: root bracket mismatch";
|
|
5025
|
+
if (skipJsonWhitespace(modClean, rootEnd + 1) !== modClean.length)
|
|
5026
|
+
return "Modified file: trailing content after root object";
|
|
5027
|
+
|
|
5028
|
+
return null;
|
|
5029
|
+
} catch (e) {
|
|
5030
|
+
return `Self-check error: ${e instanceof Error ? e.message : String(e)}`;
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
|
|
4717
5034
|
/**
|
|
4718
5035
|
* Deep-equal comparison of two values, used for post-write self-check.
|
|
4719
5036
|
* Compares all keys recursively, allowing `extraKeys` to be present in `a` but not in `b`.
|
|
@@ -4979,6 +5296,7 @@ function selfCheckFix(
|
|
|
4979
5296
|
providerLabel: string,
|
|
4980
5297
|
modelId: string,
|
|
4981
5298
|
compatKeys: Record<string, unknown>,
|
|
5299
|
+
placement: "provider" | "model" = "model",
|
|
4982
5300
|
): string | null {
|
|
4983
5301
|
try {
|
|
4984
5302
|
// Step 1: Parse both versions as JSONC (comments + trailing commas allowed).
|
|
@@ -5003,7 +5321,7 @@ function selfCheckFix(
|
|
|
5003
5321
|
if (models.length === 0) {
|
|
5004
5322
|
return `Modified file: provider "${providerLabel}".models is empty`;
|
|
5005
5323
|
}
|
|
5006
|
-
|
|
5324
|
+
|
|
5007
5325
|
// Step 4: Find and validate target model
|
|
5008
5326
|
const targetModel = models.find((m: Record<string, unknown>) => m.id === modelId);
|
|
5009
5327
|
if (!targetModel || typeof targetModel !== 'object') {
|
|
@@ -5021,7 +5339,7 @@ function selfCheckFix(
|
|
|
5021
5339
|
if (!origProvider || !origTargetModelRecord) {
|
|
5022
5340
|
return `Original file: provider/model "${providerLabel}/${modelId}" not found`;
|
|
5023
5341
|
}
|
|
5024
|
-
|
|
5342
|
+
|
|
5025
5343
|
// Step 5: Compute the EFFECTIVE merged compat (provider-level + model-level),
|
|
5026
5344
|
// mirroring Pi's mergeCompat behavior (model wins on conflicts). The fix may
|
|
5027
5345
|
// have written either level, so validation must check the merged result.
|
|
@@ -5045,7 +5363,7 @@ function selfCheckFix(
|
|
|
5045
5363
|
return `Modified file: effective compat.${k} has wrong value: expected ${JSON.stringify(v)}, got ${JSON.stringify(mergedCompat[k])}`;
|
|
5046
5364
|
}
|
|
5047
5365
|
}
|
|
5048
|
-
|
|
5366
|
+
|
|
5049
5367
|
// Step 7: Validate original structure is preserved (no accidental deletions/changes)
|
|
5050
5368
|
|
|
5051
5369
|
function isSubset(origVal: unknown, modVal: unknown, path = ''): boolean {
|
|
@@ -5071,7 +5389,18 @@ function selfCheckFix(
|
|
|
5071
5389
|
} else {
|
|
5072
5390
|
const origCompat = origObj[key] as Record<string, unknown>;
|
|
5073
5391
|
const modCompat = modObj[key] as Record<string, unknown>;
|
|
5074
|
-
|
|
5392
|
+
// Only the compat object at the level ACTUALLY edited may have
|
|
5393
|
+
// its values repaired by this fix. The un-edited level must
|
|
5394
|
+
// remain byte/structure-equivalent, so its same-name keys stay
|
|
5395
|
+
// under full validation. Using a disjunction OR (provider ||
|
|
5396
|
+
// target) here would silently skip validation at the un-edited
|
|
5397
|
+
// level, masking corruption (e.g. a buggy editor accidentally
|
|
5398
|
+
// breaking provider.compat.sendSessionAffinityHeaders while
|
|
5399
|
+
// the fix was a model-level repair). Track placement — only
|
|
5400
|
+
// the placement-resolved object's own compat may be exempt.
|
|
5401
|
+
const mayRepairThisCompat =
|
|
5402
|
+
(placement === "provider" && origObj === origProvider) ||
|
|
5403
|
+
(placement === "model" && origObj === origTargetModelRecord);
|
|
5075
5404
|
for (const ck of Object.keys(origCompat)) {
|
|
5076
5405
|
if (!(ck in modCompat)) return false;
|
|
5077
5406
|
// The fix may repair an existing wrong compat value (for example
|
|
@@ -5092,13 +5421,16 @@ function selfCheckFix(
|
|
|
5092
5421
|
if (!isSubset(origParsed, modParsed)) {
|
|
5093
5422
|
return "Modified file: original structure was altered (data loss detected)";
|
|
5094
5423
|
}
|
|
5095
|
-
|
|
5096
|
-
//
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
//
|
|
5424
|
+
|
|
5425
|
+
// Note: we intentionally do NOT enforce `modified.length >= original.length`.
|
|
5426
|
+
// The surgical editor may replace an existing compat value with a shorter one
|
|
5427
|
+
// (e.g. `false` -> `true`), which legitimately shrinks the file by a byte.
|
|
5428
|
+
// Real data loss / truncation is already caught by Step 7's isSubset
|
|
5429
|
+
// (every original key still present) and Step 8's root-bracket integrity
|
|
5430
|
+
// check below — a surviving length heuristic would false-positive on every
|
|
5431
|
+
// such value repair. (Tracked: the mofas glm-5.2 self-check failure path.)
|
|
5432
|
+
|
|
5433
|
+
// Step 8: Validate root bracket integrity with the same string/comment-aware
|
|
5102
5434
|
// scanner used for edits. Do not count raw braces: comments or strings may
|
|
5103
5435
|
// legitimately contain unmatched `{` / `}` bytes.
|
|
5104
5436
|
const modifiedClean = stripJsoncComments(modified);
|
|
@@ -5373,6 +5705,9 @@ export const __internals_for_tests = {
|
|
|
5373
5705
|
locateModelInJsonc,
|
|
5374
5706
|
composeFixInsertion,
|
|
5375
5707
|
selfCheckFix,
|
|
5708
|
+
analyzeModelsJsonForMissingEntry,
|
|
5709
|
+
composeMissingEntryInsertion,
|
|
5710
|
+
selfCheckMissingEntryInsertion,
|
|
5376
5711
|
decideFixPlacement,
|
|
5377
5712
|
chooseFixPlacement,
|
|
5378
5713
|
findExistingCompatKeysInJsonc,
|
|
@@ -5791,9 +6126,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
5791
6126
|
|
|
5792
6127
|
ensureRoutingRegistry();
|
|
5793
6128
|
|
|
6129
|
+
/**
|
|
6130
|
+
* Check whether a model has an EXPLICIT supportsLongCacheRetention: true
|
|
6131
|
+
* opt-in in models.json (either at provider-level or model-level).
|
|
6132
|
+
* Model-level compat takes precedence over provider-level (mirrors Pi's
|
|
6133
|
+
* mergeCompat behaviour: model wins on conflicts).
|
|
6134
|
+
*
|
|
6135
|
+
* Returns true ONLY when the user explicitly opted in. Returns false for:
|
|
6136
|
+
* - Explicit false (opt-out)
|
|
6137
|
+
* - In models.json but field absent (Pi defaults to true — unsafe)
|
|
6138
|
+
* - Not in models.json at all (API-logged-in providers)
|
|
6139
|
+
* - File missing/unreadable
|
|
6140
|
+
*
|
|
6141
|
+
* The caller strips prompt_cache_retention when this returns false.
|
|
6142
|
+
*/
|
|
6143
|
+
function hasExplicitLongRetentionOptIn(model: PiModel): boolean {
|
|
6144
|
+
try {
|
|
6145
|
+
const text = readFileSync(MODELS_JSON_PATH, "utf8");
|
|
6146
|
+
const parsed = parseJsonc(text);
|
|
6147
|
+
const providers = asRecord(asRecord(parsed)?.providers);
|
|
6148
|
+
if (!providers) return false;
|
|
6149
|
+
|
|
6150
|
+
const prov = asRecord(providers[model.provider]);
|
|
6151
|
+
if (!prov) return false;
|
|
6152
|
+
|
|
6153
|
+
// Check model-level first (higher priority in Pi's merge logic)
|
|
6154
|
+
const models = prov.models;
|
|
6155
|
+
if (Array.isArray(models)) {
|
|
6156
|
+
const modelEntry = models.find(m => asRecord(m)?.id === model.id);
|
|
6157
|
+
if (modelEntry) {
|
|
6158
|
+
const modelCompat = asRecord(asRecord(modelEntry)?.compat);
|
|
6159
|
+
if (modelCompat?.supportsLongCacheRetention !== undefined) {
|
|
6160
|
+
return modelCompat.supportsLongCacheRetention === true;
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6164
|
+
|
|
6165
|
+
// Check provider-level
|
|
6166
|
+
const provCompat = asRecord(prov.compat);
|
|
6167
|
+
if (provCompat?.supportsLongCacheRetention !== undefined) {
|
|
6168
|
+
return provCompat.supportsLongCacheRetention === true;
|
|
6169
|
+
}
|
|
6170
|
+
|
|
6171
|
+
return false;
|
|
6172
|
+
} catch {
|
|
6173
|
+
return false;
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
|
|
5794
6177
|
pi.on("session_start", async (event, ctx) => {
|
|
5795
6178
|
await restoreCacheStats(event.reason, ctx);
|
|
5796
|
-
if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(ctx.model, ctx) ?? ctx.model, ctx, warnedModels);
|
|
5797
6179
|
await publishStatus(ctx);
|
|
5798
6180
|
});
|
|
5799
6181
|
|
|
@@ -5915,6 +6297,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
5915
6297
|
});
|
|
5916
6298
|
|
|
5917
6299
|
pi.on("before_provider_request", (event, ctx) => {
|
|
6300
|
+
// ── Safety: strip prompt_cache_retention from payload for models that
|
|
6301
|
+
// are not authorised to send it. Pi defaults supportsLongCacheRetention
|
|
6302
|
+
// to true for all openai-completions models, but most third-party APIs
|
|
6303
|
+
// reject the parameter with 400 “Extra inputs are not permitted”.
|
|
6304
|
+
//
|
|
6305
|
+
// Gate order (first match wins):
|
|
6306
|
+
// 1. Official OpenAI → keep (trusted to support it)
|
|
6307
|
+
// 2. 400 history → strip (empirical evidence overrides user config)
|
|
6308
|
+
// 3. Explicit opt-in in models.json → keep (user explicitly wants it)
|
|
6309
|
+
// 4. Everything else → strip (safe default for third-party APIs)
|
|
6310
|
+
//
|
|
6311
|
+
// Gate 2 before Gate 3 is critical: if a user explicitly opted in but
|
|
6312
|
+
// the API returned 400, we must strip — otherwise the 400 repeats forever.
|
|
6313
|
+
if (runtimeOptimizerEnabled) {
|
|
6314
|
+
const payload = event.payload as UnknownRecord;
|
|
6315
|
+
if (payload && typeof payload.prompt_cache_retention === 'string') {
|
|
6316
|
+
const rModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
6317
|
+
if (rModel) {
|
|
6318
|
+
if (isOfficialOpenAIBaseUrl(rModel)) {
|
|
6319
|
+
// Gate 1: Official OpenAI → keep
|
|
6320
|
+
} else if (promptCacheRetention400Models.has(modelKey(rModel))) {
|
|
6321
|
+
// Gate 2: 400 history → strip (overrides user opt-in)
|
|
6322
|
+
delete payload.prompt_cache_retention;
|
|
6323
|
+
} else if (hasExplicitLongRetentionOptIn(rModel)) {
|
|
6324
|
+
// Gate 3: Explicit user opt-in → keep
|
|
6325
|
+
} else {
|
|
6326
|
+
// Gate 4: Safe default → strip
|
|
6327
|
+
delete payload.prompt_cache_retention;
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6332
|
+
|
|
5918
6333
|
if (!shouldInjectOpenAIPromptCacheKey()) return undefined;
|
|
5919
6334
|
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
5920
6335
|
if (!isOpenAICompatibleApi(requestModel?.api)) return undefined;
|
|
@@ -6110,7 +6525,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
6110
6525
|
return;
|
|
6111
6526
|
}
|
|
6112
6527
|
|
|
6113
|
-
|
|
6528
|
+
let suggestion = buildFixSuggestion(model);
|
|
6529
|
+
|
|
6530
|
+
// If no regular missing compat flags but the model has a recorded
|
|
6531
|
+
// prompt_cache_retention 400 (Pi sent `prompt_cache_retention` and
|
|
6532
|
+
// the provider rejected it), offer to override
|
|
6533
|
+
// `supportsLongCacheRetention` to false in models.json.
|
|
6534
|
+
if (!suggestion && isPromptCacheRetention400Applicable(model) && promptCacheRetention400Models.has(modelKey(model))) {
|
|
6535
|
+
const key = modelKey(model);
|
|
6536
|
+
const slashIdx = key.indexOf("/");
|
|
6537
|
+
const providerLabel = slashIdx > 0 ? key.slice(0, slashIdx) : key;
|
|
6538
|
+
suggestion = {
|
|
6539
|
+
providerLabel,
|
|
6540
|
+
modelId: model.id,
|
|
6541
|
+
compatKeys: { supportsLongCacheRetention: false },
|
|
6542
|
+
};
|
|
6543
|
+
}
|
|
6544
|
+
|
|
6114
6545
|
if (!suggestion) {
|
|
6115
6546
|
const key = modelKey(model);
|
|
6116
6547
|
cmdCtx.ui.notify(`✅ Nothing to fix for "${key}". Compat already configured.`, "info");
|
|
@@ -6120,24 +6551,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
6120
6551
|
if (!cmdCtx.hasUI) {
|
|
6121
6552
|
// No UI — refuse to write, show manual guidance instead.
|
|
6122
6553
|
const compatResult = buildCompatDiagnosis(model);
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6554
|
+
const snippet = formatMissingEntryManualSnippet(
|
|
6555
|
+
suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
6556
|
+
);
|
|
6557
|
+
const manualLines = [
|
|
6558
|
+
`❌ Non-interactive terminal detected. Auto-fix requires UI confirmation.`,
|
|
6559
|
+
"",
|
|
6560
|
+
`Edit ${getModelsJsonDisplayPath()} and run /reload.`,
|
|
6561
|
+
];
|
|
6562
|
+
if (promptCacheRetention400Models.has(modelKey(model))) {
|
|
6563
|
+
manualLines.push(
|
|
6564
|
+
"",
|
|
6565
|
+
"💡 This model returned HTTP 400 for prompt_cache_retention.",
|
|
6566
|
+
"Create or edit the entry below to override supportsLongCacheRetention to false.",
|
|
6133
6567
|
);
|
|
6134
|
-
}
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6568
|
+
}
|
|
6569
|
+
manualLines.push(
|
|
6570
|
+
"",
|
|
6571
|
+
"If the provider/model already exists in models.json, add these compat keys under",
|
|
6572
|
+
`providers["${suggestion.providerLabel}"] -> models -> entry with id "${suggestion.modelId}" -> compat:`,
|
|
6573
|
+
formatCompatKeysForInsertion(suggestion.compatKeys),
|
|
6574
|
+
);
|
|
6575
|
+
if (snippet.length > 0) {
|
|
6576
|
+
manualLines.push(
|
|
6577
|
+
"",
|
|
6578
|
+
"If the provider/model is missing (common for API-logged-in channels such as",
|
|
6579
|
+
`opencode go), add a minimal entry under "providers" (keep existing auth as-is):`,
|
|
6580
|
+
"",
|
|
6581
|
+
snippet,
|
|
6139
6582
|
);
|
|
6140
6583
|
}
|
|
6584
|
+
if (compatResult) {
|
|
6585
|
+
manualLines.push("", compatResult);
|
|
6586
|
+
}
|
|
6587
|
+
cmdCtx.ui.notify(manualLines.join("\n"), "warning");
|
|
6141
6588
|
return;
|
|
6142
6589
|
}
|
|
6143
6590
|
|
|
@@ -6150,17 +6597,127 @@ export default function (pi: ExtensionAPI) {
|
|
|
6150
6597
|
return;
|
|
6151
6598
|
}
|
|
6152
6599
|
|
|
6153
|
-
// Locate the model entry
|
|
6600
|
+
// Locate the model entry. API-logged-in providers (e.g. opencode go)
|
|
6601
|
+
// may not appear in models.json at all.
|
|
6154
6602
|
const location = locateModelInJsonc(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
6155
6603
|
if (!location) {
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6604
|
+
const diagnosis = analyzeModelsJsonForMissingEntry(originalText, suggestion.providerLabel, suggestion.modelId);
|
|
6605
|
+
if (diagnosis && cmdCtx.hasUI) {
|
|
6606
|
+
// Offer to create the missing entry.
|
|
6607
|
+
const plan = composeMissingEntryInsertion(
|
|
6608
|
+
originalText, diagnosis,
|
|
6609
|
+
suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
6610
|
+
);
|
|
6611
|
+
const checkError = selfCheckMissingEntryInsertion(
|
|
6612
|
+
originalText, plan.modifiedText,
|
|
6613
|
+
suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
6614
|
+
);
|
|
6615
|
+
if (checkError !== null) {
|
|
6616
|
+
// Fall through to manual guidance.
|
|
6617
|
+
cmdCtx.ui.notify(
|
|
6618
|
+
`❌ Self-check would fail for auto-created entry: ${checkError}\n` +
|
|
6619
|
+
`Falling back to manual guidance. No changes were made.`,
|
|
6620
|
+
"error",
|
|
6621
|
+
);
|
|
6622
|
+
// Continue to manual guidance below.
|
|
6623
|
+
} else {
|
|
6624
|
+
const keysPreview = JSON.stringify(suggestion.compatKeys, null, 2);
|
|
6625
|
+
const ts = backupTimestamp();
|
|
6626
|
+
const backupPath = `${MODELS_JSON_PATH}.backup-cache-optimizer-${ts}`;
|
|
6627
|
+
const previewLines = [
|
|
6628
|
+
`📝 Preview of changes to ${getModelsJsonDisplayPath()}:`,
|
|
6629
|
+
``,
|
|
6630
|
+
`Location: ${plan.placementLabel}`,
|
|
6631
|
+
`Compat JSON to write:`,
|
|
6632
|
+
keysPreview,
|
|
6633
|
+
``,
|
|
6634
|
+
`⚠️ Risk notice:`,
|
|
6635
|
+
` 1. This creates a new entry in models.json. Existing auth (e.g. login API tokens) is not affected.`,
|
|
6636
|
+
` 2. A timestamped backup will be written to: ${backupPath}`,
|
|
6637
|
+
` 3. You must run /reload or restart Pi for the change to take effect.`,
|
|
6638
|
+
` 4. If the file contains comments or unusual formatting, please verify the result after write.`,
|
|
6639
|
+
];
|
|
6640
|
+
if (promptCacheRetention400Models.has(modelKey(model))) {
|
|
6641
|
+
previewLines.push(
|
|
6642
|
+
"",
|
|
6643
|
+
"💡 This fix overrides supportsLongCacheRetention to false because",
|
|
6644
|
+
"a 400 prompt_cache_retention error was observed for this model.",
|
|
6645
|
+
"After applying and reloading, Pi will no longer send the",
|
|
6646
|
+
"prompt_cache_retention parameter to this provider.",
|
|
6647
|
+
);
|
|
6648
|
+
}
|
|
6649
|
+
previewLines.push("", `Apply these changes?`);
|
|
6650
|
+
const confirmed = await cmdCtx.ui.confirm("Cache Optimizer — Fix (new entry)", previewLines.join("\n"));
|
|
6651
|
+
if (confirmed) {
|
|
6652
|
+
try {
|
|
6653
|
+
await copyFile(MODELS_JSON_PATH, backupPath);
|
|
6654
|
+
const tempPath = `${MODELS_JSON_PATH}.${process.pid}.${Date.now()}.fix.tmp`;
|
|
6655
|
+
await writeFile(tempPath, plan.modifiedText, "utf8");
|
|
6656
|
+
await rename(tempPath, MODELS_JSON_PATH);
|
|
6657
|
+
|
|
6658
|
+
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
6659
|
+
const postErr = selfCheckMissingEntryInsertion(
|
|
6660
|
+
originalText, writtenText,
|
|
6661
|
+
suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys,
|
|
6662
|
+
);
|
|
6663
|
+
if (postErr !== null) {
|
|
6664
|
+
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
6665
|
+
cmdCtx.ui.notify(
|
|
6666
|
+
`❌ Post-write self-check failed: ${postErr}\n` +
|
|
6667
|
+
`The backup at ${backupPath} has been restored. No changes applied.`,
|
|
6668
|
+
"error",
|
|
6669
|
+
);
|
|
6670
|
+
return;
|
|
6671
|
+
}
|
|
6672
|
+
cmdCtx.ui.notify(
|
|
6673
|
+
`✅ Fix applied to ${getModelsJsonDisplayPath()}.\n` +
|
|
6674
|
+
`Backup saved to: ${backupPath}\n` +
|
|
6675
|
+
`Run /reload or restart Pi for the change to take effect.`,
|
|
6676
|
+
"info",
|
|
6677
|
+
);
|
|
6678
|
+
} catch (e) {
|
|
6679
|
+
cmdCtx.ui.notify(
|
|
6680
|
+
`❌ Write failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
6681
|
+
"error",
|
|
6682
|
+
);
|
|
6683
|
+
}
|
|
6684
|
+
return;
|
|
6685
|
+
}
|
|
6686
|
+
cmdCtx.ui.notify("No changes were made. Canceled by user.", "info");
|
|
6687
|
+
return;
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
|
|
6691
|
+
// Non-interactive or no diagnosis: show manual guidance.
|
|
6692
|
+
const snippet = diagnosis
|
|
6693
|
+
? formatMissingEntryManualSnippet(suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys)
|
|
6694
|
+
: formatCompatKeysForInsertion(suggestion.compatKeys);
|
|
6695
|
+
const adviceLines: string[] = [];
|
|
6696
|
+
if (!diagnosis) {
|
|
6697
|
+
adviceLines.push(
|
|
6698
|
+
`❌ Could not locate model "${suggestion.modelId}" or provider "${suggestion.providerLabel}" in ${getModelsJsonDisplayPath()}.`,
|
|
6699
|
+
"",
|
|
6700
|
+
"Providers that were added via Pi /login API (e.g. opencode go) do not have",
|
|
6701
|
+
"entries in models.json. You can create a minimal compat-only entry by hand:",
|
|
6702
|
+
);
|
|
6703
|
+
} else if (diagnosis.scenario === "provider_missing") {
|
|
6704
|
+
adviceLines.push(
|
|
6705
|
+
`ℹ️ Provider "${suggestion.providerLabel}" does not exist in ${getModelsJsonDisplayPath()}.`,
|
|
6706
|
+
`This is common for API-logged-in providers (e.g. /login ...).`,
|
|
6707
|
+
"",
|
|
6708
|
+
"Add the following minimal block under the \"providers\" key (keep your",
|
|
6709
|
+
"existing authentication as-is):",
|
|
6710
|
+
);
|
|
6711
|
+
} else {
|
|
6712
|
+
adviceLines.push(
|
|
6713
|
+
`ℹ️ Model "${suggestion.modelId}" was not found in ${getModelsJsonDisplayPath()}`,
|
|
6714
|
+
`under providers["${suggestion.providerLabel}"].`,
|
|
6715
|
+
"",
|
|
6716
|
+
"Add the following entry to the models array (keep existing auth):",
|
|
6717
|
+
);
|
|
6718
|
+
}
|
|
6719
|
+
adviceLines.push("", snippet, "", "Then save and run /reload.");
|
|
6720
|
+
cmdCtx.ui.notify(adviceLines.join("\n"), "warning");
|
|
6164
6721
|
return;
|
|
6165
6722
|
}
|
|
6166
6723
|
|
|
@@ -6170,7 +6727,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6170
6727
|
const modifiedText = composeFixInsertion(originalText, location, suggestion.compatKeys, decision.placement);
|
|
6171
6728
|
|
|
6172
6729
|
// Self-check
|
|
6173
|
-
const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
6730
|
+
const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, decision.placement);
|
|
6174
6731
|
if (checkError !== null) {
|
|
6175
6732
|
cmdCtx.ui.notify(
|
|
6176
6733
|
`❌ Self-check failed before write: ${checkError}\n` +
|
|
@@ -6203,15 +6760,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
6203
6760
|
`Placement: ${decision.placement} level — ${decision.reason}`,
|
|
6204
6761
|
`Compat JSON to write:`,
|
|
6205
6762
|
keysPreview,
|
|
6206
|
-
``,
|
|
6763
|
+
``,
|
|
6207
6764
|
`⚠️ Risk notice:`,
|
|
6208
6765
|
scopeRiskLine,
|
|
6209
6766
|
` 2. A timestamped backup will be written to: ${backupPath}`,
|
|
6210
6767
|
` 3. You must restart Pi / run /reload for the change to take effect.`,
|
|
6211
6768
|
` 4. If the file contains comments or unusual formatting, please verify the result after write.`,
|
|
6212
|
-
``,
|
|
6213
|
-
`Apply these changes?`,
|
|
6214
6769
|
];
|
|
6770
|
+
if (promptCacheRetention400Models.has(modelKey(model))) {
|
|
6771
|
+
previewLines.push(
|
|
6772
|
+
"",
|
|
6773
|
+
"💡 This fix overrides supportsLongCacheRetention to false because",
|
|
6774
|
+
"a 400 prompt_cache_retention error was observed for this model.",
|
|
6775
|
+
"After applying and reloading, Pi will no longer send the",
|
|
6776
|
+
"prompt_cache_retention parameter to this provider.",
|
|
6777
|
+
);
|
|
6778
|
+
}
|
|
6779
|
+
previewLines.push("", `Apply these changes?`);
|
|
6215
6780
|
|
|
6216
6781
|
const confirmed = await cmdCtx.ui.confirm("Cache Optimizer — Fix", previewLines.join("\n"));
|
|
6217
6782
|
if (!confirmed) {
|
|
@@ -6231,7 +6796,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6231
6796
|
|
|
6232
6797
|
// Post-write self-check (read back)
|
|
6233
6798
|
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
6234
|
-
const postCheckError = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
6799
|
+
const postCheckError = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, decision.placement);
|
|
6235
6800
|
if (postCheckError !== null) {
|
|
6236
6801
|
// Restore from backup
|
|
6237
6802
|
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
@@ -6360,7 +6925,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6360
6925
|
|
|
6361
6926
|
const menuDecision = chooseFixPlacement(originalText, location, suggestion.compatKeys, suggestion.providerLabel);
|
|
6362
6927
|
const modifiedText = composeFixInsertion(originalText, location, suggestion.compatKeys, menuDecision.placement);
|
|
6363
|
-
const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
6928
|
+
const checkError = selfCheckFix(originalText, modifiedText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement);
|
|
6364
6929
|
if (checkError !== null) {
|
|
6365
6930
|
cmdCtx.ui.notify(`❌ Self-check failed: ${checkError}\nNo changes made.`, "error");
|
|
6366
6931
|
return;
|
|
@@ -6383,13 +6948,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
6383
6948
|
`Placement: ${menuDecision.placement} level — ${menuDecision.reason}`,
|
|
6384
6949
|
`Compat JSON to write:`,
|
|
6385
6950
|
keysPreview,
|
|
6386
|
-
``,
|
|
6951
|
+
``,
|
|
6387
6952
|
`⚠️ Risk notice:`,
|
|
6388
6953
|
menuScopeRiskLine,
|
|
6389
6954
|
` 2. A timestamped backup will be written to: ${backupPath}`,
|
|
6390
6955
|
` 3. You must restart Pi / run /reload for the change to take effect.`,
|
|
6391
6956
|
` 4. If the file contains comments, verify the result after write.`,
|
|
6392
|
-
``,
|
|
6957
|
+
``,
|
|
6393
6958
|
`Apply these changes?`,
|
|
6394
6959
|
];
|
|
6395
6960
|
|
|
@@ -6406,7 +6971,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6406
6971
|
await rename(tempPath, MODELS_JSON_PATH);
|
|
6407
6972
|
|
|
6408
6973
|
const writtenText = await readFile(MODELS_JSON_PATH, "utf8");
|
|
6409
|
-
const postCheck = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys);
|
|
6974
|
+
const postCheck = selfCheckFix(originalText, writtenText, suggestion.providerLabel, suggestion.modelId, suggestion.compatKeys, menuDecision.placement);
|
|
6410
6975
|
if (postCheck !== null) {
|
|
6411
6976
|
await copyFile(backupPath, MODELS_JSON_PATH);
|
|
6412
6977
|
cmdCtx.ui.notify(`❌ Post-write check failed: ${postCheck}\nBackup restored.`, "error");
|
package/package.json
CHANGED