opencode-cache-engine 0.4.0 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-cache-engine",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "Provider-aware prompt-cache optimization and telemetry for OpenCode",
6
6
  "keywords": [
@@ -4,12 +4,7 @@ import {
4
4
  DEFAULT_CONFIG_PATH,
5
5
  DIGEST_TEMPLATE,
6
6
  affinityTelemetryFields,
7
- POLICY_GLM53,
8
- POLICY_GPT56,
9
- POLICY_MIMO26,
10
- POLICY_NEUTRAL,
11
7
  createRecorder,
12
- detectPolicy,
13
8
  detectReasoningIssues,
14
9
  digestDecision,
15
10
  ensureMetricsDir,
@@ -37,6 +32,7 @@ import {
37
32
  toolFingerprint,
38
33
  toolWireFingerprint,
39
34
  } from "./cache-engine-core.mjs"
35
+ import { resolveRuntimePolicy } from "./cache-policy-core.mjs"
40
36
 
41
37
  // ---------------------------------------------------------------------------
42
38
  // cache-engine
@@ -101,7 +97,21 @@ type Shape = {
101
97
  toolCount: number | null
102
98
  }
103
99
 
104
- type ModelInfo = { family: string; providerID: string; modelID: string }
100
+ // Resolved runtime policy (the registry's runtime descriptor). `policy` is the
101
+ // legacy telemetry/state string emitted by the pre-v0.4.0 classifier.
102
+ type PolicyRuntime = {
103
+ policy: string
104
+ isNeutral: boolean
105
+ gptCacheMetadata: boolean
106
+ envRelocation: "glm" | "mimo" | null
107
+ thinkingIntegrity: boolean
108
+ cacheRatio: "glm" | "mimo" | null
109
+ providerChange: "glm" | "mimo" | null
110
+ prefixDiagnostics: boolean
111
+ openRouterAffinity: boolean
112
+ }
113
+
114
+ type ModelInfo = { family: string; providerID: string; modelID: string; caps: PolicyRuntime }
105
115
 
106
116
  type ToolCache = { semanticToolsHash: string | null; wireToolsHash: string | null }
107
117
 
@@ -258,19 +268,21 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
258
268
  // gpt56/glm53/deepseek classification once established.
259
269
  const rememberModel = (sid: string, model: ChatParamsModel | undefined): ModelInfo | null => {
260
270
  if (!model) return null
261
- const family = detectPolicy(model)
271
+ // Single runtime source of policy classification: the registry resolver.
272
+ const caps = resolveRuntimePolicy(model) as PolicyRuntime
262
273
  const s = get(sid)
263
274
  const info: ModelInfo = {
264
- family,
275
+ family: caps.policy,
265
276
  providerID: String(model.providerID ?? ""),
266
277
  modelID: String(model.api?.id ?? model.id ?? ""),
278
+ caps,
267
279
  }
268
- if (s.modelInfo == null || (s.modelInfo.family === POLICY_NEUTRAL && family !== POLICY_NEUTRAL)) {
280
+ if (s.modelInfo == null || (s.modelInfo.caps.isNeutral && !caps.isNeutral)) {
269
281
  s.modelInfo = info
270
282
  // A title/summary request (neutral small model) may have established the
271
283
  // system baseline first. Its "powered by the model named ..." env line
272
284
  // differs from the real model's, so re-baseline on upgrade.
273
- if (s.baselineSystem !== null && s.modelInfo.family !== POLICY_NEUTRAL) {
285
+ if (s.baselineSystem !== null && !caps.isNeutral) {
274
286
  s.baselineSystem = null
275
287
  s.shape = null
276
288
  }
@@ -350,11 +362,11 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
350
362
 
351
363
  s.lastProcessedMessageID = nextProcessedCursor(firstPage, startCursor)
352
364
 
353
- const family = s.modelInfo?.family
365
+ const caps = s.modelInfo?.caps
354
366
  const glmIntegrity =
355
- family === POLICY_GLM53 &&
356
- policyEnabled(cfg, POLICY_GLM53) &&
357
- cfg.policies?.[POLICY_GLM53]?.preserveThinkingIntegrity === true
367
+ caps?.thinkingIntegrity === true &&
368
+ policyEnabled(cfg, caps.policy) &&
369
+ cfg.policies?.[caps.policy]?.preserveThinkingIntegrity === true
358
370
 
359
371
  if (glmIntegrity && reasoningNewestFirst.length > 0) {
360
372
  // messages arrive newest-first; process oldest->newest so `seen` grows
@@ -417,11 +429,11 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
417
429
  recFields.model = s.modelInfo.modelID
418
430
  recFields.policy = s.modelInfo.family
419
431
  }
420
- if (family === POLICY_GLM53) {
432
+ if (caps?.cacheRatio === "glm") {
421
433
  recFields.promptTokens = read + write + input
422
434
  recFields.glmHitRate = glmHitRatio(read, write, input)
423
435
  }
424
- if (family === POLICY_MIMO26) {
436
+ if (caps?.cacheRatio === "mimo") {
425
437
  // Provider-reported cached tokens / total prompt tokens. The runtime's
426
438
  // `input` is the non-cached prompt input and `cache.read` is the
427
439
  // cached prompt input, so total prompt tokens are derived as read +
@@ -431,7 +443,7 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
431
443
  recFields.promptTokens = promptTokens
432
444
  recFields.cachedTokens = read
433
445
  recFields.cacheHitRate = mimoHitRate(read, promptTokens)
434
- if (cfg.policies?.[POLICY_MIMO26]?.stickySession === true) {
446
+ if (cfg.policies?.[caps.policy]?.stickySession === true) {
435
447
  recFields.stickySessionId = mimoSessionIdFor(sid)
436
448
  }
437
449
  // Prefer the latest live provider identity over the latched one.
@@ -440,10 +452,10 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
440
452
  recFields.model = s.mimoProvider.modelID
441
453
  }
442
454
  }
443
- if (family === POLICY_GPT56 && s.gptInjected) {
455
+ if (caps?.gptCacheMetadata === true && s.gptInjected) {
444
456
  recFields.keyStrategy = "session"
445
- recFields.mode = cfg.policies?.[POLICY_GPT56]?.mode
446
- recFields.ttl = cfg.policies?.[POLICY_GPT56]?.ttl
457
+ recFields.mode = cfg.policies?.[caps.policy]?.mode
458
+ recFields.ttl = cfg.policies?.[caps.policy]?.ttl
447
459
  }
448
460
  rec.record(recFields)
449
461
  } else {
@@ -495,8 +507,9 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
495
507
  try {
496
508
  const model = input.model as unknown as ChatParamsModel
497
509
  const providerID = String(model?.providerID ?? "")
498
- const family = detectPolicy(model)
499
- if (family !== POLICY_MIMO26 && family !== POLICY_GLM53) return
510
+ const caps = resolveRuntimePolicy(model) as PolicyRuntime
511
+ if (!caps.openRouterAffinity) return
512
+ const family = caps.policy
500
513
 
501
514
  const hasSessionIDHeader = (headers?: Record<string, string>) =>
502
515
  Object.keys(headers ?? {}).some((name) => name.toLowerCase() === "x-session-id")
@@ -536,6 +549,7 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
536
549
  try {
537
550
  const info = rememberModel(input.sessionID, input.model as unknown as ChatParamsModel)
538
551
  const family = info?.family
552
+ const caps = info?.caps
539
553
 
540
554
  // ---- MiMo-V2.6: provider-switch diagnostics (telemetry only) ---------
541
555
  // MiMo cache lives at the provider side, so a provider change within one
@@ -545,7 +559,7 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
545
559
  // NOTE: OpenRouter's *upstream* provider selection (e.g. xiaomi/fp8) is
546
560
  // not exposed to plugins; only the OpenCode providerID/modelID are
547
561
  // observable here.
548
- if (family === POLICY_MIMO26 && policyEnabled(cfg, POLICY_MIMO26) && info) {
562
+ if (caps?.providerChange === "mimo" && policyEnabled(cfg, caps.policy) && info) {
549
563
  // Use the LIVE model identity (not the latched one) so a provider
550
564
  // switch within the session is actually observable.
551
565
  const live = input.model as unknown as ChatParamsModel
@@ -558,7 +572,7 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
558
572
  const ev = providerChangeEvent(s.mimoProvider, cur)
559
573
  if (ev.changed) {
560
574
  const sticky =
561
- cfg.policies?.[POLICY_MIMO26]?.stickySession === true
575
+ cfg.policies?.[caps.policy]?.stickySession === true
562
576
  ? { stickySessionId: mimoSessionIdFor(input.sessionID) }
563
577
  : {}
564
578
  rec.record({
@@ -566,7 +580,7 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
566
580
  sid: input.sessionID,
567
581
  ts: Date.now(),
568
582
  reason: "mimo_provider_changed",
569
- policy: POLICY_MIMO26,
583
+ policy: caps.policy,
570
584
  from: ev.from,
571
585
  to: ev.to,
572
586
  ...sticky,
@@ -579,7 +593,7 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
579
593
  }
580
594
 
581
595
  // ---- GLM-5.3 provider identity observation (telemetry only) ----------
582
- if (family === POLICY_GLM53 && policyEnabled(cfg, POLICY_GLM53) && info) {
596
+ if (caps?.providerChange === "glm" && policyEnabled(cfg, caps.policy) && info) {
583
597
  const live = input.model as unknown as ChatParamsModel
584
598
  const cur = {
585
599
  providerID: String(live?.providerID ?? ""),
@@ -594,7 +608,7 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
594
608
  sid: input.sessionID,
595
609
  ts: Date.now(),
596
610
  reason: "glm_provider_changed",
597
- policy: POLICY_GLM53,
611
+ policy: caps.policy,
598
612
  from: ev.from,
599
613
  to: ev.to,
600
614
  note: "OpenCode providerID changed; provider-specific upstream routing is not plugin-visible",
@@ -605,12 +619,12 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
605
619
  return
606
620
  }
607
621
 
608
- if (!(family === POLICY_GPT56 && policyEnabled(cfg, POLICY_GPT56))) {
622
+ if (!(caps?.gptCacheMetadata === true && policyEnabled(cfg, caps.policy))) {
609
623
  // DeepSeek / GLM / neutral: nothing to inject. GLM has no cache-key API;
610
624
  // DeepSeek caching is fully passive; we never mutate requests for them.
611
625
  return
612
626
  }
613
- const gpol = cfg.policies?.[POLICY_GPT56]
627
+ const gpol = cfg.policies?.[caps.policy]
614
628
  const applyRoot = gpol?.cacheRootKey !== false
615
629
  const compaction = input.agent === "compaction"
616
630
  const isolated = compaction && gpol?.compactionCacheIsolation === true
@@ -678,7 +692,7 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
678
692
  kind: "cache-options",
679
693
  sid: input.sessionID,
680
694
  ts: Date.now(),
681
- policy: POLICY_GPT56,
695
+ policy: caps.policy,
682
696
  provider: info.providerID,
683
697
  model: info.modelID,
684
698
  keyStrategy: applyRoot ? "cache-root" : "session",
@@ -784,43 +798,40 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
784
798
  const model = input.model as unknown as ChatParamsModel
785
799
  rememberModel(sid, model)
786
800
  const s = get(sid)
787
- const family = s.modelInfo?.family
801
+ const caps = s.modelInfo?.caps
788
802
 
789
803
  // ---- GLM-5.3 / MiMo-V2.6 input-shape stabilization ------------------
790
804
  // Relocate the identifiable volatile env block (per-day date) to the
791
805
  // tail of the single system string, content-preserving, ONLY when the
792
- // family opts into system stabilization and the block markers are
793
- // present exactly. Never touches other content/order; never applied to
794
- // other families. The runtime passes a single-element system array
795
- // (verified against the installed runtime), so no generic reordering is
796
- // involved.
806
+ // resolved runtime policy opts into system stabilization and the block
807
+ // markers are present exactly. Never touches other content/order; never
808
+ // applied to other families. The runtime passes a single-element system
809
+ // array (verified against the installed runtime), so no generic
810
+ // reordering is involved.
797
811
  //
798
812
  // In-place mutation note: request.ts keeps using its own local `system`
799
813
  // array after the hook (the trigger's returned output is ignored), so
800
814
  // reassigning `output.system = [...]` would be lost. We rewrite the
801
815
  // single element in place instead.
802
816
  let systemText = output.system.join("\n")
803
- const glmStabilize =
804
- family === POLICY_GLM53 &&
805
- policyEnabled(cfg, POLICY_GLM53) &&
806
- cfg.policies?.[POLICY_GLM53]?.stabilizeSystem === true
807
- const mimoStabilize =
808
- family === POLICY_MIMO26 &&
809
- policyEnabled(cfg, POLICY_MIMO26) &&
810
- cfg.policies?.[POLICY_MIMO26]?.stabilizeSystem === true
811
- if ((glmStabilize || mimoStabilize) && output.system.length === 1) {
817
+ const envVariant = caps?.envRelocation ?? null
818
+ const stabilize =
819
+ envVariant !== null &&
820
+ policyEnabled(cfg, caps!.policy) &&
821
+ cfg.policies?.[caps!.policy]?.stabilizeSystem === true
822
+ if (stabilize && output.system.length === 1) {
812
823
  const rel = relocateVolatileEnvBlock(output.system[0])
813
824
  if (rel.changed) {
814
825
  output.system[0] = rel.text
815
826
  systemText = rel.text
816
- if (family === POLICY_MIMO26) {
827
+ if (envVariant === "mimo") {
817
828
  log("debug", "mimo system env block relocated to suffix", { sid })
818
829
  rec.record({
819
830
  kind: "boundary",
820
831
  sid,
821
832
  ts: Date.now(),
822
833
  reason: "mimo_system_env_relocated",
823
- policy: POLICY_MIMO26,
834
+ policy: caps!.policy,
824
835
  provider: s.modelInfo?.providerID,
825
836
  model: s.modelInfo?.modelID,
826
837
  })
@@ -906,13 +917,13 @@ export const CacheEngine: Plugin = async ({ client, directory }) => {
906
917
  // MiMo-specific explicit diagnostic: the STABLE prefix changed (not
907
918
  // just the relocated volatile env suffix). Reported only; the new
908
919
  // content is never overwritten with a stale snapshot.
909
- if (family === POLICY_MIMO26 && reasons.includes("system_stable_prefix_changed")) {
920
+ if (caps?.prefixDiagnostics === true && reasons.includes("system_stable_prefix_changed")) {
910
921
  rec.record({
911
922
  kind: "boundary",
912
923
  sid,
913
924
  ts: Date.now(),
914
925
  reason: "mimo_system_prefix_changed",
915
- policy: POLICY_MIMO26,
926
+ policy: caps!.policy,
916
927
  provider: s.modelInfo?.providerID,
917
928
  model: s.modelInfo?.modelID,
918
929
  changedFields: granular,
@@ -17,8 +17,10 @@
17
17
  // `inventoryRef`. Inheritance is always explicit (`inheritsFrom`); "newer means
18
18
  // same behavior" is never an unconditional rule.
19
19
  //
20
- // This release does not wire the resolver into runtime hooks. detectPolicy()
21
- // remains the compatibility classifier until wiring is approved.
20
+ // As of v0.4.1 the runtime hook layer (cache-engine.ts) consumes
21
+ // resolveRuntimePolicy() as its single source of policy classification.
22
+ // detectPolicy() is retained as the compatibility classifier for the legacy
23
+ // POLICY_* strings.
22
24
 
23
25
  // ---------------------------------------------------------------------------
24
26
  // Model normalization (shared with the legacy classifier)
@@ -206,6 +208,36 @@ function resolveTransport(s) {
206
208
  return { id: p, kind: "direct", sessionAffinityHeader: null, stickyRouting: false, inventoryRef: "§5 OpenRouter transport" }
207
209
  }
208
210
 
211
+ // ---------------------------------------------------------------------------
212
+ // Runtime capability descriptors
213
+ //
214
+ // The runtime consumes `resolvePolicy(...).runtime` for gating. `policy` is the
215
+ // legacy telemetry/state string, so telemetry stays byte-identical. Every
216
+ // capability is explicit per registry entry: classification into a creator or
217
+ // family never implies a mutation. `legacy: false` entries always resolve to
218
+ // NEUTRAL_RUNTIME, so a future-looking model gains nothing until the registry
219
+ // explicitly says so.
220
+ // ---------------------------------------------------------------------------
221
+
222
+ const NEUTRAL_RUNTIME = Object.freeze({
223
+ policy: "neutral",
224
+ isNeutral: true,
225
+ gptCacheMetadata: false,
226
+ envRelocation: null,
227
+ thinkingIntegrity: false,
228
+ cacheRatio: null,
229
+ providerChange: null,
230
+ prefixDiagnostics: false,
231
+ openRouterAffinity: false,
232
+ })
233
+
234
+ const rt = (policy, overrides = {}) => ({
235
+ ...NEUTRAL_RUNTIME,
236
+ ...overrides,
237
+ policy,
238
+ isNeutral: policy === "neutral",
239
+ })
240
+
209
241
  // ---------------------------------------------------------------------------
210
242
  // Registry
211
243
  //
@@ -229,6 +261,7 @@ export const POLICY_REGISTRY = [
229
261
  baseline: "openai.gpt56.cache",
230
262
  overlays: ["gpt56.prompt-cache-options"],
231
263
  legacy: true,
264
+ runtime: rt("gpt56", { gptCacheMetadata: true }),
232
265
  inventoryRef: "§1 OpenAI",
233
266
  },
234
267
  {
@@ -243,7 +276,8 @@ export const POLICY_REGISTRY = [
243
276
  inheritsFrom: "gpt-5.6",
244
277
  overlays: [],
245
278
  legacy: false,
246
- note: "Documented inheritance of the GPT-5.6-and-later baseline. No CacheEngine overlay is registered for gpt-6 yet.",
279
+ runtime: rt("neutral"),
280
+ note: "Documented inheritance of the GPT-5.6-and-later baseline. No CacheEngine overlay is registered for gpt-6 yet, so the runtime stays neutral.",
247
281
  inventoryRef: "§1 OpenAI",
248
282
  },
249
283
  {
@@ -256,6 +290,13 @@ export const POLICY_REGISTRY = [
256
290
  baseline: "zai.implicit-cache",
257
291
  overlays: ["glm53.env-relocation"],
258
292
  legacy: true,
293
+ runtime: rt("glm53", {
294
+ envRelocation: "glm",
295
+ thinkingIntegrity: true,
296
+ cacheRatio: "glm",
297
+ providerChange: "glm",
298
+ openRouterAffinity: true,
299
+ }),
259
300
  inventoryRef: "§3 Z.AI GLM",
260
301
  },
261
302
  {
@@ -268,6 +309,13 @@ export const POLICY_REGISTRY = [
268
309
  baseline: "xiaomi.implicit-cache",
269
310
  overlays: ["mimo26.env-relocation"],
270
311
  legacy: true,
312
+ runtime: rt("mimo26", {
313
+ envRelocation: "mimo",
314
+ cacheRatio: "mimo",
315
+ providerChange: "mimo",
316
+ prefixDiagnostics: true,
317
+ openRouterAffinity: true,
318
+ }),
271
319
  inventoryRef: "§4 Xiaomi MiMo",
272
320
  },
273
321
  {
@@ -279,6 +327,7 @@ export const POLICY_REGISTRY = [
279
327
  baseline: "xiaomi.implicit-cache",
280
328
  overlays: [],
281
329
  legacy: false,
330
+ runtime: rt("neutral"),
282
331
  policyStatus: "documented-series-member-without-registered-overlay",
283
332
  note: "Documented as a Pro mode in the same V2.6 series, but the inventory does not establish identical cache controls and CacheEngine registers no overlay for it.",
284
333
  inventoryRef: "§4 Xiaomi MiMo",
@@ -293,6 +342,7 @@ export const POLICY_REGISTRY = [
293
342
  baseline: "deepseek.kv-cache",
294
343
  overlays: [],
295
344
  legacy: true,
345
+ runtime: rt("deepseek"),
296
346
  inventoryRef: "§2 DeepSeek",
297
347
  },
298
348
  ]
@@ -301,13 +351,16 @@ export const POLICY_REGISTRY = [
301
351
  // Explicit aliases identified by the inventory
302
352
  // ---------------------------------------------------------------------------
303
353
 
354
+ // `legacy` records whether the pre-v0.4.0 classifier already matched this alias.
355
+ // Only legacy aliases carry runtime capabilities; newer documented aliases are
356
+ // resolved for information but stay runtime-neutral (no new optimization).
304
357
  export const MODEL_ALIASES = {
305
- "gpt-5.6": { canonicalId: "gpt-5.6-sol", family: "gpt-5.6", creator: "openai", inventoryRef: "§1 OpenAI" },
306
- "gpt-daybreak-blue-latest": { canonicalId: "gpt-5.6-sol", family: "gpt-5.6", creator: "openai", inventoryRef: "§1 OpenAI" },
307
- "gpt-daybreak-red-latest": { canonicalId: "gpt-5.6-cyber", family: "gpt-5.6", creator: "openai", inventoryRef: "§1 OpenAI" },
308
- "deepseek-v4-flash": { canonicalId: "deepseek-flash", family: "deepseek", creator: "deepseek", status: "retired-legacy-id", inventoryRef: "§2 DeepSeek" },
309
- "deepseek-chat": { canonicalId: null, family: "deepseek", creator: "deepseek", status: "retired", inventoryRef: "§2 DeepSeek" },
310
- "deepseek-reasoner": { canonicalId: null, family: "deepseek", creator: "deepseek", status: "retired", inventoryRef: "§2 DeepSeek" },
358
+ "gpt-5.6": { canonicalId: "gpt-5.6-sol", family: "gpt-5.6", creator: "openai", legacy: true, inventoryRef: "§1 OpenAI" },
359
+ "gpt-daybreak-blue-latest": { canonicalId: "gpt-5.6-sol", family: "gpt-5.6", creator: "openai", legacy: false, inventoryRef: "§1 OpenAI" },
360
+ "gpt-daybreak-red-latest": { canonicalId: "gpt-5.6-cyber", family: "gpt-5.6", creator: "openai", legacy: false, inventoryRef: "§1 OpenAI" },
361
+ "deepseek-v4-flash": { canonicalId: "deepseek-flash", family: "deepseek", creator: "deepseek", legacy: true, status: "retired-legacy-id", inventoryRef: "§2 DeepSeek" },
362
+ "deepseek-chat": { canonicalId: null, family: "deepseek", creator: "deepseek", legacy: true, status: "retired", inventoryRef: "§2 DeepSeek" },
363
+ "deepseek-reasoner": { canonicalId: null, family: "deepseek", creator: "deepseek", legacy: true, status: "retired", inventoryRef: "§2 DeepSeek" },
311
364
  }
312
365
 
313
366
  // ---------------------------------------------------------------------------
@@ -320,6 +373,7 @@ function neutralResult(reason, transport) {
320
373
  family: "neutral",
321
374
  baseline: BASELINES["neutral.none"],
322
375
  overlays: [],
376
+ runtime: NEUTRAL_RUNTIME,
323
377
  transport,
324
378
  matchType: "neutral",
325
379
  matchReason: reason,
@@ -333,12 +387,19 @@ function overlaysFor(ids) {
333
387
  return (ids ?? []).map((id) => OVERLAYS[id]).filter(Boolean)
334
388
  }
335
389
 
390
+ // Only legacy entries carry runtime capabilities. A non-legacy entry (gpt-6,
391
+ // Pro UltraSpeed) resolves for information but stays neutral at runtime.
392
+ function runtimeForEntry(entry) {
393
+ return entry.legacy === false ? NEUTRAL_RUNTIME : entry.runtime ?? NEUTRAL_RUNTIME
394
+ }
395
+
336
396
  function resultFromEntry(entry, matchType, matchReason, matchedId, transport) {
337
397
  return {
338
398
  creator: entry.creator,
339
399
  family: entry.family,
340
400
  baseline: BASELINES[entry.baseline] ?? null,
341
401
  overlays: overlaysFor(entry.overlays),
402
+ runtime: runtimeForEntry(entry),
342
403
  transport,
343
404
  matchType,
344
405
  matchReason,
@@ -348,13 +409,17 @@ function resultFromEntry(entry, matchType, matchReason, matchedId, transport) {
348
409
  }
349
410
  }
350
411
 
351
- function resultFromFamily(family, creator, matchType, matchReason, matchedId, inventoryRef, note, transport) {
412
+ function resultFromFamily(family, creator, matchType, matchReason, matchedId, inventoryRef, note, transport, aliasLegacy) {
352
413
  const entry = POLICY_REGISTRY.find((e) => e.family === family && e.kind !== "exact")
414
+ // An alias is runtime-active only when both the alias and its target family
415
+ // were recognized before v0.4.0.
416
+ const active = aliasLegacy !== false && (!entry || entry.legacy !== false)
353
417
  return {
354
418
  creator,
355
419
  family,
356
420
  baseline: entry ? BASELINES[entry.baseline] ?? null : null,
357
421
  overlays: entry ? overlaysFor(entry.overlays) : [],
422
+ runtime: active && entry ? entry.runtime ?? NEUTRAL_RUNTIME : NEUTRAL_RUNTIME,
358
423
  transport,
359
424
  matchType,
360
425
  matchReason,
@@ -386,7 +451,7 @@ export function resolvePolicy(model) {
386
451
  const familyEntry = POLICY_REGISTRY.find((e) => e.family === alias.family && e.kind !== "exact")
387
452
  if (familyEntry?.requiresOpenAIish && !isOpenAIish(s)) continue
388
453
  const reason = `alias:${id}->${alias.canonicalId ?? alias.family}`
389
- return resultFromFamily(alias.family, alias.creator, "exact", reason, id, alias.inventoryRef, alias.status ?? null, transport)
454
+ return resultFromFamily(alias.family, alias.creator, "exact", reason, id, alias.inventoryRef, alias.status ?? null, transport, alias.legacy)
390
455
  }
391
456
 
392
457
  // 2. Exact model ids (documented models).
@@ -415,6 +480,13 @@ export function resolvePolicy(model) {
415
480
  return neutralResult("neutral:no-match", transport)
416
481
  }
417
482
 
483
+ // Convenience accessor for the runtime: the legacy policy string + explicit
484
+ // capability flags. This is the single source the runtime gates on; it is
485
+ // guaranteed equal to the pre-v0.4.0 detectPolicy() classification.
486
+ export function resolveRuntimePolicy(model) {
487
+ return resolvePolicy(model).runtime
488
+ }
489
+
418
490
  // Compatibility classification used by detectPolicy(). Reproduces the
419
491
  // pre-v0.4.0 behavior exactly: it considers only `legacy` registry entries,
420
492
  // excludes newer-generation/alias/exact-overlay additions, and returns a family
@@ -55,6 +55,7 @@ import {
55
55
  POLICY_REGISTRY,
56
56
  resolveLegacyFamily,
57
57
  resolvePolicy,
58
+ resolveRuntimePolicy,
58
59
  } from "../src/cache-policy-core.mjs"
59
60
 
60
61
  const asst = (id, read, write) => ({
@@ -1551,3 +1552,192 @@ test("registry is traceable and internally consistent", () => {
1551
1552
  assert.ok(alias.family)
1552
1553
  }
1553
1554
  })
1555
+
1556
+ // ===========================================================================
1557
+ // v0.4.1 runtime policy migration (behavior preservation)
1558
+ //
1559
+ // The runtime now classifies via resolveRuntimePolicy(). These tests prove the
1560
+ // resolved policy equals the legacy detectPolicy() string for every supported
1561
+ // model, and that the hook-observable behavior (GPT cache options, <env>
1562
+ // relocation, OpenRouter affinity) is unchanged. Newer/unknown models must
1563
+ // gain no mutation.
1564
+ // ===========================================================================
1565
+
1566
+ const policyCoreURL = new URL("../src/cache-policy-core.mjs", import.meta.url).href
1567
+
1568
+ test("v0.4.1: resolveRuntimePolicy.policy matches detectPolicy across a broad matrix", () => {
1569
+ const samples = [
1570
+ M("openai", "gpt-5.6"),
1571
+ M("openai", "gpt-5.6-sol"),
1572
+ M("openrouter", "openai/gpt-5.6-luna"),
1573
+ M("openai-compatible", "gpt-5.6"),
1574
+ M("openai", "gpt-6-astra"),
1575
+ M("openai", "gpt-5.5"),
1576
+ M("openai", "gpt-daybreak-blue-latest"),
1577
+ M("openai", "gpt-daybreak-red-latest"),
1578
+ M("zai", "glm-5.3"),
1579
+ M("zai", "glm-5.3-flash"),
1580
+ M("zai", "glm-5.2"),
1581
+ M("xiaomi", "mimo-v2.6-flash"),
1582
+ M("xiaomi", "mimo-v2.6-pro"),
1583
+ M("xiaomi", "mimo-v2.6-pro-ultraspeed"),
1584
+ M("xiaomi", "mimo-v2.5"),
1585
+ M("deepseek", "deepseek-v4-pro"),
1586
+ M("deepseek", "deepseek-flash"),
1587
+ M("deepseek", "deepseek-v4-flash"),
1588
+ M("deepseek", "deepseek-chat"),
1589
+ M("openrouter", "x-ai/grok-4"),
1590
+ {},
1591
+ null,
1592
+ undefined,
1593
+ 42,
1594
+ ]
1595
+ for (const m of samples) {
1596
+ assert.equal(resolveRuntimePolicy(m).policy, detectPolicy(m))
1597
+ }
1598
+ })
1599
+
1600
+ test("v0.4.1: GPT-5.6 alias keeps the overlay but a newer alias stays runtime-neutral", () => {
1601
+ // Legacy alias: gpt-5.6 -> gpt-5.6-sol (was matched by the old regex).
1602
+ const legacy = resolveRuntimePolicy(M("openai", "gpt-5.6"))
1603
+ assert.equal(legacy.policy, "gpt56")
1604
+ assert.equal(legacy.gptCacheMetadata, true)
1605
+ // Newer documented alias that the old classifier did NOT match: no mutation.
1606
+ const newer = resolveRuntimePolicy(M("openai", "gpt-daybreak-blue-latest"))
1607
+ assert.equal(newer.policy, "neutral")
1608
+ assert.equal(newer.gptCacheMetadata, false)
1609
+ assert.equal(resolvePolicy(M("openai", "gpt-daybreak-blue-latest")).family, "gpt-5.6")
1610
+ })
1611
+
1612
+ async function runPolicyMigrationProbe() {
1613
+ const home = mkdtempSync(join(tmpdir(), "ce-policy-migration-"))
1614
+ const pluginURL = new URL("../src/cache-engine.ts", import.meta.url).href
1615
+ const coreURL = new URL("../src/cache-engine-core.mjs", import.meta.url).href
1616
+ const script = `
1617
+ import assert from "node:assert/strict"
1618
+ process.env.CACHE_ENGINE_METRICS_FILE = process.env.HOME + "/policy-migration.jsonl"
1619
+ const { CacheEngine } = await import(${JSON.stringify(pluginURL)})
1620
+ const { detectPolicy } = await import(${JSON.stringify(coreURL)})
1621
+ const { resolvePolicy, resolveRuntimePolicy } = await import(${JSON.stringify(policyCoreURL)})
1622
+ const client = {
1623
+ app: { log: async () => ({}) },
1624
+ session: { get: async () => ({ data: { parentID: null } }) },
1625
+ tool: { list: async () => ({ data: [] }) },
1626
+ }
1627
+ const hooks = await CacheEngine({ client, directory: process.env.HOME })
1628
+ assert.equal(typeof hooks["chat.params"], "function")
1629
+ assert.equal(typeof hooks["chat.headers"], "function")
1630
+ assert.equal(typeof hooks["experimental.chat.system.transform"], "function")
1631
+ const SYS = ["A: keep1", "B: You are powered by the model named x. The exact model ID is acme/x", "C: <env>", "D: Today's date: 2026-08-17", "E: </env>", "F: keep2"].join("\\n")
1632
+ const CASES = [
1633
+ { name: "gpt-5.6", model: { providerID: "openai", id: "gpt-5.6", api: { id: "gpt-5.6", npm: "@ai-sdk/openai" } }, expect: { policy: "gpt56", env: false, gpt: true, header: false } },
1634
+ { name: "gpt-5.6-openrouter", model: { providerID: "openrouter", id: "openai/gpt-5.6-sol", api: { id: "openai/gpt-5.6-sol" } }, expect: { policy: "gpt56", env: false, gpt: true, header: false } },
1635
+ { name: "gpt-6-astra", model: { providerID: "openai", id: "gpt-6-astra", api: { id: "gpt-6-astra", npm: "@ai-sdk/openai" } }, expect: { policy: "neutral", env: false, gpt: false, header: false } },
1636
+ { name: "gpt-5.5", model: { providerID: "openai", id: "gpt-5.5", api: { id: "gpt-5.5", npm: "@ai-sdk/openai" } }, expect: { policy: "neutral", env: false, gpt: false, header: false } },
1637
+ { name: "gpt-daybreak-alias", model: { providerID: "openai", id: "gpt-daybreak-blue-latest", api: { id: "gpt-daybreak-blue-latest", npm: "@ai-sdk/openai" } }, expect: { policy: "neutral", env: false, gpt: false, header: false } },
1638
+ { name: "deepseek-v4-pro", model: { providerID: "deepseek", id: "deepseek-v4-pro", api: { id: "deepseek-v4-pro" } }, expect: { policy: "deepseek", env: false, gpt: false, header: false } },
1639
+ { name: "deepseek-flash", model: { providerID: "deepseek", id: "deepseek-flash", api: { id: "deepseek-flash" } }, expect: { policy: "deepseek", env: false, gpt: false, header: false } },
1640
+ { name: "glm-5.3-direct", model: { providerID: "zai", id: "glm-5.3", api: { id: "glm-5.3" } }, expect: { policy: "glm53", env: true, gpt: false, header: false } },
1641
+ { name: "glm-5.3-openrouter", model: { providerID: "openrouter", id: "z-ai/glm-5.3-flash", api: { id: "z-ai/glm-5.3-flash" } }, expect: { policy: "glm53", env: true, gpt: false, header: true } },
1642
+ { name: "glm-5.2", model: { providerID: "zai", id: "glm-5.2", api: { id: "glm-5.2" } }, expect: { policy: "neutral", env: false, gpt: false, header: false } },
1643
+ { name: "mimo-v2.6-flash-direct", model: { providerID: "xiaomi", id: "mimo-v2.6-flash", api: { id: "mimo-v2.6-flash" } }, expect: { policy: "mimo26", env: true, gpt: false, header: false } },
1644
+ { name: "mimo-v2.6-pro-openrouter", model: { providerID: "openrouter", id: "xiaomi/mimo-v2.6-pro", api: { id: "xiaomi/mimo-v2.6-pro" } }, expect: { policy: "mimo26", env: true, gpt: false, header: true } },
1645
+ { name: "mimo-v2.6-pro-ultraspeed", model: { providerID: "xiaomi", id: "mimo-v2.6-pro-ultraspeed", api: { id: "mimo-v2.6-pro-ultraspeed" } }, expect: { policy: "neutral", env: false, gpt: false, header: false } },
1646
+ { name: "mimo-v2.5", model: { providerID: "xiaomi", id: "mimo-v2.5", api: { id: "mimo-v2.5" } }, expect: { policy: "neutral", env: false, gpt: false, header: false } },
1647
+ { name: "unknown-provider", model: { providerID: "mystery-provider", id: "xiaomi/mimo-v2.6-flash", api: { id: "xiaomi/mimo-v2.6-flash" } }, expect: { policy: "mimo26", env: true, gpt: false, header: false } },
1648
+ { name: "unknown-openrouter", model: { providerID: "openrouter", id: "acme/mystery-9", api: { id: "acme/mystery-9" } }, expect: { policy: "neutral", env: false, gpt: false, header: false } },
1649
+ ]
1650
+ const results = []
1651
+ for (const c of CASES) {
1652
+ const model = c.model
1653
+ const sid = "ses_" + c.name
1654
+ const provider = { source: "config", info: { id: String(model.providerID ?? "") }, options: {} }
1655
+ const existing = { "User-Agent": "preserve", "x-custom": "preserve" }
1656
+ const paramsOut = { options: {} }
1657
+ const headersOut = { headers: { ...existing } }
1658
+ const sysOut = { system: [SYS] }
1659
+ await hooks["chat.params"]({ sessionID: sid, agent: "build", model, provider, message: { id: "msg-" + c.name, sessionID: sid, role: "user", content: "probe" } }, paramsOut)
1660
+ await hooks["experimental.chat.system.transform"]({ sessionID: sid, model, provider }, sysOut)
1661
+ await hooks["chat.headers"]({ sessionID: sid, agent: "build", model, provider, message: { id: "msg-" + c.name, sessionID: sid, role: "user", content: "probe" } }, headersOut)
1662
+ const rt = resolveRuntimePolicy(model)
1663
+ const existingHeadersPreserved = Object.entries(existing).every(([k, v]) => headersOut.headers[k] === v)
1664
+ results.push({
1665
+ name: c.name,
1666
+ runtimePolicy: rt.policy,
1667
+ detectPolicy: detectPolicy(model),
1668
+ richFamily: resolvePolicy(model).family,
1669
+ overlays: resolvePolicy(model).overlays.map((o) => o.id),
1670
+ systemRelocated: sysOut.system[0] !== SYS,
1671
+ gptOptionInjected: paramsOut.options.promptCacheKey !== undefined,
1672
+ gptOptions: paramsOut.options.promptCacheOptions ?? null,
1673
+ affinityHeaderAttached: headersOut.headers["x-session-id"] !== undefined,
1674
+ existingHeadersPreserved,
1675
+ expect: c.expect,
1676
+ })
1677
+ }
1678
+ process.stdout.write(JSON.stringify({ results }))
1679
+ `
1680
+ const stdout = execFileSync(process.execPath, ["--experimental-strip-types", "--input-type=module", "-e", script], {
1681
+ cwd: process.cwd(),
1682
+ env: { ...process.env, HOME: home },
1683
+ encoding: "utf8",
1684
+ })
1685
+ return JSON.parse(stdout.trim())
1686
+ }
1687
+
1688
+ let policyMigrationProbe
1689
+ const policyMigrationResults = async () => (policyMigrationProbe ??= runPolicyMigrationProbe())
1690
+
1691
+ test("v0.4.1: runtime policy is the resolver's and stays equal to detectPolicy (hook path)", async () => {
1692
+ const { results } = await policyMigrationResults()
1693
+ assert.ok(results.length >= 15)
1694
+ for (const r of results) {
1695
+ assert.equal(r.runtimePolicy, r.detectPolicy, `${r.name}: resolver policy must equal legacy detectPolicy`)
1696
+ assert.equal(r.runtimePolicy, r.expect.policy, `${r.name}: unexpected policy`)
1697
+ }
1698
+ })
1699
+
1700
+ test("v0.4.1: every supported model keeps its pre-migration hook behavior", async () => {
1701
+ const { results } = await policyMigrationResults()
1702
+ for (const r of results) {
1703
+ assert.equal(r.systemRelocated, r.expect.env, `${r.name}: <env> relocation`)
1704
+ assert.equal(r.gptOptionInjected, r.expect.gpt, `${r.name}: GPT cache-options injection`)
1705
+ assert.equal(r.affinityHeaderAttached, r.expect.header, `${r.name}: OpenRouter affinity header`)
1706
+ // No provider in the matrix mutates or drops pre-existing headers.
1707
+ assert.equal(r.existingHeadersPreserved, true, `${r.name}: existing headers preserved`)
1708
+ }
1709
+ })
1710
+
1711
+ test("v0.4.1: GPT-5.6 keeps promptCacheOptions implicit/30m through the resolver", async () => {
1712
+ const { results } = await policyMigrationResults()
1713
+ const gpt = results.find((r) => r.name === "gpt-5.6")
1714
+ assert.deepEqual(gpt.gptOptions, { mode: "implicit", ttl: "30m" })
1715
+ })
1716
+
1717
+ test("v0.4.1: future-looking and unknown models gain no mutation", async () => {
1718
+ const { results } = await policyMigrationResults()
1719
+ const noMutation = [
1720
+ "gpt-6-astra",
1721
+ "gpt-daybreak-alias",
1722
+ "gpt-5.5",
1723
+ "glm-5.2",
1724
+ "mimo-v2.6-pro-ultraspeed",
1725
+ "mimo-v2.5",
1726
+ "unknown-openrouter",
1727
+ ]
1728
+ for (const name of noMutation) {
1729
+ const r = results.find((x) => x.name === name)
1730
+ assert.ok(r, `${name} present`)
1731
+ assert.equal(r.gptOptionInjected, false, `${name}: no GPT options`)
1732
+ assert.equal(r.systemRelocated, false, `${name}: no <env> relocation`)
1733
+ assert.equal(r.affinityHeaderAttached, false, `${name}: no affinity header`)
1734
+ }
1735
+ })
1736
+
1737
+ test("v0.4.1: non-OpenRouter models never receive the OpenRouter header", async () => {
1738
+ const { results } = await policyMigrationResults()
1739
+ for (const name of ["glm-5.3-direct", "mimo-v2.6-flash-direct", "unknown-provider", "gpt-5.6", "deepseek-v4-pro"]) {
1740
+ const r = results.find((x) => x.name === name)
1741
+ assert.equal(r.affinityHeaderAttached, false, `${name}: no affinity header off OpenRouter`)
1742
+ }
1743
+ })