A framework-agnostic caching layer for LLM APIs: exact → semantic → prefix-cache accounting in one drop-in middleware. Wrap your OpenAI client (or point your gateway at it) and repeated requests stop costing you money.
Measured on real workloads (see Benchmark):
| 🎯 97% hit rate | ⚡ 0.7 ms P95 | 💰 ~31× prefix discount | 🚀 16,000× faster hits |
|---|---|---|---|
| L1 exact + L2 semantic serve 58/60 requests from cache | vs 6.5 s uncached | DeepSeek cache-hit input tokens | P50 3.2 s → 0.1 ms |
| Layer | Job | Cost of a hit |
|---|---|---|
| L1 exact | byte-identical requests → instant replay | ~0 ms |
| L2 semantic | paraphrased requests → replay (guardrail-checked) | 1 embed call |
| L3 prefix | never a miss — passes through and accounts the upstream prefix-cache discount (DeepSeek ~31×) | 0 extra calls |
The pain: LLM API prices keep rising and your bill is mostly repeated compute. The fix: cache the repeats (L1/L2), and stop ignoring the prefix-cache discount your provider already gives you (L3) — then show you the money saved.
pip install fusion-cachefrom fusion_cache import FusionCache
from fusion_cache.wrapper.openai import CachedOpenAI
import openai
client = CachedOpenAI(openai.OpenAI(api_key=...), cache=FusionCache())
# first call hits upstream, identical calls replay in ~0 msWant it in front of an AI agent (pi, Claude Code, opencode)? Point its
OPENAI_BASE_URLat the gateway or add the MCP server — zero code changes. See Agent Integration.
Most "semantic caches" are single-layer: they embed every query and hope a vector search is right. Most "exact caches" miss every paraphrase. fusion-cache runs three layers in order, each with a distinct job:
| Layer | Job | Mechanism | Cost of a hit |
|---|---|---|---|
| L1 exact | byte-identical requests | canonicalized SHA-256 key (normalizes whitespace/param noise) | ~0 ms, in-process |
| L2 semantic | paraphrased requests | embedding + numpy cosine vs stored rows, configurable threshold + structural guardrail | 1 embed call |
| L3 prefix | never a cache miss | pass through to DeepSeek's automatic prefix cache, capture prompt_cache_hit_tokens / prompt_cache_miss_tokens, account the discount |
0 extra calls |
L3 is the differentiator most caches ignore: DeepSeek already gives you a
~31× discount on cache-hit input tokens (off-peak, deepseek-v4-flash:
$0.007 vs $0.22 per 1M). Nobody reports it back to you as money saved —
fusion-cache does, with a pinned, configurable price model.
request ──▶ L1 exact ── hit? ──▶ replay (0 ms)
│
▼ miss
L2 semantic (embed + cosine) ── hit? ──▶ replay (guardrail-checked)
│
▼ miss
L3 pass-through ──▶ upstream (chat.completions.create)
└─ capture prompt_cache_hit/miss_tokens ──▶ $ saved
└─ populate L1 (+ L2 row)
pip install -e . # core (pydantic, httpx, numpy)
pip install -e ".[test]" # + pytest, pytest-asyncio, respx
pip install -e ".[redis]" # + optional redis.asyncio storeRequires Python ≥ 3.10.
import openai
from fusion_cache import FusionCache
from fusion_cache.wrapper.openai import CachedOpenAI
cache = FusionCache() # defaults: exact+semantic+prefix accounting on
client = CachedOpenAI(openai.OpenAI(
api_key="sk-...", # or DEEPSEEK_API_KEY
base_url="https://api.deepseek.com",
), cache=cache)
# First call → upstream (recorded as a miss).
resp = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Explain prefix caching"}],
)
# Identical call → replayed from L1 (exact hit).
# Paraphrased call → L2 semantic hit (needs an embedder key, see below).
# Either way the upstream sees stable prefixes → DeepSeek bills fewer
# input tokens, and L3 accounting reports the discount.
print(cache.stats_dict())
# {
# 'requests': 2, 'exact_hits': 1, 'semantic_hits': 0, 'prefix_hits': 1,
# 'misses': 1, 'cost_saved_usd': 0.00001704, 'hit_rate': 1.0,
# 'metrics': {...} # per-layer hit/miss, P50/P95 latency
# }Async is identical — wrap AsyncOpenAI and await the call.
L2 needs an OpenAI-compatible /embeddings endpoint. DeepSeek serves one at
https://api.deepseek.com (model deepseek-embedding); any compatible
endpoint works via base_url:
from fusion_cache import FusionCacheConfig
cache = FusionCache(FusionCacheConfig(
similarity_threshold=0.93, # raise → fewer (safer) hits
embedder={"base_url": "https://api.deepseek.com",
"api_key": "sk-...",
"model": "deepseek-embedding"},
))No embedder key → the L2 layer is skipped automatically (L1 + L3 still work).
All knobs live in FusionCacheConfig (pydantic v2):
from fusion_cache import FusionCacheConfig
FusionCacheConfig(
enable_exact=True, enable_semantic=True, enable_prefix_accounting=True,
exact_ttl=3600.0, semantic_ttl=7200.0, semantic_grace_ttl=3600.0,
similarity_threshold=0.93,
max_entries=10_000, semantic_max_entries=10_000,
price_model={ # DeepSeek off-peak snapshot (USD per 1M tokens, 2026-08)
"input_miss_per_1m": 0.22,
"input_hit_per_1m": 0.007,
"output_per_1m": 1.1,
},
)stream=True requests are fully buffered by the pipeline, stored, then
replayed chunk-by-chunk to the caller with the terminal usage chunk
preserved. MVP trades a higher TTFB for full savings and byte-exact replay
(decision documented in open-source-decision.md §6.6). Async and sync
clients both return a chunk iterator matching the wrapped client's style.
src/fusion_cache/
├── config.py # pydantic v2 config: layer toggles, TTLs, threshold,
│ # DeepSeek price model, embedder settings
├── core/
│ ├── key.py # L1 canonicalization (normalize model/temp/stream)
│ ├── pipeline.py # exact → semantic → prefix, async, per-layer TTL
│ └── replay.py # buffered streaming replay + stream text helpers
├── stores/
│ ├── base.py # Store protocol (get/set/delete, TTL, keys)
│ ├── memory.py # in-memory LRU/TTL store (default)
│ └── redis.py # optional redis.asyncio adapter (JSON + native EX)
├── semantic/
│ ├── embedder.py # OpenAI-compatible embed client (base_url configurable)
│ └── matcher.py # numpy cosine + threshold + false-positive guardrail
├── prefix/
│ └── accounting.py # prompt_cache_hit/miss_tokens → $ saved
├── metrics/
│ └── registry.py # per-layer hit/miss, hit rate, $ saved, P50/P95
├── gateway/
│ └── app.py # FastAPI OpenAI-compatible reverse proxy (v1.1)
├── cli.py # fusion-cache serve / stats / check (v1.1)
└── wrapper/
└── openai.py # CachedOpenAI: drop-in sync/async OpenAI wrapper
- MemoryStore (default): dict + LRU-by-use eviction, lazy TTL expiry, thread-safe.
- RedisStore (optional,
[redis]extra):redis.asyncioadapter, JSON entries under a prefix, TTL via nativeSET ... EX. Async-first (aget/aset/…); the wrapper drives it through the event loop.
cache.metrics.snapshot() returns per-layer hit/miss counts and hit rate,
cost saved (USD), prefix-cache tokens, and P50/P95/mean latency from a rolling
buffer. cache.stats_dict() combines it with aggregate counters. This is the
input for the v1.1 dashboard.
The FastAPI gateway is an OpenAI-compatible reverse proxy: your app talks to the gateway exactly like it talks to DeepSeek/OpenAI, and the gateway routes every request through the fusion cache before hitting the real upstream.
pip install -e ".[gateway,redis]"
# In-memory store:
fusion-cache serve --port 8000
# With Redis (shared cache across instances):
fusion-cache serve --port 8000 --redis redis://localhost:6379/0Environment variables:
| Var | Default | Purpose |
|---|---|---|
FUSION_UPSTREAM_BASE_URL |
https://api.deepseek.com |
upstream base URL |
FUSION_UPSTREAM_PROVIDER |
openai |
openai (any OpenAI-compatible endpoint) or anthropic |
FUSION_UPSTREAM_API_KEY / DEEPSEEK_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY |
— | upstream credentials (provider-dependent) |
FUSION_GATEWAY_API_KEY |
— | if set, require Authorization: Bearer <key> on /v1/* (health/metrics/dashboard stay open) |
FUSION_CORS_ORIGINS |
— | comma-separated allowed origins (e.g. https://app.example.com,https://admin.example.com) |
FUSION_UPSTREAM_RETRIES |
2 |
max retries on upstream 429 (exponential backoff) |
REDIS_URL |
— | enable RedisStore (else in-memory) |
FUSION_HOST / FUSION_PORT |
0.0.0.0 / 8000 |
bind address |
openai(default): any OpenAI-compatible endpoint — DeepSeek, OpenAI, OpenRouter, local vLLM/Ollama, etc. Just pointFUSION_UPSTREAM_BASE_URLat it.anthropic: setFUSION_UPSTREAM_PROVIDER=anthropic. The gateway translates the request to/v1/messageswithx-api-keyauth and normalizes the response back to the OpenAI shape. Note: Anthropic does not expose DeepSeek-styleprompt_cache_hit_tokens, so L3 prefix accounting reports 0 for this provider (honest — no discount to count).
| Endpoint | Purpose |
|---|---|
POST /v1/chat/completions |
OpenAI-compatible chat completions through the cache (streaming via SSE supported) |
GET /v1/models |
model list (passthrough, with fallback) |
GET /metrics |
Prometheus metrics (or JSON with Accept: application/json) |
GET /dashboard |
self-contained HTML dashboard (no CDN) |
GET /health |
liveness probe |
Non-streaming responses carry a _fusion_cache metadata block:
{
"_fusion_cache": {
"layer": "exact",
"hit": true,
"cached": true,
"prefix_hit": true,
"cost_saved_usd": 0.000017,
"latency_ms": 0.42
}
}docker compose up --build
# gateway on :8000, redis on :6379fusion-cache serve --port 8000 # start gateway
fusion-cache stats --url http://localhost:8000 # print cache stats
fusion-cache check # environment health check
fusion-cache-mcp # MCP server (stdio) for agentsfusion-cache works with AI coding agents (pi, Claude Code, opencode, Cursor) in two channels:
- Gateway: point the agent's
OPENAI_BASE_URLat the gateway — all LLM traffic flows through the cache. Zero code changes. - MCP server:
fusion-cache-mcpexposes tools (cache_stats,cache_invalidate,cache_status,cache_config) so the agent can see and manage the cache.
pip install "fusion-cache[mcp]"
fusion-cache-mcp # register with Claude Code via .mcp.json, etc.See docs/agent-integration.md for per-agent setup. Try examples/quickstart.py for a 30-second before/after demo.
| fusion-cache | GPTCache | LiteLLM | usewhale/Whale | |
|---|---|---|---|---|
| Exact → semantic → prefix fusion | ✅ built-in | ❌ semantic only | ❌ prefix = passthrough | n/a (coding agent) |
DeepSeek prefix $ accounting |
✅ first-class | ❌ | ❌ | partial (agent-internal) |
| Money-saved / hit-rate metrics | ✅ dashboard-ready JSON | ❌ | partial | agent-specific |
| Form | pip package, wrapper-first | library + vector DB | gateway service | terminal agent |
| Deployment weight | lightweight | needs Milvus + scalar store | full service | n/a |
Research notes: GPTCache is dormant (last release 2024-08), LiteLLM is a
general gateway where caching is 1 of hundreds of features, and
usewhale/Whale is a DeepSeek coding agent (its ~98% prompt-cache-hit
pitch is our differentiation target, not a competitor). No maintained project
ships fusion + DeepSeek accounting as a general middleware. See
open-source-decision.md for the full matrix.
Real end-to-end results. Run python benchmarks/bench.py with any
OpenAI-compatible endpoint (set FUSION_UPSTREAM_API_KEY +
--base-url/--model).
20 distinct tasks × 3 reps (60 requests per config), shared system prompt,
temperature=0.2, stream=False.
| Config | Req | L1 hit | L2 hit | Upstream | Hit rate | P50 (ms) | P95 (ms) |
|---|---|---|---|---|---|---|---|
| baseline (no cache) | 60 | 0 | 0 | 60 | 0% | 3214.2 | 6539.2 |
| fusion (L1+L3) | 60 | 40 | 0 | 20 | 67% | 0.2 | 3450.8 |
| fusion+sem (L1+L2+L3) | 60 | 4 | 54 | 2 | 97% | 0.1 | 0.7 |
What this shows:
- 67% of requests are served from the L1 exact cache with just the exact layer (0.2 ms median vs 3.2 s upstream — ~16,000×).
- Adding the L2 semantic layer lifts the hit rate to 97%: L1 catches the 4 identical repeats, L2 catches the other 54 (same questions asked slightly differently across the workload). Only 2/60 calls reach the upstream, and even P95 drops to 0.7 ms.
- This endpoint (commandcode's deepseek) does not enable upstream prefix
caching (
cached_tokensis always 0), so L3 accounting is 0 here — the L1/L2 layers carry the win.
The same workload against opencode's free endpoint (which does enable automatic prefix caching):
| Config | Req | L1 hit | Upstream | Hit rate | Cached-token ratio | P50 (ms) | $ saved |
|---|---|---|---|---|---|---|---|
| baseline | 60 | 0 | 60 | 0% | 53% | 7498.1 | $0.000000 |
| fusion | 60 | 40 | 20 | 67% | 53% | 0.2 | $0.000273 |
- Every request also benefits from the upstream's own prefix cache
(cached-token ratio 53%) — fusion-cache captures this and reports it as
money saved via
prompt_cache_hit_tokens/cached_tokensaccounting. - P95 stays high (~11 s) because the 20 cold misses still hit the slow free upstream.
L1 exact caching only catches identical requests. When users rephrase the
same question, only the L2 semantic layer can connect them. Benchmarked with
8 question groups × 3 paraphrases each against ox-alpha-free (the free
endpoint was flaky — 503s — so this is a 2-group subset; the script
--paraphrase mode skips failed requests instead of aborting):
| Config | Req | L1 hit | L2 hit | Upstream | Hit rate | P50 (ms) |
|---|---|---|---|---|---|---|
| fusion (L1+L3 only) | 6 | 0 | 0 | 6 | 0% | 21107.5 |
| fusion+sem (L1+L2+L3) | 6 | 0 | 5 | 1 | 83% | 0.4 |
Without the semantic layer every paraphrase misses (different text → L1
can't match) and pays the ~21 s upstream. With it, 5/6 paraphrases are served
from cache in 0.4 ms. The local deterministic embedder used here (char
n-grams) is a stand-in for any OpenAI-compatible /embeddings endpoint —
plug your own via FUSION_EMBED_BASE_URL / FUSION_EMBED_API_KEY.
- Workload: N distinct tasks with shared system prompt + stable prompt-first layout, issued 3× each interleaved (warm/cold cache mix).
- Upstream: any OpenAI-compatible endpoint (default
ox-alpha-freeon opencode.ai/zen/go/v1); pinned model +temperature=0.2,stream=False; no other middleware in the path. - Warm vs cold: cold = first run on an empty cache; warm = subsequent runs.
- Metrics: exact/semantic/prefix hit counts, hit rate, per-request
latency (P50/P95) and cost; cost =
sum(prompt_cache_miss_tokens × miss price + prompt_cache_hit_tokens × hit price + completion_tokens × output price)from real usage fields. - Report: per-layer hit table, P50/P95 before/after,
$/task before/after, and the percentage of cost reduction attributable to each layer.
Honest claims only: L3 savings are real upstream discounts (usage fields are
provider-returned); L1/L2 savings are upstream calls avoided. The original
decision-doc targets (.00 → .18 USD/task, P95 4.2s → 1.7s) were goals for
this script to reproduce — actual numbers depend on the upstream's pricing
and latency; the framework-agnostic script lets you measure your own.
The three layers map onto an active research lineage. If you're coming from the literature, this is where each layer lives:
- MeanCache: User-Centric Semantic Caching for LLM Web Services — arXiv:2403.02694 (2024). Foundational semantic-cache work: embedding similarity + cached-response reuse. This is the direct ancestor of L2.
- GPT Semantic Cache: Reducing LLM Costs and Latency via Semantic Embedding Caching — arXiv:2411.05276 (2024). Semantic embedding caching for cost and latency reduction — the same pitch fusion-cache makes.
- From Exact Hits to Close Enough: Semantic Caching for LLM Embeddings — arXiv:2603.03301 (2026). The exact→semantic progression, which is exactly the L1→L2 design here.
- Continuous Semantic Caching for Low-Cost LLM Serving — arXiv:2604.20021 (2026). Recent continuous-semantic-cache work — the direction is active.
- Closing the Calibration Gap in Semantic Caching — arXiv:2606.19719 (2026). Calibrating semantic-cache thresholds to avoid wrong hits — the academic version of our false-positive guardrail.
- Marconi: Prefix Caching for the Era of Hybrid LLMs — arXiv:2411.19379 (2024, MLSys '25 Outstanding Paper). System-level prefix caching; the academic counterpart of the automatic prefix caching DeepSeek/OpenAI provide, which L3 accounts for.
- Not All Tokens Are Worth Caching: Learning Semantic-Aware Eviction for LLM Prefix Caches — arXiv:2605.18825 (2026). Semantic-aware eviction in prefix caches — semantic + prefix fusion in the same spirit as this project.
- AgentKVShift: Efficient KV Cache Reuse for Agentic Memory Systems — arXiv:2607.21604 (2026). KV reuse for agent memory — the same idea as observational-memory-style compression; a natural evolution path for the semantic layer on long-running agents.
Apache-2.0. Built from scratch; reference material only was reused from GPTCache/LiteLLM/semcache APIs (MIT/Apache-2.0) — no code copied.