Project

SemCached

A semantic caching proxy for LLM APIs that treats the wrong-hit rate as a first-class, measured number: on human-labeled replay traffic the shipped 0.92 threshold buys a 33.1% hit rate at a 6.0% wrong-hit rate and a 32.4% cost reduction, with tenant and prompt isolation that is structural rather than probabilistic.

6.0%
wrong-hit rate at the shipped 0.92 threshold
32.4%
cost reduction on the QQP replay
33.1%
overall hit rate at 0.92
1.4 ms
exact-hit p50; semantic hit 7.3 ms

Why this project exists

LLM calls are expensive and much real traffic is near-duplicate: the same question phrased slightly differently. Caching by meaning instead of exact bytes can cut real bills — Portkey, LiteLLM, Helicone, and Cloudflare’s AI Gateway all ship this. The catch is that a wrong semantic hit silently serves someone an answer to a different question. The engineering problem is not the cache; it is the correctness accounting around the cache.

Two questions get first-class answers here. How many hits were wrong? Measured against human labels: replay traffic is built from Quora Question Pairs, with is_duplicate=0 pairs kept as hard negatives — lexically close, semantically different, exactly the pairs that punish a loose threshold. And what stops a hit crossing a tenant or prompt boundary? The index is partitioned by (tenant, cache version, model, sampling params, hash of the full conversation context); similarity search never crosses a partition, proven by a test with a deliberately over-eager fake embedder reporting cosine 1.0 for every pair.

What it does

  • Two-tier lookup: exact (SHA-256 of the normalized request — Unicode NFKC, whitespace, key order, float noise) then semantic (bge-small-en-v1.5 ONNX embedding of the final user message, brute-force cosine inside the partition only)
  • Semantic matching applies only to the final user message; system prompt, tools, and prior turns are hashed into the partition key and must match exactly — cross-conversation matching is where wrong hits explode
  • Requests with temperature above 0.3 are never cached in either direction: serving a cached sample to a caller who asked for a high-entropy distribution is a correctness bug, not a savings
  • Streaming: a stream miss is teed to the client while being assembled, cached only on a clean finish_reason: stop; a hit can be re-served as OpenAI-style SSE chunks
  • Savings are credited from upstream-reported usage stored per entry — no token heuristics in the accounting path
  • Client contract: X-Tenant, X-Cache-Version (bump to invalidate after a prompt change), bypass header, and response headers reporting hit type, similarity, and matched tag; admin endpoints for stats, flush, and purge

Measured results

QQP replay: 3,658 requests over 2,098 human-labeled duplicate clusters. A semantic hit is wrong iff the matched cached question lives in a different cluster — no LLM judge, no self-grading.

EvidenceResult
Threshold sweep (15 points)at 0.80: 48.4% hits but 20.7% wrong; at 0.92: 33.1% hits, 6.0% wrong; at 0.97: 21.8% hits, 1.7% wrong — the curve says out loud that hit rate is bought with wrong answers
Live replay through the real HTTP proxy at 0.92reproduced the offline simulation exactly: 393 exact hits, 818 semantic hits (49 wrong), 2,447 misses, 0 upstream errors
Cost accounting (gpt-4o-mini list prices, upstream-reported usage)$0.0451 saved on $0.1391 of would-be spend — 32.4%, every credited token traceable to a stored usage record
Latencyexact hit p50 1.4 ms, semantic hit p50 7.3 ms, miss (forward) p50 133.6 ms
Isolationbyte-identical questions under a different tenant, model, system prompt, or version tag cannot cross buckets even at threshold 0
Test suite24 tests, offline, no model download

The committed wrong-hit examples are the argument for the strict end of the curve: “What are the differences between a girl and a lady?” matched a different-cluster neighbor at cosine 0.9485 — embeddings simply cannot distinguish some pairs humans labeled as different.

Tech stack

ComponentChoice
ProxyFastAPI + uvicorn, OpenAI-compatible pass-through with SSE streaming
Embeddingsbge-small-en-v1.5 via ONNX Runtime on CPU
StoreTwo tiers, partitioned in-memory store behind a CacheStore interface
EvaluationQQP-derived replay traffic, threshold sweep, live HTTP replay, tradeoff curve
UpstreamDeterministic mock (SSE + usage) for replay; any OpenAI-compatible base URL in production