@wendellresearch-ai/benchwarmers 0.2.1 → 0.3.0
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.
- package/AGENTS.md +6 -1
- package/GATEWAY.md +306 -6
- package/README.md +53 -3
- package/USAGE.md +31 -0
- package/VERSION +1 -1
- package/cli.py +10 -2
- package/gateway.py +619 -93
- package/metrics/bench_collections.py +7 -3
- package/metrics/catalog.py +10 -1
- package/metrics/client.py +764 -131
- package/metrics/cost.py +38 -19
- package/metrics/endpoints.py +25 -2
- package/metrics/formats.py +1584 -0
- package/metrics/guard.py +37 -7
- package/metrics/learn.py +19 -6
- package/metrics/ledger.py +148 -0
- package/metrics/model_roles.py +13 -3
- package/metrics/pacing.py +248 -0
- package/metrics/quota.py +383 -0
- package/metrics/route.py +5 -3
- package/package.json +2 -1
- package/perf/run.py +30 -7
- package/scripts/check.py +7 -0
- package/scripts/onboard.py +10 -1
- package/scripts/test_auth.py +2 -1
- package/scripts/test_client.py +50 -6
- package/scripts/test_formats.py +558 -0
- package/scripts/test_merged.py +6 -5
- package/scripts/test_native.py +188 -0
- package/scripts/test_quota.py +334 -0
- package/scripts/test_route.py +429 -0
- package/scripts/test_serve_edit.py +10 -5
- package/scripts/test_server_json.py +79 -0
- package/scripts/test_verdict.py +5 -1
- package/scripts/test_vision.py +6 -3
- package/serve.py +170 -44
- package/ui/bench.js +6 -3
package/scripts/test_client.py
CHANGED
|
@@ -14,7 +14,14 @@ For every scenario both backends go through suite/run.py's ask_once and must
|
|
|
14
14
|
land in the same place: the same `outcome` and `failure_class`, at the suite's
|
|
15
15
|
length floor and the perf tier's, and the same text when the model answered.
|
|
16
16
|
That is what Phase 2a promises before the client may become the default. The
|
|
17
|
-
requests are compared too: model, messages, options, key and headers
|
|
17
|
+
requests are compared too: model, messages, options, key and headers, and
|
|
18
|
+
that both ask for a stream (Phase 2b). Then the built-in client's own: the
|
|
19
|
+
usage from the stream's last chunk and its ledger line, and a call refused
|
|
20
|
+
by pacing, which must land where llm's real 429 does.
|
|
21
|
+
|
|
22
|
+
The data folder is a temp one too (BENCHWARMERS_HOME, set before any module
|
|
23
|
+
reads it): the built-in client now writes a ledger line and quota records
|
|
24
|
+
for every call.
|
|
18
25
|
"""
|
|
19
26
|
|
|
20
27
|
from __future__ import annotations
|
|
@@ -29,10 +36,13 @@ import threading
|
|
|
29
36
|
import time
|
|
30
37
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
31
38
|
|
|
39
|
+
DATA = pathlib.Path(tempfile.mkdtemp(prefix="bench-client-data-"))
|
|
40
|
+
os.environ["BENCHWARMERS_HOME"] = str(DATA)
|
|
32
41
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
33
42
|
sys.path[:0] = [str(ROOT / "metrics"), str(ROOT / "suite")]
|
|
34
43
|
import client # noqa: E402
|
|
35
44
|
import endpoints # noqa: E402
|
|
45
|
+
import ledger # noqa: E402
|
|
36
46
|
import run # noqa: E402
|
|
37
47
|
|
|
38
48
|
ANSWER = "The quick brown fox jumps over the lazy dog, and then does it twice."
|
|
@@ -154,7 +164,14 @@ def main() -> int:
|
|
|
154
164
|
tmp = pathlib.Path(tempfile.mkdtemp(prefix="bench-client-"))
|
|
155
165
|
scenarios = ["ok", "short", "empty", "notfound", "ratelimit", "flaky", "html502", "slow", "notice",
|
|
156
166
|
"refused", "nokey"]
|
|
157
|
-
own = ["redirect", "trickle", "cutoff", "nochoices"] # the built-in client only
|
|
167
|
+
own = ["redirect", "trickle", "cutoff", "nochoices", "paced"] # the built-in client only
|
|
168
|
+
# The endpoint the live scenarios are on, so they share a connection ("fake")
|
|
169
|
+
# and pacing has one to limit: one call a minute for the paced model.
|
|
170
|
+
(DATA / "config").mkdir(parents=True, exist_ok=True)
|
|
171
|
+
(DATA / "config" / "endpoints.json").write_text(json.dumps(
|
|
172
|
+
{"fake": {"base": live, "key": "fake", "kind": "provider", "default": True}}), encoding="utf-8")
|
|
173
|
+
(DATA / "config" / "model-overrides.json").write_text('{"t-paced": {"limits": {"rpm": 1}}}',
|
|
174
|
+
encoding="utf-8")
|
|
158
175
|
yaml = []
|
|
159
176
|
for s in scenarios + own:
|
|
160
177
|
yaml += [f"- model_id: t-{s}", f" model_name: {'ok' if s in ('refused', 'nokey') else s}",
|
|
@@ -171,6 +188,11 @@ def main() -> int:
|
|
|
171
188
|
tmp / "options.json")
|
|
172
189
|
|
|
173
190
|
binary, failures, prompt = run.llm_binary(), 0, "Say something."
|
|
191
|
+
|
|
192
|
+
def kinds(x):
|
|
193
|
+
return ((run.outcome(x) or ("answered",))[0], (run.outcome(x, 1) or ("answered",))[0],
|
|
194
|
+
run.failure_class(x), run.failure_class(x, 1))
|
|
195
|
+
bodies_429 = {}
|
|
174
196
|
for s in scenarios:
|
|
175
197
|
bodies = {}
|
|
176
198
|
for backend in ("llm", "builtin"):
|
|
@@ -179,10 +201,8 @@ def main() -> int:
|
|
|
179
201
|
# llm needed 19-41 s for "refused" there, past a 30 s deadline.
|
|
180
202
|
bodies[backend], _ = run.ask_once(binary, f"t-{s}", prompt, {"slow": 4, "refused": 90}.get(s, 30))
|
|
181
203
|
a, b = bodies["llm"], bodies["builtin"]
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return ((run.outcome(x) or ("answered",))[0], (run.outcome(x, 1) or ("answered",))[0],
|
|
185
|
-
run.failure_class(x), run.failure_class(x, 1))
|
|
204
|
+
if s == "ratelimit":
|
|
205
|
+
bodies_429 = bodies
|
|
186
206
|
same = kinds(a) == kinds(b) and (a == b or kinds(a)[1] != "answered")
|
|
187
207
|
print(("ok " if same else "FAIL ") + f"{s:<10} {kinds(b)}")
|
|
188
208
|
if not same:
|
|
@@ -199,12 +219,36 @@ def main() -> int:
|
|
|
199
219
|
print(("ok " if ok else "FAIL ") + f"request matches: {got}, {hdr[1]}"
|
|
200
220
|
+ ("" if ok else f"\n llm sent: {want}, {hdr[0]}"))
|
|
201
221
|
failures += not ok
|
|
222
|
+
# Both stream (2b), as llm always has; the built-in client asks for usage in the stream.
|
|
223
|
+
streams = [sent[w]["body"].get("stream") for w in ("llm", "builtin")]
|
|
224
|
+
ok = streams == [True, True] and sent["builtin"]["body"].get("stream_options") == {"include_usage": True}
|
|
225
|
+
print(("ok " if ok else "FAIL ") + f"both ask for a stream (llm {streams[0]}, built-in {streams[1]},"
|
|
226
|
+
f" usage {sent['builtin']['body'].get('stream_options')})")
|
|
227
|
+
failures += not ok
|
|
202
228
|
# Retries as the SDK does them: three tries at a 429 that never clears.
|
|
203
229
|
tries = [len(seen.get((w, "ratelimit"), [])) for w in ("llm", "builtin")]
|
|
204
230
|
ok = tries == [3, 3]
|
|
205
231
|
print(("ok " if ok else "FAIL ") + f"429 tried {tries[1]} times (llm {tries[0]})")
|
|
206
232
|
failures += not ok
|
|
207
233
|
|
|
234
|
+
# Usage from the stream's last chunk, in the result and in the ledger.
|
|
235
|
+
r = client.chat("t-ok", prompt, timeout=30)
|
|
236
|
+
line = [x for x in ledger.rows() if x["alias"] == "t-ok"][-1]
|
|
237
|
+
ok = (r["usage"].get("completion_tokens") == 5 and line["tokens_in"] == 3 and line["tokens_out"] == 5
|
|
238
|
+
and line["status"] == "ok" and line["connection"] == "fake")
|
|
239
|
+
print(("ok " if ok else "FAIL ") + f"usage from the stream and its ledger line: {r['usage']}, {line}")
|
|
240
|
+
failures += not ok
|
|
241
|
+
# Pacing: the model's own rpm 1; the second call in the minute is not sent, and
|
|
242
|
+
# lands where llm's real 429 lands ("ratelimit" above), at both length floors.
|
|
243
|
+
first = client.chat("t-paced", prompt, timeout=30)
|
|
244
|
+
body = run.ask_once(binary, "t-paced", prompt, 30)[0]
|
|
245
|
+
want = kinds(bodies_429["llm"])
|
|
246
|
+
ok = not first["error"] and "not sent" in body and kinds(body) == want \
|
|
247
|
+
and len(seen.get(("builtin", "paced"), [])) == 1
|
|
248
|
+
print(("ok " if ok else "FAIL ") + f"a paced refusal lands where a real 429 does: {kinds(body)}"
|
|
249
|
+
+ ("" if ok else f"\n llm's 429: {want}; refusal: {body[:150]!r}"))
|
|
250
|
+
failures += not ok
|
|
251
|
+
|
|
208
252
|
# Where the built-in client is stricter than urllib's defaults would be.
|
|
209
253
|
r = client.chat("t-redirect", prompt, timeout=10)
|
|
210
254
|
ok = r["status"] == 302 and "not followed" in r["error"] and not any(leaked)
|