semcacher 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. semcacher-0.1.0/.github/workflows/ci.yml +39 -0
  2. semcacher-0.1.0/.github/workflows/release.yml +67 -0
  3. semcacher-0.1.0/.gitignore +15 -0
  4. semcacher-0.1.0/CHANGELOG.md +31 -0
  5. semcacher-0.1.0/CONTRIBUTING.md +27 -0
  6. semcacher-0.1.0/LICENSE +21 -0
  7. semcacher-0.1.0/PKG-INFO +375 -0
  8. semcacher-0.1.0/README.md +321 -0
  9. semcacher-0.1.0/RELEASING.md +65 -0
  10. semcacher-0.1.0/assets/icon.svg +15 -0
  11. semcacher-0.1.0/assets/logo.svg +21 -0
  12. semcacher-0.1.0/examples/anthropic_client.py +28 -0
  13. semcacher-0.1.0/examples/decorator.py +18 -0
  14. semcacher-0.1.0/examples/openai_client.py +30 -0
  15. semcacher-0.1.0/examples/quickstart.py +26 -0
  16. semcacher-0.1.0/examples/tuning_pairs.csv +21 -0
  17. semcacher-0.1.0/pyproject.toml +56 -0
  18. semcacher-0.1.0/src/semcache/__init__.py +45 -0
  19. semcacher-0.1.0/src/semcache/__main__.py +5 -0
  20. semcacher-0.1.0/src/semcache/cache.py +406 -0
  21. semcacher-0.1.0/src/semcache/cli.py +134 -0
  22. semcacher-0.1.0/src/semcache/embedders.py +177 -0
  23. semcacher-0.1.0/src/semcache/integrations.py +233 -0
  24. semcacher-0.1.0/src/semcache/pricing.py +102 -0
  25. semcacher-0.1.0/src/semcache/py.typed +0 -0
  26. semcacher-0.1.0/src/semcache/redis_store.py +373 -0
  27. semcacher-0.1.0/src/semcache/sqlite_store.py +202 -0
  28. semcacher-0.1.0/src/semcache/stats.py +156 -0
  29. semcacher-0.1.0/src/semcache/store.py +204 -0
  30. semcacher-0.1.0/src/semcache/tuning.py +273 -0
  31. semcacher-0.1.0/tests/test_cache.py +92 -0
  32. semcacher-0.1.0/tests/test_call_helpers.py +109 -0
  33. semcacher-0.1.0/tests/test_cli.py +62 -0
  34. semcacher-0.1.0/tests/test_embedders.py +54 -0
  35. semcacher-0.1.0/tests/test_integrations.py +156 -0
  36. semcacher-0.1.0/tests/test_persistence.py +108 -0
  37. semcacher-0.1.0/tests/test_pricing.py +32 -0
  38. semcacher-0.1.0/tests/test_redis_store.py +176 -0
  39. semcacher-0.1.0/tests/test_stats.py +73 -0
  40. semcacher-0.1.0/tests/test_tuning.py +145 -0
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: pip install -e ".[dev]"
21
+ - run: ruff check .
22
+ - run: ruff format --check .
23
+ - run: pytest -q
24
+ - run: python examples/quickstart.py
25
+
26
+ build:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v5
30
+ - uses: actions/setup-python@v6
31
+ with:
32
+ python-version: "3.12"
33
+ - run: pip install build twine
34
+ - run: python -m build
35
+ - run: twine check --strict dist/*
36
+ - uses: actions/upload-artifact@v5
37
+ with:
38
+ name: dist
39
+ path: dist/
@@ -0,0 +1,67 @@
1
+ name: Release
2
+
3
+ # Publishing a GitHub Release (tag vX.Y.Z) uploads that version to PyPI.
4
+ # Running this workflow by hand uploads to TestPyPI instead, as a dry run.
5
+ # Uses PyPI Trusted Publishing: no API token is stored in the repository.
6
+
7
+ on:
8
+ release:
9
+ types: [published]
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: "3.12"
20
+ - run: pip install build twine
21
+ - name: Check the release tag matches the package version
22
+ if: github.event_name == 'release'
23
+ run: |
24
+ version=$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' src/semcache/__init__.py)
25
+ tag="${GITHUB_REF_NAME#v}"
26
+ if [ "$tag" != "$version" ]; then
27
+ echo "::error::release tag $GITHUB_REF_NAME does not match __version__ = $version"
28
+ exit 1
29
+ fi
30
+ - run: python -m build
31
+ - run: twine check --strict dist/*
32
+ - uses: actions/upload-artifact@v5
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ testpypi:
38
+ if: github.event_name == 'workflow_dispatch'
39
+ needs: build
40
+ runs-on: ubuntu-latest
41
+ environment: testpypi
42
+ permissions:
43
+ id-token: write
44
+ steps:
45
+ - uses: actions/download-artifact@v5
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+ - uses: pypa/gh-action-pypi-publish@release/v1
50
+ with:
51
+ repository-url: https://test.pypi.org/legacy/
52
+
53
+ pypi:
54
+ if: github.event_name == 'release'
55
+ needs: build
56
+ runs-on: ubuntu-latest
57
+ environment:
58
+ name: pypi
59
+ url: https://pypi.org/project/semcacher/
60
+ permissions:
61
+ id-token: write
62
+ steps:
63
+ - uses: actions/download-artifact@v5
64
+ with:
65
+ name: dist
66
+ path: dist/
67
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .mypy_cache/
12
+ .coverage
13
+ htmlcov/
14
+ *.db
15
+ *.sqlite3
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - `RedisStore` (`pip install "semcacher[redis]"`): keep the cache in Redis so several
11
+ processes or servers share answers and stats. Works with any Redis-compatible
12
+ server; stats from all processes add up instead of overwriting each other.
13
+
14
+ ## [0.1.0] - 2026-11-09
15
+
16
+ ### Added
17
+ - First release on PyPI: `pip install semcacher` (import name `semcache`).
18
+ - Supports Python 3.10 and later.
19
+ - `SemanticCache` with exact-match fast path and cosine-similarity lookup.
20
+ - Embedders: dependency-free `HashingEmbedder`, `SentenceTransformerEmbedder`,
21
+ `OpenAIEmbedder`, and `CallableEmbedder` for anything else.
22
+ - `InMemoryStore` and persistent `SQLiteStore`.
23
+ - Namespace and model isolation, TTL expiry, LRU `max_entries` limit.
24
+ - Stats: hit rate, exact vs semantic hits, tokens saved, money saved and spent,
25
+ per-model breakdown; persisted with `SQLiteStore`.
26
+ - `PriceBook` with bundled Claude list prices and custom prices.
27
+ - `get_or_call`, `aget_or_call` and the `@cache.cached` decorator (sync and async).
28
+ - `wrap_openai` and `wrap_anthropic` drop-in client wrappers.
29
+ - `semcache` CLI: `stats`, `list`, `clear`, `prune`, `prices`.
30
+ - `semcache.tuning`: `calibrate()` recommends a similarity threshold from labelled
31
+ prompt pairs, and `load_pairs()` reads them from CSV.
@@ -0,0 +1,27 @@
1
+ # Contributing
2
+
3
+ Thanks for helping out!
4
+
5
+ ```bash
6
+ git clone https://github.com/sjapanjots/Semantic_Cache_LLM
7
+ cd Semantic_Cache_LLM
8
+ python -m venv .venv && source .venv/bin/activate
9
+ pip install -e ".[dev]"
10
+ ```
11
+
12
+ Before opening a pull request:
13
+
14
+ ```bash
15
+ ruff check . && ruff format --check .
16
+ pytest -q
17
+ ```
18
+
19
+ Guidelines:
20
+
21
+ - Keep the core dependency-free apart from `numpy`. Provider SDKs and model libraries
22
+ belong behind optional extras and lazy imports.
23
+ - Add a test for every behaviour change.
24
+ - Note user-visible changes in `CHANGELOG.md`.
25
+
26
+ To add a new storage back-end, subclass `semcache.BaseStore`. To add an embedder,
27
+ implement `name` and `embed(texts) -> np.ndarray` returning L2-normalised rows.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Japanjot Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,375 @@
1
+ Metadata-Version: 2.5
2
+ Name: semcacher
3
+ Version: 0.1.0
4
+ Summary: A semantic cache for LLM calls: reuse answers to questions that mean the same thing, and track hit rate and money saved.
5
+ Project-URL: Homepage, https://github.com/sjapanjots/Semantic_Cache_LLM
6
+ Project-URL: Issues, https://github.com/sjapanjots/Semantic_Cache_LLM/issues
7
+ Project-URL: Changelog, https://github.com/sjapanjots/Semantic_Cache_LLM/blob/main/CHANGELOG.md
8
+ Author: Japanjot Singh
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Japanjot Singh
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: anthropic,cache,claude,cost,embeddings,llm,openai,semantic-cache
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3 :: Only
38
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
39
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
+ Classifier: Typing :: Typed
41
+ Requires-Python: >=3.10
42
+ Requires-Dist: numpy>=1.22
43
+ Provides-Extra: dev
44
+ Requires-Dist: fakeredis>=2.20; extra == 'dev'
45
+ Requires-Dist: pytest>=7; extra == 'dev'
46
+ Requires-Dist: ruff>=0.4; extra == 'dev'
47
+ Provides-Extra: local
48
+ Requires-Dist: sentence-transformers>=2.2; extra == 'local'
49
+ Provides-Extra: openai
50
+ Requires-Dist: openai>=1.0; extra == 'openai'
51
+ Provides-Extra: redis
52
+ Requires-Dist: redis>=5; extra == 'redis'
53
+ Description-Content-Type: text/markdown
54
+
55
+ <p align="center">
56
+ <img src="https://raw.githubusercontent.com/sjapanjots/Semantic_Cache_LLM/main/assets/logo.svg" alt="semcache" width="480">
57
+ </p>
58
+
59
+ <p align="center">
60
+ <b>A semantic cache for LLM calls.</b><br>
61
+ If a new question means the same as an old one, return the saved answer.<br>
62
+ Track your hit rate and how much money it saved.
63
+ </p>
64
+
65
+ <p align="center">
66
+ <a href="https://github.com/sjapanjots/Semantic_Cache_LLM/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/sjapanjots/Semantic_Cache_LLM/actions/workflows/ci.yml/badge.svg"></a>
67
+ <img alt="Python" src="https://img.shields.io/badge/python-3.10%2B-blue">
68
+ <img alt="License" src="https://img.shields.io/badge/license-MIT-green">
69
+ </p>
70
+
71
+ ---
72
+
73
+ ## Why
74
+
75
+ Users ask the same things in different words. An exact-match cache misses
76
+ *"How do I reverse a list in Python?"* vs *"python: how to reverse a list"*, so you pay
77
+ for the same answer twice. `semcache` embeds each prompt, finds the closest one already
78
+ answered, and returns that answer when the two are similar enough. Every hit is counted
79
+ in tokens and dollars, so you can see what the cache is worth.
80
+
81
+ - **Drop-in**: wrap an `OpenAI` or `Anthropic` client, decorate a function, or call
82
+ `get` / `set` yourself.
83
+ - **Shows savings**: hit rate, exact vs semantic hits, tokens saved, money saved and
84
+ spent, broken down per model.
85
+ - **Safe defaults**: answers never cross models, namespaces, system prompts or
86
+ conversation history. Truncated or refused responses are not cached.
87
+ - **Small**: the core needs only `numpy`. Neural embedders are optional extras.
88
+ - **Persistent**: one SQLite file holds entries and stats. TTL and LRU size limits
89
+ are built in.
90
+
91
+ ## Install
92
+
93
+ ```bash
94
+ pip install semcacher # core (numpy only)
95
+ pip install "semcacher[local]" # + sentence-transformers, runs locally
96
+ pip install "semcacher[openai]" # + OpenAI embeddings
97
+ pip install "semcacher[redis]" # + shared cache in Redis
98
+ ```
99
+
100
+ You install `semcacher` and import `semcache`:
101
+
102
+ ```python
103
+ from semcache import SemanticCache
104
+ ```
105
+
106
+ From source:
107
+
108
+ ```bash
109
+ pip install git+https://github.com/sjapanjots/Semantic_Cache_LLM
110
+ ```
111
+
112
+ ## Quickstart
113
+
114
+ ```python
115
+ from semcache import SemanticCache, LLMResult
116
+
117
+ cache = SemanticCache(pricing={"demo-model": (2.50, 10.00)}) # USD per 1M input/output tokens
118
+
119
+ def call_llm(prompt):
120
+ ... # your real LLM call
121
+ return LLMResult("answer", input_tokens=1200, output_tokens=400)
122
+
123
+ cache.get_or_call("How do I reverse a list in Python?", lambda: call_llm(...), model="demo-model")
124
+ cache.get_or_call("python: how to reverse a list", lambda: call_llm(...), model="demo-model") # cache hit
125
+
126
+ print(cache.stats)
127
+ ```
128
+
129
+ Output of [`examples/quickstart.py`](examples/quickstart.py), which needs no API key:
130
+
131
+ ```text
132
+ lookups 6
133
+ hits 3 (50.0%)
134
+ exact 1
135
+ semantic 2
136
+ misses 3
137
+ tokens saved 4,800
138
+ money saved $0.0210
139
+ money spent $0.0210
140
+ savings rate 50.0%
141
+ ```
142
+
143
+ ## Usage
144
+
145
+ ### 1. Wrap your client (no other code changes)
146
+
147
+ ```python
148
+ from anthropic import Anthropic
149
+ from semcache import SemanticCache, SentenceTransformerEmbedder
150
+ from semcache.integrations import wrap_anthropic
151
+
152
+ cache = SemanticCache(embedder=SentenceTransformerEmbedder(), path="cache.db")
153
+ client = wrap_anthropic(Anthropic(), cache)
154
+
155
+ client.messages.create(
156
+ model="claude-opus-5-5",
157
+ max_tokens=1024,
158
+ messages=[{"role": "user", "content": "How should a Python dev learn Rust?"}],
159
+ )
160
+ ```
161
+
162
+ `wrap_openai(OpenAI(), cache)` does the same for `client.chat.completions.create`.
163
+ Async clients (`AsyncAnthropic`, `AsyncOpenAI`) work too. On a hit you get back the
164
+ SDK's normal response object, rebuilt from the cache.
165
+
166
+ How requests are matched:
167
+
168
+ | Part of the request | Treatment |
169
+ |---|---|
170
+ | Last user message | Compared **by meaning** |
171
+ | Model | Must match exactly |
172
+ | System prompt, earlier turns, tools, temperature, max tokens, ... | Hashed; must match exactly |
173
+ | `timeout`, `extra_headers`, `user`, `metadata` | Ignored |
174
+ | `stream=True` | Passed through, not cached |
175
+ | Responses stopped by `max_tokens` / `refusal` / `length` / `content_filter` | Not cached |
176
+
177
+ ### 2. Decorate a function
178
+
179
+ ```python
180
+ @cache.cached(model="claude-sonnet-5-5")
181
+ def ask(prompt: str) -> LLMResult:
182
+ response = my_llm(prompt)
183
+ return LLMResult(response.text, input_tokens=response.in_tok, output_tokens=response.out_tok)
184
+
185
+ ask("How many days are in a leap year?")
186
+ ask("how many days does a leap year have") # served from the cache
187
+ ```
188
+
189
+ Works on `async def` functions too. Use `key=lambda *a, **kw: ...` to choose which
190
+ argument is the prompt.
191
+
192
+ ### 3. Call `get` / `set` yourself
193
+
194
+ ```python
195
+ answer = cache.get(prompt, model="claude-opus-5-5")
196
+ if answer is None:
197
+ answer, usage = call_llm(prompt)
198
+ cache.set(prompt, answer, model="claude-opus-5-5",
199
+ input_tokens=usage.input, output_tokens=usage.output)
200
+ ```
201
+
202
+ `cache.lookup(...)` returns a `CacheHit` with `.response`, `.similarity`, `.exact`,
203
+ `.cost_saved` and the stored `.entry`.
204
+
205
+ ## Choosing an embedder and threshold
206
+
207
+ The threshold is the minimum cosine similarity for a semantic hit. Each embedder ships
208
+ a conservative default; tune it on your own traffic.
209
+
210
+ | Embedder | Install | Default threshold | Notes |
211
+ |---|---|---|---|
212
+ | `HashingEmbedder` (default) | core | 0.70 | Lexical (words and character n-grams). No model download. Catches rewording, not synonyms. |
213
+ | `SentenceTransformerEmbedder()` | `[local]` | 0.85 | Real semantic matching, runs on CPU. `all-MiniLM-L6-v2` by default. |
214
+ | `OpenAIEmbedder()` | `[openai]` | 0.90 | `text-embedding-3-small` by default. Costs a small amount per lookup. |
215
+ | `CallableEmbedder(fn, name)` | core | 0.85 | Bring any model: Voyage, Cohere, an in-house service... |
216
+
217
+ Too many wrong answers? Raise the threshold. Too few hits? Lower it, or switch to a
218
+ neural embedder. You can also override it per call: `cache.lookup(p, threshold=0.95)`.
219
+
220
+ ### Calibrating the threshold from your own data
221
+
222
+ Instead of guessing, label some prompt pairs as same or different meaning and let
223
+ `calibrate` pick the threshold:
224
+
225
+ ```python
226
+ from semcache import HashingEmbedder, SemanticCache, calibrate, load_pairs
227
+
228
+ pairs = load_pairs("examples/tuning_pairs.csv") # CSV columns: a,b,same
229
+ result = calibrate(HashingEmbedder(), pairs) # or target_precision=0.99
230
+ print(result.summary())
231
+ cache = SemanticCache(threshold=result.threshold)
232
+ ```
233
+
234
+ ```text
235
+ embedder hashing-1024-3
236
+ pairs 20 (10 same, 10 different)
237
+ recommend threshold=0.7099 (best F0.5)
238
+
239
+ threshold precision recall wrong hits F0.5
240
+ 0.7000 83.3% 50.0% 1 0.735
241
+ 0.7083 83.3% 50.0% 1 0.735
242
+ 0.7099 100.0% 50.0% 0 0.833 <-
243
+ 0.7100 100.0% 40.0% 0 0.769
244
+ ```
245
+
246
+ *Precision* is the share of hits that are correct; *recall* is the share of
247
+ same-meaning pairs that hit. By default the recommendation maximises F0.5, which
248
+ counts a wrong answer as worse than a miss. Pass `target_precision=0.99` to get the
249
+ loosest threshold that is still right 99% of the time. `result.mistakes()` lists the
250
+ pairs it gets wrong. Use the same embedder you'll use in the cache, and aim for at
251
+ least a few dozen pairs drawn from real traffic.
252
+
253
+ ## Tracking savings
254
+
255
+ ```python
256
+ s = cache.stats
257
+ s.hit_rate # 0.42
258
+ s.cost_saved # 18.73 (USD)
259
+ s.cost_spent # 25.90
260
+ s.savings_rate # 0.42 share of the would-be bill avoided
261
+ s.tokens_saved # 3_912_000
262
+ s.by_model["claude-opus-5-5"].hit_rate
263
+ s.to_dict() # for your metrics pipeline
264
+ ```
265
+
266
+ Money saved is the price of the original call, credited every time its answer is
267
+ reused. Prices come from a `PriceBook`:
268
+
269
+ ```python
270
+ from semcache import PriceBook
271
+
272
+ cache = SemanticCache(pricing={"gpt-4o-mini": (0.15, 0.60)}) # merged over defaults
273
+ cache = SemanticCache(pricing=PriceBook(default=(1.0, 3.0))) # fallback for unknown models
274
+ cache.set(prompt, answer, cost=0.0123) # or give the exact cost
275
+ ```
276
+
277
+ Claude list prices are bundled (run `semcache prices`). Prices change, so check your
278
+ provider's pricing page. Models without a price still count toward hit rate but add
279
+ $0 to savings.
280
+
281
+ ## Persistence, expiry and size limits
282
+
283
+ ```python
284
+ cache = SemanticCache(
285
+ path="cache.db", # SQLite file; entries and stats survive restarts
286
+ ttl=7 * 24 * 3600, # entries expire after a week
287
+ max_entries=50_000, # least recently used entries are evicted
288
+ namespace="support-bot",
289
+ )
290
+ ```
291
+
292
+ - Responses stored in SQLite must be JSON-serialisable. The client wrappers handle
293
+ this for you.
294
+ - A cache file remembers which embedder built it and refuses to open with a
295
+ different one, because their vectors aren't comparable.
296
+ - Use `with SemanticCache(...) as cache:` or call `cache.close()` so the latest stats
297
+ are written.
298
+
299
+ ### Sharing one cache between servers (Redis)
300
+
301
+ SQLite keeps the cache on one machine. To let several processes or servers reuse
302
+ each other's answers, keep it in Redis (or any Redis-compatible server such as
303
+ Valkey; no modules needed):
304
+
305
+ ```python
306
+ from semcache import SemanticCache
307
+ from semcache.redis_store import RedisStore
308
+
309
+ cache = SemanticCache(store=RedisStore("redis://localhost:6379/0"))
310
+ ```
311
+
312
+ - Every process sees the others' answers immediately.
313
+ - Stats are shared too: each process adds its own counts to the totals in Redis,
314
+ so `cache.stats` shows the hit rate and money saved across all of them.
315
+ - `prefix="team-a:"` keeps several caches apart in one database.
316
+ - Search runs in each process: it downloads each vector once, then only fetches
317
+ the list of entry ids per lookup. That is fast up to tens of thousands of
318
+ entries per namespace.
319
+
320
+ ## CLI
321
+
322
+ ```bash
323
+ semcache stats --db cache.db # hit rate, tokens and money saved
324
+ semcache stats --db cache.db --json
325
+ semcache list --db cache.db # most-hit entries
326
+ semcache prune --db cache.db --older-than 86400
327
+ semcache clear --db cache.db [--namespace support-bot] [--stats]
328
+ semcache prices # bundled price table
329
+ ```
330
+
331
+ ## How it works
332
+
333
+ ```text
334
+ prompt ──► normalise ──► exact match in scope? ──yes──► HIT (similarity 1.0)
335
+ │ no
336
+ ▼
337
+ embed ──► nearest neighbour in scope ──► ≥ threshold? ──yes──► HIT
338
+ │ no
339
+ ▼
340
+ MISS ──► call LLM ──► set()
341
+ ```
342
+
343
+ A *scope* is `namespace + model`; the client wrappers add a hash of the rest of the
344
+ request. Search is an exact cosine scan over a NumPy matrix per scope, fast enough for
345
+ tens of thousands of entries per scope.
346
+
347
+ ## When not to use it
348
+
349
+ - Answers that depend on time or live data ("what's the weather now"). Use a `ttl`,
350
+ or don't cache those calls.
351
+ - Prompts where small wording changes matter, such as code with specific values or
352
+ math with different numbers. Use a high threshold or a separate namespace.
353
+ - Personalised answers. Put a user or tenant id in `namespace` so answers never
354
+ leak between users.
355
+
356
+ ## Development
357
+
358
+ ```bash
359
+ pip install -e ".[dev]"
360
+ pytest -q
361
+ ruff check . && ruff format --check .
362
+ ```
363
+
364
+ See [CONTRIBUTING.md](CONTRIBUTING.md) and [CHANGELOG.md](CHANGELOG.md).
365
+
366
+ ## Roadmap
367
+
368
+ - Pluggable vector indexes (FAISS, hnswlib) for millions of entries
369
+ - Redis and Postgres/pgvector stores
370
+ - Streaming support in the client wrappers
371
+ - Optional LLM-judged verification of borderline hits
372
+
373
+ ## License
374
+
375
+ [MIT](LICENSE)