cacheverifier 0.4.0__tar.gz → 0.4.1__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.
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/CHANGELOG.md +13 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/PKG-INFO +8 -6
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/README.md +7 -5
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/__init__.py +1 -1
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/client.py +23 -14
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/pyproject.toml +1 -1
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/tests/test_client.py +25 -3
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/.github/workflows/ci.yml +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/.github/workflows/publish.yml +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/.gitignore +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/LICENSE +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/__main__.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/_healthcheck/__init__.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/_healthcheck/_base_models.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/_healthcheck/_finetune.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/_healthcheck/cli.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/integrations/__init__.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/integrations/gptcache.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/py.typed +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/examples/gptcache_example.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/examples/quickstart.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/tests/test_gptcache_integration.py +0 -0
- {cacheverifier-0.4.0 → cacheverifier-0.4.1}/tests/test_healthcheck.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
`verify()` and `verify_batch()` now treat a **rate-limited 429** (one with a
|
|
6
|
+
`Retry-After` header) like a 5xx: they log a warning and return a `degraded`
|
|
7
|
+
result (`approved=False` by default, per `fail_open`) instead of raising
|
|
8
|
+
`CacheVerifierError`. A rate limit is transient load on your request path, so
|
|
9
|
+
it should fall through to your LLM like an outage rather than throw. A 429
|
|
10
|
+
*without* `Retry-After` means the monthly verify quota is used up and still
|
|
11
|
+
raises, as do other 4xx responses (bad key, bad request).
|
|
12
|
+
`CacheVerifierError` gains a `retry_after` attribute. Upgrade before the
|
|
13
|
+
hosted service switches its rate-limit response from 503 to the documented
|
|
14
|
+
429.
|
|
15
|
+
|
|
3
16
|
## 0.4.0
|
|
4
17
|
|
|
5
18
|
`cacheverifier healthcheck --base-model` picks the model the local Health
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cacheverifier
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Python client for the hosted CacheVerifier semantic-cache verification API
|
|
5
5
|
Project-URL: Homepage, https://www.cacheverifier.com
|
|
6
6
|
Project-URL: Documentation, https://www.cacheverifier.com/docs
|
|
@@ -103,11 +103,13 @@ cv.feedback(query, answer, was_correct=True, similarity_score=0.86)
|
|
|
103
103
|
fine-tuning / feedback / monitoring calls. Warm verification is tens of
|
|
104
104
|
milliseconds server-side; 1s covers the network round trip and a cold model
|
|
105
105
|
load after a deploy without letting a stuck verifier stall your request.
|
|
106
|
-
- **Fails closed.** On a timeout, connection error,
|
|
107
|
-
raise — it logs a warning on
|
|
108
|
-
`VerifyResult` with `degraded=True`
|
|
109
|
-
to your LLM exactly as you would on
|
|
110
|
-
|
|
106
|
+
- **Fails closed.** On a timeout, connection error, 5xx, or a rate-limited 429
|
|
107
|
+
(one carrying `Retry-After`), `verify()` does not raise — it logs a warning on
|
|
108
|
+
the `cacheverifier` logger and returns a `VerifyResult` with `degraded=True`
|
|
109
|
+
and `approved=False`, so you fall through to your LLM exactly as you would on
|
|
110
|
+
a cache miss. Other `4xx` responses still raise `CacheVerifierError`: a bad
|
|
111
|
+
key, a bad request, or a 429 without `Retry-After`, which means your monthly
|
|
112
|
+
verify quota is used up.
|
|
111
113
|
|
|
112
114
|
```python
|
|
113
115
|
cv = CacheVerifier(api_key="cv_...", verify_timeout=1.0, fail_open=False) # the defaults
|
|
@@ -71,11 +71,13 @@ cv.feedback(query, answer, was_correct=True, similarity_score=0.86)
|
|
|
71
71
|
fine-tuning / feedback / monitoring calls. Warm verification is tens of
|
|
72
72
|
milliseconds server-side; 1s covers the network round trip and a cold model
|
|
73
73
|
load after a deploy without letting a stuck verifier stall your request.
|
|
74
|
-
- **Fails closed.** On a timeout, connection error,
|
|
75
|
-
raise — it logs a warning on
|
|
76
|
-
`VerifyResult` with `degraded=True`
|
|
77
|
-
to your LLM exactly as you would on
|
|
78
|
-
|
|
74
|
+
- **Fails closed.** On a timeout, connection error, 5xx, or a rate-limited 429
|
|
75
|
+
(one carrying `Retry-After`), `verify()` does not raise — it logs a warning on
|
|
76
|
+
the `cacheverifier` logger and returns a `VerifyResult` with `degraded=True`
|
|
77
|
+
and `approved=False`, so you fall through to your LLM exactly as you would on
|
|
78
|
+
a cache miss. Other `4xx` responses still raise `CacheVerifierError`: a bad
|
|
79
|
+
key, a bad request, or a 429 without `Retry-After`, which means your monthly
|
|
80
|
+
verify quota is used up.
|
|
79
81
|
|
|
80
82
|
```python
|
|
81
83
|
cv = CacheVerifier(api_key="cv_...", verify_timeout=1.0, fail_open=False) # the defaults
|
|
@@ -18,5 +18,5 @@ Research behind it: https://github.com/imxinchengyou/CacheVerifier
|
|
|
18
18
|
|
|
19
19
|
from cacheverifier.client import CacheVerifier, CacheVerifierError, VerifyResult
|
|
20
20
|
|
|
21
|
-
__version__ = "0.4.
|
|
21
|
+
__version__ = "0.4.1"
|
|
22
22
|
__all__ = ["CacheVerifier", "CacheVerifierError", "VerifyResult", "__version__"]
|
|
@@ -46,13 +46,15 @@ class CacheVerifierError(RuntimeError):
|
|
|
46
46
|
"""Raised for any non-2xx response from the API.
|
|
47
47
|
|
|
48
48
|
`status_code` is the HTTP status; `detail` is the server's error message
|
|
49
|
-
(the JSON body's `detail` field when present, otherwise the raw text)
|
|
49
|
+
(the JSON body's `detail` field when present, otherwise the raw text);
|
|
50
|
+
`retry_after` is the response's `Retry-After` header, if it sent one.
|
|
50
51
|
"""
|
|
51
52
|
|
|
52
|
-
def __init__(self, status_code: int, detail: str) -> None:
|
|
53
|
+
def __init__(self, status_code: int, detail: str, retry_after: str | None = None) -> None:
|
|
53
54
|
super().__init__(f"CacheVerifier API error {status_code}: {detail}")
|
|
54
55
|
self.status_code = status_code
|
|
55
56
|
self.detail = detail
|
|
57
|
+
self.retry_after = retry_after
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
@dataclass(frozen=True)
|
|
@@ -69,8 +71,8 @@ class VerifyResult:
|
|
|
69
71
|
fallback result (see `degraded`).
|
|
70
72
|
- `latency_ms`: server-side model inference time, not round-trip time.
|
|
71
73
|
- `degraded`: True when the verify call failed (timeout / connection error /
|
|
72
|
-
5xx) and this result was synthesized by the client
|
|
73
|
-
the API. `approved` then reflects the client's `fail_open` setting
|
|
74
|
+
5xx / rate-limited 429) and this result was synthesized by the client
|
|
75
|
+
rather than returned by the API. `approved` then reflects the client's `fail_open` setting
|
|
74
76
|
(default `False` -> `approved=False`, i.e. fall through to your LLM).
|
|
75
77
|
"""
|
|
76
78
|
|
|
@@ -112,12 +114,14 @@ class CacheVerifier:
|
|
|
112
114
|
|
|
113
115
|
- `verify_timeout` (default 1s, vs 10s for everything else) bounds how long
|
|
114
116
|
a slow or overloaded verifier can stall your request.
|
|
115
|
-
- On a timeout, connection error,
|
|
116
|
-
|
|
117
|
-
`degraded=True`.
|
|
118
|
-
`
|
|
119
|
-
it `approved=True` so
|
|
120
|
-
|
|
117
|
+
- On a timeout, connection error, 5xx, or a rate-limited 429 (one with a
|
|
118
|
+
`Retry-After` header), `verify()` does not raise -- it logs a warning and
|
|
119
|
+
returns a synthetic `VerifyResult` with `degraded=True`.
|
|
120
|
+
`fail_open=False` (the default) makes that result `approved=False` so you
|
|
121
|
+
fall through to your LLM; `fail_open=True` makes it `approved=True` so
|
|
122
|
+
you serve the cached answer anyway. Other 4xx responses still raise
|
|
123
|
+
`CacheVerifierError`: a bad key, a bad request, or a 429 without
|
|
124
|
+
`Retry-After`, which means your monthly verify quota is used up.
|
|
121
125
|
"""
|
|
122
126
|
|
|
123
127
|
def __init__(
|
|
@@ -157,7 +161,7 @@ class CacheVerifier:
|
|
|
157
161
|
def verify(self, query: str, candidate_answer: str) -> VerifyResult:
|
|
158
162
|
"""Approve or reject one gray-zone cache hit. `POST /v1/verify`.
|
|
159
163
|
|
|
160
|
-
On a timeout / connection error / 5xx this returns a synthetic
|
|
164
|
+
On a timeout / connection error / 5xx / rate-limited 429 this returns a synthetic
|
|
161
165
|
`VerifyResult` (`degraded=True`, `approved` per `fail_open`) instead of
|
|
162
166
|
raising -- the verifier being unreachable should degrade to a normal
|
|
163
167
|
cache miss, not an exception on your request path.
|
|
@@ -189,14 +193,19 @@ class CacheVerifier:
|
|
|
189
193
|
def _verify_call(self, path: str, payload: dict[str, Any]) -> Any | None:
|
|
190
194
|
"""POST a verify request with the request-path timeout. Returns the
|
|
191
195
|
parsed body, or None to signal "unavailable, fall back" on a
|
|
192
|
-
timeout / connection error / 5xx
|
|
196
|
+
timeout / connection error / 5xx / 429 with `Retry-After`. Other 4xx
|
|
197
|
+
still raises: a bad key or request is a bug to fix, and a 429 without
|
|
198
|
+
`Retry-After` is the monthly quota running out, which needs a decision
|
|
199
|
+
(top up or subscribe), not a silent month of fallbacks. A rate limit is
|
|
200
|
+
transient load and should degrade like an outage instead.
|
|
193
201
|
"""
|
|
194
202
|
try:
|
|
195
203
|
return self._post(path, json=payload, timeout=self._verify_timeout)
|
|
196
204
|
except httpx.TransportError as exc:
|
|
197
205
|
self._warn_degraded(path, exc)
|
|
198
206
|
except CacheVerifierError as exc:
|
|
199
|
-
|
|
207
|
+
rate_limited = exc.status_code == 429 and exc.retry_after is not None
|
|
208
|
+
if exc.status_code < 500 and not rate_limited:
|
|
200
209
|
raise
|
|
201
210
|
self._warn_degraded(path, exc)
|
|
202
211
|
return None
|
|
@@ -365,7 +374,7 @@ class CacheVerifier:
|
|
|
365
374
|
detail = body["detail"] if isinstance(body, dict) and "detail" in body else resp.text
|
|
366
375
|
except ValueError:
|
|
367
376
|
detail = resp.text
|
|
368
|
-
raise CacheVerifierError(resp.status_code, str(detail))
|
|
377
|
+
raise CacheVerifierError(resp.status_code, str(detail), resp.headers.get("retry-after"))
|
|
369
378
|
|
|
370
379
|
|
|
371
380
|
def _drop_none(**kwargs: Any) -> dict[str, Any]:
|
|
@@ -135,6 +135,7 @@ def test_error_response_raises_with_detail():
|
|
|
135
135
|
|
|
136
136
|
assert excinfo.value.status_code == 429
|
|
137
137
|
assert excinfo.value.detail == "free tier limit reached"
|
|
138
|
+
assert excinfo.value.retry_after is None
|
|
138
139
|
|
|
139
140
|
|
|
140
141
|
def test_error_response_without_json_body_falls_back_to_text():
|
|
@@ -204,14 +205,35 @@ def test_verify_fails_closed_on_5xx():
|
|
|
204
205
|
assert result.approved is False
|
|
205
206
|
|
|
206
207
|
|
|
207
|
-
def
|
|
208
|
+
def test_verify_fails_closed_on_rate_limit_429():
|
|
208
209
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
209
|
-
return httpx.Response(429, json={"detail": "rate limited"})
|
|
210
|
+
return httpx.Response(429, headers={"Retry-After": "1"}, json={"detail": "rate limited"})
|
|
211
|
+
|
|
212
|
+
with make_client(handler) as cv:
|
|
213
|
+
result = cv.verify("q", "a")
|
|
214
|
+
|
|
215
|
+
assert result.degraded is True
|
|
216
|
+
assert result.approved is False
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_verify_batch_degrades_on_rate_limit_429():
|
|
220
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
221
|
+
return httpx.Response(429, headers={"Retry-After": "1"}, json={"detail": "rate limited"})
|
|
222
|
+
|
|
223
|
+
with make_client(handler) as cv:
|
|
224
|
+
results = cv.verify_batch([("q1", "a1"), ("q2", "a2")])
|
|
225
|
+
|
|
226
|
+
assert all(r.degraded and not r.approved for r in results)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_verify_still_raises_on_other_4xx():
|
|
230
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
231
|
+
return httpx.Response(401, json={"detail": "invalid API key"})
|
|
210
232
|
|
|
211
233
|
with make_client(handler) as cv, pytest.raises(CacheVerifierError) as excinfo:
|
|
212
234
|
cv.verify("q", "a")
|
|
213
235
|
|
|
214
|
-
assert excinfo.value.status_code ==
|
|
236
|
+
assert excinfo.value.status_code == 401
|
|
215
237
|
|
|
216
238
|
|
|
217
239
|
def test_verify_batch_degrades_every_pair_on_failure():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|