cacheverifier 0.2.0__tar.gz → 0.3.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.3.1/CHANGELOG.md +42 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/PKG-INFO +28 -2
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/README.md +27 -1
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/__init__.py +1 -1
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/_healthcheck/_finetune.py +10 -2
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/_healthcheck/cli.py +2 -2
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/client.py +107 -7
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/integrations/gptcache.py +20 -2
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/examples/quickstart.py +6 -1
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/pyproject.toml +1 -1
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/tests/test_client.py +96 -2
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/tests/test_healthcheck.py +28 -1
- cacheverifier-0.2.0/CHANGELOG.md +0 -18
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/.github/workflows/ci.yml +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/.github/workflows/publish.yml +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/.gitignore +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/LICENSE +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/__main__.py +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/_healthcheck/__init__.py +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/integrations/__init__.py +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/cacheverifier/py.typed +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/examples/gptcache_example.py +0 -0
- {cacheverifier-0.2.0 → cacheverifier-0.3.1}/tests/test_gptcache_integration.py +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
`cacheverifier healthcheck` now actually fine-tunes. It passed no
|
|
6
|
+
`warmup_steps` to `CrossEncoder.fit`, whose default of 10000 kept a typical
|
|
7
|
+
run below a few percent of its learning rate, so `auc_tuned` came back equal
|
|
8
|
+
(or nearly equal) to `auc_baseline`. Warmup is now 10% of total steps and the
|
|
9
|
+
default is 3 epochs (`--epochs`), the same as the hosted service since
|
|
10
|
+
verifier-core 0.2.0. On a 500-row later holdout this took LmArena AUC from
|
|
11
|
+
0.903 to 0.932 and AmazonHelp from 0.551 to 0.700. Runs take about 3x as long.
|
|
12
|
+
|
|
13
|
+
## 0.3.0
|
|
14
|
+
|
|
15
|
+
Make the request-path defaults safe. `verify()` and `verify_batch()` now use a
|
|
16
|
+
separate **1s timeout** (`verify_timeout=`, vs the unchanged 10s `timeout=` for
|
|
17
|
+
control-plane calls), and on a timeout / connection error / 5xx they **fail
|
|
18
|
+
closed** instead of raising: you get a synthetic `VerifyResult` with
|
|
19
|
+
`degraded=True` and `approved=False` (fall through to your LLM). Pass
|
|
20
|
+
`fail_open=True` to have those cases return `approved=True` instead. 4xx
|
|
21
|
+
responses (bad key, bad request, rate limit) still raise `CacheVerifierError`.
|
|
22
|
+
`VerifyResult` gains a `degraded` field; `model_version` is
|
|
23
|
+
`"verify_unavailable"` on a synthesized result. The GPTCache adapter takes the
|
|
24
|
+
same `verify_timeout` / `fail_open` arguments and returns `0.0` (don't reuse) on
|
|
25
|
+
a verifier outage by default.
|
|
26
|
+
|
|
27
|
+
## 0.2.0
|
|
28
|
+
|
|
29
|
+
Add a `cacheverifier` console script with a `healthcheck` subcommand:
|
|
30
|
+
`cacheverifier healthcheck traffic.jsonl` runs the hosted Health Check
|
|
31
|
+
Report's stock-vs-fine-tuned held-out AUC evaluation (`POST /v1/finetune/dry-run`)
|
|
32
|
+
entirely offline — no queries or answers leave the machine. Needs the new
|
|
33
|
+
`healthcheck` extra (`pip install "cacheverifier[healthcheck]"`: torch,
|
|
34
|
+
sentence-transformers, numpy); the base client stays `httpx`-only.
|
|
35
|
+
`--emit-summary` writes an aggregate-only JSON file with no text.
|
|
36
|
+
|
|
37
|
+
## 0.1.0
|
|
38
|
+
|
|
39
|
+
Initial release: thin `httpx`-based client for `/v1/verify`, `/v1/verify/batch`,
|
|
40
|
+
`/v1/feedback`(`/batch`), fine-tune jobs + dry run, drift and gray-zone-threshold
|
|
41
|
+
monitoring, and usage/savings. GPTCache `SimilarityEvaluation` adapter under
|
|
42
|
+
`cacheverifier.integrations.gptcache`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cacheverifier
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -91,8 +91,34 @@ cv.feedback(query, answer, was_correct=True, similarity_score=0.86)
|
|
|
91
91
|
|---|---|
|
|
92
92
|
| `approved` | serve the cached answer (`True`) or fall through (`False`) |
|
|
93
93
|
| `score` / `threshold` | `approved` is `score >= threshold` |
|
|
94
|
-
| `model_version` | `"stock"`, `"v<id>"` (fine-tuned), or `"
|
|
94
|
+
| `model_version` | `"stock"`, `"v<id>"` (fine-tuned), `"cold_start_fail_closed"` / `"cold_start_auto_pending"`, or `"verify_unavailable"` |
|
|
95
95
|
| `latency_ms` | server-side inference time |
|
|
96
|
+
| `degraded` | `True` when the call failed and this result was synthesized client-side |
|
|
97
|
+
|
|
98
|
+
## On your request path
|
|
99
|
+
|
|
100
|
+
`verify()` runs inline with your traffic, so its defaults are conservative:
|
|
101
|
+
|
|
102
|
+
- **1s timeout** (`verify_timeout=`), separate from the 10s `timeout=` used for
|
|
103
|
+
fine-tuning / feedback / monitoring calls. Warm verification is tens of
|
|
104
|
+
milliseconds server-side; 1s covers the network round trip and a cold model
|
|
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`.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
cv = CacheVerifier(api_key="cv_...", verify_timeout=1.0, fail_open=False) # the defaults
|
|
114
|
+
|
|
115
|
+
# fail_open=True instead returns approved=True on an outage — only if a
|
|
116
|
+
# stale-or-near-miss answer is acceptable for that traffic:
|
|
117
|
+
cv = CacheVerifier(api_key="cv_...", fail_open=True)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
There is no formal uptime SLA yet, which is the other reason the fallback path
|
|
121
|
+
is a built-in default rather than left to you.
|
|
96
122
|
|
|
97
123
|
## GPTCache
|
|
98
124
|
|
|
@@ -59,8 +59,34 @@ cv.feedback(query, answer, was_correct=True, similarity_score=0.86)
|
|
|
59
59
|
|---|---|
|
|
60
60
|
| `approved` | serve the cached answer (`True`) or fall through (`False`) |
|
|
61
61
|
| `score` / `threshold` | `approved` is `score >= threshold` |
|
|
62
|
-
| `model_version` | `"stock"`, `"v<id>"` (fine-tuned), or `"
|
|
62
|
+
| `model_version` | `"stock"`, `"v<id>"` (fine-tuned), `"cold_start_fail_closed"` / `"cold_start_auto_pending"`, or `"verify_unavailable"` |
|
|
63
63
|
| `latency_ms` | server-side inference time |
|
|
64
|
+
| `degraded` | `True` when the call failed and this result was synthesized client-side |
|
|
65
|
+
|
|
66
|
+
## On your request path
|
|
67
|
+
|
|
68
|
+
`verify()` runs inline with your traffic, so its defaults are conservative:
|
|
69
|
+
|
|
70
|
+
- **1s timeout** (`verify_timeout=`), separate from the 10s `timeout=` used for
|
|
71
|
+
fine-tuning / feedback / monitoring calls. Warm verification is tens of
|
|
72
|
+
milliseconds server-side; 1s covers the network round trip and a cold model
|
|
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`.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
cv = CacheVerifier(api_key="cv_...", verify_timeout=1.0, fail_open=False) # the defaults
|
|
82
|
+
|
|
83
|
+
# fail_open=True instead returns approved=True on an outage — only if a
|
|
84
|
+
# stale-or-near-miss answer is acceptable for that traffic:
|
|
85
|
+
cv = CacheVerifier(api_key="cv_...", fail_open=True)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
There is no formal uptime SLA yet, which is the other reason the fallback path
|
|
89
|
+
is a built-in default rather than left to you.
|
|
64
90
|
|
|
65
91
|
## GPTCache
|
|
66
92
|
|
|
@@ -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.
|
|
21
|
+
__version__ = "0.3.0"
|
|
22
22
|
__all__ = ["CacheVerifier", "CacheVerifierError", "VerifyResult", "__version__"]
|
|
@@ -43,6 +43,13 @@ _CONTENT_TOKEN_BUDGET = MAX_SEQUENCE_LENGTH - 3 # [CLS] + 2x [SEP]
|
|
|
43
43
|
_MIN_QUERY_TOKENS = 32
|
|
44
44
|
|
|
45
45
|
TRAIN_BATCH_SIZE = 4
|
|
46
|
+
TRAIN_EPOCHS = 3
|
|
47
|
+
TRAIN_WARMUP_FRACTION = 0.1
|
|
48
|
+
"""Same as the hosted service (verifier-core 0.2.0). `CrossEncoder.fit`
|
|
49
|
+
defaults to `warmup_steps=10000`; without an explicit value a typical run
|
|
50
|
+
(a few hundred steps) never got past a few percent of its learning rate and
|
|
51
|
+
the "fine-tuned" model was a near monotone shift of the base model, so
|
|
52
|
+
`auc_tuned` came back equal to `auc_baseline`."""
|
|
46
53
|
MIN_TRAIN_EXAMPLES = 20
|
|
47
54
|
"""Hard floor -- below this there aren't enough rows for one meaningful
|
|
48
55
|
epoch plus a held-out split."""
|
|
@@ -183,7 +190,7 @@ def run_healthcheck(
|
|
|
183
190
|
output_dir: str,
|
|
184
191
|
*,
|
|
185
192
|
base_model: str = DEFAULT_BASE_MODEL,
|
|
186
|
-
epochs: int =
|
|
193
|
+
epochs: int = TRAIN_EPOCHS,
|
|
187
194
|
) -> HealthCheckResult:
|
|
188
195
|
"""Fine-tune `base_model` on a chronological prefix of `examples` and
|
|
189
196
|
measure held-out AUC for the stock vs. fine-tuned model.
|
|
@@ -233,7 +240,8 @@ def run_healthcheck(
|
|
|
233
240
|
# whether or not torch's stubs are installed (base CI has no torch).
|
|
234
241
|
loader: DataLoader[Any] = DataLoader(cast(Any, train_examples), shuffle=True, batch_size=TRAIN_BATCH_SIZE)
|
|
235
242
|
t0 = time.time()
|
|
236
|
-
|
|
243
|
+
warmup_steps = max(1, int(TRAIN_WARMUP_FRACTION * len(loader) * epochs))
|
|
244
|
+
tuned.fit(train_dataloader=loader, epochs=epochs, warmup_steps=warmup_steps, show_progress_bar=False)
|
|
237
245
|
train_time_seconds = time.time() - t0
|
|
238
246
|
tuned.save(output_dir)
|
|
239
247
|
|
|
@@ -48,8 +48,8 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
48
48
|
p.add_argument(
|
|
49
49
|
"--epochs",
|
|
50
50
|
type=int,
|
|
51
|
-
default=
|
|
52
|
-
help="fine-tuning epochs (default:
|
|
51
|
+
default=3,
|
|
52
|
+
help="fine-tuning epochs (default: 3, matching the hosted service)",
|
|
53
53
|
)
|
|
54
54
|
p.add_argument(
|
|
55
55
|
"--keep-model",
|
|
@@ -8,6 +8,7 @@ might be wrong. See https://www.cacheverifier.com/docs for the full API.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import logging
|
|
11
12
|
from collections.abc import Iterable, Sequence
|
|
12
13
|
from dataclasses import dataclass
|
|
13
14
|
from typing import Any
|
|
@@ -15,9 +16,30 @@ from typing import Any
|
|
|
15
16
|
import httpx
|
|
16
17
|
|
|
17
18
|
DEFAULT_BASE_URL = "https://www.cacheverifier.com"
|
|
19
|
+
#: Timeout for control-plane calls (feedback, fine-tuning, monitoring, usage).
|
|
20
|
+
#: These are not on your request path, so they get room to breathe.
|
|
18
21
|
DEFAULT_TIMEOUT = 10.0
|
|
22
|
+
#: Timeout for ``verify()`` / ``verify_batch()`` -- these ARE on your request
|
|
23
|
+
#: path. Warm calls are tens of milliseconds server-side; 1s leaves headroom
|
|
24
|
+
#: for the network round trip and a cold model load after a deploy while still
|
|
25
|
+
#: bounding the damage when the service is actually unreachable.
|
|
26
|
+
DEFAULT_VERIFY_TIMEOUT = 1.0
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
#: model_version on a synthetic result returned when the verify call itself
|
|
29
|
+
#: failed (timeout / connection error / 5xx) and the client fell back.
|
|
30
|
+
VERIFY_UNAVAILABLE = "verify_unavailable"
|
|
31
|
+
|
|
32
|
+
_log = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"DEFAULT_BASE_URL",
|
|
36
|
+
"DEFAULT_TIMEOUT",
|
|
37
|
+
"DEFAULT_VERIFY_TIMEOUT",
|
|
38
|
+
"VERIFY_UNAVAILABLE",
|
|
39
|
+
"CacheVerifier",
|
|
40
|
+
"CacheVerifierError",
|
|
41
|
+
"VerifyResult",
|
|
42
|
+
]
|
|
21
43
|
|
|
22
44
|
|
|
23
45
|
class CacheVerifierError(RuntimeError):
|
|
@@ -41,9 +63,15 @@ class VerifyResult:
|
|
|
41
63
|
- `score`: the verifier's raw score for this pair; `approved` is `score >= threshold`.
|
|
42
64
|
- `threshold`: the cutoff this call was decided against (tenant-specific once
|
|
43
65
|
you've fine-tuned; 0.0 on the shared stock model).
|
|
44
|
-
- `model_version`: `"stock"`, `"v<id>"` for a fine-tuned model,
|
|
45
|
-
`"cold_start_fail_closed"` when no model ran
|
|
66
|
+
- `model_version`: `"stock"`, `"v<id>"` for a fine-tuned model,
|
|
67
|
+
`"cold_start_fail_closed"` / `"cold_start_auto_pending"` when no model ran
|
|
68
|
+
(see `cold_start_mode`), or `"verify_unavailable"` on a synthetic
|
|
69
|
+
fallback result (see `degraded`).
|
|
46
70
|
- `latency_ms`: server-side model inference time, not round-trip time.
|
|
71
|
+
- `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
|
+
(default `False` -> `approved=False`, i.e. fall through to your LLM).
|
|
47
75
|
"""
|
|
48
76
|
|
|
49
77
|
approved: bool
|
|
@@ -51,6 +79,7 @@ class VerifyResult:
|
|
|
51
79
|
threshold: float
|
|
52
80
|
model_version: str
|
|
53
81
|
latency_ms: float
|
|
82
|
+
degraded: bool = False
|
|
54
83
|
|
|
55
84
|
@classmethod
|
|
56
85
|
def _from_json(cls, d: dict[str, Any]) -> VerifyResult:
|
|
@@ -77,6 +106,18 @@ class CacheVerifier:
|
|
|
77
106
|
|
|
78
107
|
Usable as a context manager (`with CacheVerifier(...) as cv:`) to close
|
|
79
108
|
the underlying HTTP connection pool deterministically.
|
|
109
|
+
|
|
110
|
+
Because `verify()` sits on your request path, it is treated differently
|
|
111
|
+
from every other call:
|
|
112
|
+
|
|
113
|
+
- `verify_timeout` (default 1s, vs 10s for everything else) bounds how long
|
|
114
|
+
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`.
|
|
80
121
|
"""
|
|
81
122
|
|
|
82
123
|
def __init__(
|
|
@@ -85,10 +126,14 @@ class CacheVerifier:
|
|
|
85
126
|
*,
|
|
86
127
|
base_url: str = DEFAULT_BASE_URL,
|
|
87
128
|
timeout: float = DEFAULT_TIMEOUT,
|
|
129
|
+
verify_timeout: float = DEFAULT_VERIFY_TIMEOUT,
|
|
130
|
+
fail_open: bool = False,
|
|
88
131
|
transport: httpx.BaseTransport | None = None,
|
|
89
132
|
) -> None:
|
|
90
133
|
if not api_key:
|
|
91
134
|
raise ValueError("api_key is required -- get one at https://www.cacheverifier.com")
|
|
135
|
+
self._verify_timeout = verify_timeout
|
|
136
|
+
self._fail_open = fail_open
|
|
92
137
|
self._client = httpx.Client(
|
|
93
138
|
base_url=base_url.rstrip("/"),
|
|
94
139
|
headers={"X-API-Key": api_key, "User-Agent": _user_agent()},
|
|
@@ -110,8 +155,16 @@ class CacheVerifier:
|
|
|
110
155
|
# -- core: verify ----------------------------------------------------
|
|
111
156
|
|
|
112
157
|
def verify(self, query: str, candidate_answer: str) -> VerifyResult:
|
|
113
|
-
"""Approve or reject one gray-zone cache hit. `POST /v1/verify`.
|
|
114
|
-
|
|
158
|
+
"""Approve or reject one gray-zone cache hit. `POST /v1/verify`.
|
|
159
|
+
|
|
160
|
+
On a timeout / connection error / 5xx this returns a synthetic
|
|
161
|
+
`VerifyResult` (`degraded=True`, `approved` per `fail_open`) instead of
|
|
162
|
+
raising -- the verifier being unreachable should degrade to a normal
|
|
163
|
+
cache miss, not an exception on your request path.
|
|
164
|
+
"""
|
|
165
|
+
data = self._verify_call("/v1/verify", {"query": query, "candidate_answer": candidate_answer})
|
|
166
|
+
if data is None:
|
|
167
|
+
return self._degraded_result()
|
|
115
168
|
return VerifyResult._from_json(data)
|
|
116
169
|
|
|
117
170
|
def verify_batch(self, pairs: Sequence[tuple[str, str]]) -> list[VerifyResult]:
|
|
@@ -120,11 +173,52 @@ class CacheVerifier:
|
|
|
120
173
|
|
|
121
174
|
Useful when your own retrieval returns several close candidates:
|
|
122
175
|
send them in rank order and take the first `approved` one.
|
|
176
|
+
|
|
177
|
+
Degrades the same way as `verify()`: on failure every pair comes back
|
|
178
|
+
as a `degraded` result rather than the call raising. A large batch may
|
|
179
|
+
need `verify_timeout` raised above the 1s default.
|
|
123
180
|
"""
|
|
124
181
|
items = [{"query": q, "candidate_answer": a} for q, a in pairs]
|
|
125
|
-
data = self.
|
|
182
|
+
data = self._verify_call("/v1/verify/batch", {"items": items})
|
|
183
|
+
if data is None:
|
|
184
|
+
return [self._degraded_result() for _ in items]
|
|
126
185
|
return [VerifyResult._from_json(r) for r in data["results"]]
|
|
127
186
|
|
|
187
|
+
# -- verify plumbing: tight timeout + fail-open/closed fallback --------
|
|
188
|
+
|
|
189
|
+
def _verify_call(self, path: str, payload: dict[str, Any]) -> Any | None:
|
|
190
|
+
"""POST a verify request with the request-path timeout. Returns the
|
|
191
|
+
parsed body, or None to signal "unavailable, fall back" on a
|
|
192
|
+
timeout / connection error / 5xx. 4xx still raises.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
return self._post(path, json=payload, timeout=self._verify_timeout)
|
|
196
|
+
except httpx.TransportError as exc:
|
|
197
|
+
self._warn_degraded(path, exc)
|
|
198
|
+
except CacheVerifierError as exc:
|
|
199
|
+
if exc.status_code < 500:
|
|
200
|
+
raise
|
|
201
|
+
self._warn_degraded(path, exc)
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
def _warn_degraded(self, path: str, exc: Exception) -> None:
|
|
205
|
+
_log.warning(
|
|
206
|
+
"cacheverifier: %s unavailable (%s) -- failing %s",
|
|
207
|
+
path,
|
|
208
|
+
exc,
|
|
209
|
+
"open" if self._fail_open else "closed",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _degraded_result(self) -> VerifyResult:
|
|
213
|
+
return VerifyResult(
|
|
214
|
+
approved=self._fail_open,
|
|
215
|
+
score=0.0,
|
|
216
|
+
threshold=0.0,
|
|
217
|
+
model_version=VERIFY_UNAVAILABLE,
|
|
218
|
+
latency_ms=0.0,
|
|
219
|
+
degraded=True,
|
|
220
|
+
)
|
|
221
|
+
|
|
128
222
|
# -- feedback ------------------------------------------------------
|
|
129
223
|
|
|
130
224
|
def feedback(
|
|
@@ -252,8 +346,14 @@ class CacheVerifier:
|
|
|
252
346
|
json: Any | None = None,
|
|
253
347
|
params: dict[str, Any] | None = None,
|
|
254
348
|
headers: dict[str, str] | None = None,
|
|
349
|
+
timeout: float | None = None,
|
|
255
350
|
) -> Any:
|
|
256
|
-
|
|
351
|
+
# httpx treats timeout=None as "no timeout" -- only pass it through
|
|
352
|
+
# when a caller (verify) explicitly asked for a per-request value.
|
|
353
|
+
extra: dict[str, Any] = {"timeout": timeout} if timeout is not None else {}
|
|
354
|
+
return self._unwrap(
|
|
355
|
+
self._client.post(path, json=json, params=params or None, headers=headers, **extra)
|
|
356
|
+
)
|
|
257
357
|
|
|
258
358
|
@staticmethod
|
|
259
359
|
def _unwrap(resp: httpx.Response) -> Any:
|
|
@@ -22,7 +22,12 @@ from __future__ import annotations
|
|
|
22
22
|
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
-
from cacheverifier.client import
|
|
25
|
+
from cacheverifier.client import (
|
|
26
|
+
DEFAULT_BASE_URL,
|
|
27
|
+
DEFAULT_TIMEOUT,
|
|
28
|
+
DEFAULT_VERIFY_TIMEOUT,
|
|
29
|
+
CacheVerifier,
|
|
30
|
+
)
|
|
26
31
|
|
|
27
32
|
try:
|
|
28
33
|
from gptcache.similarity_evaluation import SimilarityEvaluation as _GPTCacheBase
|
|
@@ -37,6 +42,11 @@ class CacheVerifierEvaluation(_GPTCacheBase):
|
|
|
37
42
|
hosted verifier already makes a binary approve/reject call per gray-zone
|
|
38
43
|
hit rather than a softened similarity score. Callers who want GPTCache's
|
|
39
44
|
own threshold logic on top can wrap this rather than replace it.
|
|
45
|
+
|
|
46
|
+
If the verify call times out or the service is unreachable, `evaluation()`
|
|
47
|
+
returns 0.0 (don't reuse) by default -- a verifier outage falls through to
|
|
48
|
+
a normal GPTCache miss. Pass `fail_open=True` to reuse the cached answer
|
|
49
|
+
instead in that case.
|
|
40
50
|
"""
|
|
41
51
|
|
|
42
52
|
def __init__(
|
|
@@ -45,8 +55,16 @@ class CacheVerifierEvaluation(_GPTCacheBase):
|
|
|
45
55
|
*,
|
|
46
56
|
base_url: str = DEFAULT_BASE_URL,
|
|
47
57
|
timeout: float = DEFAULT_TIMEOUT,
|
|
58
|
+
verify_timeout: float = DEFAULT_VERIFY_TIMEOUT,
|
|
59
|
+
fail_open: bool = False,
|
|
48
60
|
) -> None:
|
|
49
|
-
self._cv = CacheVerifier(
|
|
61
|
+
self._cv = CacheVerifier(
|
|
62
|
+
api_key=api_key,
|
|
63
|
+
base_url=base_url,
|
|
64
|
+
timeout=timeout,
|
|
65
|
+
verify_timeout=verify_timeout,
|
|
66
|
+
fail_open=fail_open,
|
|
67
|
+
)
|
|
50
68
|
|
|
51
69
|
def evaluation(self, src_dict: dict[str, Any], cache_dict: dict[str, Any], **_kwargs: Any) -> float:
|
|
52
70
|
query = src_dict.get("question") or src_dict.get("query", "")
|
|
@@ -28,10 +28,15 @@ def main() -> None:
|
|
|
28
28
|
query = "how do I cancel my subscription"
|
|
29
29
|
candidate = "Go to Settings > Billing > Pause subscription for a month."
|
|
30
30
|
|
|
31
|
+
# verify() sits on your request path: it uses a 1s timeout and, if the
|
|
32
|
+
# service is unreachable, returns a degraded result (approved=False here)
|
|
33
|
+
# instead of raising. Pass fail_open=True to serve the cached answer in
|
|
34
|
+
# that case instead.
|
|
31
35
|
with CacheVerifier(api_key=api_key) as cv:
|
|
32
36
|
result = cv.verify(query, candidate)
|
|
33
37
|
print(f"verify -> approved={result.approved} score={result.score:.3f} "
|
|
34
|
-
f"threshold={result.threshold} model={result.model_version}"
|
|
38
|
+
f"threshold={result.threshold} model={result.model_version} "
|
|
39
|
+
f"degraded={result.degraded}")
|
|
35
40
|
|
|
36
41
|
if result.approved:
|
|
37
42
|
answer = candidate
|
|
@@ -9,8 +9,17 @@ import pytest
|
|
|
9
9
|
from cacheverifier import CacheVerifier, CacheVerifierError, VerifyResult
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def make_client(handler):
|
|
13
|
-
return CacheVerifier(api_key="cv_test", transport=httpx.MockTransport(handler))
|
|
12
|
+
def make_client(handler, **kwargs):
|
|
13
|
+
return CacheVerifier(api_key="cv_test", transport=httpx.MockTransport(handler), **kwargs)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_OK_VERIFY = {
|
|
17
|
+
"approved": True,
|
|
18
|
+
"score": 3.5,
|
|
19
|
+
"latency_ms": 24.1,
|
|
20
|
+
"model_version": "stock",
|
|
21
|
+
"threshold": 0.0,
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
|
|
16
25
|
def test_verify_parses_result_and_sends_api_key():
|
|
@@ -142,3 +151,88 @@ def test_error_response_without_json_body_falls_back_to_text():
|
|
|
142
151
|
def test_empty_api_key_rejected():
|
|
143
152
|
with pytest.raises(ValueError):
|
|
144
153
|
CacheVerifier(api_key="")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# -- request-path timeout + fail-open/closed --------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_verify_uses_the_tight_verify_timeout_not_the_control_plane_one():
|
|
160
|
+
seen = {}
|
|
161
|
+
|
|
162
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
163
|
+
seen["timeout"] = request.extensions.get("timeout")
|
|
164
|
+
return httpx.Response(200, json=_OK_VERIFY)
|
|
165
|
+
|
|
166
|
+
with make_client(handler, timeout=10.0, verify_timeout=1.0) as cv:
|
|
167
|
+
cv.verify("q", "a")
|
|
168
|
+
|
|
169
|
+
# httpx expands a scalar timeout into per-operation values
|
|
170
|
+
assert set(seen["timeout"].values()) == {1.0}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_verify_fails_closed_on_timeout():
|
|
174
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
175
|
+
raise httpx.TimeoutException("read timed out", request=request)
|
|
176
|
+
|
|
177
|
+
with make_client(handler) as cv:
|
|
178
|
+
result = cv.verify("q", "a")
|
|
179
|
+
|
|
180
|
+
assert result.degraded is True
|
|
181
|
+
assert result.approved is False
|
|
182
|
+
assert result.model_version == "verify_unavailable"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_verify_fails_open_when_configured():
|
|
186
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
187
|
+
raise httpx.ConnectError("connection refused", request=request)
|
|
188
|
+
|
|
189
|
+
with make_client(handler, fail_open=True) as cv:
|
|
190
|
+
result = cv.verify("q", "a")
|
|
191
|
+
|
|
192
|
+
assert result.degraded is True
|
|
193
|
+
assert result.approved is True
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_verify_fails_closed_on_5xx():
|
|
197
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
198
|
+
return httpx.Response(503, text="upstream overloaded")
|
|
199
|
+
|
|
200
|
+
with make_client(handler) as cv:
|
|
201
|
+
result = cv.verify("q", "a")
|
|
202
|
+
|
|
203
|
+
assert result.degraded is True
|
|
204
|
+
assert result.approved is False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_verify_still_raises_on_4xx():
|
|
208
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
209
|
+
return httpx.Response(429, json={"detail": "rate limited"})
|
|
210
|
+
|
|
211
|
+
with make_client(handler) as cv, pytest.raises(CacheVerifierError) as excinfo:
|
|
212
|
+
cv.verify("q", "a")
|
|
213
|
+
|
|
214
|
+
assert excinfo.value.status_code == 429
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_verify_batch_degrades_every_pair_on_failure():
|
|
218
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
219
|
+
return httpx.Response(500, text="boom")
|
|
220
|
+
|
|
221
|
+
with make_client(handler) as cv:
|
|
222
|
+
results = cv.verify_batch([("q1", "a1"), ("q2", "a2"), ("q3", "a3")])
|
|
223
|
+
|
|
224
|
+
assert len(results) == 3
|
|
225
|
+
assert all(r.degraded and not r.approved for r in results)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_control_plane_call_still_uses_the_10s_default_and_raises():
|
|
229
|
+
seen = {}
|
|
230
|
+
|
|
231
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
232
|
+
seen["timeout"] = request.extensions.get("timeout")
|
|
233
|
+
return httpx.Response(200, json={"status": "ok"})
|
|
234
|
+
|
|
235
|
+
with make_client(handler, timeout=10.0, verify_timeout=1.0) as cv:
|
|
236
|
+
cv.usage()
|
|
237
|
+
|
|
238
|
+
assert set(seen["timeout"].values()) == {10.0}
|
|
@@ -32,7 +32,9 @@ class TestDispatch:
|
|
|
32
32
|
with pytest.raises(SystemExit) as e:
|
|
33
33
|
main(["--version"])
|
|
34
34
|
assert e.value.code == 0
|
|
35
|
-
|
|
35
|
+
from cacheverifier import __version__
|
|
36
|
+
|
|
37
|
+
assert f"cacheverifier {__version__}" in capsys.readouterr().out
|
|
36
38
|
|
|
37
39
|
def test_no_command_prints_help_and_returns_1(self, capsys):
|
|
38
40
|
assert main([]) == 1
|
|
@@ -96,3 +98,28 @@ def test_run_healthcheck_end_to_end(tmp_path):
|
|
|
96
98
|
assert result.n_train + result.n_calibrate + result.n_test == 36
|
|
97
99
|
assert 0.0 <= result.auc_tuned <= 1.0
|
|
98
100
|
assert (tmp_path / "model").is_dir()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.skipif(not HAS_TORCH, reason="needs the healthcheck extra (torch)")
|
|
104
|
+
def test_run_healthcheck_warms_up_over_a_tenth_of_its_steps(tmp_path, monkeypatch):
|
|
105
|
+
# CrossEncoder.fit defaults to warmup_steps=10000, which kept a
|
|
106
|
+
# few-hundred-step run from ever reaching its learning rate.
|
|
107
|
+
from sentence_transformers import CrossEncoder
|
|
108
|
+
|
|
109
|
+
from cacheverifier._healthcheck import _finetune
|
|
110
|
+
from cacheverifier._healthcheck._finetune import GrayZoneExample, run_healthcheck
|
|
111
|
+
|
|
112
|
+
seen: dict = {}
|
|
113
|
+
real_fit = CrossEncoder.fit
|
|
114
|
+
|
|
115
|
+
def spy_fit(self, *args, **kwargs):
|
|
116
|
+
seen.update(kwargs, steps_per_epoch=len(kwargs["train_dataloader"]))
|
|
117
|
+
return real_fit(self, *args, **kwargs)
|
|
118
|
+
|
|
119
|
+
monkeypatch.setattr(CrossEncoder, "fit", spy_fit)
|
|
120
|
+
examples = [GrayZoneExample(f"question {i}", f"answer {i % 3}", i % 2 == 0) for i in range(40)]
|
|
121
|
+
run_healthcheck(examples, str(tmp_path / "model"))
|
|
122
|
+
|
|
123
|
+
assert seen["epochs"] == _finetune.TRAIN_EPOCHS == 3
|
|
124
|
+
expected = max(1, int(_finetune.TRAIN_WARMUP_FRACTION * seen["steps_per_epoch"] * seen["epochs"]))
|
|
125
|
+
assert seen["warmup_steps"] == expected
|
cacheverifier-0.2.0/CHANGELOG.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.2.0
|
|
4
|
-
|
|
5
|
-
Add a `cacheverifier` console script with a `healthcheck` subcommand:
|
|
6
|
-
`cacheverifier healthcheck traffic.jsonl` runs the hosted Health Check
|
|
7
|
-
Report's stock-vs-fine-tuned held-out AUC evaluation (`POST /v1/finetune/dry-run`)
|
|
8
|
-
entirely offline — no queries or answers leave the machine. Needs the new
|
|
9
|
-
`healthcheck` extra (`pip install "cacheverifier[healthcheck]"`: torch,
|
|
10
|
-
sentence-transformers, numpy); the base client stays `httpx`-only.
|
|
11
|
-
`--emit-summary` writes an aggregate-only JSON file with no text.
|
|
12
|
-
|
|
13
|
-
## 0.1.0
|
|
14
|
-
|
|
15
|
-
Initial release: thin `httpx`-based client for `/v1/verify`, `/v1/verify/batch`,
|
|
16
|
-
`/v1/feedback`(`/batch`), fine-tune jobs + dry run, drift and gray-zone-threshold
|
|
17
|
-
monitoring, and usage/savings. GPTCache `SimilarityEvaluation` adapter under
|
|
18
|
-
`cacheverifier.integrations.gptcache`.
|
|
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
|