pi-mega-compact 0.20.12 → 0.20.14
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/dist/config/vector-cortex-early.js +89 -0
- package/dist/config/vector-cortex-flag.js +25 -0
- package/dist/config/vector-cortex.js +23 -83
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex-economics.js +20 -0
- package/dist/extensions/dashboard-server/route-dispatch.js +6 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-economics.js +59 -0
- package/dist/src/config/vector-cortex-early.js +89 -0
- package/dist/src/config/vector-cortex-flag.js +25 -0
- package/dist/src/config/vector-cortex.js +23 -83
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/cache/_economics-fixture.js +41 -0
- package/dist/src/vector-cortex/cache/compiler.js +201 -0
- package/dist/src/vector-cortex/cache/economics-emit.js +85 -0
- package/dist/src/vector-cortex/migrations/request-hash-v2.js +161 -0
- package/dist/src/vector-cortex/provider/economics-ids.js +29 -0
- package/dist/src/vector-cortex/provider/economics.js +176 -0
- package/dist/src/vector-cortex/provider/experiments.js +166 -0
- package/dist/vector-cortex/cache/_economics-fixture.js +41 -0
- package/dist/vector-cortex/cache/compiler.js +201 -0
- package/dist/vector-cortex/cache/economics-emit.js +85 -0
- package/dist/vector-cortex/migrations/request-hash-v2.js +161 -0
- package/dist/vector-cortex/provider/economics-ids.js +29 -0
- package/dist/vector-cortex/provider/economics.js +176 -0
- package/dist/vector-cortex/provider/experiments.js +166 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-CB-U7ViX.js → AreaChart-VUd_iOtO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-CB-U7ViX.js.map → AreaChart-VUd_iOtO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-DnLw0fxA.js → BarChart-BoqeJ5mM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-DnLw0fxA.js.map → BarChart-BoqeJ5mM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CjyTVDiP.js → CacheTab-1ON7meDK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CjyTVDiP.js.map → CacheTab-1ON7meDK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-Bz3QEWam.js → EventsTab-Dr9eqt1k.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-Bz3QEWam.js.map → EventsTab-Dr9eqt1k.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-CUylEvEY.js → HealthTab-Ciaigd4i.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-CUylEvEY.js.map → HealthTab-Ciaigd4i.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cz_hj_E7.js → MaintenanceTab-DjZRr3Lf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cz_hj_E7.js.map → MaintenanceTab-DjZRr3Lf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dh8brIR1.js → MemoryMapTab-BpZXHA7j.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dh8brIR1.js.map → MemoryMapTab-BpZXHA7j.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CkqSXc6e.js → MetricsTab-QemocY5k.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CkqSXc6e.js.map → MetricsTab-QemocY5k.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DGbJcx6d.js → OverviewTab-VtC1F-mO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DGbJcx6d.js.map → OverviewTab-VtC1F-mO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-BawS3nfF.js → ReposTab-CJD1cBxp.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-BawS3nfF.js.map → ReposTab-CJD1cBxp.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CUVnmYks.js → SessionsTab-Cj0erPR4.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CUVnmYks.js.map → SessionsTab-Cj0erPR4.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-cxTuyZNk.js → SetupTab-AOVROZ4G.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-cxTuyZNk.js.map → SetupTab-AOVROZ4G.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cv5SENkE.js → TimeSavedCard-ZliVn8DH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cv5SENkE.js.map → TimeSavedCard-ZliVn8DH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-yl6BBG_F.js → TurnsTab-DsoTWeIh.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-yl6BBG_F.js.map → TurnsTab-DsoTWeIh.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BBa7rp6C.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BBa7rp6C.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-DKv-0xCQ.js → WikiTab-CK8fcW1l.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-DKv-0xCQ.js.map → WikiTab-CK8fcW1l.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-CVPrO4UU.js → button-DdPcvex1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-CVPrO4UU.js.map → button-DdPcvex1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-BFLnQJEo.js → card-DXFuZph_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-BFLnQJEo.js.map → card-DXFuZph_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B-IUoLd1.js → generateCategoricalChart-BagDVGWn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B-IUoLd1.js.map → generateCategoricalChart-BagDVGWn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-Do749WlW.js → index-DVdDtRY4.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-Do749WlW.js.map → index-DVdDtRY4.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-3uFnZmtq.js → switch-CtdtGyUR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-3uFnZmtq.js.map → switch-CtdtGyUR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-C1rYLeXe.js → toggle-Cvvwlbye.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-C1rYLeXe.js.map → toggle-Cvvwlbye.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-D1qzLzgR.js → useSSE-DvlceHjV.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-D1qzLzgR.js.map → useSSE-DvlceHjV.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexEconomicsCard.tsx +41 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +8 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +14 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex-economics.ts +47 -0
- package/extensions/dashboard-server/route-dispatch.ts +5 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-economics.ts +68 -0
- package/package.json +1 -1
- package/src/config/vector-cortex-early.ts +99 -0
- package/src/config/vector-cortex-flag.ts +24 -0
- package/src/config/vector-cortex.ts +34 -89
- package/src/config.ts +1 -0
- package/src/vector-cortex/cache/_economics-fixture.ts +128 -0
- package/src/vector-cortex/cache/compiler.ts +280 -0
- package/src/vector-cortex/cache/economics-emit.ts +116 -0
- package/src/vector-cortex/migrations/request-hash-v2.ts +228 -0
- package/src/vector-cortex/provider/economics-ids.ts +38 -0
- package/src/vector-cortex/provider/economics.ts +289 -0
- package/src/vector-cortex/provider/experiments.ts +248 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-94fqVjat.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-94fqVjat.js.map +0 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/cache/compiler.ts — VC7B provider-safe crystal boundary compiler.
|
|
3
|
+
*
|
|
4
|
+
* A provider caches a PREFIX of the request. So where you cut the prompt into
|
|
5
|
+
* cacheable segments determines whether anything is reusable at all: cut too
|
|
6
|
+
* finely and every segment falls under the provider's `minPrefix` and nothing is
|
|
7
|
+
* cacheable; cut in the wrong place and a segment that changes every turn sits
|
|
8
|
+
* in front of one that never changes, invalidating everything behind it. This
|
|
9
|
+
* file turns validated source ranges plus a profile's limits into boundaries
|
|
10
|
+
* that the provider can actually cache.
|
|
11
|
+
*
|
|
12
|
+
* THE ONE INVARIANT THAT OUTRANKS EVERYTHING: THE COMPILER NEVER CHANGES
|
|
13
|
+
* REQUEST IDENTITY. It selects where boundaries FALL; it never reorders, never
|
|
14
|
+
* merges across a session, never drops a range, never rewrites bytes. The
|
|
15
|
+
* concatenation of the compiled boundaries covers exactly the same ranges, in
|
|
16
|
+
* exactly the same canonical order, as the input — so the VC5B canonical request
|
|
17
|
+
* digest and the VC7A crystal key are byte-identical before and after
|
|
18
|
+
* compilation. `boundariesPreserveIdentity()` states this as an executable
|
|
19
|
+
* check, and the acceptance suite runs it on every compiled row. If compilation
|
|
20
|
+
* could alter identity it would be a cache-poisoning engine rather than an
|
|
21
|
+
* optimizer: two different conversations could compile to one cache key.
|
|
22
|
+
*
|
|
23
|
+
* WHY MERGE FORWARD, AND ONLY WITHIN A SESSION. Ranges below `minPrefix` are not
|
|
24
|
+
* independently cacheable, so the compiler merges each undersized range into the
|
|
25
|
+
* one that FOLLOWS it in canonical order — forward, so the merged segment keeps
|
|
26
|
+
* the earlier range's start and the prefix relationship is preserved. Merging is
|
|
27
|
+
* confined to a single session because ranges from different sessions cover
|
|
28
|
+
* disjoint byte streams; a cross-session segment would claim a contiguity that
|
|
29
|
+
* does not exist. A trailing undersized run with nothing to merge into is
|
|
30
|
+
* emitted as its own boundary marked `cacheable: false` — reported honestly as
|
|
31
|
+
* uncacheable rather than padded, dropped, or silently attached backwards.
|
|
32
|
+
*
|
|
33
|
+
* BOUNDED. `maxSegments` caps the output so a caller-shaped range list cannot be
|
|
34
|
+
* turned into an unbounded segmentation (`COMP_SEGMENT_LIMIT`).
|
|
35
|
+
*
|
|
36
|
+
* PURE. No clock, no storage, no console, no network (PREVENT-PI-004 /
|
|
37
|
+
* PREVENT-011). Runs identically with `MEGACOMPACT_VC7B` on or off — the flag
|
|
38
|
+
* gates only the reporter/dashboard seam in `./economics-emit.ts`.
|
|
39
|
+
*/
|
|
40
|
+
import { createHash } from "node:crypto";
|
|
41
|
+
import { compareSpans, sortSpans, validateRanges } from "./crystal.js";
|
|
42
|
+
/** Conventional default limits (4 bytes/token is the usual English estimate). */
|
|
43
|
+
export const DEFAULT_COMPILER_LIMITS = {
|
|
44
|
+
minPrefix: 1024,
|
|
45
|
+
maxSegments: 64,
|
|
46
|
+
bytesPerToken: 4,
|
|
47
|
+
};
|
|
48
|
+
/** Covered bytes of one range. */
|
|
49
|
+
function spanBytes(s) {
|
|
50
|
+
return s.endByte - s.startByte;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Byte→token estimate. Deliberately a floor: under-estimating tokens can only
|
|
54
|
+
* make the compiler MORE conservative about calling a segment cacheable, which
|
|
55
|
+
* fails safe (a wrongly-cacheable segment would be priced as a hit it never got).
|
|
56
|
+
*/
|
|
57
|
+
export function tokensForBytes(bytes, bytesPerToken) {
|
|
58
|
+
return Math.floor(bytes / bytesPerToken);
|
|
59
|
+
}
|
|
60
|
+
/** `sha256:` digest over a segment's ranges — length-prefixed, so injective. */
|
|
61
|
+
function boundaryDigest(ranges) {
|
|
62
|
+
const h = createHash("sha256");
|
|
63
|
+
for (const s of ranges) {
|
|
64
|
+
const parts = [
|
|
65
|
+
s.sessionId,
|
|
66
|
+
s.startSeq.toString(),
|
|
67
|
+
s.endSeq.toString(),
|
|
68
|
+
String(s.startByte),
|
|
69
|
+
String(s.endByte),
|
|
70
|
+
s.digest,
|
|
71
|
+
];
|
|
72
|
+
for (const p of parts)
|
|
73
|
+
h.update(`${Buffer.byteLength(p, "utf8")}:${p}`, "utf8");
|
|
74
|
+
}
|
|
75
|
+
return `sha256:${h.digest("hex")}`;
|
|
76
|
+
}
|
|
77
|
+
/** Build one boundary from a contiguous same-session run of ranges. */
|
|
78
|
+
function makeBoundary(ranges, limits) {
|
|
79
|
+
const byteCount = ranges.reduce((n, s) => n + spanBytes(s), 0);
|
|
80
|
+
const tokenCount = tokensForBytes(byteCount, limits.bytesPerToken);
|
|
81
|
+
const first = ranges[0];
|
|
82
|
+
return {
|
|
83
|
+
schema: "crystal-boundary-v1",
|
|
84
|
+
sessionId: first === undefined ? "" : first.sessionId,
|
|
85
|
+
ranges,
|
|
86
|
+
byteCount,
|
|
87
|
+
tokenCount,
|
|
88
|
+
cacheable: tokenCount >= limits.minPrefix,
|
|
89
|
+
digest: boundaryDigest(ranges),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function validLimits(l) {
|
|
93
|
+
return (Number.isSafeInteger(l.minPrefix) &&
|
|
94
|
+
l.minPrefix >= 0 &&
|
|
95
|
+
Number.isSafeInteger(l.maxSegments) &&
|
|
96
|
+
l.maxSegments > 0 &&
|
|
97
|
+
Number.isSafeInteger(l.bytesPerToken) &&
|
|
98
|
+
l.bytesPerToken >= 1);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Compile validated ranges into provider-safe cache boundaries.
|
|
102
|
+
*
|
|
103
|
+
* Ranges are first put in canonical order (the SAME `sortSpans` order the VC7A
|
|
104
|
+
* key encoder uses — sharing the comparator is what keeps the two subsystems
|
|
105
|
+
* from disagreeing about what "canonical" means). They are then grouped by
|
|
106
|
+
* session, and within each session an undersized run is merged FORWARD until it
|
|
107
|
+
* meets `minPrefix`.
|
|
108
|
+
*
|
|
109
|
+
* Identity is verified before returning: the flattened output must equal the
|
|
110
|
+
* canonical input exactly. That check should never fire — it is a guard against
|
|
111
|
+
* a future edit to the merge loop quietly reordering or dropping a range.
|
|
112
|
+
*/
|
|
113
|
+
export function compileCrystalBoundaries(ranges, limits = DEFAULT_COMPILER_LIMITS) {
|
|
114
|
+
if (!validLimits(limits))
|
|
115
|
+
return { ok: false, codes: ["COMP_LIMIT_INVALID"] };
|
|
116
|
+
// Reuse VC7A's validator verbatim: the compiler must never accept a range set
|
|
117
|
+
// the crystal key would reject, or the two would disagree about what is legal.
|
|
118
|
+
const rangeCodes = validateRanges(ranges);
|
|
119
|
+
if (rangeCodes.length > 0)
|
|
120
|
+
return { ok: false, codes: ["COMP_RANGE_INVALID"] };
|
|
121
|
+
const sorted = sortSpans(ranges);
|
|
122
|
+
const boundaries = [];
|
|
123
|
+
let run = [];
|
|
124
|
+
const flushRun = () => {
|
|
125
|
+
if (run.length > 0) {
|
|
126
|
+
boundaries.push(makeBoundary(run, limits));
|
|
127
|
+
run = [];
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
for (const span of sorted) {
|
|
131
|
+
const head = run[0];
|
|
132
|
+
// A session change always closes the run: a boundary never spans sessions,
|
|
133
|
+
// because two sessions' byte streams are not contiguous with each other.
|
|
134
|
+
if (head !== undefined && head.sessionId !== span.sessionId)
|
|
135
|
+
flushRun();
|
|
136
|
+
run.push(span);
|
|
137
|
+
const bytes = run.reduce((n, s) => n + spanBytes(s), 0);
|
|
138
|
+
// Close as soon as the run is independently cacheable; anything smaller keeps
|
|
139
|
+
// absorbing the next range (merge-forward).
|
|
140
|
+
if (tokensForBytes(bytes, limits.bytesPerToken) >= limits.minPrefix)
|
|
141
|
+
flushRun();
|
|
142
|
+
}
|
|
143
|
+
// A trailing undersized run has nothing left to merge into: emit it honestly
|
|
144
|
+
// as a non-cacheable boundary rather than padding or dropping it.
|
|
145
|
+
flushRun();
|
|
146
|
+
if (boundaries.length > limits.maxSegments) {
|
|
147
|
+
return { ok: false, codes: ["COMP_SEGMENT_LIMIT"] };
|
|
148
|
+
}
|
|
149
|
+
if (!boundariesPreserveIdentity(sorted, boundaries)) {
|
|
150
|
+
return { ok: false, codes: ["COMP_IDENTITY_DRIFT"] };
|
|
151
|
+
}
|
|
152
|
+
let cacheableCount = 0;
|
|
153
|
+
let cacheableTokens = 0;
|
|
154
|
+
for (const b of boundaries) {
|
|
155
|
+
if (b.cacheable) {
|
|
156
|
+
cacheableCount += 1;
|
|
157
|
+
cacheableTokens += b.tokenCount;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, boundaries, cacheableCount, cacheableTokens };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The executable form of the sprint's headline invariant: flattening the
|
|
164
|
+
* compiled boundaries must reproduce the canonical input ranges EXACTLY — same
|
|
165
|
+
* ranges, same order, same pinned digests.
|
|
166
|
+
*
|
|
167
|
+
* Comparison is field-by-field on identity (session, seq bounds, byte bounds,
|
|
168
|
+
* digest), never by count or by digest alone: a compiler bug that swapped two
|
|
169
|
+
* equal-length ranges would keep the count and the byte total identical while
|
|
170
|
+
* changing what the request means.
|
|
171
|
+
*/
|
|
172
|
+
export function boundariesPreserveIdentity(input, boundaries) {
|
|
173
|
+
const flat = [];
|
|
174
|
+
for (const b of boundaries)
|
|
175
|
+
flat.push(...b.ranges);
|
|
176
|
+
const canonical = sortSpans(input);
|
|
177
|
+
if (flat.length !== canonical.length)
|
|
178
|
+
return false;
|
|
179
|
+
for (let i = 0; i < flat.length; i += 1) {
|
|
180
|
+
const a = flat[i];
|
|
181
|
+
const b = canonical[i];
|
|
182
|
+
if (a === undefined || b === undefined)
|
|
183
|
+
return false;
|
|
184
|
+
if (compareSpans(a, b) !== 0)
|
|
185
|
+
return false;
|
|
186
|
+
// compareSpans intentionally ignores the pinned digest (it orders by source
|
|
187
|
+
// position). Identity includes the covered bytes, so check it explicitly.
|
|
188
|
+
if (a.digest !== b.digest)
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Compile the ranges of an existing crystal key. Returns the boundaries plus the
|
|
195
|
+
* UNCHANGED key: compilation is an optimization of how the render is segmented,
|
|
196
|
+
* never a re-keying. Returning the same key object makes that explicit at the
|
|
197
|
+
* call site — a caller cannot accidentally pick up a "compiled key" that differs.
|
|
198
|
+
*/
|
|
199
|
+
export function compileForKey(key, limits = DEFAULT_COMPILER_LIMITS) {
|
|
200
|
+
return { key, compiled: compileCrystalBoundaries(key.sourceRanges, limits) };
|
|
201
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/cache/economics-emit.ts — VC7B event reporter seam.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `./crystal-emit.ts`: a thin `safe()` wrapper around an optional
|
|
5
|
+
* injected `emit` (unit tests pass `undefined` and stay pure), and the two event
|
|
6
|
+
* names the sprint spec requires verbatim:
|
|
7
|
+
* - `vector_cortex_cache_experiment_assigned` — a session entered an arm.
|
|
8
|
+
* - `vector_cortex_cache_economics_estimated` — net savings were computed.
|
|
9
|
+
*
|
|
10
|
+
* FLAG SEMANTICS. `computeEconomics`, `compileCrystalBoundaries` and
|
|
11
|
+
* `assignExperiment` are PURE and run REGARDLESS of `MEGACOMPACT_VC7B`. The flag
|
|
12
|
+
* gates ONLY this reporting + dashboard seam: with the flag off a session still
|
|
13
|
+
* hashes to the SAME arm, the compiler still produces the SAME boundaries, and
|
|
14
|
+
* the same net savings are still computed — we just do not announce them under
|
|
15
|
+
* the VC7B event namespace, and the dashboard reports `enabled:false` + mode C.
|
|
16
|
+
* That is what makes flag-off byte-identical to VC7A: the arithmetic is never
|
|
17
|
+
* skipped, only the emission.
|
|
18
|
+
*
|
|
19
|
+
* PAYLOAD DISCIPLINE. These events carry the ARM, the BUCKET, integer money
|
|
20
|
+
* AGGREGATES, and the evidence label — never a session id, never covered ranges,
|
|
21
|
+
* never frozen bytes, never a request or covered digest. A session id here would
|
|
22
|
+
* re-identify a user's conversation in a log file, and the bucket already carries
|
|
23
|
+
* everything needed to audit the split (SECURITY_PRIVACY — the exact ledger is
|
|
24
|
+
* not diagnostic data). The `evidence` label travels WITH every economics event
|
|
25
|
+
* so a downstream aggregator can never mistake a shadow estimate for a measured,
|
|
26
|
+
* randomized result.
|
|
27
|
+
*
|
|
28
|
+
* No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
|
|
29
|
+
* is a structured JSON event with `ts` + `event`.
|
|
30
|
+
*/
|
|
31
|
+
import { VC7B_ENABLED } from "../../config/vector-cortex.js";
|
|
32
|
+
/** Run `fn` only when an emit exists; a reporting failure is never fatal. */
|
|
33
|
+
function safe(emit, fn) {
|
|
34
|
+
if (emit === undefined)
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
fn(emit);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Non-fatal: a reporting failure must never break the agent loop.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** The event names VC7B emits, exported for the dashboard seam and tests. */
|
|
44
|
+
export const ECONOMICS_EVENT_NAMES = [
|
|
45
|
+
"vector_cortex_cache_experiment_assigned",
|
|
46
|
+
"vector_cortex_cache_economics_estimated",
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Report a session's experiment assignment.
|
|
50
|
+
*
|
|
51
|
+
* Arm + bucket + source only. The bucket is sufficient to audit that the split
|
|
52
|
+
* is being honored; the SESSION ID is deliberately absent so the event log
|
|
53
|
+
* cannot be used to reconstruct who was in which arm.
|
|
54
|
+
*/
|
|
55
|
+
export function reportCacheExperimentAssigned(emit, payload) {
|
|
56
|
+
if (!VC7B_ENABLED())
|
|
57
|
+
return;
|
|
58
|
+
safe(emit, (e) => e("vector_cortex_cache_experiment_assigned", {
|
|
59
|
+
ts: undefined,
|
|
60
|
+
event: "vector_cortex_cache_experiment_assigned",
|
|
61
|
+
experimentId: payload.experimentId,
|
|
62
|
+
arm: payload.arm,
|
|
63
|
+
bucket: payload.bucket,
|
|
64
|
+
source: payload.source,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Report computed cache economics.
|
|
69
|
+
*
|
|
70
|
+
* Integer micro-unit aggregates plus the evidence label. `netSavings` may be
|
|
71
|
+
* NEGATIVE and is reported as such — a cache that lost money is the outcome this
|
|
72
|
+
* telemetry exists to surface, so it is never clamped on the way out.
|
|
73
|
+
*/
|
|
74
|
+
export function reportCacheEconomicsEstimated(emit, payload) {
|
|
75
|
+
if (!VC7B_ENABLED())
|
|
76
|
+
return;
|
|
77
|
+
safe(emit, (e) => e("vector_cortex_cache_economics_estimated", {
|
|
78
|
+
ts: undefined,
|
|
79
|
+
event: "vector_cortex_cache_economics_estimated",
|
|
80
|
+
profileId: payload.profileId,
|
|
81
|
+
netSavings: payload.netSavings,
|
|
82
|
+
tokenSavings: payload.tokenSavings,
|
|
83
|
+
evidence: payload.evidence,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/migrations/request-hash-v2.ts — M5 request-hash-v2, COPY +
|
|
3
|
+
* VALIDATE ONLY (VC7B). The switch lands in VC7C.
|
|
4
|
+
*
|
|
5
|
+
* M5 versions the canonical request hash: the predecessor hashed the outbound
|
|
6
|
+
* request under the v1 scheme; v2 folds in the provider profile's ECONOMICS
|
|
7
|
+
* version so a pricing/TTL/exclusion change cannot silently reuse a cache
|
|
8
|
+
* identity minted under different economics. Like M4/M6 it follows the
|
|
9
|
+
* copy/validate/switch contract — but VC7B deliberately stops after validate:
|
|
10
|
+
*
|
|
11
|
+
* - copy: resumable per (profile, request) — an interrupted run resumes
|
|
12
|
+
* without duplicate rows or active-pointer drift.
|
|
13
|
+
* - validate: every v1 row has exactly one v2 row, every v2 digest re-hashes
|
|
14
|
+
* from its own declared fields, and — the M5-specific invariant —
|
|
15
|
+
* the migration is IDENTITY-PRESERVING: a v2 row must carry the
|
|
16
|
+
* same `requestDigest` as the v1 row it came from. v2 changes how
|
|
17
|
+
* a CACHE KEY is derived, never what the request IS. A row whose
|
|
18
|
+
* request digest drifted is rejected (`M5_IDENTITY_DRIFT`).
|
|
19
|
+
* - switch: NOT IMPLEMENTED IN VC7B. `m5Switch` exists so the shape of the
|
|
20
|
+
* lifecycle is reviewable, but it always refuses with
|
|
21
|
+
* `M5_SWITCH_DEFERRED` and leaves the active pointer on v1.
|
|
22
|
+
*
|
|
23
|
+
* WHY THE SWITCH IS WITHHELD. Flipping the request-hash version invalidates
|
|
24
|
+
* every crystal minted under v1 in one step. VC7C first runs the shadow
|
|
25
|
+
* comparison (v1 vs v2 digests over live traffic) so the blast radius is
|
|
26
|
+
* measured before it is taken. Shipping the switch here would mean flipping it
|
|
27
|
+
* on evidence that does not exist yet — so the function is present, refuses, and
|
|
28
|
+
* says why, rather than being a TODO comment someone later mistakes for done.
|
|
29
|
+
* Old-binary protocol: a reader that predates v2 keeps reading v1 rows, which
|
|
30
|
+
* remain untouched and authoritative for the whole of VC7B.
|
|
31
|
+
*
|
|
32
|
+
* Pure logic over an injected M5Host (deterministic + testable; no console).
|
|
33
|
+
* PREVENT-002/011/PI-004 honored.
|
|
34
|
+
*/
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
/** The active-version value the v2 pointer will be switched to — in VC7C. */
|
|
37
|
+
export const REQUEST_HASH_V2_VERSION = 2;
|
|
38
|
+
/** The legacy (predecessor) active version; VC7B never leaves this. */
|
|
39
|
+
export const REQUEST_HASH_LEGACY_VERSION = 1;
|
|
40
|
+
/** M5 failure codes. */
|
|
41
|
+
export const M5_FAIL = {
|
|
42
|
+
COPY_PARTIAL: "M5_COPY_PARTIAL",
|
|
43
|
+
COUNT_MISMATCH: "M5_COUNT_MISMATCH",
|
|
44
|
+
DIGEST_MISMATCH: "M5_DIGEST_MISMATCH",
|
|
45
|
+
IDENTITY_DRIFT: "M5_IDENTITY_DRIFT",
|
|
46
|
+
SWITCH_DEFERRED: "M5_SWITCH_DEFERRED",
|
|
47
|
+
};
|
|
48
|
+
/** Length-prefixed framing so the folded fields cannot alias. */
|
|
49
|
+
function field(value) {
|
|
50
|
+
return `${Buffer.byteLength(value, "utf8")}:${value}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The authoritative v2 hash: SHA-256 over the version tag, profile, request
|
|
54
|
+
* digest, and economics version. Length-prefixed for injectivity, exactly as the
|
|
55
|
+
* crystal key encoder frames its fields.
|
|
56
|
+
*/
|
|
57
|
+
export function deriveRequestHashV2(profileId, requestDigest, economicsVersion) {
|
|
58
|
+
return createHash("sha256")
|
|
59
|
+
.update(field("request-hash-v2") + field(profileId) + field(requestDigest) + field(economicsVersion), "utf8")
|
|
60
|
+
.digest("hex");
|
|
61
|
+
}
|
|
62
|
+
/** Derive the v2 row for one v1 row. */
|
|
63
|
+
export function deriveRequestHashRow(host, v1) {
|
|
64
|
+
const economicsVersion = host.economicsVersionOf(v1.profileId);
|
|
65
|
+
return {
|
|
66
|
+
profileId: v1.profileId,
|
|
67
|
+
// IDENTITY-PRESERVING: carried through verbatim, never re-derived.
|
|
68
|
+
requestDigest: v1.requestDigest,
|
|
69
|
+
economicsVersion,
|
|
70
|
+
hash: deriveRequestHashV2(v1.profileId, v1.requestDigest, economicsVersion),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const identity = (profileId, requestDigest) => `${profileId}::${requestDigest}`;
|
|
74
|
+
/**
|
|
75
|
+
* Copy every v1 row into a v2 row BESIDE it (resumable, idempotent). Rows already
|
|
76
|
+
* persisted with a matching derivation are left untouched; absent or stale rows
|
|
77
|
+
* are (re)written from the authoritative derivation. The active pointer is never
|
|
78
|
+
* touched — v1 remains authoritative throughout VC7B.
|
|
79
|
+
*/
|
|
80
|
+
export function m5Copy(host) {
|
|
81
|
+
const existing = new Map(host.existingV2().map((r) => [identity(r.profileId, r.requestDigest), r]));
|
|
82
|
+
const wanted = [];
|
|
83
|
+
for (const v1 of host.v1Rows()) {
|
|
84
|
+
const fresh = deriveRequestHashRow(host, v1);
|
|
85
|
+
const stored = existing.get(identity(v1.profileId, v1.requestDigest));
|
|
86
|
+
if (stored !== undefined &&
|
|
87
|
+
stored.hash === fresh.hash &&
|
|
88
|
+
stored.economicsVersion === fresh.economicsVersion) {
|
|
89
|
+
continue; // healthy, already copied
|
|
90
|
+
}
|
|
91
|
+
wanted.push(fresh);
|
|
92
|
+
}
|
|
93
|
+
if (wanted.length > 0)
|
|
94
|
+
host.putV2(wanted);
|
|
95
|
+
return { written: wanted, all: [...host.existingV2()] };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validate the copied set: exactly one v2 row per v1 row, every v2 hash
|
|
99
|
+
* re-derives from its own declared fields, no orphan v2 rows, and every v2 row's
|
|
100
|
+
* request digest equals its v1 source's (identity preservation). Never mutates.
|
|
101
|
+
*/
|
|
102
|
+
export function m5Verify(host) {
|
|
103
|
+
const codes = [];
|
|
104
|
+
const stored = host.existingV2();
|
|
105
|
+
const v1Rows = host.v1Rows();
|
|
106
|
+
const counts = new Map();
|
|
107
|
+
for (const r of stored) {
|
|
108
|
+
const id = identity(r.profileId, r.requestDigest);
|
|
109
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
110
|
+
}
|
|
111
|
+
const expected = new Set();
|
|
112
|
+
for (const v1 of v1Rows) {
|
|
113
|
+
const id = identity(v1.profileId, v1.requestDigest);
|
|
114
|
+
expected.add(id);
|
|
115
|
+
const n = counts.get(id) ?? 0;
|
|
116
|
+
if (n === 0)
|
|
117
|
+
codes.push(M5_FAIL.COPY_PARTIAL);
|
|
118
|
+
if (n > 1)
|
|
119
|
+
codes.push(M5_FAIL.COUNT_MISMATCH);
|
|
120
|
+
}
|
|
121
|
+
// Every v1 request digest that must survive the migration unchanged.
|
|
122
|
+
const v1Digests = new Set(v1Rows.map((r) => identity(r.profileId, r.requestDigest)));
|
|
123
|
+
for (const r of stored) {
|
|
124
|
+
const fresh = deriveRequestHashV2(r.profileId, r.requestDigest, r.economicsVersion);
|
|
125
|
+
if (r.hash !== fresh)
|
|
126
|
+
codes.push(M5_FAIL.DIGEST_MISMATCH);
|
|
127
|
+
const id = identity(r.profileId, r.requestDigest);
|
|
128
|
+
// A v2 row whose identity has no v1 source either drifted or is an orphan.
|
|
129
|
+
if (!v1Digests.has(id)) {
|
|
130
|
+
codes.push(expected.size > 0 ? M5_FAIL.IDENTITY_DRIFT : M5_FAIL.COUNT_MISMATCH);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { ok: codes.length === 0, codes: dedupe(codes) };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The switch — DEFERRED TO VC7C BY DESIGN.
|
|
137
|
+
*
|
|
138
|
+
* Always refuses and always leaves the active pointer on v1. It exists so the
|
|
139
|
+
* lifecycle is complete and reviewable, and so a caller that tries to switch
|
|
140
|
+
* early gets an explicit `M5_SWITCH_DEFERRED` instead of silence.
|
|
141
|
+
*/
|
|
142
|
+
export function m5Switch(host) {
|
|
143
|
+
void host;
|
|
144
|
+
return { ok: false, codes: [M5_FAIL.SWITCH_DEFERRED] };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The VC7B M5 lifecycle: copy -> verify. NO SWITCH. Returns the verify result;
|
|
148
|
+
* the active version pointer is guaranteed to still be
|
|
149
|
+
* `REQUEST_HASH_LEGACY_VERSION` on return.
|
|
150
|
+
*/
|
|
151
|
+
export function migrateRequestHashV2CopyValidate(host) {
|
|
152
|
+
m5Copy(host);
|
|
153
|
+
return m5Verify(host);
|
|
154
|
+
}
|
|
155
|
+
function dedupe(codes) {
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const c of codes)
|
|
158
|
+
if (!out.includes(c))
|
|
159
|
+
out.push(c);
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/provider/economics-ids.ts — VC7B conformance ID registrations.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from economics.ts to keep that file under the 300-line soft limit
|
|
5
|
+
* (soft-as-hard gate). The ID ranges and named rows are pure data — splitting
|
|
6
|
+
* them out mirrors how vector-cortex-breakers.ts was extracted from
|
|
7
|
+
* vector-cortex.ts. economics.ts re-exports them so no consumer import path
|
|
8
|
+
* changes.
|
|
9
|
+
*
|
|
10
|
+
* Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Registered VC7B cache-economics conformance ID range (CACHE-001..015). The
|
|
14
|
+
* acceptance aggregator reads these rows from the v2 manifest and asserts each
|
|
15
|
+
* returns its manifest `ok`/`code`.
|
|
16
|
+
*/
|
|
17
|
+
export const CACHE_IDS = Array.from({ length: 15 }, (_v, i) => `CACHE-${String(i + 1).padStart(3, "0")}`);
|
|
18
|
+
/**
|
|
19
|
+
* Registered VC7B provider-economics conformance rows (PRO-024..030), continuing
|
|
20
|
+
* VC7A's PRO-016..023. These pin the ECONOMICS half of a provider profile:
|
|
21
|
+
* pricing validity, the exclusion-proof rule, and TTL/min-prefix eligibility.
|
|
22
|
+
*/
|
|
23
|
+
export const ECONOMICS_PROVIDER_IDS = Array.from({ length: 7 }, (_v, i) => `PRO-${String(i + 24).padStart(3, "0")}`);
|
|
24
|
+
/** Named VC7B conformance assertions (the sprint's headline rows). */
|
|
25
|
+
export const ECONOMICS_NAMED_IDS = [
|
|
26
|
+
"CACHE-COST-001",
|
|
27
|
+
"CACHE-EXCLUDE-002",
|
|
28
|
+
"CACHE-RANDOM-003",
|
|
29
|
+
];
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/provider/economics.ts — VC7B cache economics (ProviderProfileV1
|
|
3
|
+
* extension).
|
|
4
|
+
*
|
|
5
|
+
* VC7A proved a render can be FROZEN and reused. VC7B answers the question that
|
|
6
|
+
* immediately follows: is reusing it actually WORTH IT? Provider prompt caching
|
|
7
|
+
* is not free — a cache WRITE typically costs MORE than an uncached token, and a
|
|
8
|
+
* cache READ costs less. So a cache is profitable only when a written prefix is
|
|
9
|
+
* read back enough times, before its TTL expires, to repay the write premium.
|
|
10
|
+
* This file is that arithmetic and nothing else.
|
|
11
|
+
*
|
|
12
|
+
* THE NET-SAVINGS MODEL. For a prefix of `cachedTokens` tokens:
|
|
13
|
+
*
|
|
14
|
+
* baseline (no cache) = cachedTokens * basePrice * (writes + hits)
|
|
15
|
+
* actual (cached) = cachedTokens * (writePrice * writes + readPrice * hits)
|
|
16
|
+
* netSavings = baseline - actual
|
|
17
|
+
*
|
|
18
|
+
* A NEGATIVE `netSavings` is a real, expected, and important outcome: it means
|
|
19
|
+
* the cache LOST money (a prefix written once and never re-read always does, by
|
|
20
|
+
* exactly the write premium). We report it rather than clamping it at zero,
|
|
21
|
+
* because a floor at zero would make every rollout look free and would hide the
|
|
22
|
+
* single failure mode this sprint exists to detect.
|
|
23
|
+
*
|
|
24
|
+
* EXACT ARITHMETIC, NO FLOATS IN THE MONEY PATH. Prices are integer
|
|
25
|
+
* MICRO-UNITS per token (1e-6 of the provider's currency unit), never floating
|
|
26
|
+
* point. `0.1 + 0.2 !== 0.3` is not an acceptable property for a cost ledger, and
|
|
27
|
+
* accumulating float error across millions of tokens produces aggregates that
|
|
28
|
+
* silently disagree with the provider's own bill. All money is integer; only the
|
|
29
|
+
* derived RATIO is a float, and it is finite-guarded.
|
|
30
|
+
*
|
|
31
|
+
* EVERY EXCLUSION NEEDS A FIXTURE (the sprint's headline safety rule). A provider
|
|
32
|
+
* profile may declare that some request field does not affect cache identity —
|
|
33
|
+
* but an UNPROVEN exclusion is how you get silent cache poisoning: the renderer
|
|
34
|
+
* folds a field the provider actually keys on, two different requests collapse to
|
|
35
|
+
* one cache entry, and one conversation is served another's prefix. So an
|
|
36
|
+
* exclusion without a `fixtureId` (or with a blank one) is REJECTED here —
|
|
37
|
+
* `ECON_EXCLUSION_UNPROVEN` — rather than trusted. There is no override.
|
|
38
|
+
*
|
|
39
|
+
* ESTIMATE VS MEASURED. Every result is labeled. Only telemetry from a
|
|
40
|
+
* RANDOMIZED live assignment may be called `measured` and fed into a causal
|
|
41
|
+
* interval; shadow or non-randomized numbers are `estimate` and are excluded
|
|
42
|
+
* from causal aggregates (see `experiments.ts`). Mixing the two would let a
|
|
43
|
+
* self-selected population masquerade as a controlled experiment.
|
|
44
|
+
*
|
|
45
|
+
* PURE. No clock, no storage, no console, no network (PREVENT-PI-004 /
|
|
46
|
+
* PREVENT-011). Runs identically with `MEGACOMPACT_VC7B` on or off — the flag
|
|
47
|
+
* gates only the reporter/dashboard seam in `../cache/economics-emit.ts`.
|
|
48
|
+
*/
|
|
49
|
+
/** A non-negative safe integer — the only shape money and counts may take. */
|
|
50
|
+
function isCount(n) {
|
|
51
|
+
return Number.isSafeInteger(n) && n >= 0;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate a profile's economics, enforcing the exclusion-proof rule.
|
|
55
|
+
*
|
|
56
|
+
* An exclusion claims "this field cannot affect provider cache identity". That
|
|
57
|
+
* claim is only as good as the fixture that proves it, so a profile carrying
|
|
58
|
+
* exclusions MUST name a `exclusionFixtureId`, and every individual exclusion
|
|
59
|
+
* must carry its own `fixtureId` too (VC5B already models that field; VC7B makes
|
|
60
|
+
* a blank one fatal instead of decorative). Returns deduplicated codes in a
|
|
61
|
+
* deterministic order.
|
|
62
|
+
*/
|
|
63
|
+
export function validateEconomics(econ, exclusions) {
|
|
64
|
+
const codes = new Set();
|
|
65
|
+
for (const n of [econ.basePrice, econ.readPrice, econ.writePrice, econ.ttlMs, econ.minPrefix]) {
|
|
66
|
+
if (!isCount(n))
|
|
67
|
+
codes.add("ECON_PRICE_INVALID");
|
|
68
|
+
}
|
|
69
|
+
// The headline rule: exclusions without a proving fixture are never trusted.
|
|
70
|
+
if (exclusions.length > 0) {
|
|
71
|
+
const id = econ.exclusionFixtureId;
|
|
72
|
+
if (id === null || id.trim() === "")
|
|
73
|
+
codes.add("ECON_EXCLUSION_UNPROVEN");
|
|
74
|
+
for (const ex of exclusions) {
|
|
75
|
+
if (ex.fixtureId.trim() === "")
|
|
76
|
+
codes.add("ECON_EXCLUSION_UNPROVEN");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const order = ["ECON_EXCLUSION_UNPROVEN", "ECON_PRICE_INVALID"];
|
|
80
|
+
return order.filter((c) => codes.has(c));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate a profile + its economics together. Convenience over
|
|
84
|
+
* `validateEconomics` for callers holding a whole `ProviderProfileV1`: the
|
|
85
|
+
* exclusion list is read from the profile itself, so the two can never disagree.
|
|
86
|
+
*/
|
|
87
|
+
export function validateProfileEconomics(profile, econ) {
|
|
88
|
+
return validateEconomics(econ, profile.excludedJsonPointers);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Hits required for ONE write to break even.
|
|
92
|
+
*
|
|
93
|
+
* One write costs `(writePrice - basePrice)` extra per token; each subsequent
|
|
94
|
+
* hit saves `(basePrice - readPrice)` per token. So break-even is the ceiling of
|
|
95
|
+
* premium/discount. Returns `null` when the cache can NEVER pay for itself
|
|
96
|
+
* (a read that costs at least as much as an uncached token) — reporting a huge
|
|
97
|
+
* number there would imply "just get more hits", which is false.
|
|
98
|
+
* Returns 0 when writing is already free or cheaper than not caching.
|
|
99
|
+
*/
|
|
100
|
+
export function breakEvenHits(econ) {
|
|
101
|
+
const premium = econ.writePrice - econ.basePrice;
|
|
102
|
+
const discount = econ.basePrice - econ.readPrice;
|
|
103
|
+
if (premium <= 0)
|
|
104
|
+
return 0;
|
|
105
|
+
if (discount <= 0)
|
|
106
|
+
return null;
|
|
107
|
+
return Math.ceil(premium / discount);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Compute net cache savings for observed (or shadow) usage.
|
|
111
|
+
*
|
|
112
|
+
* Pure integer arithmetic in micro-units. The only float is `savingsRatio`, and
|
|
113
|
+
* it is zero-guarded so a zero baseline yields 0 rather than NaN/Infinity — the
|
|
114
|
+
* sprint invariant is that every reported aggregate is FINITE.
|
|
115
|
+
*
|
|
116
|
+
* `evidence` is supplied by the caller and simply carried through: this function
|
|
117
|
+
* cannot know whether its inputs came from a randomized arm, and guessing would
|
|
118
|
+
* be exactly the mislabeling the causal rules forbid.
|
|
119
|
+
*/
|
|
120
|
+
export function computeEconomics(econ, usage, evidence) {
|
|
121
|
+
const codes = [];
|
|
122
|
+
for (const n of [econ.basePrice, econ.readPrice, econ.writePrice, econ.ttlMs, econ.minPrefix]) {
|
|
123
|
+
if (!isCount(n)) {
|
|
124
|
+
codes.push("ECON_PRICE_INVALID");
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const n of [usage.cachedTokens, usage.writeCount, usage.hitCount]) {
|
|
129
|
+
if (!isCount(n)) {
|
|
130
|
+
codes.push("ECON_USAGE_INVALID");
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (codes.length > 0)
|
|
135
|
+
return { ok: false, codes };
|
|
136
|
+
const { cachedTokens, writeCount, hitCount } = usage;
|
|
137
|
+
const baselineCost = cachedTokens * econ.basePrice * (writeCount + hitCount);
|
|
138
|
+
const actualCost = cachedTokens * (econ.writePrice * writeCount + econ.readPrice * hitCount);
|
|
139
|
+
// Exactness is the whole point of the integer model: if any product leaves the
|
|
140
|
+
// safe-integer range the result is silently wrong, so we fail instead.
|
|
141
|
+
if (!Number.isSafeInteger(baselineCost) || !Number.isSafeInteger(actualCost)) {
|
|
142
|
+
return { ok: false, codes: ["ECON_OVERFLOW"] };
|
|
143
|
+
}
|
|
144
|
+
const netSavings = baselineCost - actualCost;
|
|
145
|
+
// Tokens served at the discounted read price — the cache's token-level benefit,
|
|
146
|
+
// independent of price (a hit re-reads the whole prefix).
|
|
147
|
+
const tokenSavings = cachedTokens * hitCount;
|
|
148
|
+
const savingsRatio = baselineCost === 0 ? 0 : netSavings / baselineCost;
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
result: {
|
|
152
|
+
profileId: econ.profileId,
|
|
153
|
+
baselineCost,
|
|
154
|
+
actualCost,
|
|
155
|
+
netSavings,
|
|
156
|
+
tokenSavings,
|
|
157
|
+
savingsRatio,
|
|
158
|
+
breakEvenHits: breakEvenHits(econ),
|
|
159
|
+
evidence,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Whether a prefix is eligible to be cached at all: it must meet the profile's
|
|
165
|
+
* minimum prefix, and it must still be within TTL. A prefix below `minPrefix` is
|
|
166
|
+
* not "a small win", it is not cacheable by the provider at all.
|
|
167
|
+
*/
|
|
168
|
+
export function isCacheEligible(econ, prefixTokens, ageMs) {
|
|
169
|
+
if (!isCount(prefixTokens) || !isCount(ageMs))
|
|
170
|
+
return false;
|
|
171
|
+
return prefixTokens >= econ.minPrefix && ageMs < econ.ttlMs;
|
|
172
|
+
}
|
|
173
|
+
// Conformance ID ranges + named rows extracted to economics-ids.ts to keep this
|
|
174
|
+
// file under the 300-line soft limit (soft-as-hard gate). Re-exported here so no
|
|
175
|
+
// consumer import path changes.
|
|
176
|
+
export { CACHE_IDS, ECONOMICS_PROVIDER_IDS, ECONOMICS_NAMED_IDS, } from "./economics-ids.js";
|