Files
OmniRoute/tests/integration/semantic-cache-lemonade.test.ts
BillyOutlast d8c69b87e3 feat(cache): implement configurable dual-layer semantic caching layer (#1)
* feat(cache): implement dual-layer semantic caching with in-memory and redis vector stores

Implements production-grade, configurable semantic caching for OmniRoute.

- Dual-layer architecture: Layer 1 exact hash match (0 embedding latency) + Layer 2 vector cosine similarity search.
- Backends: in-memory vector store with L2 normalization, LRU and TTL + Redis vector store adapter with fail-open fallback.
- Embedding generation: conversation history normalization, system prompt exclusion, timeout protection.
- Streaming support: serializable SSE stream synthesis ending in data: [DONE]\n\n.
- Request overrides and telemetry headers: X-OmniRoute-Cache (HIT (exact) | HIT (semantic) | MISS), X-OmniRoute-Cache-Similarity, X-OmniRoute-Savings-Tokens, Cache-Control: no-cache, x-omniroute-no-cache, x-omniroute-cache-threshold, x-omniroute-cache-type, x-omniroute-cache-no-store, x-omniroute-cache-key.
- Unit test coverage across dual-layer search, eviction, redis resilience, and streaming replay.

* feat(cache): add default embedding client and live integration test for Lemonade server and Redis

- Add embeddingBaseUrl and embeddingApiKey configuration options to SemanticCacheConfig.
- Implement createDefaultEmbeddingGenerator for automatic OpenAI-compatible embedding integration.
- Add live verification script scripts/ad-hoc/test-semantic-cache-lemonade.ts.
- Add network-aware integration test tests/integration/semantic-cache-lemonade.test.ts for Lemonade harrier-oss-v1-0.6b and Redis vector store.

* fix(cache): address CodeRabbit review recommendations on PR #1

- Multi-tenant partition isolation: support null sentinel in StoreFilter so anonymous requests cannot match authenticated entries.
- Provider propagation: pass routed provider to non-streaming and streaming cache writes.
- Embedding resilience: race generator with timeout promise in generateEmbeddingWithTimeout to guard against uncooperative generators.
- Memory store consistency: replace older entries with identical hash on insert, and safe-guard hashToId deletion in removeEntry.
- Redis store consistency: prune expired/missing entries from candidate sets during search/stats, and delete hash mapping conditionally.
- Token telemetry: use managerResult.tokensSaved when cached response lacks usage data.
- Clova batch timeout: add AbortSignal.timeout(FETCH_TIMEOUT_MS) to fetchClovaEmbeddingBatch.

---------

Co-authored-by: John Smith <you@example.com>
2026-09-03 12:50:57 -04:00

221 lines
7.0 KiB
TypeScript

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { SemanticCacheManager } from "../../open-sse/services/cache/semanticCacheManager.ts";
import { MemoryVectorStore } from "../../open-sse/services/cache/memoryVectorStore.ts";
import { RedisVectorStore } from "../../open-sse/services/cache/redisVectorStore.ts";
import { createDefaultEmbeddingGenerator } from "../../open-sse/services/cache/embeddingClient.ts";
import { cosineSimilarity } from "../../open-sse/services/cache/vectorStore.ts";
const LEMONADE_URL = process.env.LEMONADE_URL || "http://192.168.31.147:13305/v1/embeddings";
const LEMONADE_KEY = process.env.LEMONADE_KEY || "lemonade";
const LEMONADE_MODEL = process.env.LEMONADE_MODEL || "harrier-oss-v1-0.6b";
const REDIS_URL = process.env.REDIS_URL || "redis://192.168.31.147:6379";
async function isEndpointReachable(url: string, timeoutMs = 1500): Promise<boolean> {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const res = await fetch(url, { method: "HEAD", signal: controller.signal }).catch(() => null);
clearTimeout(timer);
return res !== null;
} catch {
return false;
}
}
describe("Semantic Cache Live Integration with Lemonade & Redis", () => {
it("generates 1024-dimensional embeddings via Lemonade harrier-oss-v1-0.6b", async (t) => {
const reachable = await isEndpointReachable(LEMONADE_URL);
if (!reachable) {
t.skip(`Lemonade server not reachable at ${LEMONADE_URL}`);
return;
}
const generator = createDefaultEmbeddingGenerator({
embeddingProvider: "lemonade",
embeddingModel: LEMONADE_MODEL,
embeddingBaseUrl: LEMONADE_URL,
embeddingApiKey: LEMONADE_KEY,
});
const res = await generator("What is machine learning?");
assert.ok(res, "Should return embedding result");
assert.equal(res.embedding.length, 1024, "Vector dimension should be 1024");
const paraphraseRes = await generator("Can you explain what machine learning is?");
assert.ok(paraphraseRes);
const sim = cosineSimilarity(res.embedding, paraphraseRes.embedding);
assert.ok(sim > 0.8, `Paraphrase similarity ${sim} should be > 0.8`);
});
it("performs dual-layer lookup using MemoryVectorStore and Lemonade embeddings", async (t) => {
const reachable = await isEndpointReachable(LEMONADE_URL);
if (!reachable) {
t.skip(`Lemonade server not reachable at ${LEMONADE_URL}`);
return;
}
const generator = createDefaultEmbeddingGenerator({
embeddingProvider: "lemonade",
embeddingModel: LEMONADE_MODEL,
embeddingBaseUrl: LEMONADE_URL,
embeddingApiKey: LEMONADE_KEY,
});
const manager = new SemanticCacheManager(
{
enabled: true,
similarityThreshold: 0.8,
embeddingModel: LEMONADE_MODEL,
embeddingProvider: "lemonade",
embeddingBaseUrl: LEMONADE_URL,
embeddingApiKey: LEMONADE_KEY,
},
new MemoryVectorStore(),
generator
);
const body = {
model: "lemonade/Heimdallr-27B-GGUF",
messages: [{ role: "user", content: "What is the boiling point of water?" }],
temperature: 0,
};
await manager.store({
body,
response: {
id: "resp-boiling",
choices: [
{ message: { role: "assistant", content: "Water boils at 100 degrees Celsius." } },
],
usage: { prompt_tokens: 10, completion_tokens: 12 },
},
model: "lemonade/Heimdallr-27B-GGUF",
provider: "lemonade",
tokensSaved: 22,
});
// 1. Exact match -> Layer 1
const exact = await manager.lookup({
body,
model: "lemonade/Heimdallr-27B-GGUF",
provider: "lemonade",
});
assert.equal(exact.hit, true);
assert.equal(exact.type, "exact");
assert.equal(exact.tokensSaved, 22);
// 2. Semantic match -> Layer 2
const semantic = await manager.lookup({
body: {
model: "lemonade/Heimdallr-27B-GGUF",
messages: [{ role: "user", content: "At what temperature does water boil?" }],
temperature: 0,
},
model: "lemonade/Heimdallr-27B-GGUF",
provider: "lemonade",
});
assert.equal(semantic.hit, true);
assert.equal(semantic.type, "semantic");
assert.ok((semantic.similarity ?? 0) >= 0.8);
// 3. Unrelated -> Miss
const miss = await manager.lookup({
body: {
model: "lemonade/Heimdallr-27B-GGUF",
messages: [{ role: "user", content: "Who was Napoleon Bonaparte?" }],
temperature: 0,
},
model: "lemonade/Heimdallr-27B-GGUF",
provider: "lemonade",
});
assert.equal(miss.hit, false);
});
it("persists entries and performs vector search in live Redis", async (t) => {
const reachable = await isEndpointReachable(LEMONADE_URL);
if (!reachable) {
t.skip(`Lemonade server not reachable at ${LEMONADE_URL}`);
return;
}
const redisStore = new RedisVectorStore({
redisUrl: REDIS_URL,
keyPrefix: `omniroute:test:it:${Date.now()}:`,
});
const generator = createDefaultEmbeddingGenerator({
embeddingProvider: "lemonade",
embeddingModel: LEMONADE_MODEL,
embeddingBaseUrl: LEMONADE_URL,
embeddingApiKey: LEMONADE_KEY,
});
const manager = new SemanticCacheManager(
{
enabled: true,
backend: "redis",
similarityThreshold: 0.8,
embeddingModel: LEMONADE_MODEL,
embeddingProvider: "lemonade",
embeddingBaseUrl: LEMONADE_URL,
embeddingApiKey: LEMONADE_KEY,
},
redisStore,
generator
);
const body = {
model: "lemonade/Heimdallr-27B-GGUF",
messages: [{ role: "user", content: "How do plants perform photosynthesis?" }],
temperature: 0,
};
await manager.store({
body,
response: {
id: "resp-photosynthesis",
choices: [
{
message: {
role: "assistant",
content: "Plants convert sunlight, water, and CO2 into glucose and oxygen.",
},
},
],
usage: { prompt_tokens: 15, completion_tokens: 20 },
},
model: "lemonade/Heimdallr-27B-GGUF",
provider: "lemonade",
tokensSaved: 35,
});
// 1. Direct hash from Redis
const exact = await manager.lookup({
body,
model: "lemonade/Heimdallr-27B-GGUF",
provider: "lemonade",
});
assert.equal(exact.hit, true);
assert.equal(exact.type, "exact");
// 2. Semantic match from Redis
const semantic = await manager.lookup({
body: {
model: "lemonade/Heimdallr-27B-GGUF",
messages: [{ role: "user", content: "Explain how photosynthesis works in plants" }],
temperature: 0,
},
model: "lemonade/Heimdallr-27B-GGUF",
provider: "lemonade",
});
assert.equal(semantic.hit, true);
assert.equal(semantic.type, "semantic");
assert.ok((semantic.similarity ?? 0) >= 0.8);
// Clean up Redis keys
await manager.clear();
await redisStore.close();
});
});