cachellm-proxy 0.1.0__py3-none-any.whl

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 (43) hide show
  1. cachellm/__init__.py +13 -0
  2. cachellm/__main__.py +6 -0
  3. cachellm/api/__init__.py +5 -0
  4. cachellm/api/app.py +143 -0
  5. cachellm/api/auth.py +34 -0
  6. cachellm/api/deps.py +106 -0
  7. cachellm/api/routes_admin.py +265 -0
  8. cachellm/api/routes_chat.py +385 -0
  9. cachellm/api/sse.py +98 -0
  10. cachellm/cache/__init__.py +3 -0
  11. cachellm/cache/analytics.py +150 -0
  12. cachellm/cache/coalesce.py +63 -0
  13. cachellm/cache/entry.py +92 -0
  14. cachellm/cache/exact_store.py +33 -0
  15. cachellm/cache/keys.py +124 -0
  16. cachellm/cache/policy.py +134 -0
  17. cachellm/cache/redis_client.py +22 -0
  18. cachellm/cache/service.py +332 -0
  19. cachellm/cache/vector_store.py +217 -0
  20. cachellm/cli.py +122 -0
  21. cachellm/embeddings/__init__.py +19 -0
  22. cachellm/embeddings/base.py +38 -0
  23. cachellm/embeddings/fastembed_backend.py +75 -0
  24. cachellm/embeddings/hash_backend.py +42 -0
  25. cachellm/errors.py +72 -0
  26. cachellm/logging_setup.py +56 -0
  27. cachellm/models.py +181 -0
  28. cachellm/observability/__init__.py +6 -0
  29. cachellm/observability/metrics.py +147 -0
  30. cachellm/observability/tracing.py +107 -0
  31. cachellm/pricing.py +108 -0
  32. cachellm/providers/__init__.py +7 -0
  33. cachellm/providers/base.py +84 -0
  34. cachellm/providers/bedrock.py +238 -0
  35. cachellm/providers/fake.py +56 -0
  36. cachellm/providers/openai_compat.py +131 -0
  37. cachellm/providers/registry.py +96 -0
  38. cachellm/py.typed +0 -0
  39. cachellm/settings.py +230 -0
  40. cachellm_proxy-0.1.0.dist-info/METADATA +550 -0
  41. cachellm_proxy-0.1.0.dist-info/RECORD +43 -0
  42. cachellm_proxy-0.1.0.dist-info/WHEEL +4 -0
  43. cachellm_proxy-0.1.0.dist-info/entry_points.txt +3 -0
