Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { checkIdempotencyCache } from "./chatCore/idempotency.ts";
import { checkSemanticCache } from "./chatCore/semanticCache.ts";
import { applyClientUsageBuffer } from "./chatCore/clientUsageBuffer.ts";
import { buildPostCallGuardrailContext } from "./chatCore/postCallGuardrailContext.ts";
import { storeSemanticCacheResponse } from "./chatCore/semanticCacheStore.ts";
import { sanitizeChatRequestBody } from "./chatCore/sanitization.ts";
import {
getHeaderValueCaseInsensitive,
Expand Down Expand Up @@ -3563,22 +3564,16 @@ export async function handleChatCore({
}

// ── Phase 9.1: Cache store (non-streaming, temp=0) ──
if (
semanticCacheEnabled &&
isCacheableForWrite(body, clientRawRequest?.headers) &&
isSmallEnoughForSemanticCache(translatedResponse)
) {
const signature = generateSignature(
model,
body.messages ?? body.input,
body.temperature,
body.top_p,
apiKeyInfo?.id ?? undefined
);
const tokensSaved = usage?.prompt_tokens + usage?.completion_tokens || 0;
setCachedResponse(signature, model, translatedResponse, tokensSaved);
log?.debug?.("CACHE", `Stored response for ${model} (${tokensSaved} tokens)`);
}
storeSemanticCacheResponse({
enabled: semanticCacheEnabled,
body,
headers: clientRawRequest?.headers,
translatedResponse,
model,
apiKeyId: apiKeyInfo?.id ?? undefined,
usage,
log,
});

// ── Phase 9.2: Save for idempotency ──
// Reuse the key resolved by checkIdempotencyCache() above (single derivation per
Expand Down
73 changes: 73 additions & 0 deletions open-sse/handlers/chatCore/semanticCacheStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* chatCore semantic-cache store (Quality Gate v2 / Fase 9 — chatCore god-file decomposition,
* #3501).
*
* Extracted from handleChatCore's non-streaming success path (Phase 9.1): when semantic caching is
* enabled and the request/response are cacheable, store the translated response under its signature
* so a later temp=0 request can be served from cache. Side-effect only (cache write + debug log);
* no early-return, no outer-variable reassignment. Behaviour is byte-identical to the previous
* inline block, including the `prompt + completion || 0` token-saved precedence.
*/
import {
generateSignature as defaultGenerateSignature,
setCachedResponse as defaultSetCachedResponse,
isCacheableForWrite as defaultIsCacheableForWrite,
} from "@/lib/semanticCache";
import { isSmallEnoughForSemanticCache as defaultIsSmallEnough } from "../../utils/estimateSize.ts";

type LoggerLike = { debug?: (...args: unknown[]) => void } | null | undefined;

type CacheBody = {
messages?: unknown;
input?: unknown;
temperature?: unknown;
top_p?: unknown;
};

type UsageLike = { prompt_tokens?: number; completion_tokens?: number } | null | undefined;

export interface SemanticCacheStoreDeps {
isCacheableForWrite: typeof defaultIsCacheableForWrite;
isSmallEnoughForSemanticCache: typeof defaultIsSmallEnough;
generateSignature: typeof defaultGenerateSignature;
setCachedResponse: typeof defaultSetCachedResponse;
}

const DEFAULT_DEPS: SemanticCacheStoreDeps = {
isCacheableForWrite: defaultIsCacheableForWrite,
isSmallEnoughForSemanticCache: defaultIsSmallEnough,
generateSignature: defaultGenerateSignature,
setCachedResponse: defaultSetCachedResponse,
};

export function storeSemanticCacheResponse(
args: {
enabled: boolean;
body: CacheBody;
headers: unknown;
translatedResponse: unknown;
model: string;
apiKeyId?: string | number;
usage?: UsageLike;
log?: LoggerLike;
},
deps: SemanticCacheStoreDeps = DEFAULT_DEPS
): void {
if (
!args.enabled ||
!deps.isCacheableForWrite(args.body, args.headers) ||
!deps.isSmallEnoughForSemanticCache(args.translatedResponse)
) {
return;
}
const signature = deps.generateSignature(
args.model,
args.body.messages ?? args.body.input,
args.body.temperature,
args.body.top_p,
args.apiKeyId ?? undefined
);
const tokensSaved = args.usage?.prompt_tokens + args.usage?.completion_tokens || 0;
deps.setCachedResponse(signature, args.model, args.translatedResponse, tokensSaved);
args.log?.debug?.("CACHE", `Stored response for ${args.model} (${tokensSaved} tokens)`);
}
109 changes: 109 additions & 0 deletions tests/unit/chatcore-semantic-cache-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Characterization of storeSemanticCacheResponse — the Phase 9.1 non-streaming semantic-cache
// store extracted from handleChatCore (chatCore god-file decomposition, #3501). Deps are injected
// so the gating logic + signature derivation are observable without the real cache backend.
// Locks: the 3-way gate (enabled + cacheable-for-write + small-enough), the signature args
// (messages ?? input, temperature, top_p, apiKeyId), and the `prompt + completion || 0` token count.
import { test } from "node:test";
import assert from "node:assert/strict";

const { storeSemanticCacheResponse } = await import(
"../../open-sse/handlers/chatCore/semanticCacheStore.ts"
);

type Stored = { sig: unknown; model: string; response: unknown; tokens: number };

function makeDeps(overrides: Record<string, unknown> = {}) {
const stored: Stored[] = [];
const calls = { cacheable: 0, small: 0, signature: 0 };
const deps = {
isCacheableForWrite: (..._a: unknown[]) => {
calls.cacheable++;
return true;
},
isSmallEnoughForSemanticCache: (..._a: unknown[]) => {
calls.small++;
return true;
},
generateSignature: (...a: unknown[]) => {
calls.signature++;
return `sig:${JSON.stringify(a)}`;
},
setCachedResponse: (sig: unknown, model: string, response: unknown, tokens: number) => {
stored.push({ sig, model, response, tokens });
},
...overrides,
} as Parameters<typeof storeSemanticCacheResponse>[1];
return { deps, stored, calls };
}

function baseArgs(overrides: Record<string, unknown> = {}) {
return {
enabled: true,
body: { messages: [{ role: "user", content: "hi" }], temperature: 0, top_p: 1 },
headers: undefined,
translatedResponse: { id: "resp-1" },
model: "gpt-x",
apiKeyId: "key-1",
usage: { prompt_tokens: 10, completion_tokens: 5 },
log: undefined,
...overrides,
} as Parameters<typeof storeSemanticCacheResponse>[0];
}

test("happy path → stores under a signature, tokensSaved = prompt + completion", () => {
const { deps, stored } = makeDeps();
storeSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 1);
assert.equal(stored[0].model, "gpt-x");
assert.deepEqual(stored[0].response, { id: "resp-1" });
assert.equal(stored[0].tokens, 15);
});

test("disabled → no store, no gate calls past enabled", () => {
const { deps, stored, calls } = makeDeps();
storeSemanticCacheResponse(baseArgs({ enabled: false }), deps);
assert.equal(stored.length, 0);
assert.equal(calls.cacheable, 0);
});

test("not cacheable-for-write → no store", () => {
const { deps, stored } = makeDeps({ isCacheableForWrite: () => false });
storeSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 0);
});

test("too large → no store", () => {
const { deps, stored } = makeDeps({ isSmallEnoughForSemanticCache: () => false });
storeSemanticCacheResponse(baseArgs(), deps);
assert.equal(stored.length, 0);
});

test("signature uses messages when present, with model/temperature/top_p/apiKeyId", () => {
const { deps, stored } = makeDeps();
storeSemanticCacheResponse(baseArgs(), deps);
const sig = stored[0].sig as string;
assert.ok(sig.includes("gpt-x"));
assert.ok(sig.includes("key-1"));
});

test("falls back to body.input when messages absent", () => {
let captured: unknown[] = [];
const { deps } = makeDeps({
generateSignature: (...a: unknown[]) => {
captured = a;
return "sig";
},
});
storeSemanticCacheResponse(
baseArgs({ body: { input: "the-input", temperature: 0, top_p: 1 } }),
deps
);
// args: (model, messages ?? input, temperature, top_p, apiKeyId)
assert.equal(captured[1], "the-input");
});

test("missing usage → tokensSaved coerces to 0 (NaN || 0)", () => {
const { deps, stored } = makeDeps();
storeSemanticCacheResponse(baseArgs({ usage: undefined }), deps);
assert.equal(stored[0].tokens, 0);
});
Loading