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.
Files changed (4) hide show
  1. package/README.md +22 -15
  2. package/README.zh-CN.md +22 -14
  3. package/index.ts +1051 -198
  4. 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: fallback?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
974
- contextWindow: fallback?.contextWindow ?? 0,
975
- maxTokens: fallback?.maxTokens ?? 0,
976
- compat: fallback?.compat,
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
- return findModelInRegistry(ctx?.modelRegistry, snapshot.provider, snapshot.modelId)
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
- // Pi merges provider.compat with model.compat (model wins on conflicts)
1089
- // We approximate this by reading from ctx.model which should already have merged compat
1090
- // However, for safety, we check both levels if available
1091
- const modelCompat = (model.compat ?? {}) as CacheCompat;
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
- // Note: ctx.model from Pi should already contain merged compat,
1094
- // but we document the two-level structure for clarity
1095
- return modelCompat;
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: "total", source: "default" };
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 (isOfficialOpenAIBaseUrl(model)) return missing;
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 (isOfficialOpenAIBaseUrl(model)) return missing;
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 (isOfficialOpenAIBaseUrl(model)) return optional;
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) && isOpenAICompatibleApi(model.api) && !isPiBuiltInLlamaCppModel(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
- * Pure function (no I/O) — suitable for unit tests without touching the real
4252
- * state file at `~/.pi/agent/pi-cache-optimizer-stats.json`.
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) && !isOfficialOpenAIBaseUrl(model) && !isPiBuiltInLlamaCppModel(model);
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
- !isOfficialOpenAIBaseUrl(model) &&
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 (isOfficialOpenAIBaseUrl(model)) return false;
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
- // Official OpenAI bypass — no notes needed.
4503
- if (isOfficialOpenAIBaseUrl(model)) {
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
- let replacePersistedTotalsOnNextWrite = false;
6986
- let forceReplaceTotalsAfterFailure = false;
6987
- const pendingDeletedTotalModelKeys = new Set<string>();
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 getCacheStatsState(): CacheStatsState {
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
- statsByModel: cacheStatsByModel,
7112
- totalsByModel: cacheStatsTotalsByModel,
7113
- legacyFamily: cacheStatsLegacyFamily,
7114
- ...(currentSessionHashSet && lastActualRoutedModel
7115
- ? { lastRoutedModelBySession: { [currentSessionHash]: lastActualRoutedModel } }
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
- const prefix = `${currentSessionHash || "_nosession"}:`;
7185
- for (const key of Object.keys(cacheStatsByModel)) {
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
- replacePersistedTotalsOnNextWrite = true;
7191
- for (const key of Array.from(recentSamplesByModelKey.keys())) {
7192
- if (key.startsWith(prefix)) recentSamplesByModelKey.delete(key);
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 snapshotCacheStatsState(): CacheStatsState {
7199
- const state = getCacheStatsState();
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 writePersistedCacheStats(state, sessionHash, {
7233
- deleteModelKeys,
7234
- replaceTotals: replaceTotals || recoverFromEarlierFailure,
7235
- });
7236
- if (recoverFromEarlierFailure) forceReplaceTotalsAfterFailure = false;
8085
+ await writeStatsShardV7(instanceShardPath, shard);
7237
8086
  } catch (error) {
7238
- // The next queued write must replace totals authoritatively so a failed
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((err) => {
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
- /** Flush any pending debounced persist immediately (cancels timer + writes).
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
- await persistCacheStats(ctx);
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
- const stats = cacheStatsProcessByModel[key];
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 persistCacheStats(ctx);
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
- // First load / process start: read persisted stats and filter for
7351
- // this session's entries. If the session hash is unavailable, start
7352
- // fresh instead of loading all persisted session buckets.
7353
- const persisted = await readPersistedCacheStats();
7354
- cacheStatsByModel = filterRestorableStatsForSession(
7355
- persisted,
7356
- currentSessionHashSet ? currentSessionHash : undefined,
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 daily provider/model totals. Users may select
7411
- // current-session counters through persistent command config or the env var.
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
- try {
7484
- const text = readFileSync(MODELS_JSON_PATH, "utf8");
7485
- const parsed = parseJsonc(text);
7486
- return hasExplicitLongRetentionOptInFromConfig(parsed, model.provider, model.id);
7487
- } catch {
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 active model stats bucket, recent trend, usage
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
- if (!model) {
7930
- cmdCtx.ui.notify("No active model selected. Select a model first with /model or pi --model.", "warning");
7931
- return;
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 cache stats and trend",
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 = ["total — Daily cumulative totals (default)", "session — Current Pi conversation session", "process — Current Pi process only", "Cancel"];
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
- ? "total"
9226
+ ? "session"
8376
9227
  : modeChoice === modeOptions[1]
8377
- ? "session"
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 active model stats bucket and recent trend");
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)");