cachellm/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """CacheLLM: a drop-in semantic cache for OpenAI-compatible LLM APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["__version__", "main"]
8
+
9
+
10
+ def main() -> None: # pragma: no cover - thin console-script shim
11
+ from cachellm.cli import app
12
+
13
+ app()
cachellm/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from cachellm.cli import app
4
+
5
+ if __name__ == "__main__": # pragma: no cover
6
+ app()
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from cachellm.api.app import create_app
4
+
5
+ __all__ = ["create_app"]
cachellm/api/app.py ADDED
@@ -0,0 +1,143 @@
1
+ """FastAPI application assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from collections.abc import AsyncIterator
7
+ from typing import Any
8
+
9
+ import structlog
10
+ from fastapi import FastAPI, Request
11
+ from fastapi.exceptions import RequestValidationError
12
+ from fastapi.responses import JSONResponse, Response
13
+
14
+ from cachellm import __version__
15
+ from cachellm.api import routes_admin, routes_chat
16
+ from cachellm.api.deps import AppState, build_state, shutdown_state
17
+ from cachellm.errors import CacheLLMError
18
+ from cachellm.logging_setup import configure_logging
19
+ from cachellm.observability.tracing import setup_tracing
20
+ from cachellm.settings import Settings, get_settings
21
+
22
+ log = structlog.get_logger(__name__)
23
+
24
+ DESCRIPTION = """
25
+ A drop-in semantic cache for OpenAI-compatible LLM APIs.
26
+
27
+ Point your client's `base_url` at this service and repeated or reworded
28
+ questions are served from cache instead of the model. Every response carries
29
+ `X-Cache` headers explaining what happened.
30
+ """.strip()
31
+
32
+
33
+ def create_app(settings: Settings | None = None, state: AppState | None = None) -> FastAPI:
34
+ settings = settings or get_settings()
35
+ configure_logging(settings)
36
+
37
+ @contextlib.asynccontextmanager
38
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
39
+ app.state.cachellm = state or await build_state(settings)
40
+ current: AppState = app.state.cachellm
41
+ if not current.settings.auth_enabled:
42
+ log.warning("auth_disabled", hint="set CACHELLM_API_KEYS before exposing this proxy")
43
+ if current.settings.shadow_mode:
44
+ log.warning("shadow_mode_on", hint="cache hits are logged, never served")
45
+ if not current.settings.is_calibrated and current.settings.threshold_default <= 0:
46
+ log.warning(
47
+ "threshold_not_calibrated",
48
+ embedding_model=current.settings.embedding_model,
49
+ using=current.settings.calibrated_threshold,
50
+ hint="measured safe thresholds span 0.89-0.98 across models; run "
51
+ "`python -m bench.compare_models` on your own data before trusting this",
52
+ )
53
+ log.info(
54
+ "cachellm_started",
55
+ version=__version__,
56
+ cache_available=current.cache_available,
57
+ embedding_model=current.embedder.name,
58
+ default_provider=current.settings.default_provider,
59
+ )
60
+ try:
61
+ yield
62
+ finally:
63
+ if state is None:
64
+ await shutdown_state(current)
65
+
66
+ app = FastAPI(
67
+ title="CacheLLM",
68
+ version=__version__,
69
+ description=DESCRIPTION,
70
+ lifespan=lifespan,
71
+ docs_url="/docs",
72
+ openapi_url="/openapi.json",
73
+ )
74
+
75
+ setup_tracing(settings, app)
76
+
77
+ app.include_router(routes_chat.router, prefix="/v1", tags=["openai"])
78
+ app.include_router(routes_admin.router, prefix="/admin", tags=["admin"])
79
+
80
+ # --------------------------------------------------------------- errors
81
+ @app.exception_handler(CacheLLMError)
82
+ async def _cachellm_error(_request: Request, exc: CacheLLMError) -> JSONResponse:
83
+ return JSONResponse(status_code=exc.status_code, content=exc.envelope())
84
+
85
+ @app.exception_handler(RequestValidationError)
86
+ async def _validation_error(_request: Request, exc: RequestValidationError) -> JSONResponse:
87
+ first = exc.errors()[0] if exc.errors() else {}
88
+ location = ".".join(str(p) for p in first.get("loc", ())[1:]) or None
89
+ return JSONResponse(
90
+ status_code=400,
91
+ content={
92
+ "error": {
93
+ "message": first.get("msg", "Invalid request."),
94
+ "type": "invalid_request_error",
95
+ "param": location,
96
+ "code": None,
97
+ }
98
+ },
99
+ )
100
+
101
+ # --------------------------------------------------------------- health
102
+ @app.get("/healthz", tags=["ops"])
103
+ async def healthz() -> dict[str, Any]:
104
+ return {"status": "ok", "version": __version__}
105
+
106
+ @app.get("/readyz", tags=["ops"])
107
+ async def readyz(request: Request) -> JSONResponse:
108
+ current: AppState = request.app.state.cachellm
109
+ ready = current.cache_available
110
+ body = {
111
+ "status": "ready" if ready else "degraded",
112
+ "cache_available": current.cache_available,
113
+ "degraded_reason": current.degraded_reason or None,
114
+ "embedding_model": current.embedder.name,
115
+ }
116
+ # Degraded still returns 200: the proxy is serving, just without cache.
117
+ return JSONResponse(body, status_code=200)
118
+
119
+ @app.get("/metrics", tags=["ops"])
120
+ async def metrics(request: Request) -> Response:
121
+ current: AppState = request.app.state.cachellm
122
+ if not current.settings.metrics_enabled:
123
+ return Response(status_code=404)
124
+ if current.vectors is not None:
125
+ with contextlib.suppress(Exception):
126
+ current.metrics.entries.set(await current.vectors.count())
127
+ body, content_type = current.metrics.render()
128
+ return Response(content=body, media_type=content_type)
129
+
130
+ @app.get("/", include_in_schema=False)
131
+ async def root() -> dict[str, Any]:
132
+ return {
133
+ "name": "CacheLLM",
134
+ "version": __version__,
135
+ "docs": "/docs",
136
+ "openai_base_url": "/v1",
137
+ "endpoints": ["/v1/chat/completions", "/v1/models", "/admin/stats", "/metrics"],
138
+ }
139
+
140
+ return app
141
+
142
+
143
+ app = create_app # uvicorn factory entry point
cachellm/api/auth.py ADDED
@@ -0,0 +1,34 @@
1
+ """Client authentication.
2
+
3
+ The proxy holds provider credentials, so an unauthenticated proxy on a public
4
+ address is an open wallet. Keys are compared in constant time and never logged.
5
+ Auth is off only when no keys are configured, which is the local-dev case and
6
+ is reported loudly at startup.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hmac
12
+
13
+ from fastapi import Request
14
+
15
+ from cachellm.errors import AuthError
16
+
17
+
18
+ def extract_key(request: Request) -> str:
19
+ header = request.headers.get("authorization", "")
20
+ if header.lower().startswith("bearer "):
21
+ return header[7:].strip()
22
+ return request.headers.get("api-key", "").strip()
23
+
24
+
25
+ def verify(request: Request, keys: set[str]) -> None:
26
+ if not keys:
27
+ return
28
+ presented = extract_key(request)
29
+ if not presented:
30
+ raise AuthError("Missing API key. Pass it as `Authorization: Bearer <key>`.")
31
+ for known in keys:
32
+ if hmac.compare_digest(presented, known):
33
+ return
34
+ raise AuthError()
cachellm/api/deps.py ADDED
@@ -0,0 +1,106 @@
1
+ """Application state and its lifecycle.
2
+
3
+ One deliberate design choice here: the cache **fails open**. If Redis is
4
+ unreachable or the index cannot be built, the proxy keeps serving by forwarding
5
+ every request upstream and reports itself degraded. A cache that takes an
6
+ application down when it breaks is worse than no cache, and this is the first
7
+ thing a reviewer looks for.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+
15
+ import structlog
16
+ from fastapi import Request
17
+
18
+ from cachellm.cache.analytics import Analytics
19
+ from cachellm.cache.coalesce import SingleFlight
20
+ from cachellm.cache.exact_store import ExactStore
21
+ from cachellm.cache.redis_client import build_redis
22
+ from cachellm.cache.service import CacheService
23
+ from cachellm.cache.vector_store import VectorStore
24
+ from cachellm.embeddings import build_embedder
25
+ from cachellm.embeddings.base import Embedder
26
+ from cachellm.observability.metrics import Metrics, get_metrics
27
+ from cachellm.providers.registry import ProviderRegistry
28
+ from cachellm.settings import Settings
29
+
30
+ log = structlog.get_logger(__name__)
31
+
32
+
33
+ @dataclass
34
+ class AppState:
35
+ settings: Settings
36
+ metrics: Metrics
37
+ providers: ProviderRegistry
38
+ embedder: Embedder
39
+ singleflight: SingleFlight = field(default_factory=SingleFlight)
40
+ redis: Any = None
41
+ vectors: VectorStore | None = None
42
+ exact: ExactStore | None = None
43
+ analytics: Analytics | None = None
44
+ cache: CacheService | None = None
45
+ cache_available: bool = False
46
+ degraded_reason: str = ""
47
+
48
+ @property
49
+ def caching_on(self) -> bool:
50
+ return self.cache_available and self.cache is not None and self.settings.enabled
51
+
52
+
53
+ async def build_state(
54
+ settings: Settings,
55
+ *,
56
+ embedder: Embedder | None = None,
57
+ providers: ProviderRegistry | None = None,
58
+ ) -> AppState:
59
+ state = AppState(
60
+ settings=settings,
61
+ metrics=get_metrics(),
62
+ providers=providers or ProviderRegistry(settings),
63
+ embedder=embedder or build_embedder(settings),
64
+ )
65
+ try:
66
+ redis = build_redis(settings)
67
+ await redis.ping()
68
+ vectors = VectorStore(redis, settings)
69
+ await vectors.connect()
70
+ exact = ExactStore(redis, settings)
71
+ analytics = Analytics(redis, settings)
72
+ state.redis = redis
73
+ state.vectors = vectors
74
+ state.exact = exact
75
+ state.analytics = analytics
76
+ state.cache = CacheService(
77
+ settings=settings,
78
+ embedder=state.embedder,
79
+ vectors=vectors,
80
+ exact=exact,
81
+ analytics=analytics,
82
+ )
83
+ state.cache_available = True
84
+ except Exception as exc: # degrade, never crash the proxy
85
+ state.cache_available = False
86
+ state.degraded_reason = f"{type(exc).__name__}: {exc}"
87
+ log.error("cache_unavailable_failing_open", error=state.degraded_reason)
88
+
89
+ try:
90
+ await state.embedder.warmup()
91
+ except Exception as exc: # embeddings are optional for pass-through
92
+ log.error("embedder_warmup_failed", error=str(exc)[:300])
93
+ state.cache_available = False
94
+ state.degraded_reason = f"embedder unavailable: {exc}"
95
+
96
+ return state
97
+
98
+
99
+ async def shutdown_state(state: AppState) -> None:
100
+ await state.providers.close()
101
+ if state.redis is not None:
102
+ await state.redis.aclose()
103
+
104
+
105
+ def get_state(request: Request) -> AppState:
106
+ return request.app.state.cachellm # type: ignore[no-any-return]
@@ -0,0 +1,265 @@
1
+ """Operator endpoints: stats, invalidation, near-miss analysis, threshold tuning.
2
+
3
+ These are what turn the cache from a black box into something a team is willing
4
+ to leave switched on. Every one of them answers a question an operator actually
5
+ asks: what is it doing, what would a different threshold do, and how do I make
6
+ it forget something right now.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import structlog
15
+ from fastapi import APIRouter, Body, Query, Request
16
+ from pydantic import BaseModel, Field
17
+
18
+ from cachellm.api.auth import verify
19
+ from cachellm.api.deps import get_state
20
+ from cachellm.errors import CacheLLMError
21
+
22
+ log = structlog.get_logger(__name__)
23
+ router = APIRouter()
24
+
25
+ DEFAULT_SWEEP = [0.80, 0.84, 0.86, 0.88, 0.90, 0.92, 0.94, 0.96, 0.98, 0.99]
26
+
27
+
28
+ class InvalidateRequest(BaseModel):
29
+ namespace: str | None = None
30
+ model: str | None = None
31
+ all: bool = False
32
+
33
+
34
+ class LabelledPair(BaseModel):
35
+ a: str
36
+ b: str
37
+ duplicate: bool
38
+
39
+
40
+ class SweepRequest(BaseModel):
41
+ pairs: list[LabelledPair] = Field(min_length=1)
42
+ thresholds: list[float] | None = None
43
+
44
+
45
+ def _require_cache(request: Request) -> Any:
46
+ state = get_state(request)
47
+ if not state.cache_available or state.cache is None:
48
+ raise CacheLLMError(
49
+ 503,
50
+ f"Cache backend unavailable: {state.degraded_reason or 'not connected'}",
51
+ "api_error",
52
+ code="cache_unavailable",
53
+ )
54
+ return state
55
+
56
+
57
+ @router.get("/stats")
58
+ async def stats(request: Request) -> dict[str, Any]:
59
+ state = get_state(request)
60
+ if state.settings.require_auth_for_admin:
61
+ verify(request, state.settings.client_keys)
62
+ if not state.cache_available or state.cache is None:
63
+ return {
64
+ "cache_available": False,
65
+ "caching_enabled": state.settings.enabled,
66
+ "degraded_reason": state.degraded_reason or "backend not connected",
67
+ }
68
+ data = await state.cache.stats()
69
+ data["cache_available"] = True
70
+ data["caching_enabled"] = state.settings.enabled
71
+ data["shadow_mode"] = state.settings.shadow_mode
72
+ data["in_flight"] = state.singleflight.in_flight
73
+ state.metrics.entries.set(data.get("entries", 0))
74
+ return data
75
+
76
+
77
+ @router.get("/config")
78
+ async def config(request: Request) -> dict[str, Any]:
79
+ state = get_state(request)
80
+ if state.settings.require_auth_for_admin:
81
+ verify(request, state.settings.client_keys)
82
+ s = state.settings
83
+ return {
84
+ "enabled": s.enabled,
85
+ "shadow_mode": s.shadow_mode,
86
+ "embedding": {
87
+ "backend": s.embedding_backend,
88
+ "model": s.embedding_model,
89
+ "dim": s.expected_dim(),
90
+ },
91
+ "thresholds": {
92
+ c: s.threshold_for(c)
93
+ for c in (
94
+ "factual",
95
+ "classification",
96
+ "creative",
97
+ "volatile",
98
+ "conversational",
99
+ "default",
100
+ )
101
+ },
102
+ "ttl_seconds": {
103
+ c: s.ttl_for(c)
104
+ for c in (
105
+ "factual",
106
+ "classification",
107
+ "creative",
108
+ "volatile",
109
+ "conversational",
110
+ "default",
111
+ )
112
+ },
113
+ "rules": {
114
+ "max_cacheable_temperature": s.max_cacheable_temperature,
115
+ "cache_multi_turn": s.cache_multi_turn,
116
+ "cache_json_mode": s.cache_json_mode,
117
+ "cache_tool_calls": s.cache_tool_calls,
118
+ "pii_guard": s.pii_guard,
119
+ "strip_filler_words": s.strip_filler_words,
120
+ "max_prompt_chars": s.max_prompt_chars,
121
+ },
122
+ "default_provider": s.default_provider,
123
+ "auth_enabled": s.auth_enabled,
124
+ }
125
+
126
+
127
+ @router.post("/invalidate")
128
+ async def invalidate(request: Request, body: InvalidateRequest) -> dict[str, Any]:
129
+ state = _require_cache(request)
130
+ if state.settings.require_auth_for_admin:
131
+ verify(request, state.settings.client_keys)
132
+ if not (body.namespace or body.model or body.all):
133
+ raise CacheLLMError(400, "Pass one of `namespace`, `model` or `all`.", param="namespace")
134
+ removed = await state.cache.invalidate(
135
+ namespace=body.namespace, model=body.model, drop_all=body.all
136
+ )
137
+ return {
138
+ "removed_keys": removed,
139
+ "namespace": body.namespace,
140
+ "model": body.model,
141
+ "all": body.all,
142
+ }
143
+
144
+
145
+ @router.get("/near-misses")
146
+ async def near_misses(request: Request, limit: int = Query(50, ge=1, le=500)) -> dict[str, Any]:
147
+ state = _require_cache(request)
148
+ if state.settings.require_auth_for_admin:
149
+ verify(request, state.settings.client_keys)
150
+ rows = await state.analytics.near_misses(limit=limit)
151
+ return {"count": len(rows), "near_misses": rows}
152
+
153
+
154
+ @router.get("/near-miss-histogram")
155
+ async def near_miss_histogram(
156
+ request: Request, buckets: int = Query(20, ge=5, le=100)
157
+ ) -> dict[str, Any]:
158
+ """Cumulative view: how many more requests each lower threshold would serve."""
159
+ state = _require_cache(request)
160
+ if state.settings.require_auth_for_admin:
161
+ verify(request, state.settings.client_keys)
162
+ return {"histogram": await state.analytics.near_miss_histogram(buckets=buckets)}
163
+
164
+
165
+ @router.get("/entries")
166
+ async def entries(request: Request, limit: int = Query(20, ge=1, le=200)) -> dict[str, Any]:
167
+ state = _require_cache(request)
168
+ if state.settings.require_auth_for_admin:
169
+ verify(request, state.settings.client_keys)
170
+ keys = await state.vectors.scan_keys()
171
+ rows = []
172
+ for key in keys[:limit]:
173
+ entry_id = key.split(state.settings.entry_prefix, 1)[-1]
174
+ entry = await state.vectors.get(entry_id)
175
+ if entry is None:
176
+ continue
177
+ summary = entry.summary()
178
+ if not state.settings.log_prompts:
179
+ summary["prompt"] = summary["prompt"][:120]
180
+ summary["response_text"] = summary["response_text"][:160]
181
+ rows.append(summary)
182
+ return {"total_keys": len(keys), "returned": len(rows), "entries": rows}
183
+
184
+
185
+ @router.post("/reset-stats")
186
+ async def reset_stats(request: Request) -> dict[str, str]:
187
+ state = _require_cache(request)
188
+ if state.settings.require_auth_for_admin:
189
+ verify(request, state.settings.client_keys)
190
+ await state.analytics.reset()
191
+ return {"status": "counters reset"}
192
+
193
+
194
+ @router.post("/threshold-sweep")
195
+ async def threshold_sweep(request: Request, body: SweepRequest = Body(...)) -> dict[str, Any]:
196
+ """Score labelled prompt pairs at several thresholds.
197
+
198
+ This is the answer to the only question that really matters when tuning a
199
+ semantic cache: at threshold X, how many extra requests do I serve, and how
200
+ many of those are actually different questions wearing similar words?
201
+
202
+ ``precision`` is the share of would-be cache hits that were genuine
203
+ duplicates. ``false_positive_rate`` is the share of *non*-duplicate pairs
204
+ that would wrongly hit. Read them together: hit rate you can only spend if
205
+ precision holds.
206
+ """
207
+ state = get_state(request)
208
+ if state.settings.require_auth_for_admin:
209
+ verify(request, state.settings.client_keys)
210
+
211
+ thresholds = sorted(body.thresholds or DEFAULT_SWEEP)
212
+ texts_a = [p.a for p in body.pairs]
213
+ texts_b = [p.b for p in body.pairs]
214
+ labels = np.array([p.duplicate for p in body.pairs], dtype=bool)
215
+
216
+ vectors_a = await state.embedder.embed_batch(texts_a)
217
+ vectors_b = await state.embedder.embed_batch(texts_b)
218
+ sims = np.array(
219
+ [float(np.dot(a, b)) for a, b in zip(vectors_a, vectors_b, strict=True)],
220
+ dtype=np.float32,
221
+ )
222
+
223
+ positives = int(labels.sum())
224
+ negatives = int((~labels).sum())
225
+ rows: list[dict[str, Any]] = []
226
+ for threshold in thresholds:
227
+ predicted = sims >= threshold
228
+ tp = int((predicted & labels).sum())
229
+ fp = int((predicted & ~labels).sum())
230
+ fn = int((~predicted & labels).sum())
231
+ precision = tp / (tp + fp) if (tp + fp) else 1.0
232
+ recall = tp / positives if positives else 0.0
233
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
234
+ rows.append(
235
+ {
236
+ "threshold": round(threshold, 4),
237
+ "would_hit": tp + fp,
238
+ "hit_rate": round((tp + fp) / len(body.pairs), 4),
239
+ "true_hits": tp,
240
+ "false_hits": fp,
241
+ "missed_duplicates": fn,
242
+ "precision": round(precision, 4),
243
+ "recall": round(recall, 4),
244
+ "f1": round(f1, 4),
245
+ "false_positive_rate": round(fp / negatives, 4) if negatives else 0.0,
246
+ }
247
+ )
248
+
249
+ best = max(rows, key=lambda r: r["f1"]) if rows else None
250
+ safe = [r for r in rows if r["false_positive_rate"] <= 0.01]
251
+ return {
252
+ "pairs": len(body.pairs),
253
+ "duplicates": positives,
254
+ "non_duplicates": negatives,
255
+ "embedding_model": state.embedder.name,
256
+ "similarity": {
257
+ "duplicates_mean": round(float(sims[labels].mean()), 4) if positives else None,
258
+ "non_duplicates_mean": round(float(sims[~labels].mean()), 4) if negatives else None,
259
+ },
260
+ "sweep": rows,
261
+ "best_f1": best,
262
+ "lowest_threshold_under_1pct_false_positives": (
263
+ min(safe, key=lambda r: r["threshold"]) if safe else None
264
+ ),
265
+ }