opencode-cache-engine 0.3.6 → 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/docs/cache-policy-inventory.md +313 -0
- package/package.json +1 -1
- package/src/cache-engine-core.mjs +16 -50
- package/src/cache-engine.ts +60 -49
- package/src/cache-policy-core.mjs +512 -0
- package/test/cache-engine.test.mjs +425 -0
package/src/cache-engine.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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 &&
|
|
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
|
|
365
|
+
const caps = s.modelInfo?.caps
|
|
354
366
|
const glmIntegrity =
|
|
355
|
-
|
|
356
|
-
policyEnabled(cfg,
|
|
357
|
-
cfg.policies?.[
|
|
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 (
|
|
432
|
+
if (caps?.cacheRatio === "glm") {
|
|
421
433
|
recFields.promptTokens = read + write + input
|
|
422
434
|
recFields.glmHitRate = glmHitRatio(read, write, input)
|
|
423
435
|
}
|
|
424
|
-
if (
|
|
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?.[
|
|
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 (
|
|
455
|
+
if (caps?.gptCacheMetadata === true && s.gptInjected) {
|
|
444
456
|
recFields.keyStrategy = "session"
|
|
445
|
-
recFields.mode = cfg.policies?.[
|
|
446
|
-
recFields.ttl = cfg.policies?.[
|
|
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
|
|
499
|
-
if (
|
|
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 (
|
|
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?.[
|
|
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:
|
|
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 (
|
|
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:
|
|
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 (!(
|
|
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?.[
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
793
|
-
// present exactly. Never touches other content/order; never
|
|
794
|
-
// other families. The runtime passes a single-element system
|
|
795
|
-
// (verified against the installed runtime), so no generic
|
|
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
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
cfg
|
|
807
|
-
|
|
808
|
-
|
|
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 (
|
|
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:
|
|
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 (
|
|
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:
|
|
926
|
+
policy: caps!.policy,
|
|
916
927
|
provider: s.modelInfo?.providerID,
|
|
917
928
|
model: s.modelInfo?.modelID,
|
|
918
929
|
changedFields: granular,
|