cacheverifier 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.
@@ -0,0 +1,22 @@
1
+ """cacheverifier -- Python client for the hosted CacheVerifier API.
2
+
3
+ CacheVerifier verifies semantic-cache hits: given a query and a candidate
4
+ cached answer, it approves or rejects serving that answer from cache, so a
5
+ similarity match that is close but wrong doesn't become a silent error.
6
+
7
+ from cacheverifier import CacheVerifier
8
+
9
+ cv = CacheVerifier(api_key="cv_...")
10
+ if cv.verify(query, candidate_answer).approved:
11
+ ... # serve from cache
12
+ else:
13
+ ... # fall through to your LLM
14
+
15
+ Docs: https://www.cacheverifier.com/docs
16
+ Research behind it: https://github.com/imxinchengyou/CacheVerifier
17
+ """
18
+
19
+ from cacheverifier.client import CacheVerifier, CacheVerifierError, VerifyResult
20
+
21
+ __version__ = "0.1.0"
22
+ __all__ = ["CacheVerifier", "CacheVerifierError", "VerifyResult", "__version__"]
@@ -0,0 +1,278 @@
1
+ """Thin Python client for the hosted CacheVerifier API (https://www.cacheverifier.com).
2
+
3
+ CacheVerifier does not run your semantic cache or do similarity search. Your
4
+ cache backend does its own lookup first; you call `verify()` only on the
5
+ candidates in the similarity "gray zone", where a plain threshold match
6
+ might be wrong. See https://www.cacheverifier.com/docs for the full API.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Iterable, Sequence
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ import httpx
16
+
17
+ DEFAULT_BASE_URL = "https://www.cacheverifier.com"
18
+ DEFAULT_TIMEOUT = 10.0
19
+
20
+ __all__ = ["CacheVerifier", "CacheVerifierError", "VerifyResult"]
21
+
22
+
23
+ class CacheVerifierError(RuntimeError):
24
+ """Raised for any non-2xx response from the API.
25
+
26
+ `status_code` is the HTTP status; `detail` is the server's error message
27
+ (the JSON body's `detail` field when present, otherwise the raw text).
28
+ """
29
+
30
+ def __init__(self, status_code: int, detail: str) -> None:
31
+ super().__init__(f"CacheVerifier API error {status_code}: {detail}")
32
+ self.status_code = status_code
33
+ self.detail = detail
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class VerifyResult:
38
+ """One `/v1/verify` decision.
39
+
40
+ - `approved`: serve the cached answer (True) or fall through to your LLM (False).
41
+ - `score`: the verifier's raw score for this pair; `approved` is `score >= threshold`.
42
+ - `threshold`: the cutoff this call was decided against (tenant-specific once
43
+ you've fine-tuned; 0.0 on the shared stock model).
44
+ - `model_version`: `"stock"`, `"v<id>"` for a fine-tuned model, or
45
+ `"cold_start_fail_closed"` when no model ran (see `cold_start_mode`).
46
+ - `latency_ms`: server-side model inference time, not round-trip time.
47
+ """
48
+
49
+ approved: bool
50
+ score: float
51
+ threshold: float
52
+ model_version: str
53
+ latency_ms: float
54
+
55
+ @classmethod
56
+ def _from_json(cls, d: dict[str, Any]) -> VerifyResult:
57
+ return cls(
58
+ approved=bool(d["approved"]),
59
+ score=float(d["score"]),
60
+ threshold=float(d["threshold"]),
61
+ model_version=str(d["model_version"]),
62
+ latency_ms=float(d["latency_ms"]),
63
+ )
64
+
65
+
66
+ class CacheVerifier:
67
+ """Client for the hosted CacheVerifier API.
68
+
69
+ from cacheverifier import CacheVerifier
70
+
71
+ cv = CacheVerifier(api_key="cv_...")
72
+ result = cv.verify("how do I cancel", "Go to Settings > Billing > Pause.")
73
+ if result.approved:
74
+ ... # serve the cached answer
75
+ else:
76
+ ... # fall through to your LLM
77
+
78
+ Usable as a context manager (`with CacheVerifier(...) as cv:`) to close
79
+ the underlying HTTP connection pool deterministically.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ api_key: str,
85
+ *,
86
+ base_url: str = DEFAULT_BASE_URL,
87
+ timeout: float = DEFAULT_TIMEOUT,
88
+ transport: httpx.BaseTransport | None = None,
89
+ ) -> None:
90
+ if not api_key:
91
+ raise ValueError("api_key is required -- get one at https://www.cacheverifier.com")
92
+ self._client = httpx.Client(
93
+ base_url=base_url.rstrip("/"),
94
+ headers={"X-API-Key": api_key, "User-Agent": _user_agent()},
95
+ timeout=timeout,
96
+ transport=transport,
97
+ )
98
+
99
+ # -- lifecycle ---------------------------------------------------------
100
+
101
+ def close(self) -> None:
102
+ self._client.close()
103
+
104
+ def __enter__(self) -> CacheVerifier:
105
+ return self
106
+
107
+ def __exit__(self, *_exc: object) -> None:
108
+ self.close()
109
+
110
+ # -- core: verify ----------------------------------------------------
111
+
112
+ def verify(self, query: str, candidate_answer: str) -> VerifyResult:
113
+ """Approve or reject one gray-zone cache hit. `POST /v1/verify`."""
114
+ data = self._post("/v1/verify", json={"query": query, "candidate_answer": candidate_answer})
115
+ return VerifyResult._from_json(data)
116
+
117
+ def verify_batch(self, pairs: Sequence[tuple[str, str]]) -> list[VerifyResult]:
118
+ """Verify many `(query, candidate_answer)` pairs in one request and
119
+ one batched forward pass. `POST /v1/verify/batch` (max 100 items).
120
+
121
+ Useful when your own retrieval returns several close candidates:
122
+ send them in rank order and take the first `approved` one.
123
+ """
124
+ items = [{"query": q, "candidate_answer": a} for q, a in pairs]
125
+ data = self._post("/v1/verify/batch", json={"items": items})
126
+ return [VerifyResult._from_json(r) for r in data["results"]]
127
+
128
+ # -- feedback ------------------------------------------------------
129
+
130
+ def feedback(
131
+ self,
132
+ query: str,
133
+ candidate_answer: str,
134
+ was_correct: bool,
135
+ *,
136
+ similarity_score: float | None = None,
137
+ stale: bool = False,
138
+ idempotency_key: str | None = None,
139
+ ) -> int:
140
+ """Record whether a served (or considered) hit was actually correct.
141
+ Returns the created row id. `POST /v1/feedback`.
142
+
143
+ This is the ground-truth signal fine-tuning and drift monitoring
144
+ learn from -- you supply it from a thumbs-down, a reopened ticket,
145
+ manual review, etc.
146
+
147
+ - `similarity_score`: your cache backend's own score for this
148
+ candidate, if you still have it. Only used by
149
+ `gray_zone_threshold()`; safe to omit.
150
+ - `stale`: set instead of a bare `was_correct=False` when the answer
151
+ is wrong ONLY because a fact changed (price, date, ...), not a
152
+ semantic mismatch. Stale rows are excluded from training and drift.
153
+ - `idempotency_key`: pass a stable key to make retries safe.
154
+ """
155
+ payload: dict[str, Any] = {
156
+ "query": query,
157
+ "candidate_answer": candidate_answer,
158
+ "was_correct": was_correct,
159
+ "stale": stale,
160
+ }
161
+ if similarity_score is not None:
162
+ payload["similarity_score"] = similarity_score
163
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
164
+ return int(self._post("/v1/feedback", json=payload, headers=headers)["id"])
165
+
166
+ def feedback_batch(self, items: Iterable[dict[str, Any]]) -> list[int]:
167
+ """Bulk-upload feedback rows (max 500). Each item is a dict with
168
+ `query`, `candidate_answer`, `was_correct`, and optionally
169
+ `similarity_score` / `stale`. Returns the created row ids.
170
+ `POST /v1/feedback/batch`.
171
+ """
172
+ data = self._post("/v1/feedback/batch", json={"items": list(items)})
173
+ return [int(i) for i in data["ids"]]
174
+
175
+ # -- fine-tuning -------------------------------------------------
176
+
177
+ def finetune(self, *, target_risk: float | None = None, cost_ratio: float | None = None) -> dict[str, Any]:
178
+ """Kick off fine-tuning on all feedback submitted so far. Returns the
179
+ job (poll `get_finetune_job(job['id'])` for completion + AUC).
180
+ `POST /v1/finetune/jobs`.
181
+
182
+ - `target_risk`: e.g. `0.01` to also certify a threshold at 1%
183
+ false-reuse risk via Conformal Risk Control.
184
+ - `cost_ratio`: pick the live threshold by cost (how many times more
185
+ a wrong reuse costs than a miss) instead of the default Youden's J.
186
+ """
187
+ return self._post("/v1/finetune/jobs", params=_drop_none(target_risk=target_risk, cost_ratio=cost_ratio))
188
+
189
+ def dry_run(
190
+ self,
191
+ examples: Sequence[dict[str, Any]],
192
+ *,
193
+ target_risk: float | None = None,
194
+ cost_ratio: float | None = None,
195
+ ) -> dict[str, Any]:
196
+ """Baseline-vs-fine-tuned AUC on `examples` given directly here --
197
+ nothing is written to your feedback history and no model is
198
+ deployed. Each example is a dict with `query`, `candidate_answer`,
199
+ `was_correct` (and optional `stale`). `POST /v1/finetune/dry-run`.
200
+ """
201
+ return self._post(
202
+ "/v1/finetune/dry-run",
203
+ json={"examples": list(examples)},
204
+ params=_drop_none(target_risk=target_risk, cost_ratio=cost_ratio),
205
+ )
206
+
207
+ def get_finetune_job(self, job_id: int) -> dict[str, Any]:
208
+ """`GET /v1/finetune/jobs/{id}`."""
209
+ return self._get(f"/v1/finetune/jobs/{job_id}")
210
+
211
+ def list_finetune_jobs(self) -> list[dict[str, Any]]:
212
+ """`GET /v1/finetune/jobs`."""
213
+ return self._get("/v1/finetune/jobs")
214
+
215
+ def activate_model_version(self, model_version_id: int) -> dict[str, Any]:
216
+ """Promote a model that finished as `held_for_review`.
217
+ `POST /v1/finetune/model-versions/{id}/activate`.
218
+ """
219
+ return self._post(f"/v1/finetune/model-versions/{model_version_id}/activate")
220
+
221
+ # -- monitoring / usage ----------------------------------------
222
+
223
+ def drift_status(self) -> dict[str, Any]:
224
+ """`GET /v1/monitor/drift-status` (needs an active fine-tuned model)."""
225
+ return self._get("/v1/monitor/drift-status")
226
+
227
+ def gray_zone_threshold(self) -> dict[str, Any]:
228
+ """Recommend a tau_high for YOUR cache backend, replaying feedback
229
+ rows that included `similarity_score`. `GET /v1/monitor/gray-zone-threshold`.
230
+ """
231
+ return self._get("/v1/monitor/gray-zone-threshold")
232
+
233
+ def usage(self) -> dict[str, Any]:
234
+ """`GET /v1/usage/status`."""
235
+ return self._get("/v1/usage/status")
236
+
237
+ def savings(self) -> dict[str, Any]:
238
+ """This period's estimated LLM calls and wrong hits avoided.
239
+ `GET /v1/usage/savings`.
240
+ """
241
+ return self._get("/v1/usage/savings")
242
+
243
+ # -- plumbing ----------------------------------------------------
244
+
245
+ def _get(self, path: str) -> Any:
246
+ return self._unwrap(self._client.get(path))
247
+
248
+ def _post(
249
+ self,
250
+ path: str,
251
+ *,
252
+ json: Any | None = None,
253
+ params: dict[str, Any] | None = None,
254
+ headers: dict[str, str] | None = None,
255
+ ) -> Any:
256
+ return self._unwrap(self._client.post(path, json=json, params=params or None, headers=headers))
257
+
258
+ @staticmethod
259
+ def _unwrap(resp: httpx.Response) -> Any:
260
+ if resp.is_success:
261
+ return resp.json()
262
+ detail: str
263
+ try:
264
+ body = resp.json()
265
+ detail = body["detail"] if isinstance(body, dict) and "detail" in body else resp.text
266
+ except ValueError:
267
+ detail = resp.text
268
+ raise CacheVerifierError(resp.status_code, str(detail))
269
+
270
+
271
+ def _drop_none(**kwargs: Any) -> dict[str, Any]:
272
+ return {k: v for k, v in kwargs.items() if v is not None}
273
+
274
+
275
+ def _user_agent() -> str:
276
+ from cacheverifier import __version__
277
+
278
+ return f"cacheverifier-python/{__version__} httpx/{httpx.__version__}"
@@ -0,0 +1,7 @@
1
+ """Cache-backend integrations for CacheVerifier.
2
+
3
+ Each integration wraps `cacheverifier.CacheVerifier` in the extension point
4
+ a given cache library already exposes, so verification is a drop-in change
5
+ with no upstream fork required. GPTCache is the first; the same pattern
6
+ applies to any backend with a "should I trust this candidate" hook.
7
+ """
@@ -0,0 +1,84 @@
1
+ """GPTCache integration: a `SimilarityEvaluation` that calls the hosted
2
+ CacheVerifier `/v1/verify` endpoint instead of relying on cosine/dot-product
3
+ similarity alone.
4
+
5
+ GPTCache's `SimilarityEvaluation` interface (`evaluation()` + `range()`) is
6
+ a zero-friction integration point: any subclass drops straight into a
7
+ GPTCache pipeline's `similarity_evaluation=` argument, no upstream PR
8
+ required.
9
+
10
+ from gptcache import cache
11
+ from cacheverifier.integrations.gptcache import CacheVerifierEvaluation
12
+
13
+ cache.init(
14
+ similarity_evaluation=CacheVerifierEvaluation(api_key="cv_..."),
15
+ ...
16
+ )
17
+
18
+ `gptcache` is an optional dependency -- `pip install "cacheverifier[gptcache]"`.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Any
24
+
25
+ from cacheverifier.client import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, CacheVerifier
26
+
27
+ try:
28
+ from gptcache.similarity_evaluation import SimilarityEvaluation as _GPTCacheBase
29
+ except ImportError: # keep importable without gptcache installed
30
+ _GPTCacheBase = object
31
+
32
+
33
+ class CacheVerifierEvaluation(_GPTCacheBase):
34
+ """Drop-in replacement for GPTCache's built-in similarity evaluators.
35
+
36
+ `evaluation()` returns 1.0 (reuse) or 0.0 (don't) -- binary, because the
37
+ hosted verifier already makes a binary approve/reject call per gray-zone
38
+ hit rather than a softened similarity score. Callers who want GPTCache's
39
+ own threshold logic on top can wrap this rather than replace it.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ api_key: str,
45
+ *,
46
+ base_url: str = DEFAULT_BASE_URL,
47
+ timeout: float = DEFAULT_TIMEOUT,
48
+ ) -> None:
49
+ self._cv = CacheVerifier(api_key=api_key, base_url=base_url, timeout=timeout)
50
+
51
+ def evaluation(self, src_dict: dict[str, Any], cache_dict: dict[str, Any], **_kwargs: Any) -> float:
52
+ query = src_dict.get("question") or src_dict.get("query", "")
53
+ candidate_answer = cache_dict.get("answer", "")
54
+ return 1.0 if self._cv.verify(query, candidate_answer).approved else 0.0
55
+
56
+ def range(self) -> tuple[float, float]:
57
+ return 0.0, 1.0
58
+
59
+ def report_feedback(
60
+ self,
61
+ query: str,
62
+ candidate_answer: str,
63
+ was_correct: bool,
64
+ similarity_score: float | None = None,
65
+ ) -> None:
66
+ """Not part of GPTCache's interface -- call it once you know whether
67
+ a served hit was actually correct (a thumbs-down, a reopened
68
+ ticket, ...). Feeds `POST /v1/feedback`, which fine-tuning and drift
69
+ monitoring both consume.
70
+
71
+ `similarity_score` is optional: pass GPTCache's own vector-search
72
+ score for this candidate if you still have it when you learn
73
+ `was_correct` -- that is what lets the service later recommend a
74
+ tau_high for your GPTCache config (`gray_zone_threshold()`).
75
+ """
76
+ self._cv.feedback(
77
+ query,
78
+ candidate_answer,
79
+ was_correct,
80
+ similarity_score=similarity_score,
81
+ )
82
+
83
+ def close(self) -> None:
84
+ self._cv.close()
cacheverifier/py.typed ADDED
File without changes
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.5
2
+ Name: cacheverifier
3
+ Version: 0.1.0
4
+ Summary: Python client for the hosted CacheVerifier semantic-cache verification API
5
+ Project-URL: Homepage, https://www.cacheverifier.com
6
+ Project-URL: Documentation, https://www.cacheverifier.com/docs
7
+ Project-URL: Source, https://github.com/imxinchengyou/cacheverifier-python
8
+ Project-URL: Research, https://github.com/imxinchengyou/CacheVerifier
9
+ Author: Chengyou Xin
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: cache-verification,cross-encoder,gptcache,llm,llm-caching,semantic-cache,semantic-cache-verification
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: httpx>=0.24
21
+ Provides-Extra: dev
22
+ Requires-Dist: mypy>=1.5; extra == 'dev'
23
+ Requires-Dist: pytest>=7; extra == 'dev'
24
+ Requires-Dist: ruff>=0.4; extra == 'dev'
25
+ Provides-Extra: gptcache
26
+ Requires-Dist: gptcache>=0.1.30; extra == 'gptcache'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # cacheverifier
30
+
31
+ [![CI](https://github.com/imxinchengyou/cacheverifier-python/actions/workflows/ci.yml/badge.svg)](https://github.com/imxinchengyou/cacheverifier-python/actions/workflows/ci.yml)
32
+ [![PyPI](https://img.shields.io/pypi/v/cacheverifier)](https://pypi.org/project/cacheverifier/)
33
+ [![Python](https://img.shields.io/pypi/pyversions/cacheverifier)](https://pypi.org/project/cacheverifier/)
34
+
35
+ Python client for **[CacheVerifier](https://www.cacheverifier.com)** — a hosted API that
36
+ verifies semantic-cache hits. Given a query and a candidate cached answer, it approves or
37
+ rejects serving that answer from cache, so a similarity match that is *close but wrong*
38
+ doesn't become a silent error in your app.
39
+
40
+ CacheVerifier does **not** run your cache or do similarity search. Your cache backend does
41
+ its own lookup first; you call `verify()` only on the candidates in the similarity "gray
42
+ zone", where a plain threshold match might be wrong.
43
+
44
+ - Docs / API reference: <https://www.cacheverifier.com/docs>
45
+ - Why similarity ≠ correctness: <https://www.cacheverifier.com/why-similarity-fails>
46
+ - The research behind it (paper + benchmarks): <https://github.com/imxinchengyou/CacheVerifier>
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install cacheverifier
52
+ # with the GPTCache adapter:
53
+ pip install "cacheverifier[gptcache]"
54
+ ```
55
+
56
+ Requires Python 3.9+. The only runtime dependency is `httpx`.
57
+
58
+ ## Quickstart
59
+
60
+ Get a free API key at <https://www.cacheverifier.com> (self-serve verify and fine-tuning
61
+ are free forever, no card).
62
+
63
+ ```python
64
+ from cacheverifier import CacheVerifier
65
+
66
+ cv = CacheVerifier(api_key="cv_...")
67
+
68
+ query = "how do I cancel my subscription"
69
+ candidate = "Go to Settings > Billing > Pause subscription for a month." # from your cache
70
+
71
+ result = cv.verify(query, candidate)
72
+ if result.approved:
73
+ answer = candidate # verified hit — skip the LLM call
74
+ else:
75
+ answer = call_your_llm(query) # not trustworthy — fall through
76
+
77
+ # Later, once you know if it was actually right (thumbs-down, reopened ticket, ...):
78
+ cv.feedback(query, answer, was_correct=True, similarity_score=0.86)
79
+ ```
80
+
81
+ `verify()` returns a `VerifyResult`:
82
+
83
+ | field | meaning |
84
+ |---|---|
85
+ | `approved` | serve the cached answer (`True`) or fall through (`False`) |
86
+ | `score` / `threshold` | `approved` is `score >= threshold` |
87
+ | `model_version` | `"stock"`, `"v<id>"` (fine-tuned), or `"cold_start_fail_closed"` |
88
+ | `latency_ms` | server-side inference time |
89
+
90
+ ## GPTCache
91
+
92
+ Drop the verifier into a GPTCache pipeline as its similarity evaluator — no fork required:
93
+
94
+ ```python
95
+ from gptcache import cache
96
+ from cacheverifier.integrations.gptcache import CacheVerifierEvaluation
97
+
98
+ evaluator = CacheVerifierEvaluation(api_key="cv_...")
99
+ cache.init(similarity_evaluation=evaluator, ...)
100
+
101
+ # when you learn a served hit's real outcome:
102
+ evaluator.report_feedback(query, answer, was_correct=False, similarity_score=0.9)
103
+ ```
104
+
105
+ See [`examples/gptcache_example.py`](examples/gptcache_example.py).
106
+
107
+ ## Fine-tuning
108
+
109
+ Once you have ~20+ feedback rows (the service found fine-tuning is often a net negative
110
+ below ~1,000 on the hardest data — see the [research](https://github.com/imxinchengyou/CacheVerifier)),
111
+ train a verifier on your own gray-zone labels:
112
+
113
+ ```python
114
+ job = cv.finetune() # or cv.finetune(target_risk=0.01, cost_ratio=5.0)
115
+ job = cv.get_finetune_job(job["id"]) # poll until status == "done"
116
+ print(job["auc_baseline"], job["auc_tuned"])
117
+
118
+ # a model can finish as "held_for_review" — promote it explicitly:
119
+ if job.get("result_model_version"):
120
+ cv.activate_model_version(job["result_model_version"])
121
+ ```
122
+
123
+ `cv.dry_run([...])` reports the same baseline-vs-tuned AUC on examples you pass directly,
124
+ without writing anything or deploying a model.
125
+
126
+ ## API surface
127
+
128
+ | method | endpoint |
129
+ |---|---|
130
+ | `verify(query, candidate_answer)` | `POST /v1/verify` |
131
+ | `verify_batch(pairs)` | `POST /v1/verify/batch` |
132
+ | `feedback(...)` / `feedback_batch(items)` | `POST /v1/feedback` / `/batch` |
133
+ | `finetune(...)` / `dry_run(examples, ...)` | `POST /v1/finetune/jobs` / `/dry-run` |
134
+ | `get_finetune_job(id)` / `list_finetune_jobs()` | `GET /v1/finetune/jobs[/id]` |
135
+ | `activate_model_version(id)` | `POST /v1/finetune/model-versions/{id}/activate` |
136
+ | `drift_status()` | `GET /v1/monitor/drift-status` |
137
+ | `gray_zone_threshold()` | `GET /v1/monitor/gray-zone-threshold` |
138
+ | `usage()` / `savings()` | `GET /v1/usage/status` / `/savings` |
139
+
140
+ Non-2xx responses raise `CacheVerifierError` (`.status_code`, `.detail`).
141
+
142
+ ## License
143
+
144
+ MIT — see [`LICENSE`](LICENSE). (The [research repository](https://github.com/imxinchengyou/CacheVerifier)
145
+ is separately licensed; this client is not.)
@@ -0,0 +1,9 @@
1
+ cacheverifier/__init__.py,sha256=LpVluxpTzZj0ibaiJ3t6qZiJALwistUrwrblW3bSAS0,810
2
+ cacheverifier/client.py,sha256=9jwg8cR6ilLxwgGiTbtAeGD5tG0ZAelOyWu22kkbJwk,10654
3
+ cacheverifier/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ cacheverifier/integrations/__init__.py,sha256=KGmIlBfvEaCTAUfYu-0BQO11C9y26TRnpfkfFq3__cY,345
5
+ cacheverifier/integrations/gptcache.py,sha256=rb7aCTawddQJrqYfV-lV0fJTPwo1v6xlThASTJjL-uQ,3034
6
+ cacheverifier-0.1.0.dist-info/METADATA,sha256=7H9Cs8tnAC8_8AuLg4pA5VzeyTTYKijFZMU19_WUfBs,5852
7
+ cacheverifier-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
8
+ cacheverifier-0.1.0.dist-info/licenses/LICENSE,sha256=fzYJYKhCMP-AdgdQm_oMP0J-8qF4k2bSC_t1d_8Ag3o,1069
9
+ cacheverifier-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chengyou Xin
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.