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.
Files changed (23) hide show
  1. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/CHANGELOG.md +13 -0
  2. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/PKG-INFO +8 -6
  3. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/README.md +7 -5
  4. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/__init__.py +1 -1
  5. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/client.py +23 -14
  6. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/pyproject.toml +1 -1
  7. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/tests/test_client.py +25 -3
  8. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/.github/workflows/ci.yml +0 -0
  9. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/.github/workflows/publish.yml +0 -0
  10. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/.gitignore +0 -0
  11. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/LICENSE +0 -0
  12. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/__main__.py +0 -0
  13. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/_healthcheck/__init__.py +0 -0
  14. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/_healthcheck/_base_models.py +0 -0
  15. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/_healthcheck/_finetune.py +0 -0
  16. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/_healthcheck/cli.py +0 -0
  17. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/integrations/__init__.py +0 -0
  18. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/integrations/gptcache.py +0 -0
  19. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/cacheverifier/py.typed +0 -0
  20. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/examples/gptcache_example.py +0 -0
  21. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/examples/quickstart.py +0 -0
  22. {cacheverifier-0.4.0 → cacheverifier-0.4.1}/tests/test_gptcache_integration.py +0 -0
  23. {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.0
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, or 5xx, `verify()` does not
107
- raise — it logs a warning on the `cacheverifier` logger and returns a
108
- `VerifyResult` with `degraded=True` and `approved=False`, so you fall through
109
- to your LLM exactly as you would on a cache miss. A `4xx` (bad key, bad
110
- request, rate limit) still raises `CacheVerifierError`.
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, or 5xx, `verify()` does not
75
- raise — it logs a warning on the `cacheverifier` logger and returns a
76
- `VerifyResult` with `degraded=True` and `approved=False`, so you fall through
77
- to your LLM exactly as you would on a cache miss. A `4xx` (bad key, bad
78
- request, rate limit) still raises `CacheVerifierError`.
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.0"
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 rather than returned by
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, or 5xx, `verify()` does not raise -- it
116
- logs a warning and returns a synthetic `VerifyResult` with
117
- `degraded=True`. `fail_open=False` (the default) makes that result
118
- `approved=False` so you fall through to your LLM; `fail_open=True` makes
119
- it `approved=True` so you serve the cached answer anyway. 4xx responses
120
- (bad key, bad request, rate limit) still raise `CacheVerifierError`.
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. 4xx still raises.
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
- if exc.status_code < 500:
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]:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cacheverifier"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "Python client for the hosted CacheVerifier semantic-cache verification API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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 test_verify_still_raises_on_4xx():
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 == 429
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