pi-cache-optimizer 2.8.3 → 2.8.5
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 +22 -15
- package/README.zh-CN.md +22 -14
- package/index.ts +1051 -198
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmod, copyFile, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { constants as fsConstants, readFileSync } from "node:fs";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, copyFile, lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants, readFileSync, watch } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import {
|
|
@@ -21,6 +21,7 @@ type CacheRetentionEnvSnapshot = {
|
|
|
21
21
|
const PI_CACHE_RETENTION_ENV = "PI_CACHE_RETENTION";
|
|
22
22
|
const LONG_CACHE_RETENTION_VALUE = "long";
|
|
23
23
|
const PI_CACHE_RETENTION_BASELINE_SYMBOL = Symbol.for("pi.cache.optimizer.retention-baseline.v1");
|
|
24
|
+
const ROUTED_FALLBACK_MODEL_SYMBOL = Symbol("pi-cache-optimizer.routed-fallback-model");
|
|
24
25
|
|
|
25
26
|
type CacheRetentionBaselineV1 = {
|
|
26
27
|
version: 1;
|
|
@@ -102,7 +103,19 @@ const STATUS_KEY = "pi-cache-stats";
|
|
|
102
103
|
const STATE_DIR = getAgentDir();
|
|
103
104
|
const STATE_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-stats.json");
|
|
104
105
|
const LEGACY_STATE_FILE_PATH = join(STATE_DIR, "deepseek-cache-optimizer-stats.json");
|
|
106
|
+
const SHARD_STATE_DIR = join(STATE_DIR, "pi-cache-optimizer-stats.d");
|
|
107
|
+
const SHARD_FILES_DIR = join(SHARD_STATE_DIR, "shards");
|
|
108
|
+
const SHARD_EPOCH_DIR = join(SHARD_STATE_DIR, "epochs");
|
|
109
|
+
const SHARD_MODEL_EPOCH_DIR = join(SHARD_EPOCH_DIR, "models");
|
|
110
|
+
const SHARD_MAINTENANCE_DIR = join(SHARD_STATE_DIR, "maintenance");
|
|
111
|
+
const SHARD_GLOBAL_EPOCH_PATH = join(SHARD_EPOCH_DIR, "global.json");
|
|
112
|
+
const SHARD_CLEANUP_LOCK_PATH = join(SHARD_MAINTENANCE_DIR, "cleanup.lock");
|
|
113
|
+
const SHARD_CLEANUP_MARKER_PATH = join(SHARD_MAINTENANCE_DIR, "last-cleanup");
|
|
105
114
|
const CONFIG_FILE_PATH = join(STATE_DIR, "pi-cache-optimizer-config.json");
|
|
115
|
+
const SHARD_RETENTION_MS = 48 * 60 * 60 * 1000;
|
|
116
|
+
const SHARD_TEMP_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
117
|
+
const SHARD_CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
118
|
+
const SHARD_CLEANUP_LOCK_STALE_MS = 60 * 60 * 1000;
|
|
106
119
|
const CACHE_PROVIDER_IDS: CacheProviderId[] = ["deepseek", "openai", "claude", "gemini"];
|
|
107
120
|
const OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_OPENAI_CACHE_KEY";
|
|
108
121
|
const NO_OPENAI_CACHE_KEY_ENV = "PI_CACHE_OPTIMIZER_NO_OPENAI_CACHE_KEY";
|
|
@@ -224,6 +237,10 @@ type CacheCompat = {
|
|
|
224
237
|
cacheControlFormat?: string;
|
|
225
238
|
forceAdaptiveThinking?: boolean;
|
|
226
239
|
allowEmptySignature?: boolean;
|
|
240
|
+
openRouterRouting?: UnknownRecord;
|
|
241
|
+
vercelGatewayRouting?: UnknownRecord;
|
|
242
|
+
chatTemplateKwargs?: UnknownRecord;
|
|
243
|
+
chatTemplateArgs?: UnknownRecord;
|
|
227
244
|
};
|
|
228
245
|
|
|
229
246
|
type CacheStats = {
|
|
@@ -379,6 +396,46 @@ type OptimizedSystemPrompt = {
|
|
|
379
396
|
* Contains only numeric counters and booleans — never message content, prompts,
|
|
380
397
|
* payloads, headers, API keys, or model outputs.
|
|
381
398
|
*/
|
|
399
|
+
type PersistedStatsShardV7 = {
|
|
400
|
+
version: 7;
|
|
401
|
+
kind: "pi-cache-optimizer-shard";
|
|
402
|
+
instanceId: string;
|
|
403
|
+
sessionHash: string;
|
|
404
|
+
process: {
|
|
405
|
+
pid: number;
|
|
406
|
+
ppid: number;
|
|
407
|
+
instanceStartedAt: number;
|
|
408
|
+
};
|
|
409
|
+
lifecycle: {
|
|
410
|
+
state: "active" | "closed";
|
|
411
|
+
createdAt: number;
|
|
412
|
+
updatedAt: number;
|
|
413
|
+
closedAt?: number;
|
|
414
|
+
};
|
|
415
|
+
day: string;
|
|
416
|
+
globalEpoch: string;
|
|
417
|
+
models: Record<string, {
|
|
418
|
+
modelEpoch: string;
|
|
419
|
+
provider: string;
|
|
420
|
+
modelId: string;
|
|
421
|
+
modelName?: string;
|
|
422
|
+
api?: string;
|
|
423
|
+
stats: CacheStats;
|
|
424
|
+
}>;
|
|
425
|
+
lastRoutedModel?: PersistedRoutedModelRef;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
type ShardAggregate = {
|
|
429
|
+
bySession: Record<string, Record<string, CacheStats>>;
|
|
430
|
+
totalsByModel: Record<string, CacheStats>;
|
|
431
|
+
instancesBySession: Record<string, number>;
|
|
432
|
+
instancesBySessionModel: Record<string, Record<string, number>>;
|
|
433
|
+
sessionsByModel: Record<string, number>;
|
|
434
|
+
instancesByModel: Record<string, number>;
|
|
435
|
+
modelRefsByKey: Record<string, PersistedRoutedModelRef>;
|
|
436
|
+
lastRoutedModelBySession: Record<string, PersistedRoutedModelRef>;
|
|
437
|
+
};
|
|
438
|
+
|
|
382
439
|
type CacheUsageSample = {
|
|
383
440
|
timestamp: number;
|
|
384
441
|
hit: boolean;
|
|
@@ -961,19 +1018,23 @@ function resolveActiveRouteSnapshot(
|
|
|
961
1018
|
}
|
|
962
1019
|
|
|
963
1020
|
function routeSnapshotToPiModel(snapshot: PiRouteSnapshot, fallback?: PiModel): PiModel {
|
|
1021
|
+
const sameIdentity = fallback?.provider === snapshot.provider && fallback?.id === snapshot.modelId;
|
|
964
1022
|
return {
|
|
965
|
-
...(fallback ?? {}),
|
|
1023
|
+
...(sameIdentity ? fallback ?? {} : {}),
|
|
966
1024
|
id: snapshot.modelId,
|
|
967
1025
|
name: snapshot.canonicalModelId ?? snapshot.modelId,
|
|
968
1026
|
provider: snapshot.provider,
|
|
969
|
-
api: snapshot.api ?? fallback?.api ?? "",
|
|
970
|
-
baseUrl: fallback?.baseUrl ?? "",
|
|
971
|
-
reasoning: fallback?.reasoning ?? false,
|
|
972
|
-
input: fallback?.input ?? ["text"],
|
|
973
|
-
cost:
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1027
|
+
api: snapshot.api ?? (sameIdentity ? fallback?.api : undefined) ?? "",
|
|
1028
|
+
baseUrl: sameIdentity ? fallback?.baseUrl ?? "" : "",
|
|
1029
|
+
reasoning: sameIdentity ? fallback?.reasoning ?? false : false,
|
|
1030
|
+
input: sameIdentity ? fallback?.input ?? ["text"] : ["text"],
|
|
1031
|
+
cost: sameIdentity
|
|
1032
|
+
? fallback?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
1033
|
+
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1034
|
+
contextWindow: sameIdentity ? fallback?.contextWindow ?? 0 : 0,
|
|
1035
|
+
maxTokens: sameIdentity ? fallback?.maxTokens ?? 0 : 0,
|
|
1036
|
+
compat: sameIdentity ? fallback?.compat : undefined,
|
|
1037
|
+
[ROUTED_FALLBACK_MODEL_SYMBOL]: true,
|
|
977
1038
|
} as PiModel;
|
|
978
1039
|
}
|
|
979
1040
|
|
|
@@ -989,6 +1050,27 @@ function findModelInRegistry(registry: ModelRegistryLike | undefined, provider:
|
|
|
989
1050
|
return all.find((candidate) => candidate.provider === provider && candidate.id === id);
|
|
990
1051
|
}
|
|
991
1052
|
|
|
1053
|
+
function isRoutedFallbackModel(model: PiModel | undefined): boolean {
|
|
1054
|
+
return !!model && (model as PiModel & Record<symbol, unknown>)[ROUTED_FALLBACK_MODEL_SYMBOL] === true;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function applyConfiguredTransportToModel(model: PiModel, config: unknown): PiModel {
|
|
1058
|
+
const providers = asRecord(asRecord(config)?.providers);
|
|
1059
|
+
const provider = asRecord(providers?.[model.provider]);
|
|
1060
|
+
const customModel = Array.isArray(provider?.models)
|
|
1061
|
+
? asRecord(provider.models.find((entry: unknown) => asRecord(entry)?.id === model.id))
|
|
1062
|
+
: undefined;
|
|
1063
|
+
const configuredApi = customModel?.api ?? provider?.api;
|
|
1064
|
+
const configuredBaseUrl = customModel?.baseUrl ?? provider?.baseUrl;
|
|
1065
|
+
const api = isNonEmptyString(model.api)
|
|
1066
|
+
? model.api
|
|
1067
|
+
: (isNonEmptyString(configuredApi) ? configuredApi : model.api);
|
|
1068
|
+
const baseUrl = isNonEmptyString(model.baseUrl)
|
|
1069
|
+
? model.baseUrl
|
|
1070
|
+
: (isNonEmptyString(configuredBaseUrl) ? configuredBaseUrl : model.baseUrl);
|
|
1071
|
+
return api === model.api && baseUrl === model.baseUrl ? model : { ...model, api, baseUrl };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
992
1074
|
function resolveRouteModel(
|
|
993
1075
|
model: PiModel | undefined,
|
|
994
1076
|
ctx?: ContextWithOptionalModelRegistry,
|
|
@@ -996,8 +1078,9 @@ function resolveRouteModel(
|
|
|
996
1078
|
const snapshot = resolveActiveRouteSnapshot(model, ctx);
|
|
997
1079
|
if (!snapshot) return undefined;
|
|
998
1080
|
|
|
999
|
-
|
|
1081
|
+
const resolved = findModelInRegistry(ctx?.modelRegistry, snapshot.provider, snapshot.modelId)
|
|
1000
1082
|
?? routeSnapshotToPiModel(snapshot, model);
|
|
1083
|
+
return applyConfiguredTransportToModel(resolved, readEffectiveCompatConfig());
|
|
1001
1084
|
}
|
|
1002
1085
|
|
|
1003
1086
|
function isVirtualRoutingModel(model: PiModel | undefined, ctx?: Pick<ExtensionContext, "sessionManager">): boolean {
|
|
@@ -1082,17 +1165,286 @@ function getNonNegativeNumber(record: UnknownRecord, key: string): number | unde
|
|
|
1082
1165
|
* Model-level compat takes precedence over provider-level compat for overlapping keys.
|
|
1083
1166
|
* This matches Pi's model-registry.js mergeCompat behavior.
|
|
1084
1167
|
*/
|
|
1168
|
+
const NESTED_COMPAT_KEYS = [
|
|
1169
|
+
"openRouterRouting",
|
|
1170
|
+
"vercelGatewayRouting",
|
|
1171
|
+
"chatTemplateKwargs",
|
|
1172
|
+
"chatTemplateArgs",
|
|
1173
|
+
] as const;
|
|
1174
|
+
|
|
1175
|
+
function mergeCacheCompat(...sources: Array<UnknownRecord | undefined>): CacheCompat {
|
|
1176
|
+
const merged: CacheCompat = {};
|
|
1177
|
+
for (const source of sources) {
|
|
1178
|
+
if (!source) continue;
|
|
1179
|
+
const previousNested = Object.fromEntries(
|
|
1180
|
+
NESTED_COMPAT_KEYS.map((key) => [key, asRecord(merged[key])]),
|
|
1181
|
+
) as Partial<Record<(typeof NESTED_COMPAT_KEYS)[number], UnknownRecord | undefined>>;
|
|
1182
|
+
Object.assign(merged, source);
|
|
1183
|
+
for (const key of NESTED_COMPAT_KEYS) {
|
|
1184
|
+
const baseValue = previousNested[key];
|
|
1185
|
+
const overrideValue = asRecord(source[key]);
|
|
1186
|
+
if (baseValue || overrideValue) {
|
|
1187
|
+
merged[key] = { ...baseValue, ...overrideValue };
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return merged;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function getEffectiveCompatSources(model: PiModel, config: unknown): Array<{ source: "provider" | "model" | "runtime" | "modelOverride"; compat: UnknownRecord }> {
|
|
1195
|
+
const root = asRecord(config);
|
|
1196
|
+
const providers = asRecord(root?.providers);
|
|
1197
|
+
const provider = asRecord(providers?.[model.provider]);
|
|
1198
|
+
const providerCompat = asRecord(provider?.compat);
|
|
1199
|
+
const customModel = Array.isArray(provider?.models)
|
|
1200
|
+
? provider.models.find((entry: unknown) => asRecord(entry)?.id === model.id)
|
|
1201
|
+
: undefined;
|
|
1202
|
+
const customModelCompat = asRecord(asRecord(customModel)?.compat);
|
|
1203
|
+
const runtimeCompat = asRecord(model.compat);
|
|
1204
|
+
const modelOverride = asRecord(asRecord(provider?.modelOverrides)?.[model.id]);
|
|
1205
|
+
const modelOverrideCompat = asRecord(modelOverride?.compat);
|
|
1206
|
+
|
|
1207
|
+
return [
|
|
1208
|
+
...(providerCompat ? [{ source: "provider" as const, compat: providerCompat }] : []),
|
|
1209
|
+
...(customModelCompat ? [{ source: "model" as const, compat: customModelCompat }] : []),
|
|
1210
|
+
...(runtimeCompat ? [{ source: "runtime" as const, compat: runtimeCompat }] : []),
|
|
1211
|
+
...(modelOverrideCompat ? [{ source: "modelOverride" as const, compat: modelOverrideCompat }] : []),
|
|
1212
|
+
];
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function resolveEffectiveCompatFromConfig(model: PiModel, config: unknown): CacheCompat {
|
|
1216
|
+
// Pi's effective precedence is provider → models[] → runtime model →
|
|
1217
|
+
// modelOverrides. The runtime layer is intentionally included between the
|
|
1218
|
+
// config model and modelOverride: extension providers can replace the model
|
|
1219
|
+
// object after lower config layers were applied, while the override remains
|
|
1220
|
+
// Pi's highest-precedence user layer.
|
|
1221
|
+
return mergeCacheCompat(...getEffectiveCompatSources(model, config).map(({ compat }) => compat));
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function getEffectiveCompatValueSource(
|
|
1225
|
+
model: PiModel,
|
|
1226
|
+
config: unknown,
|
|
1227
|
+
key: keyof CacheCompat,
|
|
1228
|
+
): "provider" | "model" | "runtime" | "modelOverride" | undefined {
|
|
1229
|
+
let source: "provider" | "model" | "runtime" | "modelOverride" | undefined;
|
|
1230
|
+
for (const candidate of getEffectiveCompatSources(model, config)) {
|
|
1231
|
+
if (Object.prototype.hasOwnProperty.call(candidate.compat, key)) source = candidate.source;
|
|
1232
|
+
}
|
|
1233
|
+
return source;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function isOptionalString(value: unknown): boolean {
|
|
1237
|
+
return value === undefined || (typeof value === "string" && value.length > 0);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function isOptionalBoolean(value: unknown): boolean {
|
|
1241
|
+
return value === undefined || typeof value === "boolean";
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
1245
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function isRecordOfStrings(value: unknown): boolean {
|
|
1249
|
+
const record = asRecord(value);
|
|
1250
|
+
return !!record && Object.values(record).every((entry) => typeof entry === "string");
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function isThinkingLevelMap(value: unknown): boolean {
|
|
1254
|
+
const map = asRecord(value);
|
|
1255
|
+
if (!map) return false;
|
|
1256
|
+
return ["off", "minimal", "low", "medium", "high", "xhigh", "max"].every((key) =>
|
|
1257
|
+
map[key] === undefined || map[key] === null || typeof map[key] === "string"
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function isValidModelCostTier(value: unknown): boolean {
|
|
1262
|
+
const tier = asRecord(value);
|
|
1263
|
+
return !!tier
|
|
1264
|
+
&& isFiniteNumber(tier.inputTokensAbove)
|
|
1265
|
+
&& isFiniteNumber(tier.input)
|
|
1266
|
+
&& isFiniteNumber(tier.output)
|
|
1267
|
+
&& isFiniteNumber(tier.cacheRead)
|
|
1268
|
+
&& isFiniteNumber(tier.cacheWrite);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function isValidModelCost(value: unknown, partial: boolean): boolean {
|
|
1272
|
+
const cost = asRecord(value);
|
|
1273
|
+
if (!cost) return false;
|
|
1274
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
|
|
1275
|
+
if ((!partial || cost[key] !== undefined) && !isFiniteNumber(cost[key])) return false;
|
|
1276
|
+
}
|
|
1277
|
+
return cost.tiers === undefined || (Array.isArray(cost.tiers) && cost.tiers.every(isValidModelCostTier));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function isValidModelDefinition(value: unknown): boolean {
|
|
1281
|
+
const model = asRecord(value);
|
|
1282
|
+
if (!model || !isNonEmptyString(model.id)) return false;
|
|
1283
|
+
if (!isOptionalString(model.name) || !isOptionalString(model.api) || !isOptionalString(model.baseUrl)) return false;
|
|
1284
|
+
if (!isOptionalBoolean(model.reasoning) || !isValidCompatRecord(model.compat)) return false;
|
|
1285
|
+
if (model.thinkingLevelMap !== undefined && !isThinkingLevelMap(model.thinkingLevelMap)) return false;
|
|
1286
|
+
if (model.input !== undefined && (!Array.isArray(model.input) || !model.input.every((entry) => entry === "text" || entry === "image"))) return false;
|
|
1287
|
+
if (model.cost !== undefined && !isValidModelCost(model.cost, false)) return false;
|
|
1288
|
+
if (model.contextWindow !== undefined && !isFiniteNumber(model.contextWindow)) return false;
|
|
1289
|
+
if (model.maxTokens !== undefined && !isFiniteNumber(model.maxTokens)) return false;
|
|
1290
|
+
if (model.samplingParams !== undefined && !asRecord(model.samplingParams)) return false;
|
|
1291
|
+
if (model.headers !== undefined && !isRecordOfStrings(model.headers)) return false;
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function isValidModelOverride(value: unknown): boolean {
|
|
1296
|
+
const override = asRecord(value);
|
|
1297
|
+
if (!override || !isValidCompatRecord(override.compat)) return false;
|
|
1298
|
+
if (!isOptionalString(override.name) || !isOptionalBoolean(override.reasoning)) return false;
|
|
1299
|
+
if (override.thinkingLevelMap !== undefined && !isThinkingLevelMap(override.thinkingLevelMap)) return false;
|
|
1300
|
+
if (override.input !== undefined && (!Array.isArray(override.input) || !override.input.every((entry) => entry === "text" || entry === "image"))) return false;
|
|
1301
|
+
if (override.cost !== undefined && !isValidModelCost(override.cost, true)) return false;
|
|
1302
|
+
if (override.contextWindow !== undefined && !isFiniteNumber(override.contextWindow)) return false;
|
|
1303
|
+
if (override.maxTokens !== undefined && !isFiniteNumber(override.maxTokens)) return false;
|
|
1304
|
+
if (override.samplingParams !== undefined && !asRecord(override.samplingParams)) return false;
|
|
1305
|
+
if (override.headers !== undefined && !isRecordOfStrings(override.headers)) return false;
|
|
1306
|
+
return true;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function isValidOpenAICompletionsCompat(compat: UnknownRecord): boolean {
|
|
1310
|
+
const booleanKeys = [
|
|
1311
|
+
"supportsStore", "supportsDeveloperRole", "supportsReasoningEffort",
|
|
1312
|
+
"supportsUsageInStreaming", "requiresToolResultName", "requiresAssistantAfterToolResult",
|
|
1313
|
+
"requiresThinkingAsText", "requiresReasoningContentOnAssistantMessages",
|
|
1314
|
+
"supportsOpenAIGrammarTools", "supportsStrictMode", "sendSessionAffinityHeaders",
|
|
1315
|
+
"supportsLongCacheRetention",
|
|
1316
|
+
];
|
|
1317
|
+
if (booleanKeys.some((key) => !isOptionalBoolean(compat[key]))) return false;
|
|
1318
|
+
if (compat.maxTokensField !== undefined && compat.maxTokensField !== "max_completion_tokens" && compat.maxTokensField !== "max_tokens") return false;
|
|
1319
|
+
if (compat.thinkingFormat !== undefined && ![
|
|
1320
|
+
"openai", "openrouter", "together", "baseten", "deepseek", "zai", "qwen",
|
|
1321
|
+
"chat-template", "qwen-chat-template", "string-thinking", "ant-ling",
|
|
1322
|
+
].includes(String(compat.thinkingFormat))) return false;
|
|
1323
|
+
if (compat.cacheControlFormat !== undefined && compat.cacheControlFormat !== "anthropic") return false;
|
|
1324
|
+
if (compat.deferredToolsMode !== undefined && compat.deferredToolsMode !== "kimi") return false;
|
|
1325
|
+
if (compat.sessionAffinityFormat !== undefined && !["openai", "openai-nosession", "openrouter"].includes(String(compat.sessionAffinityFormat))) return false;
|
|
1326
|
+
for (const key of NESTED_COMPAT_KEYS) {
|
|
1327
|
+
if (compat[key] !== undefined && !asRecord(compat[key])) return false;
|
|
1328
|
+
}
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function isValidOpenAIResponsesCompat(compat: UnknownRecord): boolean {
|
|
1333
|
+
const booleanKeys = [
|
|
1334
|
+
"supportsDeveloperRole", "supportsLongCacheRetention", "supportsStrictMode",
|
|
1335
|
+
"supportsOpenAIGrammarTools", "supportsAdditionalTools", "supportsToolSearch",
|
|
1336
|
+
];
|
|
1337
|
+
if (booleanKeys.some((key) => !isOptionalBoolean(compat[key]))) return false;
|
|
1338
|
+
return compat.sessionAffinityFormat === undefined
|
|
1339
|
+
|| ["openai", "openai-nosession", "openrouter"].includes(String(compat.sessionAffinityFormat));
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function isValidAnthropicMessagesCompat(compat: UnknownRecord): boolean {
|
|
1343
|
+
return [
|
|
1344
|
+
"supportsEagerToolInputStreaming", "supportsLongCacheRetention",
|
|
1345
|
+
"sendSessionAffinityHeaders", "supportsCacheControlOnTools", "supportsTemperature",
|
|
1346
|
+
"forceAdaptiveThinking", "allowEmptySignature", "supportsStrictTools", "supportsToolReferences",
|
|
1347
|
+
].every((key) => isOptionalBoolean(compat[key]));
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function isValidCompatRecord(value: unknown): boolean {
|
|
1351
|
+
if (value === undefined) return true;
|
|
1352
|
+
const compat = asRecord(value);
|
|
1353
|
+
return !!compat && (
|
|
1354
|
+
isValidOpenAICompletionsCompat(compat)
|
|
1355
|
+
|| isValidOpenAIResponsesCompat(compat)
|
|
1356
|
+
|| isValidAnthropicMessagesCompat(compat)
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function isValidModelsConfigForEffectiveCompat(value: unknown): boolean {
|
|
1361
|
+
const root = asRecord(value);
|
|
1362
|
+
const providers = asRecord(root?.providers);
|
|
1363
|
+
if (!root || !providers) return false;
|
|
1364
|
+
|
|
1365
|
+
for (const providerValue of Object.values(providers)) {
|
|
1366
|
+
const provider = asRecord(providerValue);
|
|
1367
|
+
if (!provider) return false;
|
|
1368
|
+
if (!isOptionalString(provider.name) || !isOptionalString(provider.baseUrl) || !isOptionalString(provider.apiKey) || !isOptionalString(provider.api)) return false;
|
|
1369
|
+
if (!isOptionalBoolean(provider.authHeader) || !isValidCompatRecord(provider.compat)) return false;
|
|
1370
|
+
if (provider.oauth !== undefined && provider.oauth !== "radius") return false;
|
|
1371
|
+
if (provider.headers !== undefined && !isRecordOfStrings(provider.headers)) return false;
|
|
1372
|
+
if (provider.models !== undefined && (!Array.isArray(provider.models) || !provider.models.every(isValidModelDefinition))) return false;
|
|
1373
|
+
const overrides = provider.modelOverrides === undefined ? undefined : asRecord(provider.modelOverrides);
|
|
1374
|
+
if (provider.modelOverrides !== undefined && (!overrides || !Object.values(overrides).every(isValidModelOverride))) return false;
|
|
1375
|
+
}
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function readEffectiveCompatConfig(): unknown | undefined {
|
|
1380
|
+
try {
|
|
1381
|
+
const parsed = parseJsonc(readFileSync(MODELS_JSON_PATH, "utf8"));
|
|
1382
|
+
return isValidModelsConfigForEffectiveCompat(parsed) ? parsed : undefined;
|
|
1383
|
+
} catch {
|
|
1384
|
+
return undefined;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1085
1388
|
function getCompat(model: PiModel | undefined): CacheCompat {
|
|
1086
1389
|
if (!model) return {} as CacheCompat;
|
|
1390
|
+
return resolveEffectiveCompatFromConfig(model, readEffectiveCompatConfig());
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function hasProviderHeader(headers: Record<string, unknown>, name: string): boolean {
|
|
1394
|
+
const normalized = name.toLowerCase();
|
|
1395
|
+
return Object.keys(headers).some((headerName) => headerName.toLowerCase() === normalized);
|
|
1396
|
+
}
|
|
1087
1397
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1398
|
+
function setProviderHeaderIfMissing(
|
|
1399
|
+
headers: Record<string, string | null | undefined>,
|
|
1400
|
+
name: string,
|
|
1401
|
+
value: string,
|
|
1402
|
+
): boolean {
|
|
1403
|
+
if (hasProviderHeader(headers, name)) return false;
|
|
1404
|
+
headers[name] = value;
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1092
1407
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1408
|
+
function addEffectiveSessionAffinityHeaders(
|
|
1409
|
+
headers: Record<string, string | null | undefined>,
|
|
1410
|
+
model: PiModel | undefined,
|
|
1411
|
+
sessionId: string | undefined,
|
|
1412
|
+
effectiveCompat?: CacheCompat,
|
|
1413
|
+
optimizerEnabled: boolean = runtimeOptimizerEnabled,
|
|
1414
|
+
effectiveSource?: "provider" | "model" | "runtime" | "modelOverride",
|
|
1415
|
+
): boolean {
|
|
1416
|
+
if (!optimizerEnabled || !model || !isNonEmptyString(sessionId)) return false;
|
|
1417
|
+
if (!isOpenAICompatibleProxyApi(model.api) || !isNonEmptyString(model.baseUrl) || isOfficialOpenAIBaseUrl(model)) return false;
|
|
1418
|
+
const config = effectiveCompat === undefined || effectiveSource === undefined
|
|
1419
|
+
? readEffectiveCompatConfig()
|
|
1420
|
+
: undefined;
|
|
1421
|
+
const compat = effectiveCompat ?? resolveEffectiveCompatFromConfig(model, config);
|
|
1422
|
+
const source = effectiveSource
|
|
1423
|
+
?? getEffectiveCompatValueSource(model, config, "sendSessionAffinityHeaders")
|
|
1424
|
+
?? (Object.prototype.hasOwnProperty.call(asRecord(model.compat) ?? {}, "sendSessionAffinityHeaders") ? "runtime" : undefined);
|
|
1425
|
+
if (compat.sendSessionAffinityHeaders !== true) return false;
|
|
1426
|
+
|
|
1427
|
+
// Pi already handles an effective runtime-model true. The bridge is only for
|
|
1428
|
+
// effective values that live in models.json layers and were lost when an
|
|
1429
|
+
// extension provider rebuilt/replaced the runtime model object.
|
|
1430
|
+
if (source === "runtime") return false;
|
|
1431
|
+
|
|
1432
|
+
const format = compat.sessionAffinityFormat ?? (
|
|
1433
|
+
lower(model.provider).includes("openrouter") || lower(model.baseUrl).includes("openrouter.ai")
|
|
1434
|
+
? "openrouter"
|
|
1435
|
+
: "openai"
|
|
1436
|
+
);
|
|
1437
|
+
const value = sessionId.trim();
|
|
1438
|
+
let changed = false;
|
|
1439
|
+
if (format === "openrouter") {
|
|
1440
|
+
return setProviderHeaderIfMissing(headers, "x-session-id", value);
|
|
1441
|
+
}
|
|
1442
|
+
if (format === "openai") {
|
|
1443
|
+
changed = setProviderHeaderIfMissing(headers, "session_id", value) || changed;
|
|
1444
|
+
}
|
|
1445
|
+
changed = setProviderHeaderIfMissing(headers, "x-client-request-id", value) || changed;
|
|
1446
|
+
changed = setProviderHeaderIfMissing(headers, "x-session-affinity", value) || changed;
|
|
1447
|
+
return changed;
|
|
1096
1448
|
}
|
|
1097
1449
|
|
|
1098
1450
|
/** Join display-only path fragments without resolving them for I/O. */
|
|
@@ -1171,6 +1523,7 @@ const CACHE_OPTIMIZER_COMMANDS = [
|
|
|
1171
1523
|
] as const;
|
|
1172
1524
|
const CACHE_OPTIMIZER_CONFIG_ARGUMENTS = ["footer-mode"] as const;
|
|
1173
1525
|
const CACHE_OPTIMIZER_FOOTER_MODES = ["total", "session", "process"] as const;
|
|
1526
|
+
const CACHE_OPTIMIZER_STATS_ARGUMENTS = ["all", "contributors"] as const;
|
|
1174
1527
|
|
|
1175
1528
|
function filterCommandCompletionItems(
|
|
1176
1529
|
values: readonly string[],
|
|
@@ -1209,9 +1562,18 @@ function getCacheOptimizerArgumentCompletions(argumentPrefix: string): CommandCo
|
|
|
1209
1562
|
if (subcommandPrefix === "config") {
|
|
1210
1563
|
return filterCommandCompletionItems(CACHE_OPTIMIZER_CONFIG_ARGUMENTS, "", "config");
|
|
1211
1564
|
}
|
|
1565
|
+
if (subcommandPrefix === "stats") {
|
|
1566
|
+
return filterCommandCompletionItems(CACHE_OPTIMIZER_STATS_ARGUMENTS, "", "stats");
|
|
1567
|
+
}
|
|
1212
1568
|
return filterCommandCompletionItems(CACHE_OPTIMIZER_COMMANDS, parts[0]);
|
|
1213
1569
|
}
|
|
1214
1570
|
|
|
1571
|
+
if (parts[0].toLowerCase() === "stats") {
|
|
1572
|
+
return parts.length === 2
|
|
1573
|
+
? filterCommandCompletionItems(CACHE_OPTIMIZER_STATS_ARGUMENTS, parts[1], "stats")
|
|
1574
|
+
: null;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1215
1577
|
if (parts[0].toLowerCase() !== "config") return null;
|
|
1216
1578
|
|
|
1217
1579
|
if (parts.length === 2) {
|
|
@@ -1267,7 +1629,7 @@ function resolveFooterStatsMode(
|
|
|
1267
1629
|
): { mode: FooterStatsMode; source: FooterStatsModeSource } {
|
|
1268
1630
|
if (configuredMode) return { mode: configuredMode, source: "config" };
|
|
1269
1631
|
const envMode = parseFooterStatsMode(env[FOOTER_MODE_ENV]);
|
|
1270
|
-
return envMode ? { mode: envMode, source: "env" } : { mode: "
|
|
1632
|
+
return envMode ? { mode: envMode, source: "env" } : { mode: "session", source: "default" };
|
|
1271
1633
|
}
|
|
1272
1634
|
|
|
1273
1635
|
function footerStatsMode(
|
|
@@ -1564,6 +1926,14 @@ function isKimiCodingEmptySignatureModel(model: PiModel | undefined): boolean {
|
|
|
1564
1926
|
}
|
|
1565
1927
|
|
|
1566
1928
|
function isAdaptiveThinkingCompatApplicable(model: PiModel): boolean {
|
|
1929
|
+
// A routed registry miss may preserve the upstream API/model identity while
|
|
1930
|
+
// lacking a verified endpoint. Do not diagnose or suggest compat fixes for
|
|
1931
|
+
// that incomplete transport metadata; ordinary direct Anthropic models still
|
|
1932
|
+
// use their normal native adaptive-thinking check.
|
|
1933
|
+
if (isRoutedFallbackModel(model) && !isNonEmptyString(model.baseUrl)) {
|
|
1934
|
+
return false;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1567
1937
|
return lower(model.api) === "anthropic-messages"
|
|
1568
1938
|
&& (isAdaptiveGenerationModel(model) || isKimiCodingAdaptiveModel(model));
|
|
1569
1939
|
}
|
|
@@ -2389,13 +2759,17 @@ function isOfficialOpenAIBaseUrl(model: PiModel): boolean {
|
|
|
2389
2759
|
}
|
|
2390
2760
|
}
|
|
2391
2761
|
|
|
2762
|
+
function isKnownThirdPartyOpenAIEndpoint(model: PiModel): boolean {
|
|
2763
|
+
return isNonEmptyString(model.baseUrl) && !isOfficialOpenAIBaseUrl(model);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2392
2766
|
function describeMissingOpenAIFamilyProxyCompat(model: PiModel): string[] {
|
|
2393
2767
|
const compat = getCompat(model);
|
|
2394
2768
|
const missing: string[] = [];
|
|
2395
2769
|
|
|
2396
2770
|
if (!isOpenAIFamilyModel(model)) return missing;
|
|
2397
2771
|
if (!isOpenAICompatibleProxyApi(model.api)) return missing;
|
|
2398
|
-
if (
|
|
2772
|
+
if (!isKnownThirdPartyOpenAIEndpoint(model)) return missing;
|
|
2399
2773
|
|
|
2400
2774
|
if (compat.sendSessionAffinityHeaders !== true) {
|
|
2401
2775
|
missing.push("sendSessionAffinityHeaders");
|
|
@@ -2415,7 +2789,7 @@ function describeMissingOpenAICompatibleProxyCompat(model: PiModel): string[] {
|
|
|
2415
2789
|
const missing: string[] = [];
|
|
2416
2790
|
|
|
2417
2791
|
if (!isOpenAICompatibleProxyApi(model.api)) return missing;
|
|
2418
|
-
if (
|
|
2792
|
+
if (!isKnownThirdPartyOpenAIEndpoint(model)) return missing;
|
|
2419
2793
|
if (isPiBuiltInLlamaCppModel(model)) return missing;
|
|
2420
2794
|
|
|
2421
2795
|
if (compat.sendSessionAffinityHeaders === undefined) {
|
|
@@ -2443,7 +2817,7 @@ function describeOptionalOpenAICompatibleProxyCompat(model: PiModel): string[] {
|
|
|
2443
2817
|
const optional: string[] = [];
|
|
2444
2818
|
|
|
2445
2819
|
if (!isOpenAICompatibleProxyApi(model.api)) return optional;
|
|
2446
|
-
if (
|
|
2820
|
+
if (!isKnownThirdPartyOpenAIEndpoint(model)) return optional;
|
|
2447
2821
|
if (isPiBuiltInLlamaCppModel(model)) return optional;
|
|
2448
2822
|
|
|
2449
2823
|
if (compat.supportsLongCacheRetention !== true) {
|
|
@@ -2628,7 +3002,10 @@ function describeMissingDeepSeekCompat(model: PiModel): string[] {
|
|
|
2628
3002
|
}
|
|
2629
3003
|
|
|
2630
3004
|
function isDeepSeekCompatCheckApplicable(model: PiModel): boolean {
|
|
2631
|
-
return isDeepSeekLikeModel(model)
|
|
3005
|
+
return isDeepSeekLikeModel(model)
|
|
3006
|
+
&& isOpenAICompatibleApi(model.api)
|
|
3007
|
+
&& isKnownThirdPartyOpenAIEndpoint(model)
|
|
3008
|
+
&& !isPiBuiltInLlamaCppModel(model);
|
|
2632
3009
|
}
|
|
2633
3010
|
|
|
2634
3011
|
function describeMissingCacheCompatForModel(model: PiModel): string[] {
|
|
@@ -3840,6 +4217,87 @@ function formatRecentTrendSummary(samples: CacheUsageSample[], maxCount: number)
|
|
|
3840
4217
|
/**
|
|
3841
4218
|
* Build the output for `/cache-optimizer stats`.
|
|
3842
4219
|
*/
|
|
4220
|
+
function formatCompactStats(stats: CacheStats): string {
|
|
4221
|
+
const percent = stats.totalInputTokens > 0
|
|
4222
|
+
? (stats.cachedInputTokens / stats.totalInputTokens) * 100
|
|
4223
|
+
: 0;
|
|
4224
|
+
return `${stats.hitRequests}/${stats.totalRequests}·${formatTokenCount(stats.cachedInputTokens)}/${formatTokenCount(stats.totalInputTokens)} ${percent.toFixed(1)}%`;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
function modelFromStatsKey(key: string, ref?: PersistedRoutedModelRef): PiModel | undefined {
|
|
4228
|
+
const slash = key.indexOf("/");
|
|
4229
|
+
if (slash <= 0 || slash >= key.length - 1) return undefined;
|
|
4230
|
+
return routedModelRefToPiModel(ref ?? { provider: key.slice(0, slash), id: key.slice(slash + 1) });
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
function sortStatsEntries(entries: Array<[string, CacheStats]>, activeModelKey?: string): Array<[string, CacheStats]> {
|
|
4234
|
+
return entries.sort(([left], [right]) => {
|
|
4235
|
+
if (left === activeModelKey) return -1;
|
|
4236
|
+
if (right === activeModelKey) return 1;
|
|
4237
|
+
return left.localeCompare(right);
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
function buildSessionStatsOutput(
|
|
4242
|
+
sessionModels: Record<string, CacheStats>,
|
|
4243
|
+
activeModel?: PiModel,
|
|
4244
|
+
modelRefsByKey: Record<string, PersistedRoutedModelRef> = {},
|
|
4245
|
+
): string {
|
|
4246
|
+
const activeKey = activeModel ? modelKey(activeModel) : undefined;
|
|
4247
|
+
const models = { ...sessionModels };
|
|
4248
|
+
if (activeModel && selectAdapterForModel(activeModel) && !models[activeKey!]) {
|
|
4249
|
+
models[activeKey!] = emptyCacheStats();
|
|
4250
|
+
}
|
|
4251
|
+
const entries = sortStatsEntries(Object.entries(models), activeKey);
|
|
4252
|
+
if (entries.length === 0) return "ℹ️ No cache statistics recorded for the current session today.";
|
|
4253
|
+
const lines = ["Scope: current session", `Day: ${currentLocalDay()}`, `Models: ${entries.length}`];
|
|
4254
|
+
for (const [key, stats] of entries) {
|
|
4255
|
+
const model = modelFromStatsKey(key, modelRefsByKey[key]);
|
|
4256
|
+
const adapter = model ? selectAdapterForModel(model) : undefined;
|
|
4257
|
+
lines.push("", `── ${key} ──`, `Adapter: ${adapter?.label ?? "Unknown cache adapter"}`);
|
|
4258
|
+
lines.push(`Requests: ${stats.hitRequests} hit / ${stats.totalRequests} total`);
|
|
4259
|
+
lines.push(`Cached tokens: ${formatTokenCount(stats.cachedInputTokens)} / ${formatTokenCount(stats.totalInputTokens)} input · ${stats.totalInputTokens > 0 ? `${((stats.cachedInputTokens / stats.totalInputTokens) * 100).toFixed(1)}%` : "0.0%"}`);
|
|
4260
|
+
lines.push(`Summary: ${formatCompactStats(stats)}`);
|
|
4261
|
+
if (stats.cacheWriteInputTokens > 0) lines.push(`Cache write: ${formatTokenCount(stats.cacheWriteInputTokens)}`);
|
|
4262
|
+
}
|
|
4263
|
+
return lines.join("\n");
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4266
|
+
function buildAllStatsOutput(aggregate: ShardAggregate): string {
|
|
4267
|
+
const entries = sortStatsEntries(Object.entries(aggregate.totalsByModel));
|
|
4268
|
+
if (entries.length === 0) return "ℹ️ No local cache statistics recorded today.";
|
|
4269
|
+
const sessionCount = Object.keys(aggregate.instancesBySession).length;
|
|
4270
|
+
const instanceCount = Object.values(aggregate.instancesBySession).reduce((sum, value) => sum + value, 0);
|
|
4271
|
+
const lines = ["Scope: all local sessions", `Day: ${currentLocalDay()}`, `Sessions: ${sessionCount}`, `Instances: ${instanceCount}`];
|
|
4272
|
+
for (const [key, stats] of entries) {
|
|
4273
|
+
const model = modelFromStatsKey(key, aggregate.modelRefsByKey[key]);
|
|
4274
|
+
const adapter = model ? selectAdapterForModel(model) : undefined;
|
|
4275
|
+
lines.push("", `── ${key} ──`, `Adapter: ${adapter?.label ?? "Unknown cache adapter"}`);
|
|
4276
|
+
lines.push(`Sessions: ${aggregate.sessionsByModel[key] ?? 0} · Instances: ${aggregate.instancesByModel[key] ?? 0}`);
|
|
4277
|
+
lines.push(`Requests: ${stats.hitRequests} hit / ${stats.totalRequests} total`);
|
|
4278
|
+
lines.push(`Cached tokens: ${formatTokenCount(stats.cachedInputTokens)} / ${formatTokenCount(stats.totalInputTokens)} input · ${stats.totalInputTokens > 0 ? `${((stats.cachedInputTokens / stats.totalInputTokens) * 100).toFixed(1)}%` : "0.0%"}`);
|
|
4279
|
+
lines.push(`Summary: ${formatCompactStats(stats)}`);
|
|
4280
|
+
if (stats.cacheWriteInputTokens > 0) lines.push(`Cache write: ${formatTokenCount(stats.cacheWriteInputTokens)}`);
|
|
4281
|
+
}
|
|
4282
|
+
return lines.join("\n");
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
function buildContributorsStatsOutput(aggregate: ShardAggregate, activeModel: PiModel | undefined, currentSessionHash?: string): string {
|
|
4286
|
+
if (!activeModel) return "ℹ️ No active model selected.";
|
|
4287
|
+
const key = modelKey(activeModel);
|
|
4288
|
+
const rows = Object.entries(aggregate.bySession)
|
|
4289
|
+
.filter(([, models]) => models[key])
|
|
4290
|
+
.sort(([left], [right]) => left === currentSessionHash ? -1 : right === currentSessionHash ? 1 : left.localeCompare(right));
|
|
4291
|
+
if (rows.length === 0) return `ℹ️ No contributors recorded for ${key} today.`;
|
|
4292
|
+
const lines = [`Model: ${key}`, "Scope: contributing local sessions today"];
|
|
4293
|
+
let otherIndex = 0;
|
|
4294
|
+
for (const [sessionHash, models] of rows) {
|
|
4295
|
+
const label = sessionHash === currentSessionHash ? "Current session" : `Other session ${++otherIndex}`;
|
|
4296
|
+
lines.push("", label, ` Instances: ${aggregate.instancesBySessionModel[sessionHash]?.[key] ?? 0}`, ` ${formatCompactStats(models[key])}`);
|
|
4297
|
+
}
|
|
4298
|
+
return lines.join("\n");
|
|
4299
|
+
}
|
|
4300
|
+
|
|
3843
4301
|
function buildStatsOutput(model: PiModel | undefined, adapter: CacheProviderAdapter | undefined, stats: CacheStats | undefined, recentSamples: CacheUsageSample[]): string {
|
|
3844
4302
|
const lines: string[] = [];
|
|
3845
4303
|
|
|
@@ -4248,8 +4706,8 @@ function filterRestorableStatsForSession(
|
|
|
4248
4706
|
* `restoreCacheStats` can migrate them on the next load before the session
|
|
4249
4707
|
* id is known.
|
|
4250
4708
|
*
|
|
4251
|
-
*
|
|
4252
|
-
*
|
|
4709
|
+
* Historical v1-v6 pure helper (no runtime I/O). Permanent migration tests
|
|
4710
|
+
* exercise it directly; v7 startup never reads or writes the obsolete file.
|
|
4253
4711
|
*/
|
|
4254
4712
|
function mergeCacheSessions(
|
|
4255
4713
|
existingSessions: Record<string, Record<string, CacheStats>>,
|
|
@@ -4409,15 +4867,312 @@ async function writePersistedCacheStats(
|
|
|
4409
4867
|
await rename(tempPath, STATE_FILE_PATH);
|
|
4410
4868
|
}
|
|
4411
4869
|
|
|
4870
|
+
function modelEpochPath(modelKeyValue: string): string {
|
|
4871
|
+
return join(SHARD_MODEL_EPOCH_DIR, `${createHash("sha256").update(modelKeyValue).digest("hex")}.json`);
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4874
|
+
function initialEpoch(scope: string): string {
|
|
4875
|
+
return `initial:${scope}`;
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
function parseEpochRecord(value: unknown): string | undefined {
|
|
4879
|
+
const record = asRecord(value);
|
|
4880
|
+
return record?.version === 1 && isNonEmptyString(record.epoch) ? record.epoch.trim() : undefined;
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
async function readEpochFile(path: string, fallback: string): Promise<string> {
|
|
4884
|
+
try {
|
|
4885
|
+
return parseEpochRecord(JSON.parse(await readFile(path, "utf8"))) ?? fallback;
|
|
4886
|
+
} catch {
|
|
4887
|
+
return fallback;
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
async function readGlobalStatsEpoch(): Promise<string> {
|
|
4892
|
+
return readEpochFile(SHARD_GLOBAL_EPOCH_PATH, initialEpoch("global"));
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
async function readModelStatsEpoch(modelKeyValue: string): Promise<string> {
|
|
4896
|
+
return readEpochFile(modelEpochPath(modelKeyValue), initialEpoch(`model:${modelKeyValue}`));
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
async function writeStatsEpoch(path: string, epoch: string): Promise<void> {
|
|
4900
|
+
await mkdir(dirname(path), { recursive: true });
|
|
4901
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
4902
|
+
await writeFile(tempPath, JSON.stringify({ version: 1, epoch, createdAt: Date.now() }, null, 2) + "\n", "utf8");
|
|
4903
|
+
await rename(tempPath, path);
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
async function advanceGlobalStatsEpoch(): Promise<string> {
|
|
4907
|
+
const epoch = `${Date.now()}-${randomUUID()}`;
|
|
4908
|
+
await writeStatsEpoch(SHARD_GLOBAL_EPOCH_PATH, epoch);
|
|
4909
|
+
return epoch;
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
async function advanceModelStatsEpoch(modelKeyValue: string): Promise<string> {
|
|
4913
|
+
const epoch = `${Date.now()}-${randomUUID()}`;
|
|
4914
|
+
await writeStatsEpoch(modelEpochPath(modelKeyValue), epoch);
|
|
4915
|
+
return epoch;
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4918
|
+
function parsePersistedStatsShardV7(value: unknown): PersistedStatsShardV7 | undefined {
|
|
4919
|
+
const record = asRecord(value);
|
|
4920
|
+
const processRecord = asRecord(record?.process);
|
|
4921
|
+
const lifecycle = asRecord(record?.lifecycle);
|
|
4922
|
+
const rawModels = asRecord(record?.models);
|
|
4923
|
+
if (
|
|
4924
|
+
record?.version !== 7 ||
|
|
4925
|
+
record.kind !== "pi-cache-optimizer-shard" ||
|
|
4926
|
+
!isNonEmptyString(record.instanceId) ||
|
|
4927
|
+
!isNonEmptyString(record.sessionHash) ||
|
|
4928
|
+
!processRecord ||
|
|
4929
|
+
!Number.isInteger(processRecord.pid) ||
|
|
4930
|
+
!Number.isInteger(processRecord.ppid) ||
|
|
4931
|
+
typeof processRecord.instanceStartedAt !== "number" ||
|
|
4932
|
+
!lifecycle ||
|
|
4933
|
+
(lifecycle.state !== "active" && lifecycle.state !== "closed") ||
|
|
4934
|
+
typeof lifecycle.createdAt !== "number" ||
|
|
4935
|
+
typeof lifecycle.updatedAt !== "number" ||
|
|
4936
|
+
!isNonEmptyString(record.day) ||
|
|
4937
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(record.day) ||
|
|
4938
|
+
!isNonEmptyString(record.globalEpoch) ||
|
|
4939
|
+
!rawModels
|
|
4940
|
+
) return undefined;
|
|
4941
|
+
|
|
4942
|
+
const models: PersistedStatsShardV7["models"] = {};
|
|
4943
|
+
for (const [key, rawEntry] of Object.entries(rawModels)) {
|
|
4944
|
+
const entry = asRecord(rawEntry);
|
|
4945
|
+
const stats = parseCacheStats(entry?.stats);
|
|
4946
|
+
if (
|
|
4947
|
+
!entry || !stats || !isNonEmptyString(entry.modelEpoch) ||
|
|
4948
|
+
!isNonEmptyString(entry.provider) || !isNonEmptyString(entry.modelId) ||
|
|
4949
|
+
key !== `${entry.provider.trim()}/${entry.modelId.trim()}`
|
|
4950
|
+
) continue;
|
|
4951
|
+
models[key] = {
|
|
4952
|
+
modelEpoch: entry.modelEpoch.trim(),
|
|
4953
|
+
provider: entry.provider.trim(),
|
|
4954
|
+
modelId: entry.modelId.trim(),
|
|
4955
|
+
...(isNonEmptyString(entry.modelName) ? { modelName: entry.modelName.trim() } : {}),
|
|
4956
|
+
...(isNonEmptyString(entry.api) ? { api: entry.api.trim() } : {}),
|
|
4957
|
+
stats,
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4412
4960
|
|
|
4961
|
+
const lastRoutedModel = parsePersistedRoutedModelRef(record.lastRoutedModel);
|
|
4962
|
+
return {
|
|
4963
|
+
version: 7,
|
|
4964
|
+
kind: "pi-cache-optimizer-shard",
|
|
4965
|
+
instanceId: record.instanceId.trim(),
|
|
4966
|
+
sessionHash: record.sessionHash.trim(),
|
|
4967
|
+
process: {
|
|
4968
|
+
pid: Number(processRecord.pid),
|
|
4969
|
+
ppid: Number(processRecord.ppid),
|
|
4970
|
+
instanceStartedAt: processRecord.instanceStartedAt,
|
|
4971
|
+
},
|
|
4972
|
+
lifecycle: {
|
|
4973
|
+
state: lifecycle.state,
|
|
4974
|
+
createdAt: lifecycle.createdAt,
|
|
4975
|
+
updatedAt: lifecycle.updatedAt,
|
|
4976
|
+
...(typeof lifecycle.closedAt === "number" ? { closedAt: lifecycle.closedAt } : {}),
|
|
4977
|
+
},
|
|
4978
|
+
day: record.day,
|
|
4979
|
+
globalEpoch: record.globalEpoch.trim(),
|
|
4980
|
+
models,
|
|
4981
|
+
...(lastRoutedModel ? { lastRoutedModel } : {}),
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
async function writeStatsShardV7(path: string, shard: PersistedStatsShardV7): Promise<void> {
|
|
4986
|
+
await mkdir(dirname(path), { recursive: true });
|
|
4987
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
4988
|
+
await writeFile(tempPath, JSON.stringify(shard, null, 2) + "\n", "utf8");
|
|
4989
|
+
await rename(tempPath, path);
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
async function readValidStatsShardsV7(directory: string = SHARD_FILES_DIR): Promise<PersistedStatsShardV7[]> {
|
|
4993
|
+
let names: string[];
|
|
4994
|
+
try {
|
|
4995
|
+
names = await readdir(directory);
|
|
4996
|
+
} catch (error) {
|
|
4997
|
+
if (getErrorCode(error) === "ENOENT") return [];
|
|
4998
|
+
throw error;
|
|
4999
|
+
}
|
|
5000
|
+
const shards: PersistedStatsShardV7[] = [];
|
|
5001
|
+
for (const name of names) {
|
|
5002
|
+
if (!/^[0-9a-f-]+\.json$/i.test(name)) continue;
|
|
5003
|
+
const path = join(directory, name);
|
|
5004
|
+
try {
|
|
5005
|
+
const info = await lstat(path);
|
|
5006
|
+
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
5007
|
+
const parsed = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
|
|
5008
|
+
const filenameInstanceId = name.slice(0, -".json".length);
|
|
5009
|
+
if (parsed && parsed.instanceId === filenameInstanceId) shards.push(parsed);
|
|
5010
|
+
} catch {
|
|
5011
|
+
// Ignore malformed or transiently unavailable shards. Atomic writers will
|
|
5012
|
+
// publish a complete replacement on the next successful update.
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
return shards;
|
|
5016
|
+
}
|
|
5017
|
+
|
|
5018
|
+
async function aggregateStatsShardsV7(
|
|
5019
|
+
shards: PersistedStatsShardV7[],
|
|
5020
|
+
day: string = currentLocalDay(),
|
|
5021
|
+
): Promise<ShardAggregate> {
|
|
5022
|
+
const globalEpoch = await readGlobalStatsEpoch();
|
|
5023
|
+
const modelEpochs = new Map<string, string>();
|
|
5024
|
+
const result: ShardAggregate = {
|
|
5025
|
+
bySession: {},
|
|
5026
|
+
totalsByModel: {},
|
|
5027
|
+
instancesBySession: {},
|
|
5028
|
+
instancesBySessionModel: {},
|
|
5029
|
+
sessionsByModel: {},
|
|
5030
|
+
instancesByModel: {},
|
|
5031
|
+
modelRefsByKey: {},
|
|
5032
|
+
lastRoutedModelBySession: {},
|
|
5033
|
+
};
|
|
5034
|
+
const routedUpdatedAt = new Map<string, number>();
|
|
5035
|
+
const modelRefUpdatedAt = new Map<string, number>();
|
|
5036
|
+
const modelSessions = new Map<string, Set<string>>();
|
|
5037
|
+
|
|
5038
|
+
for (const shard of shards) {
|
|
5039
|
+
if (shard.day !== day || shard.globalEpoch !== globalEpoch) continue;
|
|
5040
|
+
let contributed = false;
|
|
5041
|
+
if (shard.lastRoutedModel && (routedUpdatedAt.get(shard.sessionHash) ?? -1) < shard.lifecycle.updatedAt) {
|
|
5042
|
+
routedUpdatedAt.set(shard.sessionHash, shard.lifecycle.updatedAt);
|
|
5043
|
+
result.lastRoutedModelBySession[shard.sessionHash] = shard.lastRoutedModel;
|
|
5044
|
+
}
|
|
5045
|
+
for (const [key, entry] of Object.entries(shard.models)) {
|
|
5046
|
+
let epoch = modelEpochs.get(key);
|
|
5047
|
+
if (!epoch) {
|
|
5048
|
+
epoch = await readModelStatsEpoch(key);
|
|
5049
|
+
modelEpochs.set(key, epoch);
|
|
5050
|
+
}
|
|
5051
|
+
if (entry.modelEpoch !== epoch || entry.stats.day !== day) continue;
|
|
5052
|
+
contributed = true;
|
|
5053
|
+
const sessionModels = result.bySession[shard.sessionHash] ??= {};
|
|
5054
|
+
sessionModels[key] = mergeCacheStatsForTotal(sessionModels[key], entry.stats);
|
|
5055
|
+
result.totalsByModel[key] = mergeCacheStatsForTotal(result.totalsByModel[key], entry.stats);
|
|
5056
|
+
result.instancesByModel[key] = (result.instancesByModel[key] ?? 0) + 1;
|
|
5057
|
+
if ((modelRefUpdatedAt.get(key) ?? -1) < shard.lifecycle.updatedAt) {
|
|
5058
|
+
modelRefUpdatedAt.set(key, shard.lifecycle.updatedAt);
|
|
5059
|
+
result.modelRefsByKey[key] = {
|
|
5060
|
+
provider: entry.provider,
|
|
5061
|
+
id: entry.modelId,
|
|
5062
|
+
name: entry.modelName ?? entry.modelId,
|
|
5063
|
+
};
|
|
5064
|
+
}
|
|
5065
|
+
const sessionInstances = result.instancesBySessionModel[shard.sessionHash] ??= {};
|
|
5066
|
+
sessionInstances[key] = (sessionInstances[key] ?? 0) + 1;
|
|
5067
|
+
const sessions = modelSessions.get(key) ?? new Set<string>();
|
|
5068
|
+
sessions.add(shard.sessionHash);
|
|
5069
|
+
modelSessions.set(key, sessions);
|
|
5070
|
+
}
|
|
5071
|
+
if (contributed) {
|
|
5072
|
+
result.instancesBySession[shard.sessionHash] = (result.instancesBySession[shard.sessionHash] ?? 0) + 1;
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
for (const [key, sessions] of modelSessions) result.sessionsByModel[key] = sessions.size;
|
|
5076
|
+
return result;
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
async function loadStatsShardAggregateV7(directory: string = SHARD_FILES_DIR): Promise<ShardAggregate> {
|
|
5080
|
+
return aggregateStatsShardsV7(await readValidStatsShardsV7(directory));
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
function isProcessAlive(pid: number): boolean {
|
|
5084
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
5085
|
+
try {
|
|
5086
|
+
process.kill(pid, 0);
|
|
5087
|
+
return true;
|
|
5088
|
+
} catch (error) {
|
|
5089
|
+
return getErrorCode(error) === "EPERM";
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
|
|
5093
|
+
async function removeLegacyStatsFiles(): Promise<void> {
|
|
5094
|
+
for (const path of [STATE_FILE_PATH, LEGACY_STATE_FILE_PATH]) {
|
|
5095
|
+
try {
|
|
5096
|
+
await unlink(path);
|
|
5097
|
+
} catch (error) {
|
|
5098
|
+
if (getErrorCode(error) !== "ENOENT") console.warn(`${LOG_PREFIX}: failed to remove obsolete stats file ${path}`, error);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
|
|
5103
|
+
async function cleanupStatsShardsV7(now = Date.now(), directory: string = SHARD_FILES_DIR): Promise<number> {
|
|
5104
|
+
let names: string[];
|
|
5105
|
+
try {
|
|
5106
|
+
names = await readdir(directory);
|
|
5107
|
+
} catch (error) {
|
|
5108
|
+
return getErrorCode(error) === "ENOENT" ? 0 : Promise.reject(error);
|
|
5109
|
+
}
|
|
5110
|
+
const today = currentLocalDay();
|
|
5111
|
+
let removed = 0;
|
|
5112
|
+
for (const name of names) {
|
|
5113
|
+
const isShard = /^[0-9a-f-]+\.json$/i.test(name);
|
|
5114
|
+
const isTemp = name.endsWith(".tmp");
|
|
5115
|
+
if (!isShard && !isTemp) continue;
|
|
5116
|
+
const path = join(directory, name);
|
|
5117
|
+
try {
|
|
5118
|
+
const info = await lstat(path);
|
|
5119
|
+
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
5120
|
+
if (isTemp) {
|
|
5121
|
+
if (now - info.mtimeMs < SHARD_TEMP_RETENTION_MS) continue;
|
|
5122
|
+
} else {
|
|
5123
|
+
const parsed = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
|
|
5124
|
+
if (parsed?.day === today) continue;
|
|
5125
|
+
const updatedAt = parsed?.lifecycle.updatedAt ?? info.mtimeMs;
|
|
5126
|
+
if (now - updatedAt < SHARD_RETENTION_MS) continue;
|
|
5127
|
+
if (parsed?.lifecycle.state === "active" && isProcessAlive(parsed.process.pid)) continue;
|
|
5128
|
+
}
|
|
5129
|
+
await unlink(path);
|
|
5130
|
+
removed += 1;
|
|
5131
|
+
} catch {
|
|
5132
|
+
// Best-effort maintenance must never block the extension.
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
return removed;
|
|
5136
|
+
}
|
|
5137
|
+
|
|
5138
|
+
async function maybeCleanupStatsShardsV7(now = Date.now()): Promise<void> {
|
|
5139
|
+
await mkdir(SHARD_MAINTENANCE_DIR, { recursive: true });
|
|
5140
|
+
try {
|
|
5141
|
+
const marker = await stat(SHARD_CLEANUP_MARKER_PATH);
|
|
5142
|
+
if (now - marker.mtimeMs < SHARD_CLEANUP_INTERVAL_MS) return;
|
|
5143
|
+
} catch {}
|
|
5144
|
+
|
|
5145
|
+
try {
|
|
5146
|
+
await mkdir(SHARD_CLEANUP_LOCK_PATH);
|
|
5147
|
+
} catch (error) {
|
|
5148
|
+
if (getErrorCode(error) !== "EEXIST") return;
|
|
5149
|
+
try {
|
|
5150
|
+
const lock = await stat(SHARD_CLEANUP_LOCK_PATH);
|
|
5151
|
+
if (now - lock.mtimeMs <= SHARD_CLEANUP_LOCK_STALE_MS) return;
|
|
5152
|
+
await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
|
|
5153
|
+
await mkdir(SHARD_CLEANUP_LOCK_PATH);
|
|
5154
|
+
} catch {
|
|
5155
|
+
return;
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
try {
|
|
5160
|
+
await cleanupStatsShardsV7(now);
|
|
5161
|
+
const tempPath = `${SHARD_CLEANUP_MARKER_PATH}.${process.pid}.${randomUUID()}.tmp`;
|
|
5162
|
+
await writeFile(tempPath, `${now}\n`, "utf8");
|
|
5163
|
+
await rename(tempPath, SHARD_CLEANUP_MARKER_PATH);
|
|
5164
|
+
} finally {
|
|
5165
|
+
await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
4413
5168
|
|
|
4414
5169
|
function isCompatCheckApplicable(model: PiModel): boolean {
|
|
4415
|
-
return isOpenAICompatibleProxyApi(model.api) &&
|
|
5170
|
+
return isOpenAICompatibleProxyApi(model.api) && isKnownThirdPartyOpenAIEndpoint(model) && !isPiBuiltInLlamaCppModel(model);
|
|
4416
5171
|
}
|
|
4417
5172
|
|
|
4418
5173
|
function isPromptCacheRetention400Applicable(model: PiModel): boolean {
|
|
4419
5174
|
return isOpenAICompatibleApi(model.api) &&
|
|
4420
|
-
|
|
5175
|
+
isKnownThirdPartyOpenAIEndpoint(model) &&
|
|
4421
5176
|
!isPiBuiltInLlamaCppModel(model) &&
|
|
4422
5177
|
getCompat(model).supportsLongCacheRetention === true;
|
|
4423
5178
|
}
|
|
@@ -4445,6 +5200,7 @@ function isExplicitPromptCacheRetentionUnsupportedApplicable(model: PiModel): bo
|
|
|
4445
5200
|
*/
|
|
4446
5201
|
function isSessionAffinity403Applicable(model: PiModel): boolean {
|
|
4447
5202
|
if (!isOpenAICompatibleProxyApi(model.api)) return false;
|
|
5203
|
+
if (!isKnownThirdPartyOpenAIEndpoint(model)) return false;
|
|
4448
5204
|
if (isPiBuiltInLlamaCppModel(model)) return false;
|
|
4449
5205
|
return getCompat(model).sendSessionAffinityHeaders === true;
|
|
4450
5206
|
}
|
|
@@ -4461,7 +5217,7 @@ function isSessionAffinity403Applicable(model: PiModel): boolean {
|
|
|
4461
5217
|
*/
|
|
4462
5218
|
function isOpenAISdkHeader403Applicable(model: PiModel): boolean {
|
|
4463
5219
|
if (!isOpenAICompatibleProxyApi(model.api)) return false;
|
|
4464
|
-
if (
|
|
5220
|
+
if (!isKnownThirdPartyOpenAIEndpoint(model)) return false;
|
|
4465
5221
|
if (isPiBuiltInLlamaCppModel(model)) return false;
|
|
4466
5222
|
return getCompat(model).sendSessionAffinityHeaders !== true;
|
|
4467
5223
|
}
|
|
@@ -4499,8 +5255,9 @@ function describeRouterChannelDiagnostics(model: PiModel): string[] {
|
|
|
4499
5255
|
return notes;
|
|
4500
5256
|
}
|
|
4501
5257
|
|
|
4502
|
-
//
|
|
4503
|
-
|
|
5258
|
+
// Unknown/default endpoints and official OpenAI are not diagnosable as
|
|
5259
|
+
// third-party router channels. Request-header bridging also fails closed.
|
|
5260
|
+
if (!isKnownThirdPartyOpenAIEndpoint(model)) {
|
|
4504
5261
|
return notes;
|
|
4505
5262
|
}
|
|
4506
5263
|
|
|
@@ -4667,6 +5424,13 @@ function getCompatCheckNotApplicableLines(model: PiModel): string[] {
|
|
|
4667
5424
|
];
|
|
4668
5425
|
}
|
|
4669
5426
|
|
|
5427
|
+
if (isOpenAICompatibleApi(api) && !isNonEmptyString(model.baseUrl)) {
|
|
5428
|
+
return [
|
|
5429
|
+
"ℹ️ Compat check not applicable for this model.",
|
|
5430
|
+
" Upstream endpoint metadata is unavailable; session-affinity header injection is disabled.",
|
|
5431
|
+
];
|
|
5432
|
+
}
|
|
5433
|
+
|
|
4670
5434
|
return ["ℹ️ Compat check not applicable for this model."];
|
|
4671
5435
|
}
|
|
4672
5436
|
|
|
@@ -6718,6 +7482,7 @@ export const __internals_for_tests = {
|
|
|
6718
7482
|
buildSafeOpenAIProxyCompatSuggestion,
|
|
6719
7483
|
getPromptCacheRetentionUnsupportedHint,
|
|
6720
7484
|
isOfficialOpenAIBaseUrl,
|
|
7485
|
+
isKnownThirdPartyOpenAIEndpoint,
|
|
6721
7486
|
isCompatCheckApplicable,
|
|
6722
7487
|
isPromptCacheRetention400Applicable,
|
|
6723
7488
|
isSessionAffinity403Applicable,
|
|
@@ -6838,6 +7603,11 @@ export const __internals_for_tests = {
|
|
|
6838
7603
|
buildOpenAIProxyCompatWarningText,
|
|
6839
7604
|
getModelIdNameTokenValues,
|
|
6840
7605
|
getAssistantMessageModelTokenValues,
|
|
7606
|
+
mergeCacheCompat,
|
|
7607
|
+
resolveEffectiveCompatFromConfig,
|
|
7608
|
+
getEffectiveCompatValueSource,
|
|
7609
|
+
isValidModelsConfigForEffectiveCompat,
|
|
7610
|
+
addEffectiveSessionAffinityHeaders,
|
|
6841
7611
|
getCompat,
|
|
6842
7612
|
modelKey,
|
|
6843
7613
|
modelFromAssistantMessage,
|
|
@@ -6876,6 +7646,10 @@ export const __internals_for_tests = {
|
|
|
6876
7646
|
// Recent sample / stats output / diagnosis helpers
|
|
6877
7647
|
MAX_RECENT_SAMPLES,
|
|
6878
7648
|
buildStatsOutput,
|
|
7649
|
+
buildSessionStatsOutput,
|
|
7650
|
+
buildAllStatsOutput,
|
|
7651
|
+
buildContributorsStatsOutput,
|
|
7652
|
+
formatCompactStats,
|
|
6879
7653
|
buildLowHitDiagnosis,
|
|
6880
7654
|
formatRecentTrendSummary,
|
|
6881
7655
|
formatHitRatio,
|
|
@@ -6908,6 +7682,8 @@ export const __internals_for_tests = {
|
|
|
6908
7682
|
parseRouteSnapshot,
|
|
6909
7683
|
resolveActiveRouteSnapshot,
|
|
6910
7684
|
routeSnapshotToPiModel,
|
|
7685
|
+
isRoutedFallbackModel,
|
|
7686
|
+
applyConfiguredTransportToModel,
|
|
6911
7687
|
resolveRouteModel,
|
|
6912
7688
|
isVirtualRoutingModel,
|
|
6913
7689
|
installCacheHintsService,
|
|
@@ -6923,8 +7699,23 @@ export const __internals_for_tests = {
|
|
|
6923
7699
|
createSerializedAsyncRunner,
|
|
6924
7700
|
writePersistedCacheStats,
|
|
6925
7701
|
readPersistedCacheStats,
|
|
7702
|
+
parsePersistedStatsShardV7,
|
|
7703
|
+
writeStatsShardV7,
|
|
7704
|
+
readValidStatsShardsV7,
|
|
7705
|
+
aggregateStatsShardsV7,
|
|
7706
|
+
loadStatsShardAggregateV7,
|
|
7707
|
+
cleanupStatsShardsV7,
|
|
7708
|
+
removeLegacyStatsFiles,
|
|
7709
|
+
readGlobalStatsEpoch,
|
|
7710
|
+
readModelStatsEpoch,
|
|
7711
|
+
advanceGlobalStatsEpoch,
|
|
7712
|
+
advanceModelStatsEpoch,
|
|
7713
|
+
modelEpochPath,
|
|
6926
7714
|
STATE_FILE_PATH,
|
|
6927
7715
|
LEGACY_STATE_FILE_PATH,
|
|
7716
|
+
SHARD_STATE_DIR,
|
|
7717
|
+
SHARD_FILES_DIR,
|
|
7718
|
+
SHARD_GLOBAL_EPOCH_PATH,
|
|
6928
7719
|
STATE_DIR,
|
|
6929
7720
|
// JSONC surgical edit helpers
|
|
6930
7721
|
MODELS_JSON_PATH,
|
|
@@ -6981,17 +7772,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
6981
7772
|
let lastStatusText: string | undefined;
|
|
6982
7773
|
let persistenceWarningShown = false;
|
|
6983
7774
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
7775
|
+
let shardRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
7776
|
+
let shardWatcher: ReturnType<typeof watch> | undefined;
|
|
6984
7777
|
const enqueuePersist = createSerializedAsyncRunner();
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
const
|
|
7778
|
+
const instanceId = randomUUID();
|
|
7779
|
+
const instanceStartedAt = Date.now();
|
|
7780
|
+
const instanceShardPath = join(SHARD_FILES_DIR, `${instanceId}.json`);
|
|
7781
|
+
const modelApiByKey = new Map<string, string>();
|
|
7782
|
+
const modelNameByKey = new Map<string, string>();
|
|
7783
|
+
const modelEpochByKey = new Map<string, string>();
|
|
7784
|
+
let currentGlobalEpoch = initialEpoch("global");
|
|
6988
7785
|
let integrityNotificationShown = false;
|
|
6989
7786
|
let currentSessionId = "";
|
|
6990
7787
|
let currentSessionHash = "";
|
|
6991
7788
|
let currentSessionHashSet = false;
|
|
6992
7789
|
let lastActualRoutedModel: PersistedRoutedModelRef | undefined;
|
|
6993
7790
|
let latestCacheHint: PiCacheHintSnapshot | undefined;
|
|
7791
|
+
let shardCreatedAt = Date.now();
|
|
6994
7792
|
const PERSIST_DEBOUNCE_MS = 2000;
|
|
7793
|
+
const SHARD_REFRESH_DEBOUNCE_MS = 250;
|
|
6995
7794
|
|
|
6996
7795
|
function buildObservedRuntimeFixSuggestion(model: PiModel): FixSuggestion | undefined {
|
|
6997
7796
|
const key = modelKey(model);
|
|
@@ -7106,17 +7905,102 @@ export default function (pi: ExtensionAPI) {
|
|
|
7106
7905
|
recentSamplesByModelKey.clear();
|
|
7107
7906
|
}
|
|
7108
7907
|
|
|
7109
|
-
function
|
|
7908
|
+
async function refreshShardAggregate(): Promise<ShardAggregate> {
|
|
7909
|
+
const persistedShards = await readValidStatsShardsV7();
|
|
7910
|
+
const shards = persistedShards.filter((shard) => shard.instanceId !== instanceId);
|
|
7911
|
+
if (currentSessionHashSet) shards.push(buildCurrentStatsShard());
|
|
7912
|
+
const aggregate = await aggregateStatsShardsV7(shards);
|
|
7913
|
+
|
|
7914
|
+
// A reset from another process can advance epochs while this instance is
|
|
7915
|
+
// idle. Adopt those epochs during every aggregate refresh, not only when a
|
|
7916
|
+
// later message_end arrives, so this instance cannot keep an old in-memory
|
|
7917
|
+
// process bucket visible in `process` mode or rewrite stale counters during
|
|
7918
|
+
// shutdown. Model-scoped resets clear only the affected local bucket.
|
|
7919
|
+
const latestGlobalEpoch = await readGlobalStatsEpoch();
|
|
7920
|
+
if (currentGlobalEpoch !== latestGlobalEpoch) {
|
|
7921
|
+
currentGlobalEpoch = latestGlobalEpoch;
|
|
7922
|
+
cacheStatsProcessByModel = {};
|
|
7923
|
+
modelEpochByKey.clear();
|
|
7924
|
+
modelApiByKey.clear();
|
|
7925
|
+
modelNameByKey.clear();
|
|
7926
|
+
lastActualRoutedModel = undefined;
|
|
7927
|
+
} else {
|
|
7928
|
+
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7929
|
+
const latestModelEpoch = await readModelStatsEpoch(key);
|
|
7930
|
+
if ((modelEpochByKey.get(key) ?? initialEpoch(`model:${key}`)) === latestModelEpoch) continue;
|
|
7931
|
+
delete cacheStatsProcessByModel[key];
|
|
7932
|
+
modelEpochByKey.set(key, latestModelEpoch);
|
|
7933
|
+
modelApiByKey.delete(key);
|
|
7934
|
+
modelNameByKey.delete(key);
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
|
|
7938
|
+
cacheStatsByModel = {};
|
|
7939
|
+
for (const [sessionHash, models] of Object.entries(aggregate.bySession)) {
|
|
7940
|
+
for (const [key, stats] of Object.entries(models)) {
|
|
7941
|
+
cacheStatsByModel[`${sessionHash}:${key}`] = stats;
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
cacheStatsTotalsByModel = aggregate.totalsByModel;
|
|
7945
|
+
if (currentSessionHashSet) {
|
|
7946
|
+
lastActualRoutedModel = aggregate.lastRoutedModelBySession[currentSessionHash];
|
|
7947
|
+
}
|
|
7948
|
+
lastStatusText = undefined;
|
|
7949
|
+
return aggregate;
|
|
7950
|
+
}
|
|
7951
|
+
|
|
7952
|
+
async function ensureCurrentEpochs(): Promise<void> {
|
|
7953
|
+
currentGlobalEpoch = await readGlobalStatsEpoch();
|
|
7954
|
+
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7955
|
+
modelEpochByKey.set(key, await readModelStatsEpoch(key));
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
|
|
7959
|
+
function buildCurrentStatsShard(state: "active" | "closed" = "active"): PersistedStatsShardV7 {
|
|
7960
|
+
const now = Date.now();
|
|
7961
|
+
const models: PersistedStatsShardV7["models"] = {};
|
|
7962
|
+
for (const [key, stats] of Object.entries(cacheStatsProcessByModel)) {
|
|
7963
|
+
const slash = key.indexOf("/");
|
|
7964
|
+
if (slash <= 0 || slash >= key.length - 1) continue;
|
|
7965
|
+
models[key] = {
|
|
7966
|
+
modelEpoch: modelEpochByKey.get(key) ?? initialEpoch(`model:${key}`),
|
|
7967
|
+
provider: key.slice(0, slash),
|
|
7968
|
+
modelId: key.slice(slash + 1),
|
|
7969
|
+
...(modelNameByKey.get(key) ? { modelName: modelNameByKey.get(key) } : {}),
|
|
7970
|
+
...(modelApiByKey.get(key) ? { api: modelApiByKey.get(key) } : {}),
|
|
7971
|
+
stats: cloneCacheStats(stats),
|
|
7972
|
+
};
|
|
7973
|
+
}
|
|
7110
7974
|
return {
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7975
|
+
version: 7,
|
|
7976
|
+
kind: "pi-cache-optimizer-shard",
|
|
7977
|
+
instanceId,
|
|
7978
|
+
sessionHash: currentSessionHash || "_nosession",
|
|
7979
|
+
process: { pid: process.pid, ppid: process.ppid, instanceStartedAt },
|
|
7980
|
+
lifecycle: {
|
|
7981
|
+
state,
|
|
7982
|
+
createdAt: shardCreatedAt,
|
|
7983
|
+
updatedAt: now,
|
|
7984
|
+
...(state === "closed" ? { closedAt: now } : {}),
|
|
7985
|
+
},
|
|
7986
|
+
day: currentLocalDay(),
|
|
7987
|
+
globalEpoch: currentGlobalEpoch,
|
|
7988
|
+
models,
|
|
7989
|
+
...(lastActualRoutedModel ? { lastRoutedModel: { ...lastActualRoutedModel } } : {}),
|
|
7117
7990
|
};
|
|
7118
7991
|
}
|
|
7119
7992
|
|
|
7993
|
+
function scheduleShardRefresh(ctx?: ExtensionContext, model?: PiModel): void {
|
|
7994
|
+
if (shardRefreshTimer !== null) clearTimeout(shardRefreshTimer);
|
|
7995
|
+
shardRefreshTimer = setTimeout(() => {
|
|
7996
|
+
shardRefreshTimer = null;
|
|
7997
|
+
void refreshShardAggregate()
|
|
7998
|
+
.then(() => publishStatus(ctx as ExtensionContext, model ?? ctx?.model))
|
|
7999
|
+
.catch(() => undefined);
|
|
8000
|
+
}, SHARD_REFRESH_DEBOUNCE_MS);
|
|
8001
|
+
shardRefreshTimer.unref?.();
|
|
8002
|
+
}
|
|
8003
|
+
|
|
7120
8004
|
/** Look up visible cumulative stats for a model, falling back to legacy family. */
|
|
7121
8005
|
function getStatsForModel(model: PiModel | undefined, adapter: CacheProviderAdapter): CacheStats {
|
|
7122
8006
|
if (model) {
|
|
@@ -7166,79 +8050,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
7166
8050
|
return created;
|
|
7167
8051
|
}
|
|
7168
8052
|
|
|
7169
|
-
function resetStatsForModel(model: PiModel): void {
|
|
8053
|
+
async function resetStatsForModel(model: PiModel): Promise<void> {
|
|
7170
8054
|
const displayKey = modelKey(model);
|
|
8055
|
+
const nextEpoch = await advanceModelStatsEpoch(displayKey);
|
|
8056
|
+
modelEpochByKey.set(displayKey, nextEpoch);
|
|
8057
|
+
delete cacheStatsProcessByModel[displayKey];
|
|
8058
|
+
delete cacheStatsTotalsByModel[displayKey];
|
|
7171
8059
|
for (const key of Object.keys(cacheStatsByModel)) {
|
|
7172
8060
|
if (modelKeyFromSessionScoped(key) === displayKey) delete cacheStatsByModel[key];
|
|
7173
8061
|
}
|
|
7174
|
-
delete cacheStatsTotalsByModel[displayKey];
|
|
7175
|
-
delete cacheStatsProcessByModel[displayKey];
|
|
7176
|
-
pendingDeletedTotalModelKeys.add(displayKey);
|
|
7177
8062
|
for (const key of Array.from(recentSamplesByModelKey.keys())) {
|
|
7178
8063
|
if (modelKeyFromSessionScoped(key) === displayKey) recentSamplesByModelKey.delete(key);
|
|
7179
8064
|
}
|
|
7180
8065
|
lastStatusText = undefined;
|
|
7181
8066
|
}
|
|
7182
8067
|
|
|
7183
|
-
function resetCurrentSessionStats(): void {
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
if (key.startsWith(prefix)) delete cacheStatsByModel[key];
|
|
7187
|
-
}
|
|
7188
|
-
cacheStatsProcessByModel = {};
|
|
8068
|
+
async function resetCurrentSessionStats(): Promise<void> {
|
|
8069
|
+
currentGlobalEpoch = await advanceGlobalStatsEpoch();
|
|
8070
|
+
cacheStatsByModel = {};
|
|
7189
8071
|
cacheStatsTotalsByModel = {};
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
8072
|
+
cacheStatsProcessByModel = {};
|
|
8073
|
+
modelEpochByKey.clear();
|
|
8074
|
+
modelApiByKey.clear();
|
|
8075
|
+
modelNameByKey.clear();
|
|
8076
|
+
clearRecentSamples();
|
|
7194
8077
|
lastActualRoutedModel = undefined;
|
|
7195
8078
|
lastStatusText = undefined;
|
|
7196
8079
|
}
|
|
7197
8080
|
|
|
7198
|
-
function
|
|
7199
|
-
const
|
|
7200
|
-
const statsByModel = Object.fromEntries(
|
|
7201
|
-
Object.entries(state.statsByModel).map(([key, stats]) => [key, { ...stats }]),
|
|
7202
|
-
);
|
|
7203
|
-
const totalsByModel = Object.fromEntries(
|
|
7204
|
-
Object.entries(state.totalsByModel).map(([key, stats]) => [key, { ...stats }]),
|
|
7205
|
-
);
|
|
7206
|
-
const legacyFamily: Partial<Record<CacheProviderId, CacheStats>> = {};
|
|
7207
|
-
for (const id of CACHE_PROVIDER_IDS) {
|
|
7208
|
-
const stats = state.legacyFamily[id];
|
|
7209
|
-
if (stats) legacyFamily[id] = { ...stats };
|
|
7210
|
-
}
|
|
7211
|
-
const lastRoutedModelBySession = state.lastRoutedModelBySession
|
|
7212
|
-
? Object.fromEntries(
|
|
7213
|
-
Object.entries(state.lastRoutedModelBySession).map(([key, model]) => [key, { ...model }]),
|
|
7214
|
-
)
|
|
7215
|
-
: undefined;
|
|
7216
|
-
return { statsByModel, totalsByModel, legacyFamily, lastRoutedModelBySession };
|
|
7217
|
-
}
|
|
7218
|
-
|
|
7219
|
-
function persistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
7220
|
-
const state = snapshotCacheStatsState();
|
|
7221
|
-
const sessionHash = currentSessionHashSet ? currentSessionHash : undefined;
|
|
7222
|
-
const deleteModelKeys = Array.from(pendingDeletedTotalModelKeys);
|
|
7223
|
-
const replaceTotals = replacePersistedTotalsOnNextWrite;
|
|
7224
|
-
// This queued write owns the current intents. Later mutations can enqueue
|
|
7225
|
-
// independent intents without an earlier completion clearing them.
|
|
7226
|
-
pendingDeletedTotalModelKeys.clear();
|
|
7227
|
-
replacePersistedTotalsOnNextWrite = false;
|
|
7228
|
-
|
|
8081
|
+
function persistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
|
|
8082
|
+
const shard = buildCurrentStatsShard(lifecycleState);
|
|
7229
8083
|
return enqueuePersist(async () => {
|
|
7230
|
-
const recoverFromEarlierFailure = forceReplaceTotalsAfterFailure;
|
|
7231
8084
|
try {
|
|
7232
|
-
await
|
|
7233
|
-
deleteModelKeys,
|
|
7234
|
-
replaceTotals: replaceTotals || recoverFromEarlierFailure,
|
|
7235
|
-
});
|
|
7236
|
-
if (recoverFromEarlierFailure) forceReplaceTotalsAfterFailure = false;
|
|
8085
|
+
await writeStatsShardV7(instanceShardPath, shard);
|
|
7237
8086
|
} catch (error) {
|
|
7238
|
-
|
|
7239
|
-
// reset/delete cannot merge stale persisted counters back into state.
|
|
7240
|
-
forceReplaceTotalsAfterFailure = true;
|
|
7241
|
-
console.warn(`${LOG_PREFIX}: failed to persist cache stats`, error);
|
|
8087
|
+
console.warn(`${LOG_PREFIX}: failed to persist cache stats shard`, error);
|
|
7242
8088
|
if (!persistenceWarningShown) {
|
|
7243
8089
|
persistenceWarningShown = true;
|
|
7244
8090
|
ctx?.ui.notify(
|
|
@@ -7250,61 +8096,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
7250
8096
|
});
|
|
7251
8097
|
}
|
|
7252
8098
|
|
|
7253
|
-
/** Schedule a debounced persist. Coalesces rapid message_end writes
|
|
7254
|
-
* into a single disk write after PERSIST_DEBOUNCE_MS of silence.
|
|
7255
|
-
* In-memory stats remain instantly up-to-date for the footer; only
|
|
7256
|
-
* the on-disk persistence is delayed. */
|
|
7257
8099
|
function schedulePersistCacheStats(ctx?: ExtensionContext): void {
|
|
7258
8100
|
if (persistTimer !== null) clearTimeout(persistTimer);
|
|
7259
8101
|
persistTimer = setTimeout(() => {
|
|
7260
8102
|
persistTimer = null;
|
|
7261
|
-
persistCacheStats(ctx).catch((
|
|
7262
|
-
console.warn(`${LOG_PREFIX}: debounced persist failed`, err);
|
|
7263
|
-
});
|
|
8103
|
+
void persistCacheStats(ctx).then(() => refreshShardAggregate()).catch(() => undefined);
|
|
7264
8104
|
}, PERSIST_DEBOUNCE_MS);
|
|
8105
|
+
persistTimer.unref?.();
|
|
7265
8106
|
}
|
|
7266
8107
|
|
|
7267
|
-
|
|
7268
|
-
* Used on reload and day-rollover where immediate durability matters. */
|
|
7269
|
-
async function flushPersistCacheStats(ctx?: ExtensionContext): Promise<void> {
|
|
8108
|
+
async function flushPersistCacheStats(ctx?: ExtensionContext, lifecycleState: "active" | "closed" = "active"): Promise<void> {
|
|
7270
8109
|
if (persistTimer !== null) {
|
|
7271
8110
|
clearTimeout(persistTimer);
|
|
7272
8111
|
persistTimer = null;
|
|
7273
8112
|
}
|
|
7274
|
-
|
|
8113
|
+
// Adopt resets performed by another Pi process before capturing the shard
|
|
8114
|
+
// snapshot. Otherwise shutdown/command flush could rewrite stale local
|
|
8115
|
+
// counters under an old epoch even though aggregation already hid them.
|
|
8116
|
+
await refreshShardAggregate();
|
|
8117
|
+
await persistCacheStats(ctx, lifecycleState);
|
|
8118
|
+
await refreshShardAggregate();
|
|
7275
8119
|
}
|
|
7276
8120
|
|
|
7277
8121
|
async function rollOverStatsIfNeeded(ctx?: ExtensionContext): Promise<void> {
|
|
7278
8122
|
const day = currentLocalDay();
|
|
7279
8123
|
let changed = false;
|
|
7280
|
-
|
|
7281
|
-
// Roll over per-model entries.
|
|
7282
|
-
for (const key of Object.keys(cacheStatsByModel)) {
|
|
7283
|
-
const stats = cacheStatsByModel[key];
|
|
7284
|
-
if (stats && stats.day !== day) {
|
|
7285
|
-
cacheStatsByModel[key] = emptyCacheStats(day);
|
|
7286
|
-
changed = true;
|
|
7287
|
-
}
|
|
7288
|
-
}
|
|
7289
|
-
|
|
7290
|
-
// Roll over cumulative provider/model totals.
|
|
7291
8124
|
for (const key of Object.keys(cacheStatsProcessByModel)) {
|
|
7292
|
-
|
|
7293
|
-
if (stats && stats.day !== day) {
|
|
8125
|
+
if (cacheStatsProcessByModel[key]?.day !== day) {
|
|
7294
8126
|
cacheStatsProcessByModel[key] = emptyCacheStats(day);
|
|
7295
8127
|
changed = true;
|
|
7296
8128
|
}
|
|
7297
8129
|
}
|
|
7298
|
-
|
|
7299
|
-
for (const key of Object.keys(cacheStatsTotalsByModel)) {
|
|
7300
|
-
const stats = cacheStatsTotalsByModel[key];
|
|
7301
|
-
if (stats && stats.day !== day) {
|
|
7302
|
-
cacheStatsTotalsByModel[key] = emptyCacheStats(day);
|
|
7303
|
-
changed = true;
|
|
7304
|
-
}
|
|
7305
|
-
}
|
|
7306
|
-
|
|
7307
|
-
// Roll over legacy family entries.
|
|
7308
8130
|
for (const id of CACHE_PROVIDER_IDS) {
|
|
7309
8131
|
const stats = cacheStatsLegacyFamily[id];
|
|
7310
8132
|
if (stats && stats.day !== day) {
|
|
@@ -7312,57 +8134,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
7312
8134
|
changed = true;
|
|
7313
8135
|
}
|
|
7314
8136
|
}
|
|
7315
|
-
|
|
7316
8137
|
if (changed) {
|
|
8138
|
+
shardCreatedAt = Date.now();
|
|
7317
8139
|
lastStatusText = undefined;
|
|
7318
|
-
await
|
|
8140
|
+
await ensureCurrentEpochs();
|
|
8141
|
+
await flushPersistCacheStats(ctx);
|
|
7319
8142
|
}
|
|
7320
8143
|
}
|
|
7321
8144
|
|
|
7322
8145
|
async function restoreCacheStats(reason: string, ctx: ExtensionContext): Promise<void> {
|
|
7323
8146
|
syncSessionHash(ctx);
|
|
7324
|
-
|
|
8147
|
+
lastStatusText = undefined;
|
|
8148
|
+
cacheStatsProcessByModel = {};
|
|
8149
|
+
cacheStatsLegacyFamily = emptyAllCacheStats();
|
|
8150
|
+
modelEpochByKey.clear();
|
|
8151
|
+
modelApiByKey.clear();
|
|
8152
|
+
modelNameByKey.clear();
|
|
8153
|
+
currentGlobalEpoch = await readGlobalStatsEpoch();
|
|
7325
8154
|
if (reason === "reload") {
|
|
7326
|
-
// /reload: preserve session-scoped stats (same session hash).
|
|
7327
|
-
// Pi extension reload creates a fresh closure, so cacheStatsByModel
|
|
7328
|
-
// starts empty. Read persisted data and filter for current session.
|
|
7329
|
-
lastStatusText = undefined;
|
|
7330
8155
|
lastPromptIntegrityWarningAt = 0;
|
|
7331
8156
|
integrityNotificationShown = false;
|
|
7332
8157
|
clearRecentSamples();
|
|
7333
|
-
|
|
7334
|
-
const persisted = await readPersistedCacheStats();
|
|
7335
|
-
cacheStatsByModel = filterRestorableStatsForSession(
|
|
7336
|
-
persisted,
|
|
7337
|
-
currentSessionHashSet ? currentSessionHash : undefined,
|
|
7338
|
-
);
|
|
7339
|
-
cacheStatsProcessByModel = {};
|
|
7340
|
-
cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
|
|
7341
|
-
cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
|
|
7342
|
-
lastActualRoutedModel = currentSessionHashSet
|
|
7343
|
-
? persisted?.lastRoutedModelBySession?.[currentSessionHash]
|
|
7344
|
-
: undefined;
|
|
7345
|
-
|
|
7346
|
-
await rollOverStatsIfNeeded(ctx);
|
|
7347
|
-
return;
|
|
7348
8158
|
}
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
);
|
|
7358
|
-
cacheStatsProcessByModel = {};
|
|
7359
|
-
cacheStatsTotalsByModel = persisted?.totalsByModel ?? {};
|
|
7360
|
-
cacheStatsLegacyFamily = persisted?.legacyFamily ?? emptyAllCacheStats();
|
|
7361
|
-
lastActualRoutedModel = currentSessionHashSet
|
|
7362
|
-
? persisted?.lastRoutedModelBySession?.[currentSessionHash]
|
|
7363
|
-
: undefined;
|
|
7364
|
-
lastStatusText = undefined;
|
|
7365
|
-
await rollOverStatsIfNeeded(ctx);
|
|
8159
|
+
await removeLegacyStatsFiles();
|
|
8160
|
+
await mkdir(SHARD_FILES_DIR, { recursive: true });
|
|
8161
|
+
await refreshShardAggregate();
|
|
8162
|
+
await flushPersistCacheStats(ctx);
|
|
8163
|
+
await maybeCleanupStatsShardsV7();
|
|
8164
|
+
// Maintenance may remove expired shard files after the initial refresh.
|
|
8165
|
+
// Re-scan before publishing so startup/footer command state cannot retain
|
|
8166
|
+
// counters from files that no longer exist.
|
|
8167
|
+
await refreshShardAggregate();
|
|
7366
8168
|
}
|
|
7367
8169
|
|
|
7368
8170
|
async function publishStatus(ctx: ExtensionContext, model: PiModel | undefined = ctx.model): Promise<void> {
|
|
@@ -7407,8 +8209,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7407
8209
|
}
|
|
7408
8210
|
|
|
7409
8211
|
if (adapter) {
|
|
7410
|
-
// Footer mode defaults to
|
|
7411
|
-
// current-
|
|
8212
|
+
// Footer mode defaults to current-session provider/model totals. Users may
|
|
8213
|
+
// select all-current-day or current-process counters through persistent
|
|
8214
|
+
// command config or the environment variable.
|
|
7412
8215
|
const stats = displayModel
|
|
7413
8216
|
? selectFooterStatsForModel(mode, sessionHash, cacheStatsByModel, cacheStatsTotalsByModel, displayModel, cacheStatsProcessByModel)
|
|
7414
8217
|
: undefined;
|
|
@@ -7480,25 +8283,45 @@ export default function (pi: ExtensionAPI) {
|
|
|
7480
8283
|
* The caller strips prompt_cache_retention when this returns false.
|
|
7481
8284
|
*/
|
|
7482
8285
|
function hasExplicitLongRetentionOptIn(model: PiModel): boolean {
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
return false;
|
|
7489
|
-
}
|
|
8286
|
+
return hasExplicitLongRetentionOptInFromConfig(
|
|
8287
|
+
readEffectiveCompatConfig(),
|
|
8288
|
+
model.provider,
|
|
8289
|
+
model.id,
|
|
8290
|
+
);
|
|
7490
8291
|
}
|
|
7491
8292
|
|
|
7492
8293
|
pi.on("session_start", async (event, ctx) => {
|
|
7493
8294
|
if (runtimeOptimizerEnabled) requestLongCacheRetention();
|
|
7494
8295
|
await restoreCacheStats(event.reason, ctx);
|
|
8296
|
+
if (ctx.mode === "tui" && !shardWatcher) {
|
|
8297
|
+
try {
|
|
8298
|
+
shardWatcher = watch(SHARD_FILES_DIR, () => scheduleShardRefresh(ctx));
|
|
8299
|
+
shardWatcher.unref?.();
|
|
8300
|
+
} catch {
|
|
8301
|
+
// Lifecycle and explicit command refreshes remain authoritative.
|
|
8302
|
+
}
|
|
8303
|
+
}
|
|
8304
|
+
await publishStatus(ctx);
|
|
8305
|
+
});
|
|
8306
|
+
|
|
8307
|
+
pi.on("tool_execution_end", async (_event, ctx) => {
|
|
8308
|
+
await refreshShardAggregate();
|
|
8309
|
+
await publishStatus(ctx);
|
|
8310
|
+
});
|
|
8311
|
+
|
|
8312
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
8313
|
+
await refreshShardAggregate();
|
|
7495
8314
|
await publishStatus(ctx);
|
|
7496
8315
|
});
|
|
7497
8316
|
|
|
7498
8317
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
7499
8318
|
try {
|
|
7500
|
-
await flushPersistCacheStats(ctx);
|
|
8319
|
+
await flushPersistCacheStats(ctx, "closed");
|
|
7501
8320
|
} finally {
|
|
8321
|
+
if (shardRefreshTimer !== null) clearTimeout(shardRefreshTimer);
|
|
8322
|
+
shardRefreshTimer = null;
|
|
8323
|
+
shardWatcher?.close();
|
|
8324
|
+
shardWatcher = undefined;
|
|
7502
8325
|
latestCacheHint = undefined;
|
|
7503
8326
|
delete getProtocolGlobal().__piCacheOptimizerCacheKey__;
|
|
7504
8327
|
uninstallCacheHintsService();
|
|
@@ -7508,6 +8331,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7508
8331
|
|
|
7509
8332
|
pi.on("model_select", async (event, ctx) => {
|
|
7510
8333
|
if (runtimeOptimizerEnabled) notifyCacheCompatIfNeeded(resolveRouteModel(event.model, ctx) ?? event.model, ctx, warnedModels);
|
|
8334
|
+
await refreshShardAggregate();
|
|
7511
8335
|
await publishStatus(ctx, event.model);
|
|
7512
8336
|
});
|
|
7513
8337
|
|
|
@@ -7631,6 +8455,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
7631
8455
|
return {};
|
|
7632
8456
|
});
|
|
7633
8457
|
|
|
8458
|
+
pi.on("before_provider_headers", (event, ctx) => {
|
|
8459
|
+
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
8460
|
+
addEffectiveSessionAffinityHeaders(
|
|
8461
|
+
event.headers,
|
|
8462
|
+
requestModel,
|
|
8463
|
+
ctx.sessionManager.getSessionId(),
|
|
8464
|
+
);
|
|
8465
|
+
});
|
|
8466
|
+
|
|
7634
8467
|
pi.on("before_provider_request", (event, ctx) => {
|
|
7635
8468
|
const requestModel = resolveRouteModel(ctx.model, ctx) ?? ctx.model;
|
|
7636
8469
|
|
|
@@ -7862,6 +8695,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
7862
8695
|
// Update session, process-local, and cumulative buckets for the actual
|
|
7863
8696
|
// routed model. The process bucket is intentionally never persisted.
|
|
7864
8697
|
if (statsModel) {
|
|
8698
|
+
const key = modelKey(statsModel);
|
|
8699
|
+
const latestGlobalEpoch = await readGlobalStatsEpoch();
|
|
8700
|
+
const latestModelEpoch = await readModelStatsEpoch(key);
|
|
8701
|
+
if (currentGlobalEpoch !== latestGlobalEpoch) {
|
|
8702
|
+
currentGlobalEpoch = latestGlobalEpoch;
|
|
8703
|
+
cacheStatsProcessByModel = {};
|
|
8704
|
+
modelEpochByKey.clear();
|
|
8705
|
+
}
|
|
8706
|
+
if (modelEpochByKey.get(key) !== latestModelEpoch) {
|
|
8707
|
+
delete cacheStatsProcessByModel[key];
|
|
8708
|
+
modelEpochByKey.set(key, latestModelEpoch);
|
|
8709
|
+
}
|
|
8710
|
+
modelApiByKey.set(key, statsModel.api ?? "");
|
|
8711
|
+
modelNameByKey.set(key, statsModel.name || statsModel.id);
|
|
7865
8712
|
const sk = sessionModelKey(statsModel);
|
|
7866
8713
|
addUsageToCacheStats(getOrCreateStatsByModelKey(sk), usage);
|
|
7867
8714
|
addUsageToCacheStats(getOrCreateProcessStatsForModel(statsModel), usage);
|
|
@@ -7871,6 +8718,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7871
8718
|
}
|
|
7872
8719
|
|
|
7873
8720
|
schedulePersistCacheStats(ctx);
|
|
8721
|
+
await refreshShardAggregate();
|
|
7874
8722
|
await publishStatus(ctx, statsModel);
|
|
7875
8723
|
});
|
|
7876
8724
|
|
|
@@ -7881,7 +8729,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7881
8729
|
// disable — disable runtime prompt/cache optimizations for this process
|
|
7882
8730
|
// doctor — show current model/provider/api/baseUrl/compat status
|
|
7883
8731
|
// with low-hit diagnosis
|
|
7884
|
-
// stats — show
|
|
8732
|
+
// stats — show detailed current-session models; `stats all` shows all local shards
|
|
7885
8733
|
// compat — show compat suggestion with file path
|
|
7886
8734
|
// config footer-mode total|session|process — persist footer mode override
|
|
7887
8735
|
// fix — auto-fix compat issues (writes models.json, requires UI)
|
|
@@ -7900,17 +8748,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7900
8748
|
|
|
7901
8749
|
if (subcommand === "enable") {
|
|
7902
8750
|
setRuntimeOptimizerEnabled(true);
|
|
7903
|
-
resetCurrentSessionStats();
|
|
8751
|
+
await resetCurrentSessionStats();
|
|
7904
8752
|
await flushPersistCacheStats(cmdCtx);
|
|
7905
8753
|
await publishStatus(cmdCtx, model);
|
|
7906
8754
|
cmdCtx.ui.notify(`✅ Pi Cache Optimizer enabled for this Pi process. Local footer stats were reset for before/after comparison.\n${formatOptimizerRuntimeMode()}`, "info");
|
|
7907
8755
|
} else if (subcommand === "disable") {
|
|
7908
8756
|
setRuntimeOptimizerEnabled(false);
|
|
7909
|
-
resetCurrentSessionStats();
|
|
8757
|
+
await resetCurrentSessionStats();
|
|
7910
8758
|
await flushPersistCacheStats(cmdCtx);
|
|
7911
8759
|
await publishStatus(cmdCtx, model);
|
|
7912
8760
|
cmdCtx.ui.notify(`⏸️ Pi Cache Optimizer disabled for this Pi process. Local footer stats were reset and will keep collecting while disabled for comparison.\n${formatOptimizerRuntimeMode()}`, "warning");
|
|
7913
8761
|
} else if (subcommand === "doctor") {
|
|
8762
|
+
await refreshShardAggregate();
|
|
7914
8763
|
if (!model) {
|
|
7915
8764
|
cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
|
|
7916
8765
|
return;
|
|
@@ -7926,16 +8775,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7926
8775
|
: diagnosis;
|
|
7927
8776
|
cmdCtx.ui.notify(fullDiagnosis, "info");
|
|
7928
8777
|
} else if (subcommand === "stats") {
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
8778
|
+
const aggregate = await refreshShardAggregate();
|
|
8779
|
+
const statsMode = commandParts[1];
|
|
8780
|
+
if (statsMode === "all") {
|
|
8781
|
+
cmdCtx.ui.notify(buildAllStatsOutput(aggregate), "info");
|
|
8782
|
+
} else if (statsMode === "contributors") {
|
|
8783
|
+
cmdCtx.ui.notify(buildContributorsStatsOutput(aggregate, model, currentSessionHashSet ? currentSessionHash : undefined), "info");
|
|
8784
|
+
} else if (statsMode) {
|
|
8785
|
+
cmdCtx.ui.notify("Usage: /cache-optimizer stats [all|contributors]", "info");
|
|
8786
|
+
} else {
|
|
8787
|
+
const sessionModels = currentSessionHashSet ? aggregate.bySession[currentSessionHash] ?? {} : {};
|
|
8788
|
+
cmdCtx.ui.notify(buildSessionStatsOutput(sessionModels, model, aggregate.modelRefsByKey), "info");
|
|
7932
8789
|
}
|
|
7933
|
-
const adapter = selectAdapterForModel(model);
|
|
7934
|
-
const sk = model ? sessionModelKey(model) : undefined;
|
|
7935
|
-
const statsState = model ? cacheStatsTotalsByModel[modelKey(model)] : undefined;
|
|
7936
|
-
const samples = sk ? getRecentSamples(sk) : [];
|
|
7937
|
-
const output = buildStatsOutput(model, adapter, statsState, samples);
|
|
7938
|
-
cmdCtx.ui.notify(output, "info");
|
|
7939
8790
|
} else if (subcommand === "config") {
|
|
7940
8791
|
const configKey = commandParts[1];
|
|
7941
8792
|
const requestedMode = commandParts[2];
|
|
@@ -7998,7 +8849,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7998
8849
|
// Reset local footer stats for the effective active model. If the
|
|
7999
8850
|
// selected model is a virtual router and the protocol exposes a live
|
|
8000
8851
|
// route, this clears the real upstream bucket, not the router shell.
|
|
8001
|
-
resetStatsForModel(model);
|
|
8852
|
+
await resetStatsForModel(model);
|
|
8002
8853
|
|
|
8003
8854
|
// Persist immediately.
|
|
8004
8855
|
await flushPersistCacheStats(cmdCtx);
|
|
@@ -8348,7 +9199,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8348
9199
|
"Enable — Turn on runtime optimizations",
|
|
8349
9200
|
"Disable — Turn off runtime optimizations",
|
|
8350
9201
|
"Doctor — Show cache configuration",
|
|
8351
|
-
"Stats — Show
|
|
9202
|
+
"Stats — Show current-session model statistics",
|
|
8352
9203
|
"Compat — Show compat suggestion",
|
|
8353
9204
|
"Fix — Auto-fix compat issues (writes models.json)",
|
|
8354
9205
|
"Footer mode — Choose total, session, or process stats",
|
|
@@ -8369,12 +9220,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
8369
9220
|
} else if (choice === menuOptions[5]) {
|
|
8370
9221
|
await handleCacheOptimizerCommand("fix", cmdCtx);
|
|
8371
9222
|
} else if (choice === menuOptions[6]) {
|
|
8372
|
-
const modeOptions = ["
|
|
9223
|
+
const modeOptions = ["session — Current Pi conversation session (default)", "total — All local sessions today", "process — Current extension instance only", "Cancel"];
|
|
8373
9224
|
const modeChoice = await cmdCtx.ui.select("Footer cache stats mode", modeOptions);
|
|
8374
9225
|
const nextMode = modeChoice === modeOptions[0]
|
|
8375
|
-
? "
|
|
9226
|
+
? "session"
|
|
8376
9227
|
: modeChoice === modeOptions[1]
|
|
8377
|
-
? "
|
|
9228
|
+
? "total"
|
|
8378
9229
|
: modeChoice === modeOptions[2]
|
|
8379
9230
|
? "process"
|
|
8380
9231
|
: undefined;
|
|
@@ -8392,7 +9243,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
8392
9243
|
diagnosis.push(" enable — Enable prompt/cache optimizations for this Pi process");
|
|
8393
9244
|
diagnosis.push(" disable — Disable prompt/cache optimizations for this Pi process");
|
|
8394
9245
|
diagnosis.push(" doctor — Show current model/provider/api/baseUrl/compat and low-hit diagnosis");
|
|
8395
|
-
diagnosis.push(" stats — Show
|
|
9246
|
+
diagnosis.push(" stats — Show detailed statistics for every model in the current session");
|
|
9247
|
+
diagnosis.push(" stats all — Show detailed totals for every model across all local sessions");
|
|
9248
|
+
diagnosis.push(" stats contributors — Show per-session contributors for the active model");
|
|
8396
9249
|
diagnosis.push(" compat — Show compat suggestion with edit location");
|
|
8397
9250
|
diagnosis.push(" config footer-mode total|session|process — Persist the footer stats mode");
|
|
8398
9251
|
diagnosis.push(" fix — Auto-fix compat issues (writes models.json, requires UI)");
|