pi-cache-optimizer 2.8.16 → 2.8.18
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 +21 -17
- package/README.zh-CN.md +21 -17
- package/index.ts +260 -11412
- package/package.json +8 -6
- package/src/adapters.ts +250 -0
- package/src/atomic-fs.ts +300 -0
- package/src/command-completion.ts +114 -0
- package/src/command.ts +1012 -0
- package/src/common.ts +49 -0
- package/src/compat-advice.ts +320 -0
- package/src/compat-config.ts +99 -0
- package/src/config.ts +374 -0
- package/src/diagnostics.ts +757 -0
- package/src/fix-types.ts +66 -0
- package/src/jsonc.ts +602 -0
- package/src/model-detect.ts +66 -0
- package/src/model-identity.ts +368 -0
- package/src/models-json-fix.ts +1982 -0
- package/src/paths.ts +18 -0
- package/src/prompt-cache-key-config.ts +406 -0
- package/src/prompt-rewrite.ts +294 -0
- package/src/provider-errors.ts +328 -0
- package/src/request-payload.ts +197 -0
- package/src/request-state.ts +102 -0
- package/src/retention.ts +82 -0
- package/src/routing.ts +551 -0
- package/src/stats-report.ts +771 -0
- package/src/stats-store.ts +500 -0
- package/src/tool-ordering.ts +209 -0
- package/src/usage.ts +172 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { type PiModel, type UnknownRecord, asRecord, isNonEmptyString, lower } from "./common.ts";
|
|
3
|
+
import { type CacheCompat, getEffectiveCompatSources, resolveEffectiveCompatFromConfig } from "./compat-config.ts";
|
|
4
|
+
import { type PersistedCacheOptimizerConfig, type PersistedCacheOptimizerConfigV3, persistedCacheOptimizerConfig, runtimeOptimizerEnabled } from "./config.ts";
|
|
5
|
+
import { getAssistantRecord, isOfficialOpenAIBaseUrl, isOpenAICompatibleProxyApi, modelKey, readEffectiveCompatConfig } from "./model-identity.ts";
|
|
6
|
+
|
|
7
|
+
export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
8
|
+
|
|
9
|
+
export function clampPromptCacheKey(key: string | undefined): string | undefined {
|
|
10
|
+
const normalized = key?.trim();
|
|
11
|
+
if (!normalized) return undefined;
|
|
12
|
+
|
|
13
|
+
const chars = Array.from(normalized);
|
|
14
|
+
if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return normalized;
|
|
15
|
+
return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getEffectiveCompatValueSource(
|
|
19
|
+
model: PiModel,
|
|
20
|
+
config: unknown,
|
|
21
|
+
key: keyof CacheCompat,
|
|
22
|
+
): "provider" | "model" | "runtime" | "modelOverride" | undefined {
|
|
23
|
+
let source: "provider" | "model" | "runtime" | "modelOverride" | undefined;
|
|
24
|
+
for (const candidate of getEffectiveCompatSources(model, config)) {
|
|
25
|
+
if (Object.prototype.hasOwnProperty.call(candidate.compat, key)) source = candidate.source;
|
|
26
|
+
}
|
|
27
|
+
return source;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hasProviderHeader(headers: Record<string, unknown>, name: string): boolean {
|
|
31
|
+
const normalized = name.toLowerCase();
|
|
32
|
+
return Object.keys(headers).some((headerName) => headerName.toLowerCase() === normalized);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setProviderHeaderIfMissing(
|
|
36
|
+
headers: Record<string, string | null | undefined>,
|
|
37
|
+
name: string,
|
|
38
|
+
value: string,
|
|
39
|
+
): boolean {
|
|
40
|
+
if (hasProviderHeader(headers, name)) return false;
|
|
41
|
+
headers[name] = value;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function addEffectiveSessionAffinityHeaders(
|
|
46
|
+
headers: Record<string, string | null | undefined>,
|
|
47
|
+
model: PiModel | undefined,
|
|
48
|
+
sessionId: string | undefined,
|
|
49
|
+
effectiveCompat?: CacheCompat,
|
|
50
|
+
optimizerEnabled: boolean = runtimeOptimizerEnabled,
|
|
51
|
+
effectiveSource?: "provider" | "model" | "runtime" | "modelOverride",
|
|
52
|
+
): boolean {
|
|
53
|
+
if (!optimizerEnabled || !model || !isNonEmptyString(sessionId)) return false;
|
|
54
|
+
if (!isOpenAICompatibleProxyApi(model.api) || !isNonEmptyString(model.baseUrl) || isOfficialOpenAIBaseUrl(model)) return false;
|
|
55
|
+
const config = effectiveCompat === undefined || effectiveSource === undefined
|
|
56
|
+
? readEffectiveCompatConfig()
|
|
57
|
+
: undefined;
|
|
58
|
+
const compat = effectiveCompat ?? resolveEffectiveCompatFromConfig(model, config);
|
|
59
|
+
const source = effectiveSource
|
|
60
|
+
?? getEffectiveCompatValueSource(model, config, "sendSessionAffinityHeaders")
|
|
61
|
+
?? (Object.prototype.hasOwnProperty.call(asRecord(model.compat) ?? {}, "sendSessionAffinityHeaders") ? "runtime" : undefined);
|
|
62
|
+
if (compat.sendSessionAffinityHeaders !== true) return false;
|
|
63
|
+
|
|
64
|
+
// Pi already handles an effective runtime-model true. The bridge is only for
|
|
65
|
+
// effective values that live in models.json layers and were lost when an
|
|
66
|
+
// extension provider rebuilt/replaced the runtime model object.
|
|
67
|
+
if (source === "runtime") return false;
|
|
68
|
+
|
|
69
|
+
const format = compat.sessionAffinityFormat ?? (
|
|
70
|
+
lower(model.provider).includes("openrouter") || lower(model.baseUrl).includes("openrouter.ai")
|
|
71
|
+
? "openrouter"
|
|
72
|
+
: "openai"
|
|
73
|
+
);
|
|
74
|
+
const value = sessionId.trim();
|
|
75
|
+
let changed = false;
|
|
76
|
+
if (format === "openrouter") {
|
|
77
|
+
return setProviderHeaderIfMissing(headers, "x-session-id", value);
|
|
78
|
+
}
|
|
79
|
+
if (format === "openai") {
|
|
80
|
+
changed = setProviderHeaderIfMissing(headers, "session_id", value) || changed;
|
|
81
|
+
}
|
|
82
|
+
changed = setProviderHeaderIfMissing(headers, "x-client-request-id", value) || changed;
|
|
83
|
+
changed = setProviderHeaderIfMissing(headers, "x-session-affinity", value) || changed;
|
|
84
|
+
return changed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function shouldInjectOpenAIPromptCacheKeyForModel(model: PiModel | undefined): boolean {
|
|
88
|
+
// Pi 1.0+ owns prompt_cache_key for Responses/Codex transports. Keep this
|
|
89
|
+
// extension's fallback limited to openai-completions proxies, where Pi's
|
|
90
|
+
// provider extensions may not supply the session key.
|
|
91
|
+
return isOpenAICompatibleProxyApi(model?.api);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isPromptCacheKeyOmittedForModel(model: PiModel | undefined, config: PersistedCacheOptimizerConfig | PersistedCacheOptimizerConfigV3 = persistedCacheOptimizerConfig): boolean {
|
|
95
|
+
if (!model || !isOpenAICompatibleProxyApi(model.api)) return false;
|
|
96
|
+
return "promptCacheKey" in config && config.promptCacheKey?.omit?.includes(modelKey(model)) === true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function omitOpenAIPromptCacheKeys(payload: unknown): unknown | undefined {
|
|
100
|
+
const record = asRecord(payload);
|
|
101
|
+
if (!record || (!Object.prototype.hasOwnProperty.call(record, "prompt_cache_key") && !Object.prototype.hasOwnProperty.call(record, "promptCacheKey"))) return undefined;
|
|
102
|
+
const copy = { ...record };
|
|
103
|
+
delete copy.prompt_cache_key;
|
|
104
|
+
delete copy.promptCacheKey;
|
|
105
|
+
return copy;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function collectAnthropicCacheControlsInWireOrder(payload: unknown): UnknownRecord[] {
|
|
109
|
+
const record = asRecord(payload);
|
|
110
|
+
if (!record) return [];
|
|
111
|
+
|
|
112
|
+
const controls: UnknownRecord[] = [];
|
|
113
|
+
const collectFromBlock = (value: unknown): void => {
|
|
114
|
+
const cacheControl = asRecord(asRecord(value)?.cache_control);
|
|
115
|
+
if (cacheControl?.type === "ephemeral") controls.push(cacheControl);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Anthropic processes cache breakpoints in tools → system → messages order.
|
|
119
|
+
if (Array.isArray(record.tools)) {
|
|
120
|
+
for (const tool of record.tools) collectFromBlock(tool);
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(record.system)) {
|
|
123
|
+
for (const block of record.system) collectFromBlock(block);
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(record.messages)) {
|
|
126
|
+
for (const message of record.messages) {
|
|
127
|
+
const content = asRecord(message)?.content;
|
|
128
|
+
if (!Array.isArray(content)) continue;
|
|
129
|
+
for (const block of content) collectFromBlock(block);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return controls;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Anthropic requires every 1h cache breakpoint to precede every 5m breakpoint.
|
|
138
|
+
* An ephemeral cache_control without ttl uses Anthropic's default 5m retention.
|
|
139
|
+
* If the final serialized payload contains a short → long transition, downgrade
|
|
140
|
+
* all 1h breakpoints to the default 5m so the request remains valid.
|
|
141
|
+
*/
|
|
142
|
+
export function downgradeAnthropicLongCacheControls(payload: unknown): boolean {
|
|
143
|
+
let changed = false;
|
|
144
|
+
for (const control of collectAnthropicCacheControlsInWireOrder(payload)) {
|
|
145
|
+
if (control.ttl === "1h") {
|
|
146
|
+
delete control.ttl;
|
|
147
|
+
changed = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return changed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function hasAnthropicCacheTtlOrderError(message: unknown): boolean {
|
|
154
|
+
const record = getAssistantRecord(message);
|
|
155
|
+
if (record?.stopReason !== "error" || typeof record.errorMessage !== "string") return false;
|
|
156
|
+
|
|
157
|
+
const error = lower(record.errorMessage);
|
|
158
|
+
return error.includes("cache_control") &&
|
|
159
|
+
error.includes("ttl='1h'") &&
|
|
160
|
+
error.includes("ttl='5m'") &&
|
|
161
|
+
error.includes("must not come after");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function normalizeAnthropicCacheControlTtlOrder(payload: unknown): boolean {
|
|
165
|
+
const controls = collectAnthropicCacheControlsInWireOrder(payload);
|
|
166
|
+
let seenShort = false;
|
|
167
|
+
let hasInvalidLongAfterShort = false;
|
|
168
|
+
|
|
169
|
+
for (const control of controls) {
|
|
170
|
+
const ttl = control.ttl;
|
|
171
|
+
if (ttl === undefined || ttl === "5m") {
|
|
172
|
+
seenShort = true;
|
|
173
|
+
} else if (ttl === "1h" && seenShort) {
|
|
174
|
+
hasInvalidLongAfterShort = true;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!hasInvalidLongAfterShort) return false;
|
|
179
|
+
|
|
180
|
+
return downgradeAnthropicLongCacheControls(payload);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function addOpenAIPromptCacheKey(payload: unknown, cacheKey: string | undefined): unknown | undefined {
|
|
184
|
+
const record = asRecord(payload);
|
|
185
|
+
const normalizedCacheKey = clampPromptCacheKey(cacheKey);
|
|
186
|
+
if (!record || !normalizedCacheKey) return undefined;
|
|
187
|
+
|
|
188
|
+
if (hasEffectivePromptCacheKey(record)) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { ...record, prompt_cache_key: normalizedCacheKey };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function hasEffectivePromptCacheKey(record: UnknownRecord): boolean {
|
|
196
|
+
return isNonEmptyString(record.prompt_cache_key) || isNonEmptyString(record.promptCacheKey);
|
|
197
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type PiModel, isNonEmptyString } from "./common.ts";
|
|
2
|
+
import { type CacheCompat } from "./compat-config.ts";
|
|
3
|
+
import { getCompat } from "./model-identity.ts";
|
|
4
|
+
|
|
5
|
+
export const REQUEST_SNAPSHOT_COMPAT_KEYS: Array<keyof CacheCompat> = [
|
|
6
|
+
"supportsStore",
|
|
7
|
+
"supportsDeveloperRole",
|
|
8
|
+
"supportsReasoningEffort",
|
|
9
|
+
"supportsUsageInStreaming",
|
|
10
|
+
"supportsStrictMode",
|
|
11
|
+
"maxTokensField",
|
|
12
|
+
"sendSessionAffinityHeaders",
|
|
13
|
+
"sessionAffinityFormat",
|
|
14
|
+
"supportsLongCacheRetention",
|
|
15
|
+
"thinkingFormat",
|
|
16
|
+
"requiresReasoningContentOnAssistantMessages",
|
|
17
|
+
"cacheControlFormat",
|
|
18
|
+
"forceAdaptiveThinking",
|
|
19
|
+
"allowEmptySignature",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function snapshotCompatForDiagnostics(model: PiModel): CacheCompat {
|
|
23
|
+
const source = getCompat(model);
|
|
24
|
+
const snapshot: CacheCompat = {};
|
|
25
|
+
const mutableSnapshot = snapshot as Record<string, unknown>;
|
|
26
|
+
for (const key of REQUEST_SNAPSHOT_COMPAT_KEYS) {
|
|
27
|
+
const value = source[key];
|
|
28
|
+
if (value !== undefined) mutableSnapshot[key] = value;
|
|
29
|
+
}
|
|
30
|
+
return snapshot;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function snapshotBaseUrlForDiagnostics(value: unknown): string {
|
|
34
|
+
if (!isNonEmptyString(value)) return "";
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(value);
|
|
37
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") || !url.hostname) return "";
|
|
38
|
+
// Request correlation and doctor only need endpoint identity. Strip userinfo and
|
|
39
|
+
// query/fragment material so a provider URL cannot carry credentials into
|
|
40
|
+
// the process-local snapshot.
|
|
41
|
+
url.username = "";
|
|
42
|
+
url.password = "";
|
|
43
|
+
url.search = "";
|
|
44
|
+
url.hash = "";
|
|
45
|
+
return url.toString();
|
|
46
|
+
} catch {
|
|
47
|
+
// Never echo malformed/opaque input: a best-effort string fallback can
|
|
48
|
+
// retain credentials when the URL could not be parsed.
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function snapshotProviderRequestModel(model: PiModel | undefined): PiModel | undefined {
|
|
54
|
+
if (!model) return undefined;
|
|
55
|
+
|
|
56
|
+
// The provider response hooks do not receive the model that initiated the
|
|
57
|
+
// request (Pi's runner only forwards status/headers). Keep a process-local,
|
|
58
|
+
// credential-blind metadata snapshot so a model switch or route change
|
|
59
|
+
// between request and response cannot retarget a protocol observation. Do
|
|
60
|
+
// not retain the caller's complete model object or any request data.
|
|
61
|
+
return {
|
|
62
|
+
provider: model.provider,
|
|
63
|
+
id: model.id,
|
|
64
|
+
name: model.name,
|
|
65
|
+
api: model.api,
|
|
66
|
+
baseUrl: snapshotBaseUrlForDiagnostics(model.baseUrl),
|
|
67
|
+
compat: snapshotCompatForDiagnostics(model),
|
|
68
|
+
reasoning: model.reasoning ?? false,
|
|
69
|
+
input: model.input ?? ["text"],
|
|
70
|
+
cost: model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
71
|
+
contextWindow: model.contextWindow ?? 0,
|
|
72
|
+
maxTokens: model.maxTokens ?? 0,
|
|
73
|
+
} as PiModel;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Some provider requests never produce a finalized assistant message: Pi's
|
|
77
|
+
// prompt-cache warming (0.86+) replays a request with maxTokens: 1 outside the
|
|
78
|
+
// agent loop, so message_end never consumes its lifecycle record. Cap the
|
|
79
|
+
// process-local list and drop the oldest completed record first.
|
|
80
|
+
export const MAX_PROVIDER_REQUEST_STATES = 32;
|
|
81
|
+
|
|
82
|
+
export function pruneProviderRequestStates<T extends { responseReceived: boolean }>(
|
|
83
|
+
states: T[],
|
|
84
|
+
max = MAX_PROVIDER_REQUEST_STATES,
|
|
85
|
+
): void {
|
|
86
|
+
while (states.length > max) {
|
|
87
|
+
const completed = states.findIndex((state) => state.responseReceived);
|
|
88
|
+
states.splice(completed >= 0 ? completed : 0, 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ProviderRequestState = {
|
|
93
|
+
model: PiModel;
|
|
94
|
+
/** Model selection visible to Pi when this request was dispatched. */
|
|
95
|
+
selectionModelKey: string;
|
|
96
|
+
/** Whether that selection routed to a physical model for this request. */
|
|
97
|
+
virtualSelection: boolean;
|
|
98
|
+
responseReceived: boolean;
|
|
99
|
+
correlationAmbiguous: boolean;
|
|
100
|
+
// A native virtual request whose dispatched id matched several providers.
|
|
101
|
+
identityAmbiguous?: boolean;
|
|
102
|
+
};
|
package/src/retention.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type MutableEnv } from "./common.ts";
|
|
2
|
+
|
|
3
|
+
export type CacheRetentionEnvSnapshot = {
|
|
4
|
+
wasSet: boolean;
|
|
5
|
+
value?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const PI_CACHE_RETENTION_ENV = "PI_CACHE_RETENTION";
|
|
9
|
+
|
|
10
|
+
export const LONG_CACHE_RETENTION_VALUE = "long";
|
|
11
|
+
|
|
12
|
+
export const PI_CACHE_RETENTION_BASELINE_SYMBOL = Symbol.for("pi.cache.optimizer.retention-baseline.v1");
|
|
13
|
+
|
|
14
|
+
export type CacheRetentionBaselineV1 = {
|
|
15
|
+
version: 1;
|
|
16
|
+
snapshot: CacheRetentionEnvSnapshot;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function captureCacheRetentionEnv(env: MutableEnv = process.env): CacheRetentionEnvSnapshot {
|
|
20
|
+
return {
|
|
21
|
+
wasSet: Object.prototype.hasOwnProperty.call(env, PI_CACHE_RETENTION_ENV),
|
|
22
|
+
value: env[PI_CACHE_RETENTION_ENV],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function requestLongCacheRetention(env: MutableEnv = process.env): void {
|
|
27
|
+
if (!env[PI_CACHE_RETENTION_ENV] || env[PI_CACHE_RETENTION_ENV] !== LONG_CACHE_RETENTION_VALUE) {
|
|
28
|
+
env[PI_CACHE_RETENTION_ENV] = LONG_CACHE_RETENTION_VALUE;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function restoreCacheRetentionEnv(snapshot: CacheRetentionEnvSnapshot, env: MutableEnv = process.env): void {
|
|
33
|
+
if (snapshot.wasSet) {
|
|
34
|
+
env[PI_CACHE_RETENTION_ENV] = snapshot.value;
|
|
35
|
+
} else {
|
|
36
|
+
delete env[PI_CACHE_RETENTION_ENV];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isCacheRetentionBaselineV1(value: unknown): value is CacheRetentionBaselineV1 {
|
|
41
|
+
if (typeof value !== "object" || value === null) return false;
|
|
42
|
+
const record = value as { version?: unknown; snapshot?: unknown };
|
|
43
|
+
if (record.version !== 1 || typeof record.snapshot !== "object" || record.snapshot === null) return false;
|
|
44
|
+
const snapshot = record.snapshot as { wasSet?: unknown; value?: unknown };
|
|
45
|
+
return typeof snapshot.wasSet === "boolean" &&
|
|
46
|
+
(snapshot.value === undefined || typeof snapshot.value === "string");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getOrCaptureCacheRetentionBaseline(
|
|
50
|
+
env: MutableEnv = process.env,
|
|
51
|
+
globals: Record<symbol, unknown> = globalThis as Record<symbol, unknown>,
|
|
52
|
+
): CacheRetentionEnvSnapshot {
|
|
53
|
+
const existing = globals[PI_CACHE_RETENTION_BASELINE_SYMBOL];
|
|
54
|
+
if (isCacheRetentionBaselineV1(existing)) return { ...existing.snapshot };
|
|
55
|
+
|
|
56
|
+
const snapshot = captureCacheRetentionEnv(env);
|
|
57
|
+
globals[PI_CACHE_RETENTION_BASELINE_SYMBOL] = { version: 1, snapshot: { ...snapshot } };
|
|
58
|
+
return snapshot;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const STARTUP_CACHE_RETENTION_ENV = getOrCaptureCacheRetentionBaseline();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Pi Cache Optimizer (formerly pi-deepseek-cache-optimizer)
|
|
65
|
+
*
|
|
66
|
+
* What it does:
|
|
67
|
+
* 1. Reorders Pi's system prompt so stable content is sent before dynamic context.
|
|
68
|
+
* 2. Sets PI_CACHE_RETENTION=long at extension load time.
|
|
69
|
+
* 3. Warns once for provider/model cache compat gaps where the signal is conservative.
|
|
70
|
+
* 4. Shows lightweight persisted provider-specific cache stats in Pi's footer.
|
|
71
|
+
* 5. Offers disabled-by-default deterministic built-in tool ordering when
|
|
72
|
+
* explicitly opted in.
|
|
73
|
+
*
|
|
74
|
+
* Provider prompt/KV caches are provider-side and best-effort. This extension improves
|
|
75
|
+
* the odds of cache hits; it cannot guarantee hits, especially through proxies.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
// ============================================================
|
|
79
|
+
// Automatically request long prompt-cache retention when Pi supports it.
|
|
80
|
+
// /cache-optimizer disable restores the startup value for this Pi process.
|
|
81
|
+
// ============================================================
|
|
82
|
+
requestLongCacheRetention();
|