CAELUM can store the answers to requests your application has already made and serve them again instantly, instead of calling (and paying) the provider a second time.
Caching is disabled by default. Without cache=True, CAELUM behaves exactly like the
provider SDK.
Exact cache
from caelum import OpenAI
client = OpenAI(cache=True)
client.chat.completions.create(model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}])
# Same request again: served from the local cache, no API call, no cost.
client.chat.completions.create(model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}])
Check whether a response came from the cache:
import caelum
response = client.chat.completions.create(...)
caelum.cache_hit(response) # True / False
caelum.cache_type(response) # "exact", "semantic" or None
What counts as "the same request"
The cache key is a SHA-256 hash of every parameter that can change the answer:
model, messages, temperature, top_p, max_tokens, tools, tool_choice,
response_format, seed, stop, and any other generation parameter.
Only transport parameters are ignored: timeout, extra_headers, user, metadata,
store, stream_options, safety_identifier and prompt-caching hints.
Requests sent to different servers (base_url) or with different credentials (API key,
organization, project) never share answers: in a multi-tenant app, one customer's key is
never served another customer's cached answer. Only a short hash of the key is used to
tell them apart; the key itself is never written to the cache file. Rotating a key
therefore starts with an empty cache for the new key.
What is never cached
- Streaming requests (
stream=True). - Requests with
tool_choice="required". - Failed requests (errors are never stored).
- Structured outputs through
chat.completions.parse(...): the call is forwarded to the SDK untouched (usecreatewithresponse_formatto get caching).
Identical requests sent at the same time (for example with asyncio.gather or
threads) all reach the API: the first answer is stored only once it arrives. Duplicates
sent after that are served from the cache.
Options
| Option | Default | Description |
|---|---|---|
cache |
False |
Enable the exact cache. |
cache_ttl |
86400 (24 h) |
Entry lifetime in seconds. None = never expires. |
cache_path |
~/.caelum/cache.sqlite |
Cache file location. $CAELUM_HOME changes the base directory. |
cache_store |
— | Custom storage backend implementing caelum.cache.CacheStore. |
client = OpenAI(cache=True, cache_ttl=3600, cache_path="./my-cache.sqlite")
Storage and privacy
- The cache is a local SQLite file on your machine. Nothing is sent anywhere.
- Values are stored as JSON — never
pickle, which could execute code from a tampered file. - On Linux and macOS the cache folder is created readable by you only (
0700, file0600): it contains your prompts and answers, which may include personal data. An existing folder or file keeps its permissions. - Safe with forking servers (gunicorn, uwsgi, multiprocessing): each worker process opens its own connections to the cache file.
- The cache never breaks your application: if the cache file is unreadable, read-only, corrupted or the disk is full, CAELUM logs a warning, disables the cache and calls the API normally.
Performance
Measured on 1,000 requests with 30% duplicates (see benchmarks/results/):
- 30% of API calls saved (cost saved scales with your duplicate rate).
- Under 1 ms p95 overhead on cache misses (measured +0.25 to +0.56 ms; target: < 5 ms).
- ~0.1 ms per cache hit, versus hundreds of milliseconds for a real API call.
Semantic cache
Reuse answers to questions that are phrased differently but mean the same thing:
import caelum
from caelum import OpenAI
client = OpenAI(semantic_cache=True)
client.chat.completions.create(model="gpt-4o-mini", messages=[{"role": "user", "content": "How do I return an item?"}])
# Same meaning, different words: served from the semantic cache.
response = client.chat.completions.create(model="gpt-4o-mini", messages=[{"role": "user", "content": "How can I send back a product?"}])
caelum.cache_type(response) # "semantic"
The semantic cache is disabled by default and must be enabled explicitly. Computing
an embedding sends the question text to the embedding provider (OpenAI
text-embedding-3-small, about $0.02 per million tokens). With the default verifier, the
two questions are also sent to gpt-4o-mini when a candidate is found. Embeddings and
answers are stored only on your machine.
How it decides
- The last user message is turned into an embedding.
- CAELUM finds the closest previous question with a cosine similarity of at least
semantic_threshold. - A verifier (a small, cheap model) checks that both questions ask for exactly the same information before the answer is reused. It is asked in both orders and must say YES twice. If it says NO, fails, or times out, the request simply goes to the API.
- Only questions asked in exactly the same context are compared: same model, system prompt, conversation history, tools and generation parameters. Only the wording of the last question may differ.
Order of lookups: exact cache → semantic cache → API. A semantic hit is also saved in
the exact cache (when cache=True), so the same wording is answered next time without
even computing an embedding.
Why a verifier
Embedding similarity alone cannot tell "same meaning" from "opposite meaning":
"Convert 10 miles to kilometers" and "Convert 10 kilometers to miles" score 0.95, while
"Do you offer refunds?" and "Can I get my money back?" score only 0.57. Measured on 50
paraphrase pairs and 50 look-alike pairs (English and French,
benchmarks/scripts/bench_semantic_verifier.py):
| Setup | Paraphrases reused | Wrong answers (look-alikes) |
|---|---|---|
| Embeddings only, threshold 0.97 | 0-2% | 0% |
| Embeddings only, threshold 0.85 | 16% | 20% |
Verifier gpt-4o-mini (default), floor 0.60 |
70-76% | 0% |
The verifier was also tested in realistic conditions (each new question compared with all 100 stored questions): no wrong answer was served. End to end through the real OpenAI API (one shared cache, all 100 pairs): 37/50 paraphrases reused, 0/50 wrong answers, versus 0/50 reused with embeddings alone. Five prompt-injection attempts (a stored question telling the judge to reply YES) were all rejected by the default verifier. These figures come from our calibration set; validate on your own traffic before relying on them.
Cost and latency
- A verification is only made when a candidate is found (similarity at least 0.60).
- A few hundred input tokens on
gpt-4o-mini: about $0.00006 per check (measured: $0.0071 for 128 checks in our real-API test). Tracked inclient.metrics(verifier_cost_usd,semantic_verifications,semantic_rejections). - Latency: one or two short calls (a few hundred milliseconds each) in sync code, run concurrently in async code. A cache hit still saves the full answer generation.
Multi-user apps: read this first
The exact cache only reuses an answer for the same request (same system prompt, history and parameters), so it is safe to share between users of the same app.
The semantic cache reuses the answer of another user's similar question. Questions are written by end users, so a malicious user could try to phrase a question that tricks the verifier into accepting a match, and get their (manipulated) answer served to others. CAELUM passes each question to the verifier as escaped data and tells it to ignore any instruction inside, but no LLM judge is perfectly immune to prompt injection. If your users are untrusted and answers could be harmful when reused:
- keep the semantic cache off (
semantic_cache=False, the default), or - separate users, for example with one
cache_pathper user or per tenant, or by putting the user or tenant in the system prompt (different context = never shared).
Never semantically cached
- Streaming requests and
tool_choice="required". - Messages containing images, audio, files or tool results (text-only questions only).
- The embeddings endpoint (returning the vector of a similar text would be wrong).
Options
| Option | Default | Description |
|---|---|---|
semantic_cache |
False |
Enable the semantic cache. |
semantic_verifier |
gpt-4o-mini through the same client (OpenAI) |
A caelum.LLMVerifier, any object with verify() / averify(), or False to disable verification. |
semantic_threshold |
0.60 with a verifier, 0.97 without |
Minimum cosine similarity for a candidate, in (0, 1]. |
semantic_embedder |
OpenAI, through the same client | Custom embedder (see below). |
cache_ttl, cache_path |
shared with the exact cache | Lifetime and location. |
Without a verifier (semantic_verifier=False), do not lower the threshold below 0.97
unless you have validated it on your own traffic: look-alike questions would get wrong
answers.
Choosing the verifier
from caelum import LLMVerifier, OpenAI
# Another OpenAI-compatible judge, e.g. through OpenRouter:
verifier = LLMVerifier(
base_url="https://openrouter.ai/api/v1",
api_key="sk-or-...",
model="mistralai/mistral-small-24b-instruct-2501",
)
client = OpenAI(semantic_cache=True, semantic_verifier=verifier)
If your OpenAI client points to another provider (base_url), pass a verifier explicitly:
the default model name gpt-4o-mini may not exist there (verification would then fail
safely and nothing would be reused). Small instruction models work best; reasoning models
are slow and may return nothing within the one-word budget.
Anthropic and other providers
Anthropic has no embeddings API, so an embedder must be passed explicitly (the question text is then sent to OpenAI for the embedding). There is no default verifier either: pass one, or the strict 0.97 threshold is used.
from caelum import Anthropic, LLMVerifier, OpenAIEmbedder
client = Anthropic(
semantic_cache=True,
semantic_embedder=OpenAIEmbedder(api_key="sk-..."),
semantic_verifier=LLMVerifier(api_key="sk-..."),
)
Any object with a model property and an embed(text) method (and optionally an async
aembed(text)) returning caelum.cache.EmbeddingResult can be used as an embedder.
Performance
- Up to 10,000 embeddings are kept (oldest evicted first).
- Search is restricted to the matching context, then scans candidates in memory. For
large caches, install the
fastextra (pip install "caelum-ai[fast]") to use numpy. - The embedding call (and the verification, when a candidate is found) adds network latency on requests that go through the semantic cache. This is the price of opting in; the default path (no semantic cache) is unaffected.
- Cache files written by older versions keep working, but their semantic entries have no stored question text, so they are never reused when a verifier is on.
Inspecting the cache
caelum cache stats # default file: ~/.caelum/cache.sqlite
caelum cache stats --path my-cache.sqlite --json
python -m caelum cache stats # same thing, without the console script
CAELUM cache
File: /home/me/.caelum/cache.sqlite
Size on disk: 2.4 MB
Exact entries: 1250 (40 expired)
Semantic entries: 310 (0 expired)
Live entries: 1520
Oldest / newest: 2026-10-30 09:12 / 2026-11-02 17:10
Value per reuse: $ 1.8423 (estimated cost of one more call for every live entry)
By model:
gpt-4o-mini-2024-07-18 1100 entries $ 0.2310
claude-sonnet-5-5 420 entries $ 1.6113
The command never changes your cached entries (SQLite may create its temporary
-wal/-shm files next to the cache, which are counted in "Size on disk"). "Value per reuse" is what serving every live entry once
more would save, from the token counts stored with each answer and the prices in
caelum.pricing. Total savings over time are not stored in the file: they are counted
per client, in client.metrics.