@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.
@@ -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
- def kinds(x):
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)