mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
roadmap #21: result memoization for deterministic engines (opt-in, default off). Fixed the cache-key under-specification (folded model+supportsVision so lite image-strip can't serve a wrong cached body across vision/non-vision targets) + regression test. Locally validated release-green. Integrated into release/v3.8.40.
This commit is contained in:
committed by
GitHub
parent
a71157a1b8
commit
43da1bb98c
@@ -10,6 +10,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(compression): result memoization for deterministic engines (opt-in)** — caches `(input, config) → result` for provably pure, stateless modes (`lite`/`standard`/`rtk` and stacked pipelines of `{lite,caveman,rtk}`) to skip recompute on the hot path. Opt-in via `memoizeCompressionResults` (default off → zero behavior change). Conservative opt-in whitelist (stateful `ccr`/`session-dedup` — which write the cross-request CCR store — and model-backed `ultra`/`aggressive`/`llmlingua` are never cached), principal-scoped (skipped without a principal, so no cross-principal body leak), and clone-on-store + clone-on-read. Tier-3 item of the compression feature-extraction roadmap (#21).
|
||||
- **feat(compression): inline transparency annotation** — surfaces `tokens=847→312; rules: filler×8, dedup×2` derived from existing compression stats. The `X-OmniRoute-Compression` response header is extended **append-only** (the `mode; source=X` prefix stays byte-identical, so existing header parsers don't break) and the compression studio cockpit shows a matching badge. Zero new computation — it aggregates the `rulesApplied`/`techniquesUsed` already on the stats. Tier-3 item of the compression feature-extraction roadmap (#18).
|
||||
- **feat(compression): saliency heatmap in the compression studio** — the preview studio can now color each token by saliency: `ultra` per-token `scoreToken` (0–1, green→red gradient) or universal kept/removed from the existing diff. A dry-run visualization behind a toggle (no cost on a normal preview; backward-compatible when off). Completes the visualization half of roadmap item #13 (the A/B comparison shipped in [#5080](https://github.com/diegosouzapw/OmniRoute/pull/5080)).
|
||||
- **feat(compression): composite-command splitter for RTK detection** — `cd /x && git status` now detects as `git-status` (previously the whole string was treated as one command and matched no filter). A quote-aware top-level tokenizer splits on `&&`/`||`/`;` (never inside quotes or `$(…)`/backtick subshells) and feeds the **last** segment to RTK command detection, so every RTK filter/renderer fires on commands wrapped in `cd … &&`/`||`/`;` chains. O(n), no RegExp over the command (ReDoS-safe). Tier-3 item of the compression feature-extraction roadmap (#16).
|
||||
|
||||
@@ -162,7 +162,8 @@
|
||||
"open-sse/services/combo.ts": 3368,
|
||||
"_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).",
|
||||
"_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (<cap); the residual growth is the duplicated thin-wrapper signatures + the extracted bodies' dispatch boundary, guarded by a byte-identical parity test (riskGateIntegration). Default off (DEFAULT_COMPRESSION_CONFIG unchanged). Not extractable without hiding the dispatch boundary, mirroring prior compression rebaselines. Structural shrink tracked in #3501.",
|
||||
"open-sse/services/compression/strategySelector.ts": 899,
|
||||
"_rebaseline_2026_06_29_5286_memoization": "PR #5286 own growth: strategySelector.ts 899->960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (<cap); the chokepoint wiring here is not extractable. Structural shrink of this hot-path file tracked in #3501.",
|
||||
"open-sse/services/compression/strategySelector.ts": 960,
|
||||
"open-sse/services/rateLimitManager.ts": 1035,
|
||||
"open-sse/services/tokenRefresh.ts": 2103,
|
||||
"open-sse/services/usage.ts": 3454,
|
||||
|
||||
83
open-sse/services/compression/resultMemo.ts
Normal file
83
open-sse/services/compression/resultMemo.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { CompressionConfig, CompressionMode, CompressionResult } from "./types.ts";
|
||||
|
||||
export const MEMO_CAP = 5_000;
|
||||
|
||||
const memoMap = new Map<string, CompressionResult>();
|
||||
|
||||
// Opt-IN whitelist (NOT opt-out): cache only engines proven pure + STATELESS across
|
||||
// requests. Excluded on purpose: `ccr` and `session-dedup` write to the cross-request
|
||||
// CCR store (`ccr/index.ts` ccrStore; session-dedup imports storeBlock), so their output
|
||||
// depends on prior state → not safe to memoize; `ultra`/`aggressive`/`llmlingua` are
|
||||
// model-backed/non-deterministic. Any NEW engine is excluded until explicitly vetted.
|
||||
const DETERMINISTIC_ENGINES = new Set(["lite", "caveman", "rtk"]);
|
||||
|
||||
/** Top-level modes safe to cache (whitelist — any unknown/new mode defaults to false). */
|
||||
const DETERMINISTIC_MODES = new Set<CompressionMode>(["lite", "standard", "rtk"]);
|
||||
|
||||
export function isDeterministicMode(mode: CompressionMode, config?: CompressionConfig): boolean {
|
||||
if (mode === "stacked") {
|
||||
const pipeline = config?.stackedPipeline;
|
||||
if (!pipeline || pipeline.length === 0) return false;
|
||||
return pipeline.every((step) => DETERMINISTIC_ENGINES.has(step.engine));
|
||||
}
|
||||
return DETERMINISTIC_MODES.has(mode);
|
||||
}
|
||||
|
||||
function sha256hex(text: string): string {
|
||||
return crypto.createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
export function makeMemoKey(
|
||||
body: Record<string, unknown>,
|
||||
mode: CompressionMode,
|
||||
config: CompressionConfig,
|
||||
principalId?: string,
|
||||
model?: string,
|
||||
supportsVision?: boolean | null
|
||||
): string {
|
||||
const bodyHash = sha256hex(JSON.stringify(body));
|
||||
// model + supportsVision MUST be part of the key: the `lite` engine strips data:image
|
||||
// URLs only when vision is unsupported (replaceImageUrls / modelSupportsVision), so the
|
||||
// same (body, config) yields a DIFFERENT result per target — omitting them returns a
|
||||
// wrong (image-stripped or image-kept) cached body across vision/non-vision targets.
|
||||
return sha256hex(
|
||||
JSON.stringify({
|
||||
bodyHash,
|
||||
mode,
|
||||
config,
|
||||
principalId: principalId ?? null,
|
||||
model: model ?? null,
|
||||
supportsVision: supportsVision ?? null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function boundedSet(key: string, value: CompressionResult): void {
|
||||
if (!memoMap.has(key) && memoMap.size >= MEMO_CAP) {
|
||||
const firstKey = memoMap.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
memoMap.delete(firstKey);
|
||||
}
|
||||
}
|
||||
memoMap.set(key, value);
|
||||
}
|
||||
|
||||
export function memoLookup(key: string): CompressionResult | null {
|
||||
const hit = memoMap.get(key);
|
||||
if (!hit) return null;
|
||||
// Return a clone so downstream mutation cannot corrupt the cached value.
|
||||
return JSON.parse(JSON.stringify(hit)) as CompressionResult;
|
||||
}
|
||||
|
||||
export function memoStore(key: string, result: CompressionResult): void {
|
||||
// Clone on STORE too (memoLookup already clones on read). Storing the caller's live
|
||||
// object would let a later mutation of it (e.g. an async engine holding a sub-ref)
|
||||
// corrupt the cached entry. Both ends isolated ⇒ the cache is immutable once stored.
|
||||
boundedSet(key, JSON.parse(JSON.stringify(result)) as CompressionResult);
|
||||
}
|
||||
|
||||
/** For tests only — clears the in-process memo store. */
|
||||
export function clearMemoStore(): void {
|
||||
memoMap.clear();
|
||||
}
|
||||
@@ -40,10 +40,16 @@ import {
|
||||
withCompressionEntrypointGuards,
|
||||
withCompressionEntrypointGuardsAsync,
|
||||
} from "./entrypointWrap.ts";
|
||||
import { makeMemoKey, memoLookup, memoStore, isDeterministicMode } from "./resultMemo.ts";
|
||||
export { resolveCacheAwareConfig } from "./cacheAwareConfig.ts";
|
||||
|
||||
// Re-export so existing importers (resolver test + chatCore dynamic import) keep resolving.
|
||||
export { planFromHeader, formatCompressionMeta, formatCompressionAnnotation, buildNamedComboLookup };
|
||||
export {
|
||||
planFromHeader,
|
||||
formatCompressionMeta,
|
||||
formatCompressionAnnotation,
|
||||
buildNamedComboLookup,
|
||||
};
|
||||
|
||||
/** Named-combo map: combo id → its stacked pipeline (operator-defined profiles). */
|
||||
type NamedCombos = Record<string, CompressionPipelineStep[]>;
|
||||
@@ -267,6 +273,32 @@ function runCompression(
|
||||
if (mode === "off") {
|
||||
return { body, compressed: false, stats: null };
|
||||
}
|
||||
if (
|
||||
options?.config?.memoizeCompressionResults === true &&
|
||||
// Only memoize for an explicit principal — a missing principalId would collapse
|
||||
// authenticated callers into the shared anonymous (null) key space and let one
|
||||
// principal receive another's cached body. No principal ⇒ skip the cache.
|
||||
typeof options?.principalId === "string" &&
|
||||
options.principalId.length > 0 &&
|
||||
isDeterministicMode(mode, options.config)
|
||||
) {
|
||||
const key = makeMemoKey(
|
||||
body,
|
||||
mode,
|
||||
options.config,
|
||||
options.principalId,
|
||||
options.model,
|
||||
options.supportsVision
|
||||
);
|
||||
const hit = memoLookup(key);
|
||||
if (hit) return hit;
|
||||
const result = runCompression({ ...body }, mode, {
|
||||
...options,
|
||||
config: { ...options.config, memoizeCompressionResults: false },
|
||||
});
|
||||
memoStore(key, result);
|
||||
return memoLookup(key)!;
|
||||
}
|
||||
if (mode === "rtk") {
|
||||
return applyRtkCompression(body, {
|
||||
// Selecting the "rtk" mode IS the enable signal — run it even if the per-engine
|
||||
@@ -416,6 +448,32 @@ async function runCompressionAsync(
|
||||
cachingContext?: CachingDetectionContext;
|
||||
}
|
||||
): Promise<CompressionResult> {
|
||||
if (
|
||||
options?.config?.memoizeCompressionResults === true &&
|
||||
// Only memoize for an explicit principal — a missing principalId would collapse
|
||||
// authenticated callers into the shared anonymous (null) key space and let one
|
||||
// principal receive another's cached body. No principal ⇒ skip the cache.
|
||||
typeof options?.principalId === "string" &&
|
||||
options.principalId.length > 0 &&
|
||||
isDeterministicMode(mode, options.config)
|
||||
) {
|
||||
const key = makeMemoKey(
|
||||
body,
|
||||
mode,
|
||||
options.config,
|
||||
options.principalId,
|
||||
options.model,
|
||||
options.supportsVision
|
||||
);
|
||||
const hit = memoLookup(key);
|
||||
if (hit) return hit;
|
||||
const result = await runCompressionAsync({ ...body }, mode, {
|
||||
...options,
|
||||
config: { ...options.config, memoizeCompressionResults: false },
|
||||
});
|
||||
memoStore(key, result);
|
||||
return memoLookup(key)!;
|
||||
}
|
||||
if (mode === "stacked") {
|
||||
const adapter = adaptBodyForCompression(body);
|
||||
const result = await applyStackedCompressionAsync(
|
||||
@@ -783,7 +841,10 @@ function runStackedCompression(
|
||||
continue;
|
||||
}
|
||||
mergeStackStep(acc, step.engine, result);
|
||||
if (decideStep(result, bailout).advance && gateAdvance(result, currentBody, fidelityGate, acc, step.engine)) {
|
||||
if (
|
||||
decideStep(result, bailout).advance &&
|
||||
gateAdvance(result, currentBody, fidelityGate, acc, step.engine)
|
||||
) {
|
||||
currentBody = result.body;
|
||||
compressed = true;
|
||||
}
|
||||
@@ -867,7 +928,10 @@ async function runStackedCompressionAsync(
|
||||
continue;
|
||||
}
|
||||
mergeStackStep(acc, step.engine, result);
|
||||
if (decideStep(result, bailout).advance && gateAdvance(result, currentBody, fidelityGate, acc, step.engine)) {
|
||||
if (
|
||||
decideStep(result, bailout).advance &&
|
||||
gateAdvance(result, currentBody, fidelityGate, acc, step.engine)
|
||||
) {
|
||||
currentBody = result.body;
|
||||
compressed = true;
|
||||
}
|
||||
|
||||
@@ -195,6 +195,8 @@ export interface CompressionConfig {
|
||||
* swallowed; the lazy first-call path still applies. Default false.
|
||||
*/
|
||||
ultraSlmPrewarm?: boolean;
|
||||
/** Opt-in result memoization for deterministic engines only (default off). */
|
||||
memoizeCompressionResults?: boolean;
|
||||
}
|
||||
|
||||
export interface CompressionStats {
|
||||
|
||||
343
tests/unit/compression/result-memo.test.ts
Normal file
343
tests/unit/compression/result-memo.test.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
import { describe, it, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
memoLookup,
|
||||
memoStore,
|
||||
makeMemoKey,
|
||||
isDeterministicMode,
|
||||
clearMemoStore,
|
||||
MEMO_CAP,
|
||||
} from "../../../open-sse/services/compression/resultMemo.ts";
|
||||
import type { CompressionResult } from "../../../open-sse/services/compression/types.ts";
|
||||
import { DEFAULT_COMPRESSION_CONFIG } from "../../../open-sse/services/compression/types.ts";
|
||||
import { applyCompression } from "../../../open-sse/services/compression/strategySelector.ts";
|
||||
|
||||
const baseBody = {
|
||||
messages: [{ role: "user", content: "hello world compress me please" }],
|
||||
model: "gpt-4",
|
||||
};
|
||||
|
||||
const memoConfig = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
defaultMode: "lite" as const,
|
||||
memoizeCompressionResults: true,
|
||||
};
|
||||
|
||||
const noMemoConfig = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
defaultMode: "lite" as const,
|
||||
memoizeCompressionResults: false,
|
||||
};
|
||||
|
||||
describe("resultMemo unit", () => {
|
||||
beforeEach(() => {
|
||||
clearMemoStore();
|
||||
});
|
||||
|
||||
it("memoLookup returns null on miss", () => {
|
||||
const key = makeMemoKey(baseBody, "lite", noMemoConfig, "p1");
|
||||
assert.equal(memoLookup(key), null);
|
||||
});
|
||||
|
||||
it("memoStore then memoLookup returns the stored result", () => {
|
||||
const key = makeMemoKey(baseBody, "lite", memoConfig, "p1");
|
||||
const result: CompressionResult = {
|
||||
body: { ...baseBody, _compressed: true },
|
||||
compressed: true,
|
||||
stats: {
|
||||
originalTokens: 10,
|
||||
compressedTokens: 8,
|
||||
savingsPercent: 20,
|
||||
techniquesUsed: ["lite"],
|
||||
mode: "lite",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
memoStore(key, result);
|
||||
const hit = memoLookup(key);
|
||||
assert.notEqual(hit, null);
|
||||
assert.equal(hit!.compressed, true);
|
||||
});
|
||||
|
||||
it("different principalId produces a different key (MISS)", () => {
|
||||
const key1 = makeMemoKey(baseBody, "lite", memoConfig, "principal-A");
|
||||
const key2 = makeMemoKey(baseBody, "lite", memoConfig, "principal-B");
|
||||
assert.notEqual(key1, key2);
|
||||
|
||||
const result: CompressionResult = {
|
||||
body: { ...baseBody },
|
||||
compressed: false,
|
||||
stats: null,
|
||||
};
|
||||
memoStore(key1, result);
|
||||
assert.equal(memoLookup(key2), null);
|
||||
});
|
||||
|
||||
it("cached body is a COPY — mutating returned body does not corrupt next hit", () => {
|
||||
const key = makeMemoKey(baseBody, "lite", memoConfig, "p1");
|
||||
const result: CompressionResult = {
|
||||
body: { messages: [{ role: "user", content: "original" }] },
|
||||
compressed: true,
|
||||
stats: null,
|
||||
};
|
||||
memoStore(key, result);
|
||||
|
||||
const hit1 = memoLookup(key);
|
||||
assert.notEqual(hit1, null);
|
||||
// Mutate the returned body
|
||||
(hit1!.body as Record<string, unknown>)["injected"] = "evil";
|
||||
|
||||
// Second lookup should not see the mutation
|
||||
const hit2 = memoLookup(key);
|
||||
assert.notEqual(hit2, null);
|
||||
assert.equal((hit2!.body as Record<string, unknown>)["injected"], undefined);
|
||||
});
|
||||
|
||||
it("FIFO eviction: after MEMO_CAP entries, oldest is evicted", () => {
|
||||
const firstKey = makeMemoKey({ x: 0 }, "lite", memoConfig, "evict-test");
|
||||
const stub: CompressionResult = { body: {}, compressed: false, stats: null };
|
||||
memoStore(firstKey, stub);
|
||||
|
||||
// Fill up to and beyond cap
|
||||
for (let i = 1; i <= MEMO_CAP; i++) {
|
||||
const k = makeMemoKey({ x: i }, "lite", memoConfig, "evict-test");
|
||||
memoStore(k, stub);
|
||||
}
|
||||
|
||||
// The first entry should have been evicted
|
||||
assert.equal(memoLookup(firstKey), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDeterministicMode", () => {
|
||||
it("lite is deterministic", () => {
|
||||
assert.equal(isDeterministicMode("lite", DEFAULT_COMPRESSION_CONFIG), true);
|
||||
});
|
||||
|
||||
it("standard is deterministic", () => {
|
||||
assert.equal(isDeterministicMode("standard", DEFAULT_COMPRESSION_CONFIG), true);
|
||||
});
|
||||
|
||||
it("rtk is deterministic", () => {
|
||||
assert.equal(isDeterministicMode("rtk", DEFAULT_COMPRESSION_CONFIG), true);
|
||||
});
|
||||
|
||||
it("off is NOT deterministic (nothing to cache)", () => {
|
||||
assert.equal(isDeterministicMode("off", DEFAULT_COMPRESSION_CONFIG), false);
|
||||
});
|
||||
|
||||
it("aggressive is NOT deterministic (pluggable summarizer)", () => {
|
||||
assert.equal(isDeterministicMode("aggressive", DEFAULT_COMPRESSION_CONFIG), false);
|
||||
});
|
||||
|
||||
it("ultra is NOT deterministic (SLM tier)", () => {
|
||||
assert.equal(isDeterministicMode("ultra", DEFAULT_COMPRESSION_CONFIG), false);
|
||||
});
|
||||
|
||||
it("stacked with only deterministic engines IS deterministic", () => {
|
||||
const cfg = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
stackedPipeline: [
|
||||
{ engine: "rtk" as const, intensity: "standard" as const },
|
||||
{ engine: "caveman" as const, intensity: "full" as const },
|
||||
],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfg), true);
|
||||
});
|
||||
|
||||
it("stacked with ultra engine is NOT deterministic", () => {
|
||||
const cfg = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
stackedPipeline: [{ engine: "ultra" as const }, { engine: "caveman" as const }],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfg), false);
|
||||
});
|
||||
|
||||
it("stacked with aggressive engine is NOT deterministic", () => {
|
||||
const cfg = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
stackedPipeline: [{ engine: "aggressive" as const }, { engine: "rtk" as const }],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfg), false);
|
||||
});
|
||||
|
||||
it("stacked with llmlingua engine is NOT deterministic", () => {
|
||||
const cfg = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
stackedPipeline: [{ engine: "llmlingua" as const }],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfg), false);
|
||||
});
|
||||
|
||||
// Stateful engines write to the cross-request CCR store (storeBlock): caching their
|
||||
// output would skip the side-effect on a HIT, leaving CCR markers pointing at blocks
|
||||
// that were never stored → broken `retrieve`. These MUST stay excluded from the memo.
|
||||
it("stacked with ccr engine is NOT deterministic (writes cross-request CCR store)", () => {
|
||||
const cfg = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
stackedPipeline: [{ engine: "ccr" as const }, { engine: "caveman" as const }],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfg), false);
|
||||
});
|
||||
|
||||
it("stacked with session-dedup engine is NOT deterministic (storeBlock side-effect)", () => {
|
||||
const cfg = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
stackedPipeline: [{ engine: "session-dedup" as const }, { engine: "rtk" as const }],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfg), false);
|
||||
});
|
||||
|
||||
it("stacked with ionizer engine is NOT deterministic (storeBlock side-effect)", () => {
|
||||
const cfg = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
stackedPipeline: [{ engine: "ionizer" as const }, { engine: "lite" as const }],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfg), false);
|
||||
});
|
||||
|
||||
it("stacked with headroom engine is NOT deterministic (excluded until vetted)", () => {
|
||||
const cfg = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
stackedPipeline: [{ engine: "headroom" as const }, { engine: "lite" as const }],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfg), false);
|
||||
});
|
||||
|
||||
it("stacked with empty/undefined pipeline is NOT deterministic (safe default)", () => {
|
||||
const cfg = { ...DEFAULT_COMPRESSION_CONFIG, stackedPipeline: undefined };
|
||||
assert.equal(isDeterministicMode("stacked", cfg), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyCompression with memoization", () => {
|
||||
beforeEach(() => {
|
||||
clearMemoStore();
|
||||
});
|
||||
|
||||
it("flag OFF: two identical calls both compute (no caching path)", () => {
|
||||
let callCount = 0;
|
||||
// We can't easily spy on internal engine, so we verify via deterministic output
|
||||
// equality between independent calls (proving cache isn't interfering).
|
||||
// Use a body that will be lightly compressed.
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "The quick brown fox jumps over the lazy dog. ".repeat(20) },
|
||||
],
|
||||
model: "gpt-4",
|
||||
};
|
||||
|
||||
const r1 = applyCompression(body, "lite", { config: noMemoConfig, principalId: "u1" });
|
||||
const r2 = applyCompression(body, "lite", { config: noMemoConfig, principalId: "u1" });
|
||||
|
||||
// Both compute — results should be equal (deterministic) but not the same object reference
|
||||
assert.deepEqual(r1.stats?.mode, r2.stats?.mode);
|
||||
// The memo store should be empty (flag was OFF)
|
||||
const key = makeMemoKey(body, "lite", noMemoConfig, "u1");
|
||||
assert.equal(memoLookup(key), null);
|
||||
});
|
||||
|
||||
it("flag ON + deterministic mode: 2nd call hits memo (stats identical)", () => {
|
||||
const body = {
|
||||
messages: [{ role: "user", content: "Memoization test content. ".repeat(15) }],
|
||||
model: "gpt-4",
|
||||
};
|
||||
|
||||
const r1 = applyCompression(body, "lite", { config: memoConfig, principalId: "u1" });
|
||||
const r2 = applyCompression(body, "lite", { config: memoConfig, principalId: "u1" });
|
||||
|
||||
// Both should have same stats (2nd hit from cache)
|
||||
assert.deepEqual(r1.stats?.savingsPercent, r2.stats?.savingsPercent);
|
||||
assert.deepEqual(r1.stats?.originalTokens, r2.stats?.originalTokens);
|
||||
|
||||
// Verify cache was populated
|
||||
const key = makeMemoKey(body, "lite", memoConfig, "u1");
|
||||
assert.notEqual(memoLookup(key), null);
|
||||
});
|
||||
|
||||
it("flag ON + deterministic mode: different principalId = MISS", () => {
|
||||
const body = {
|
||||
messages: [{ role: "user", content: "Cross-principal test content. ".repeat(10) }],
|
||||
model: "gpt-4",
|
||||
};
|
||||
|
||||
applyCompression(body, "lite", { config: memoConfig, principalId: "principal-A" });
|
||||
// principal-B should NOT hit the cache for principal-A's result
|
||||
const keyB = makeMemoKey(body, "lite", memoConfig, "principal-B");
|
||||
assert.equal(memoLookup(keyB), null);
|
||||
});
|
||||
|
||||
it("flag ON + ultra mode: NOT cached", () => {
|
||||
const body = {
|
||||
messages: [{ role: "user", content: "Ultra mode should not be cached. ".repeat(10) }],
|
||||
model: "gpt-4",
|
||||
};
|
||||
|
||||
applyCompression(body, "ultra", { config: memoConfig, principalId: "u1" });
|
||||
const key = makeMemoKey(body, "ultra", memoConfig, "u1");
|
||||
assert.equal(memoLookup(key), null);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Core-review hardening regressions ──────────────────────────────────────
|
||||
describe("resultMemo — core review hardening", () => {
|
||||
beforeEach(() => clearMemoStore());
|
||||
|
||||
it("stacked pipeline with a stateful engine (ccr/session-dedup) is NOT deterministic", () => {
|
||||
// ccr + session-dedup write to the cross-request CCR store → output depends on prior
|
||||
// state → must never be cached, even though they are not model-backed.
|
||||
const cfgCcr = {
|
||||
...memoConfig,
|
||||
stackedPipeline: [{ engine: "rtk" as const }, { engine: "ccr" as const }],
|
||||
};
|
||||
const cfgDedup = { ...memoConfig, stackedPipeline: [{ engine: "session-dedup" as const }] };
|
||||
assert.equal(isDeterministicMode("stacked", cfgCcr), false);
|
||||
assert.equal(isDeterministicMode("stacked", cfgDedup), false);
|
||||
// the pure default pipeline [rtk, caveman] stays cacheable
|
||||
const cfgPure = {
|
||||
...memoConfig,
|
||||
stackedPipeline: [{ engine: "rtk" as const }, { engine: "caveman" as const }],
|
||||
};
|
||||
assert.equal(isDeterministicMode("stacked", cfgPure), true);
|
||||
// an unknown/new mode is NOT cached by default (opt-in whitelist)
|
||||
assert.equal(isDeterministicMode("totally-new-mode" as never, memoConfig), false);
|
||||
});
|
||||
|
||||
it("a missing principalId is never memoized (no anonymous↔authenticated key collision)", () => {
|
||||
const body = {
|
||||
messages: [{ role: "user", content: "no principal here please" }],
|
||||
model: "gpt-4",
|
||||
};
|
||||
applyCompression(body, "lite", { config: memoConfig }); // no principalId
|
||||
const key = makeMemoKey(body, "lite", memoConfig, undefined);
|
||||
assert.equal(memoLookup(key), null);
|
||||
});
|
||||
|
||||
it("mutating the result after store does not corrupt the cache (clone-on-store)", () => {
|
||||
const key = "mutate-after-store";
|
||||
const result: CompressionResult = {
|
||||
body: { messages: [{ role: "user", content: "original" }] },
|
||||
compressed: true,
|
||||
stats: null,
|
||||
};
|
||||
memoStore(key, result);
|
||||
// mutate the caller's object AFTER storing
|
||||
(result.body.messages as Array<{ content: string }>)[0].content = "TAMPERED";
|
||||
const got = memoLookup(key);
|
||||
assert.equal((got!.body.messages as Array<{ content: string }>)[0].content, "original");
|
||||
});
|
||||
|
||||
it("key folds in model + supportsVision (lite image-strip depends on vision capability)", () => {
|
||||
// Regression: lite strips data:image URLs only when vision is unsupported, so the same
|
||||
// (body, config, principal) yields a DIFFERENT result per target. The key MUST include
|
||||
// model + supportsVision, else a non-vision target's image-stripped body is served to a
|
||||
// vision-capable target (and vice-versa).
|
||||
const k = (model?: string, vision?: boolean | null) =>
|
||||
makeMemoKey(baseBody, "lite", memoConfig, "p1", model, vision);
|
||||
assert.notEqual(k("gpt-4", false), k("gpt-4", true), "supportsVision must change the key");
|
||||
assert.notEqual(k("gpt-4", true), k("gemini-2", true), "model must change the key");
|
||||
assert.equal(k("gpt-4", true), k("gpt-4", true), "same inputs => same key (deterministic)");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user