mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
fix(sse): preserve original body for semantic cache signature — fixes 0% hit rate (#9775)
The semantic cache signature (generateSignature) was computed over different bodies at read-time vs write-time in handleChatCore. The cache read at Phase 9.1 uses the original body, but the writes at Phase 9.1 (non-streaming) and Phase 9.2 (streaming) used the body after sanitizeChatRequestBody() and injectMemoryAndSkills() mutated messages. Since the digest includes messages, every request stored under a key no later request would look up — 0% hit rate, every request billed. Fix: snapshot bodyForCacheWrite right after the cache read and use it for both write paths, so the write-time signature equals the read-time one. TDD: tests/unit/cache-signature-roundtrip.test.ts proves the mutated body produces a different signature (bug) and the preserved snapshot produces an identical one (fix). Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
5e02cf7623
commit
0481246bf4
@@ -1037,6 +1037,13 @@ export async function handleChatCore({
|
||||
|
||||
log?.debug?.("FORMAT", `${sourceFormat} → ${targetFormat} | stream=${stream}`);
|
||||
|
||||
// Preserve original body for cache signature — the body variable is mutated
|
||||
// multiple times below (sanitization, memory/skills injection) before the
|
||||
// cache store path runs at Phase 9.1 (non-streaming) / Phase 9.2 (streaming).
|
||||
// Without this snapshot, the write-time signature differs from the read-time
|
||||
// one, producing 0% hit rate. (#cache-signature-asymmetry)
|
||||
const bodyForCacheWrite = body;
|
||||
|
||||
// ── Phase 9.1: Semantic cache check (temp=0, any streaming mode) ──
|
||||
const cacheHit = await checkSemanticCache({
|
||||
semanticCacheEnabled,
|
||||
@@ -4568,7 +4575,7 @@ export async function handleChatCore({
|
||||
// ── Phase 9.1: Cache store (non-streaming, temp=0) ──
|
||||
storeSemanticCacheResponse({
|
||||
enabled: semanticCacheEnabled,
|
||||
body,
|
||||
body: bodyForCacheWrite,
|
||||
headers: clientRawRequest?.headers,
|
||||
translatedResponse,
|
||||
model,
|
||||
@@ -4922,7 +4929,7 @@ export async function handleChatCore({
|
||||
enabled: semanticCacheEnabled,
|
||||
streamStatus,
|
||||
streamResponseBody,
|
||||
body,
|
||||
body: bodyForCacheWrite,
|
||||
headers: clientRawRequest?.headers,
|
||||
model,
|
||||
apiKeyId: apiKeyInfo?.id ?? undefined,
|
||||
|
||||
100
tests/unit/cache-signature-roundtrip.test.ts
Normal file
100
tests/unit/cache-signature-roundtrip.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// TDD regression guard: semantic cache read-time vs write-time signature must match.
|
||||
// Without bodyForCacheWrite, the body variable is mutated between the cache read
|
||||
// (Phase 9.1, chatCore.ts:1055) and the cache write (Phase 9.1 non-streaming:4540 /
|
||||
// Phase 9.2 streaming:4896) by sanitizeChatRequestBody() and injectMemoryAndSkills().
|
||||
// The digest includes messages, so mutations produce a different key → 0% hit rate.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { generateSignature } = await import("../../src/lib/semanticCache.ts");
|
||||
|
||||
const BASE_BODY = {
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: "What is the capital of France?" }],
|
||||
temperature: 0,
|
||||
top_p: 1,
|
||||
};
|
||||
|
||||
// Mutations that handleChatCore applies between cache read and cache write.
|
||||
function simulateSanitizeChatRequestBody(body: Record<string, unknown>): Record<string, unknown> {
|
||||
return { ...body, _sanitized: true };
|
||||
}
|
||||
|
||||
function simulateInjectMemoryAndSkills(body: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
...body,
|
||||
messages: [
|
||||
{ role: "system", content: "[Memory: user likes programming]" },
|
||||
...(body.messages as Array<unknown>),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test("cache signature must be identical before and after body mutations", async () => {
|
||||
// Compute signature at read-time (original body, line 1055).
|
||||
const readSignature = await generateSignature(
|
||||
BASE_BODY.model,
|
||||
BASE_BODY.messages,
|
||||
BASE_BODY.temperature,
|
||||
BASE_BODY.top_p
|
||||
);
|
||||
|
||||
// Simulate the mutations that happen between cache read and cache write.
|
||||
let mutatedBody = simulateSanitizeChatRequestBody(BASE_BODY);
|
||||
mutatedBody = simulateInjectMemoryAndSkills(mutatedBody);
|
||||
|
||||
// Compute signature at write-time using the mutated body (the bug).
|
||||
const writeSignatureMutated = await generateSignature(
|
||||
mutatedBody.model,
|
||||
mutatedBody.messages,
|
||||
mutatedBody.temperature,
|
||||
mutatedBody.top_p
|
||||
);
|
||||
|
||||
// Compute signature at write-time using the preserved snapshot (the fix).
|
||||
const writeSignaturePreserved = await generateSignature(
|
||||
BASE_BODY.model,
|
||||
BASE_BODY.messages,
|
||||
BASE_BODY.temperature,
|
||||
BASE_BODY.top_p
|
||||
);
|
||||
|
||||
// The mutated signature MUST differ — this is the bug.
|
||||
assert.notStrictEqual(
|
||||
readSignature,
|
||||
writeSignatureMutated,
|
||||
"BUG: mutated body produces different signature — cache has 0% hit rate"
|
||||
);
|
||||
|
||||
// The preserved snapshot MUST match — this is the fix.
|
||||
assert.strictEqual(
|
||||
readSignature,
|
||||
writeSignaturePreserved,
|
||||
"FIX: preserved snapshot must produce the same signature as read-time"
|
||||
);
|
||||
});
|
||||
|
||||
test("cache signature with memory injection changes messages", async () => {
|
||||
const bodyWithMemory = {
|
||||
...BASE_BODY,
|
||||
messages: [
|
||||
{ role: "system", content: "[Memory: context from previous conversation]" },
|
||||
...BASE_BODY.messages,
|
||||
],
|
||||
};
|
||||
|
||||
const sigOriginal = await generateSignature(
|
||||
BASE_BODY.model,
|
||||
BASE_BODY.messages,
|
||||
BASE_BODY.temperature,
|
||||
BASE_BODY.top_p
|
||||
);
|
||||
const sigWithMemory = await generateSignature(
|
||||
bodyWithMemory.model,
|
||||
bodyWithMemory.messages,
|
||||
bodyWithMemory.temperature,
|
||||
bodyWithMemory.top_p
|
||||
);
|
||||
|
||||
assert.notStrictEqual(sigOriginal, sigWithMemory);
|
||||
});
|
||||
Reference in New Issue
Block a user