@wendellresearch-ai/benchwarmers 0.1.1 → 0.2.1
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 +147 -147
- package/BENCHMARKS.md +262 -262
- package/GATEWAY.md +272 -271
- package/NOTICE +28 -28
- package/PRIVACY.md +46 -46
- package/README.md +368 -275
- package/SECURITY.md +83 -83
- package/TERMS.md +22 -22
- package/USAGE.md +170 -155
- package/VERSION +1 -1
- package/bin/benchwarmers.js +16 -72
- package/cli.py +666 -0
- package/examples/config/gateway.json +5 -5
- package/examples/config/model-roles.json +11 -11
- package/gateway.py +755 -712
- package/launcher.py +120 -0
- package/mcp_server.py +266 -0
- package/metrics/alerts.py +116 -116
- package/metrics/aliases.py +41 -41
- package/metrics/bench_collections.py +141 -141
- package/metrics/catalog.py +295 -295
- package/metrics/client.py +394 -373
- package/metrics/cliproxy.py +191 -191
- package/metrics/connect.py +691 -691
- package/metrics/cost.py +152 -152
- package/metrics/decide.py +227 -227
- package/metrics/discover.py +318 -318
- package/metrics/endpoints.py +436 -436
- package/metrics/guard.py +617 -592
- package/metrics/learn.py +221 -221
- package/metrics/litellm_hub.py +206 -206
- package/metrics/model_roles.py +84 -84
- package/metrics/offload.py +166 -166
- package/metrics/owui.py +205 -205
- package/metrics/paths.py +60 -60
- package/metrics/route.py +579 -574
- package/metrics/scheduler.py +275 -275
- package/metrics/sources.py +167 -167
- package/metrics/wrong.py +144 -144
- package/package.json +68 -64
- package/perf/README.md +89 -89
- package/perf/run.py +261 -261
- package/scripts/auto_retire.py +367 -367
- package/scripts/check.py +108 -104
- package/scripts/check_deprecations.py +378 -378
- package/scripts/check_themes.py +103 -103
- package/scripts/init.py +124 -124
- package/scripts/list_served.py +78 -78
- package/scripts/notice.py +78 -78
- package/scripts/onboard.py +401 -331
- package/scripts/provider_tags.py +129 -129
- package/scripts/reprefix.py +136 -136
- package/scripts/sync_config.py +105 -105
- package/scripts/test_auth.py +255 -255
- package/scripts/test_cli.py +189 -0
- package/scripts/test_client.py +233 -233
- package/scripts/test_connect.py +222 -222
- package/scripts/test_first_run.py +180 -180
- package/scripts/test_hubs.py +328 -328
- package/scripts/test_launcher.py +67 -0
- package/scripts/test_llm_log.py +85 -0
- package/scripts/test_mcp.py +178 -0
- package/scripts/test_merged.py +214 -193
- package/scripts/test_no_owui.py +291 -291
- package/scripts/test_owui.py +90 -90
- package/scripts/test_serve_edit.py +448 -386
- package/scripts/test_verdict.py +98 -98
- package/scripts/test_vision.py +228 -228
- package/serve.py +3904 -3662
- package/suite/mine.py +154 -154
- package/suite/pace.py +145 -145
- package/suite/run.py +1046 -1040
- package/suite/seed.py +421 -421
- package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
- package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
- package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
- package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
- package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
- package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
- package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
- package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
- package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
- package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
- package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
- package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
- package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
- package/ui/bench.css +430 -424
- package/ui/bench.js +1756 -1555
- package/ui/themes/dark.css +35 -35
- package/ui/themes/hardwood.css +52 -52
- package/ui/themes/rainforest.css +32 -32
package/GATEWAY.md
CHANGED
|
@@ -1,271 +1,272 @@
|
|
|
1
|
-
# Bench gateway — design
|
|
2
|
-
|
|
3
|
-
Status: **built** (2026-10-16): `python gateway.py`, then point an OpenAI client
|
|
4
|
-
at `http://127.0.0.1:8123/v1` with a collection as the model, e.g. `bench/core`.
|
|
5
|
-
Diagram: `.archify/architecture-bench-gateway-20261016-182500/bench-gateway.html`.
|
|
6
|
-
|
|
7
|
-
## The idea in one paragraph
|
|
8
|
-
|
|
9
|
-
The bench gets its own OpenAI-compatible endpoint whose only job is **the bench
|
|
10
|
-
method**: one prompt, several different models, every answer back, every
|
|
11
|
-
answer logged. You define **collections** — named groups of models such as
|
|
12
|
-
`core` or `reviewers` — on the bench. A caller asks for `bench/<name>` and the
|
|
13
|
-
prompt goes to every member of that collection plus one or two **explorer**
|
|
14
|
-
seats the bench fills by staleness, so the roster keeps getting evidence even
|
|
15
|
-
when the same collection is called every day. Each answer comes back labelled
|
|
16
|
-
by model and seat with a call id; the caller checks the answers and records a
|
|
17
|
-
verdict by call id, which lands in the evidence `route.py` reads. Using the
|
|
18
|
-
bench and measuring it stay the same act, without anyone retyping an offload.
|
|
19
|
-
|
|
20
|
-
Single-model calls are **not** the bench's job: chat and coding agents keep
|
|
21
|
-
calling the upstream relay directly. The relay stays the engine; the bench
|
|
22
|
-
gateway sits beside it for the multi-model work.
|
|
23
|
-
|
|
24
|
-
## Collections and explorer seats
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
explorers = min(max_explorers, max_calls - members) defaults: 2 and 5
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
| Members | Explorers | Calls |
|
|
31
|
-
|---|---|---|
|
|
32
|
-
| 1 | 2 | 3 |
|
|
33
|
-
| 2 | 2 | 4 |
|
|
34
|
-
| 3 | 2 | 5 |
|
|
35
|
-
| 4 | 1 | 5 |
|
|
36
|
-
| 5 | 0 | 5 |
|
|
37
|
-
|
|
38
|
-
Two to four members is recommended. Explorers are never a member or a member's
|
|
39
|
-
model family. Collections live in `config/collections.json`, the limits in
|
|
40
|
-
`config/gateway.json`; both are edited in the dashboard's
|
|
41
|
-
**Collections** tab and every change is logged in `config/changes.jsonl`.
|
|
42
|
-
|
|
43
|
-
**Naming:** `bench/<name>`, lower-case letters, digits and `-`. The name says
|
|
44
|
-
what the collection is *for* (`reviewers`, `design`), not who is in it, because
|
|
45
|
-
members change. The `bench/` prefix keeps collections apart from real model ids
|
|
46
|
-
in clients that merge model lists.
|
|
47
|
-
|
|
48
|
-
**Choosing a collection by task:** each collection has a `description` (what
|
|
49
|
-
it is for, up to 300 characters) and `tags` (up to 8 lower-case words).
|
|
50
|
-
`GET /v1/models` returns them with the members, shape and explorer count, so an
|
|
51
|
-
orchestrator can list the collections and pick the one that fits before it
|
|
52
|
-
calls. Write the description for that reader.
|
|
53
|
-
|
|
54
|
-
**No default collection.** A request that names no collection, or one that
|
|
55
|
-
does not exist, is refused (400 / 404) with every collection's description and
|
|
56
|
-
tags in the message, so a caller that could not choose learns how to. A
|
|
57
|
-
`bench/auto` that guessed for it was considered and rejected: the caller knows
|
|
58
|
-
the task, and a guess would run the wrong models silently.
|
|
59
|
-
|
|
60
|
-
## Prompt size
|
|
61
|
-
|
|
62
|
-
The gateway estimates the prompt (system text included) at characters / 4 plus
|
|
63
|
-
10%, which errs high. If that is larger than any **member's** context window,
|
|
64
|
-
the request is refused with **413** naming each member that is too small and
|
|
65
|
-
its window, so the caller can trim the prompt or pick another collection;
|
|
66
|
-
`GET /v1/models` gives each collection's `together.context_window` (the smallest
|
|
67
|
-
known among its members) to check first. Explorer seats skip models too small
|
|
68
|
-
for the prompt, as well as retiring and rate-limited ones.
|
|
69
|
-
|
|
70
|
-
Context windows come from `metrics/catalog.py`: your overrides in the model
|
|
71
|
-
popup, then windows learned from real "maximum context length" errors (only
|
|
72
|
-
ever lower), then the daily refresh from each model's own endpoint listing,
|
|
73
|
-
models.dev and FreeLLMAPI. A model with no known window is never refused.
|
|
74
|
-
Each image adds 2,500 tokens to the estimate (see Images).
|
|
75
|
-
|
|
76
|
-
## Images
|
|
77
|
-
|
|
78
|
-
A request whose messages carry OpenAI `image_url` parts, with an `https://`
|
|
79
|
-
URL or a `data:image/...;base64,` URL, goes to every seat with its messages
|
|
80
|
-
exactly as sent:
|
|
81
|
-
|
|
82
|
-
```json
|
|
83
|
-
{"model": "bench/vision", "messages": [{"role": "user", "content": [
|
|
84
|
-
{"type": "text", "text": "What is this chair, and what might it fetch?"},
|
|
85
|
-
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}]}]}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
- **Through the built-in client**, whatever `BENCH_CLIENT` says: the `llm`
|
|
89
|
-
CLI takes a prompt, not inline images. A text-only request takes the same
|
|
90
|
-
path as before.
|
|
91
|
-
- **Only seats that can see.** A model sits only if the catalog says it takes
|
|
92
|
-
images: its input types from its endpoint's listing, models.dev or
|
|
93
|
-
OpenRouter, or **Vision / OCR** ticked in its model popup. A member that
|
|
94
|
-
cannot, or that the catalog knows nothing about, is not asked and is listed
|
|
95
|
-
in `bench.skipped` with why (`{"seat", "alias", "why"}`; a grey chip on the
|
|
96
|
-
Gateway card); so is a `with` nomination. Explorer seats go only to models
|
|
97
|
-
that take images. With no seat that can see, the request is refused (400)
|
|
98
|
-
rather than answered blind. Unknown counts as cannot because a model that
|
|
99
|
-
cannot see answers anyway, describing a photo it never got, and the popup
|
|
100
|
-
fixes it in one click. A model reached through the Responses API (a ChatGPT
|
|
101
|
-
plan) gets no images yet.
|
|
102
|
-
- **A vision collection** is members that take images; `GET /v1/models` lists
|
|
103
|
-
`vision` in its `together.capabilities` when every member does. An image
|
|
104
|
-
request on a collection with no shape takes the shape `vision` (general and
|
|
105
|
-
light models), so verdicts on image work are counted apart.
|
|
106
|
-
- **Size.** The body cap is 20 MB: a photo as a data URL is 4/3 of its file,
|
|
107
|
-
and 20 MB is the tightest whole-request limit among the big providers
|
|
108
|
-
(Gemini's for inline data). Above it the gateway answers 413 and says to
|
|
109
|
-
send http(s) URLs or smaller images; models scale a photo down to about
|
|
110
|
-
1,500-2,000 pixels on its long side anyway. Each image counts 2,500 tokens
|
|
111
|
-
in the context check, which errs high: OpenAI's, Anthropic's and Gemini's
|
|
112
|
-
models spend up to about 1,600 at their usual detail.
|
|
113
|
-
- **Never written down.** In `runs/gateway/calls.jsonl`, the offload log,
|
|
114
|
-
learned limits, error text (an upstream may echo the request) and the
|
|
115
|
-
dashboard, an image is a placeholder: `[image: data URL, 412 KB]`, or an
|
|
116
|
-
http URL without its query string or user:password
|
|
117
|
-
(`[image: https://photos.example/chair.jpg?...]`). The gateway never
|
|
118
|
-
fetches an image URL; the upstream does.
|
|
119
|
-
- **Merged replies.** The merger gets the request as text with those
|
|
120
|
-
placeholders, plus the seats' answers. Its job is to reconcile what the
|
|
121
|
-
seats said, not to look again, so any model can merge and a disagreement
|
|
122
|
-
about the picture is reported, not settled.
|
|
123
|
-
|
|
124
|
-
## The reply
|
|
125
|
-
|
|
126
|
-
`choices[0].message.content` is markdown: one section per seat with its call
|
|
127
|
-
id, status and time. Beside it, a `bench` field carries the same seats as data,
|
|
128
|
-
so a caller need not parse the markdown (OpenAI clients ignore unknown fields;
|
|
129
|
-
a streaming client gets it on the first chunk):
|
|
130
|
-
|
|
131
|
-
```json
|
|
132
|
-
"bench": {"panel": "p20261017-...", "collection": "bench/core", "shape": "review",
|
|
133
|
-
"shape_by": "set on the collection",
|
|
134
|
-
"seats": [{"call": "p20261017-....member1", "seat": "member", "alias": "…",
|
|
135
|
-
"status": "ok|empty|error", "seconds": 12.3, "answer": "…", "error": ""}],
|
|
136
|
-
"verdict": "POST http://127.0.0.1:8123/bench/verdict {…}"}
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
**Merged replies (opt-in).** A collection's `reply` is `all` (the default) or
|
|
140
|
-
`merged`; a request overrides it with `"bench": {"reply": "merged"}` in the
|
|
141
|
-
body, or a `:merged` / `:all` suffix on the model name for clients that can
|
|
142
|
-
only pick a name (`bench/reviewers:merged`), and `GET /v1/models` lists each
|
|
143
|
-
collection in its other mode too. Merged adds one call after the panel: a
|
|
144
|
-
merger writes a single answer saying where the seats agreed, where they
|
|
145
|
-
disagreed and who said what, and that becomes `content`. The merger is the
|
|
146
|
-
collection's `merger`, or when empty the member with the most useful verdicts
|
|
147
|
-
on the request's shape, else the router's primary, else the first member that
|
|
148
|
-
fits. It gets its own call id (`<panel>.merger`, in `bench.merger`, the
|
|
149
|
-
offload log and the Gateway card) so a verdict can judge the merge; `bench`
|
|
150
|
-
still carries every seat. If no merger has the context window for the
|
|
151
|
-
request plus every answer, or it fails, the reply is every answer with a
|
|
152
|
-
one-line note (`bench.note`). A streaming client gets it as one chunk, like
|
|
153
|
-
any reply.
|
|
154
|
-
|
|
155
|
-
A verdict is `POST /bench/verdict` with `{"call", "useful": true|false, "note",
|
|
156
|
-
"real": [...], "false": [...]}`, `offload.py verdict <call id>`, or the verdict
|
|
157
|
-
dialog in the dashboard's Collections tab, under Gateway (click a seat). Sending the same verdict
|
|
158
|
-
twice writes nothing.
|
|
159
|
-
|
|
160
|
-
## The decision model's job (optional)
|
|
161
|
-
|
|
162
|
-
A collection's members are fixed; only the explorer seats are chosen per
|
|
163
|
-
request, and they should go to models suited to the task — no code-completion
|
|
164
|
-
model on a design question. A collection may set its **shape** (review, design,
|
|
165
|
-
enumeration, …), and then that decides. Left on *automatic*, a decision model
|
|
166
|
-
labels the prompt, when one that takes choice questions is onboarded and not ad
|
|
167
|
-
hoc; otherwise, or on failure or confidence under 0.6, the shape is `general`.
|
|
168
|
-
With no explorer seats to fill, nothing is asked.
|
|
169
|
-
|
|
170
|
-
## Who may call
|
|
171
|
-
|
|
172
|
-
| Where it runs | Set | The gateway asks for |
|
|
173
|
-
|---|---|---|
|
|
174
|
-
| This machine, as installed | nothing | no key: every caller is `local`, any bearer is ignored |
|
|
175
|
-
| This machine, one key per app | `BENCHWARMERS_REQUIRE_KEYS=1` | a gateway key; the dashboard stays open |
|
|
176
|
-
| LAN, Docker or cloud | `BENCHWARMERS_ADMIN_TOKEN`, `BENCH_BIND`, `BENCHWARMERS_PUBLIC_URL` | a gateway key on every `/v1/...` and `/bench/...`; the dashboard asks for the token |
|
|
177
|
-
| A browser app on another origin | `BENCH_ALLOWED_ORIGINS=https://app.example` (comma-separated), plus one of the rows above | as that row says |
|
|
178
|
-
|
|
179
|
-
**Gateway keys** are made on Settings, sent as `Authorization: Bearer <key>`,
|
|
180
|
-
shown once and kept as a SHA-256. Each has a name, written on every call it
|
|
181
|
-
makes (`key` in `calls.jsonl`, "Asked by" on the Gateway card), and a "last
|
|
182
|
-
used" on Settings. Optional, set when it is made: the **collections** it may
|
|
183
|
-
call (others are refused with 403, and `GET /v1/models` lists only its own)
|
|
184
|
-
and **requests a minute** (429 with `Retry-After` past it; counted per
|
|
185
|
-
process). Keys start `bwk_`, so a leaked one is recognisable, and Settings
|
|
186
|
-
shows each key's last four characters to tell which. A missing, malformed or
|
|
187
|
-
unknown key is refused with 401, a message saying which, and
|
|
188
|
-
`WWW-Authenticate: Bearer`.
|
|
189
|
-
|
|
190
|
-
**CORS** is answered on the gateway's routes only, for an allowed origin
|
|
191
|
-
(`BENCH_ALLOWED_ORIGINS`, and `BENCHWARMERS_PUBLIC_URL`'s): the preflight
|
|
192
|
-
allows `Authorization`, `Content-Type` and any other plain header names it
|
|
193
|
-
asks for (the OpenAI JS SDK sends `X-Stainless-*`), and every reply echoes
|
|
194
|
-
the origin with `Vary: Origin`. Never `*`, never credentials: the key travels
|
|
195
|
-
in a header, not a cookie. Such an origin may be cross-site; every other
|
|
196
|
-
browser request stays same-origin, and the dashboard answers no preflight. A
|
|
197
|
-
key in a page's code is visible to whoever opens it, so give a browser app a
|
|
198
|
-
key limited to its collection and a rate, or keep the key on the app's server.
|
|
199
|
-
|
|
200
|
-
## Components
|
|
201
|
-
|
|
202
|
-
| Part | Job |
|
|
203
|
-
|---|---|
|
|
204
|
-
| Gateway | `gateway.py`, stdlib server on 127.0.0.1. `GET /v1/models` lists the collections with description, tags and make-up; `POST /v1/chat/completions` runs one, images included. A streaming client gets the finished answer as one chunk. |
|
|
205
|
-
| Collections | `metrics/bench_collections.py` reads the two config files and computes explorer seats. |
|
|
206
|
-
| Seat router | `route.panel(members=…, explorers=n)` fills explorer seats by staleness for the shape. |
|
|
207
|
-
| Shape picker | Optional decision model (above). |
|
|
208
|
-
| Call log | `runs/gateway/calls.jsonl`, gitignored: every seat's answer, status, timing and call id. |
|
|
209
|
-
| Evidence | Each seat is recorded in `metrics/offloads.jsonl` as an unjudged offload when it answers; `offload.py verdict <call id>` or `POST /bench/verdict` replaces it. |
|
|
210
|
-
| Collections tab | The collections and their form, then the gateway's recent requests: every seat's status and verdict, and how many verdicts are owed. Clicking a seat opens its answer and the verdict form. |
|
|
211
|
-
| Catalog | `metrics/catalog.py`: context window, output limit, rate limits and capabilities per model, refreshed daily (scheduled task WRBench-Catalog); a free model whose price goes above zero raises a "went paid" alert. |
|
|
212
|
-
| Discover tab | `metrics/discover.py`: models.dev and FreeLLMAPI merged into one list of models worth adding, free first, marked by whether you can already reach the provider and whether the model is on the bench (on another provider counts as "also on bench"). |
|
|
213
|
-
| Merged replies | Opt-in: one merger answer as `content`, every seat still in `bench` (The reply). |
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
- **The
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
- **
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1
|
+
# Bench gateway — design
|
|
2
|
+
|
|
3
|
+
Status: **built** (2026-10-16): `python gateway.py`, then point an OpenAI client
|
|
4
|
+
at `http://127.0.0.1:8123/v1` with a collection as the model, e.g. `bench/core`.
|
|
5
|
+
Diagram: `.archify/architecture-bench-gateway-20261016-182500/bench-gateway.html`.
|
|
6
|
+
|
|
7
|
+
## The idea in one paragraph
|
|
8
|
+
|
|
9
|
+
The bench gets its own OpenAI-compatible endpoint whose only job is **the bench
|
|
10
|
+
method**: one prompt, several different models, every answer back, every
|
|
11
|
+
answer logged. You define **collections** — named groups of models such as
|
|
12
|
+
`core` or `reviewers` — on the bench. A caller asks for `bench/<name>` and the
|
|
13
|
+
prompt goes to every member of that collection plus one or two **explorer**
|
|
14
|
+
seats the bench fills by staleness, so the roster keeps getting evidence even
|
|
15
|
+
when the same collection is called every day. Each answer comes back labelled
|
|
16
|
+
by model and seat with a call id; the caller checks the answers and records a
|
|
17
|
+
verdict by call id, which lands in the evidence `route.py` reads. Using the
|
|
18
|
+
bench and measuring it stay the same act, without anyone retyping an offload.
|
|
19
|
+
|
|
20
|
+
Single-model calls are **not** the bench's job: chat and coding agents keep
|
|
21
|
+
calling the upstream relay directly. The relay stays the engine; the bench
|
|
22
|
+
gateway sits beside it for the multi-model work.
|
|
23
|
+
|
|
24
|
+
## Collections and explorer seats
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
explorers = min(max_explorers, max_calls - members) defaults: 2 and 5
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Members | Explorers | Calls |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 1 | 2 | 3 |
|
|
33
|
+
| 2 | 2 | 4 |
|
|
34
|
+
| 3 | 2 | 5 |
|
|
35
|
+
| 4 | 1 | 5 |
|
|
36
|
+
| 5 | 0 | 5 |
|
|
37
|
+
|
|
38
|
+
Two to four members is recommended. Explorers are never a member or a member's
|
|
39
|
+
model family. Collections live in `config/collections.json`, the limits in
|
|
40
|
+
`config/gateway.json`; both are edited in the dashboard's
|
|
41
|
+
**Collections** tab and every change is logged in `config/changes.jsonl`.
|
|
42
|
+
|
|
43
|
+
**Naming:** `bench/<name>`, lower-case letters, digits and `-`. The name says
|
|
44
|
+
what the collection is *for* (`reviewers`, `design`), not who is in it, because
|
|
45
|
+
members change. The `bench/` prefix keeps collections apart from real model ids
|
|
46
|
+
in clients that merge model lists.
|
|
47
|
+
|
|
48
|
+
**Choosing a collection by task:** each collection has a `description` (what
|
|
49
|
+
it is for, up to 300 characters) and `tags` (up to 8 lower-case words).
|
|
50
|
+
`GET /v1/models` returns them with the members, shape and explorer count, so an
|
|
51
|
+
orchestrator can list the collections and pick the one that fits before it
|
|
52
|
+
calls. Write the description for that reader.
|
|
53
|
+
|
|
54
|
+
**No default collection.** A request that names no collection, or one that
|
|
55
|
+
does not exist, is refused (400 / 404) with every collection's description and
|
|
56
|
+
tags in the message, so a caller that could not choose learns how to. A
|
|
57
|
+
`bench/auto` that guessed for it was considered and rejected: the caller knows
|
|
58
|
+
the task, and a guess would run the wrong models silently.
|
|
59
|
+
|
|
60
|
+
## Prompt size
|
|
61
|
+
|
|
62
|
+
The gateway estimates the prompt (system text included) at characters / 4 plus
|
|
63
|
+
10%, which errs high. If that is larger than any **member's** context window,
|
|
64
|
+
the request is refused with **413** naming each member that is too small and
|
|
65
|
+
its window, so the caller can trim the prompt or pick another collection;
|
|
66
|
+
`GET /v1/models` gives each collection's `together.context_window` (the smallest
|
|
67
|
+
known among its members) to check first. Explorer seats skip models too small
|
|
68
|
+
for the prompt, as well as retiring and rate-limited ones.
|
|
69
|
+
|
|
70
|
+
Context windows come from `metrics/catalog.py`: your overrides in the model
|
|
71
|
+
popup, then windows learned from real "maximum context length" errors (only
|
|
72
|
+
ever lower), then the daily refresh from each model's own endpoint listing,
|
|
73
|
+
models.dev and FreeLLMAPI. A model with no known window is never refused.
|
|
74
|
+
Each image adds 2,500 tokens to the estimate (see Images).
|
|
75
|
+
|
|
76
|
+
## Images
|
|
77
|
+
|
|
78
|
+
A request whose messages carry OpenAI `image_url` parts, with an `https://`
|
|
79
|
+
URL or a `data:image/...;base64,` URL, goes to every seat with its messages
|
|
80
|
+
exactly as sent:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{"model": "bench/vision", "messages": [{"role": "user", "content": [
|
|
84
|
+
{"type": "text", "text": "What is this chair, and what might it fetch?"},
|
|
85
|
+
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}]}]}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **Through the built-in client**, whatever `BENCH_CLIENT` says: the `llm`
|
|
89
|
+
CLI takes a prompt, not inline images. A text-only request takes the same
|
|
90
|
+
path as before.
|
|
91
|
+
- **Only seats that can see.** A model sits only if the catalog says it takes
|
|
92
|
+
images: its input types from its endpoint's listing, models.dev or
|
|
93
|
+
OpenRouter, or **Vision / OCR** ticked in its model popup. A member that
|
|
94
|
+
cannot, or that the catalog knows nothing about, is not asked and is listed
|
|
95
|
+
in `bench.skipped` with why (`{"seat", "alias", "why"}`; a grey chip on the
|
|
96
|
+
Gateway card); so is a `with` nomination. Explorer seats go only to models
|
|
97
|
+
that take images. With no seat that can see, the request is refused (400)
|
|
98
|
+
rather than answered blind. Unknown counts as cannot because a model that
|
|
99
|
+
cannot see answers anyway, describing a photo it never got, and the popup
|
|
100
|
+
fixes it in one click. A model reached through the Responses API (a ChatGPT
|
|
101
|
+
plan) gets no images yet.
|
|
102
|
+
- **A vision collection** is members that take images; `GET /v1/models` lists
|
|
103
|
+
`vision` in its `together.capabilities` when every member does. An image
|
|
104
|
+
request on a collection with no shape takes the shape `vision` (general and
|
|
105
|
+
light models), so verdicts on image work are counted apart.
|
|
106
|
+
- **Size.** The body cap is 20 MB: a photo as a data URL is 4/3 of its file,
|
|
107
|
+
and 20 MB is the tightest whole-request limit among the big providers
|
|
108
|
+
(Gemini's for inline data). Above it the gateway answers 413 and says to
|
|
109
|
+
send http(s) URLs or smaller images; models scale a photo down to about
|
|
110
|
+
1,500-2,000 pixels on its long side anyway. Each image counts 2,500 tokens
|
|
111
|
+
in the context check, which errs high: OpenAI's, Anthropic's and Gemini's
|
|
112
|
+
models spend up to about 1,600 at their usual detail.
|
|
113
|
+
- **Never written down.** In `runs/gateway/calls.jsonl`, the offload log,
|
|
114
|
+
learned limits, error text (an upstream may echo the request) and the
|
|
115
|
+
dashboard, an image is a placeholder: `[image: data URL, 412 KB]`, or an
|
|
116
|
+
http URL without its query string or user:password
|
|
117
|
+
(`[image: https://photos.example/chair.jpg?...]`). The gateway never
|
|
118
|
+
fetches an image URL; the upstream does.
|
|
119
|
+
- **Merged replies.** The merger gets the request as text with those
|
|
120
|
+
placeholders, plus the seats' answers. Its job is to reconcile what the
|
|
121
|
+
seats said, not to look again, so any model can merge and a disagreement
|
|
122
|
+
about the picture is reported, not settled.
|
|
123
|
+
|
|
124
|
+
## The reply
|
|
125
|
+
|
|
126
|
+
`choices[0].message.content` is markdown: one section per seat with its call
|
|
127
|
+
id, status and time. Beside it, a `bench` field carries the same seats as data,
|
|
128
|
+
so a caller need not parse the markdown (OpenAI clients ignore unknown fields;
|
|
129
|
+
a streaming client gets it on the first chunk):
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
"bench": {"panel": "p20261017-...", "collection": "bench/core", "shape": "review",
|
|
133
|
+
"shape_by": "set on the collection",
|
|
134
|
+
"seats": [{"call": "p20261017-....member1", "seat": "member", "alias": "…",
|
|
135
|
+
"status": "ok|empty|error", "seconds": 12.3, "answer": "…", "error": ""}],
|
|
136
|
+
"verdict": "POST http://127.0.0.1:8123/bench/verdict {…}"}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Merged replies (opt-in).** A collection's `reply` is `all` (the default) or
|
|
140
|
+
`merged`; a request overrides it with `"bench": {"reply": "merged"}` in the
|
|
141
|
+
body, or a `:merged` / `:all` suffix on the model name for clients that can
|
|
142
|
+
only pick a name (`bench/reviewers:merged`), and `GET /v1/models` lists each
|
|
143
|
+
collection in its other mode too. Merged adds one call after the panel: a
|
|
144
|
+
merger writes a single answer saying where the seats agreed, where they
|
|
145
|
+
disagreed and who said what, and that becomes `content`. The merger is the
|
|
146
|
+
collection's `merger`, or when empty the member with the most useful verdicts
|
|
147
|
+
on the request's shape, else the router's primary, else the first member that
|
|
148
|
+
fits. It gets its own call id (`<panel>.merger`, in `bench.merger`, the
|
|
149
|
+
offload log and the Gateway card) so a verdict can judge the merge; `bench`
|
|
150
|
+
still carries every seat. If no merger has the context window for the
|
|
151
|
+
request plus every answer, or it fails, the reply is every answer with a
|
|
152
|
+
one-line note (`bench.note`). A streaming client gets it as one chunk, like
|
|
153
|
+
any reply.
|
|
154
|
+
|
|
155
|
+
A verdict is `POST /bench/verdict` with `{"call", "useful": true|false, "note",
|
|
156
|
+
"real": [...], "false": [...]}`, `offload.py verdict <call id>`, or the verdict
|
|
157
|
+
dialog in the dashboard's Collections tab, under Gateway (click a seat). Sending the same verdict
|
|
158
|
+
twice writes nothing.
|
|
159
|
+
|
|
160
|
+
## The decision model's job (optional)
|
|
161
|
+
|
|
162
|
+
A collection's members are fixed; only the explorer seats are chosen per
|
|
163
|
+
request, and they should go to models suited to the task — no code-completion
|
|
164
|
+
model on a design question. A collection may set its **shape** (review, design,
|
|
165
|
+
enumeration, …), and then that decides. Left on *automatic*, a decision model
|
|
166
|
+
labels the prompt, when one that takes choice questions is onboarded and not ad
|
|
167
|
+
hoc; otherwise, or on failure or confidence under 0.6, the shape is `general`.
|
|
168
|
+
With no explorer seats to fill, nothing is asked.
|
|
169
|
+
|
|
170
|
+
## Who may call
|
|
171
|
+
|
|
172
|
+
| Where it runs | Set | The gateway asks for |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| This machine, as installed | nothing | no key: every caller is `local`, any bearer is ignored |
|
|
175
|
+
| This machine, one key per app | `BENCHWARMERS_REQUIRE_KEYS=1` | a gateway key; the dashboard stays open |
|
|
176
|
+
| LAN, Docker or cloud | `BENCHWARMERS_ADMIN_TOKEN`, `BENCH_BIND`, `BENCHWARMERS_PUBLIC_URL` | a gateway key on every `/v1/...` and `/bench/...`; the dashboard asks for the token |
|
|
177
|
+
| A browser app on another origin | `BENCH_ALLOWED_ORIGINS=https://app.example` (comma-separated), plus one of the rows above | as that row says |
|
|
178
|
+
|
|
179
|
+
**Gateway keys** are made on Settings, sent as `Authorization: Bearer <key>`,
|
|
180
|
+
shown once and kept as a SHA-256. Each has a name, written on every call it
|
|
181
|
+
makes (`key` in `calls.jsonl`, "Asked by" on the Gateway card), and a "last
|
|
182
|
+
used" on Settings. Optional, set when it is made: the **collections** it may
|
|
183
|
+
call (others are refused with 403, and `GET /v1/models` lists only its own)
|
|
184
|
+
and **requests a minute** (429 with `Retry-After` past it; counted per
|
|
185
|
+
process). Keys start `bwk_`, so a leaked one is recognisable, and Settings
|
|
186
|
+
shows each key's last four characters to tell which. A missing, malformed or
|
|
187
|
+
unknown key is refused with 401, a message saying which, and
|
|
188
|
+
`WWW-Authenticate: Bearer`.
|
|
189
|
+
|
|
190
|
+
**CORS** is answered on the gateway's routes only, for an allowed origin
|
|
191
|
+
(`BENCH_ALLOWED_ORIGINS`, and `BENCHWARMERS_PUBLIC_URL`'s): the preflight
|
|
192
|
+
allows `Authorization`, `Content-Type` and any other plain header names it
|
|
193
|
+
asks for (the OpenAI JS SDK sends `X-Stainless-*`), and every reply echoes
|
|
194
|
+
the origin with `Vary: Origin`. Never `*`, never credentials: the key travels
|
|
195
|
+
in a header, not a cookie. Such an origin may be cross-site; every other
|
|
196
|
+
browser request stays same-origin, and the dashboard answers no preflight. A
|
|
197
|
+
key in a page's code is visible to whoever opens it, so give a browser app a
|
|
198
|
+
key limited to its collection and a rate, or keep the key on the app's server.
|
|
199
|
+
|
|
200
|
+
## Components
|
|
201
|
+
|
|
202
|
+
| Part | Job |
|
|
203
|
+
|---|---|
|
|
204
|
+
| Gateway | `gateway.py`, stdlib server on 127.0.0.1. `GET /v1/models` lists the collections with description, tags and make-up; `POST /v1/chat/completions` runs one, images included. A streaming client gets the finished answer as one chunk. |
|
|
205
|
+
| Collections | `metrics/bench_collections.py` reads the two config files and computes explorer seats. |
|
|
206
|
+
| Seat router | `route.panel(members=…, explorers=n)` fills explorer seats by staleness for the shape. |
|
|
207
|
+
| Shape picker | Optional decision model (above). |
|
|
208
|
+
| Call log | `runs/gateway/calls.jsonl`, gitignored: every seat's answer, status, timing and call id. |
|
|
209
|
+
| Evidence | Each seat is recorded in `metrics/offloads.jsonl` as an unjudged offload when it answers; `offload.py verdict <call id>` or `POST /bench/verdict` replaces it. |
|
|
210
|
+
| Collections tab | The collections and their form, then the gateway's recent requests: every seat's status and verdict, and how many verdicts are owed. Clicking a seat opens its answer and the verdict form. |
|
|
211
|
+
| Catalog | `metrics/catalog.py`: context window, output limit, rate limits and capabilities per model, refreshed daily (scheduled task WRBench-Catalog); a free model whose price goes above zero raises a "went paid" alert. |
|
|
212
|
+
| Discover tab | `metrics/discover.py`: models.dev and FreeLLMAPI merged into one list of models worth adding, free first, marked by whether you can already reach the provider and whether the model is on the bench (on another provider counts as "also on bench"). |
|
|
213
|
+
| Merged replies | Opt-in: one merger answer as `content`, every seat still in `bench` (The reply). |
|
|
214
|
+
| MCP server | `mcp_server.py` (`benchwarmers mcp`): the same three routes as MCP tools for agents, `list_collections`, `ask` and `verdict`, over HTTP to a running server (`BENCHWARMERS_URL`, `BENCHWARMERS_KEY`), so it is a caller like any other. The Claude Code plugin in `plugin/` wraps it with a command and a skill. |
|
|
215
|
+
|
|
216
|
+
## Rules
|
|
217
|
+
|
|
218
|
+
- **The bench method only.** No single-model routing.
|
|
219
|
+
- **The caller judges.** Only the caller can check an answer against the real
|
|
220
|
+
code. Models fail together, so agreement between seats is not a verdict.
|
|
221
|
+
- **Every seat is named** in the response, including ones that failed or came
|
|
222
|
+
back empty, because who answered is itself evidence about the roster.
|
|
223
|
+
- **Free models only**, so rate limits are the constraint; a failed seat is
|
|
224
|
+
reported and logged, not retried into the limit.
|
|
225
|
+
- **Privacy:** prompts go only to providers the bench already uses.
|
|
226
|
+
- **Bind to 127.0.0.1**, as the dashboard does. Elsewhere only with an admin
|
|
227
|
+
token set, and then every call carries a gateway key made on the
|
|
228
|
+
dashboard's Settings page (`Authorization: Bearer <key>`); see "Who may
|
|
229
|
+
call". The dashboard answers the same routes on its own port, so one port
|
|
230
|
+
can serve both (README, "Security").
|
|
231
|
+
|
|
232
|
+
## What is and isn't new
|
|
233
|
+
|
|
234
|
+
A git-for-me search (2026-10-16) found gateways (mozilla-ai/otari, Gozar,
|
|
235
|
+
NadirClaw), councils (karpathy/llm-council, togethercomputer/MoA) and
|
|
236
|
+
observability platforms (opik). None serves a curated multi-model collection as
|
|
237
|
+
an OpenAI model name with exploration seats, and none closes the loop from a
|
|
238
|
+
caller's verdict back into who gets seated next. LiteLLM was rejected: a second
|
|
239
|
+
copy of the alias table and a plugin to write bench evidence.
|
|
240
|
+
|
|
241
|
+
## Decisions (2026-10-16)
|
|
242
|
+
|
|
243
|
+
- **No council route, for now.** A council would have the members review each
|
|
244
|
+
other and one model merge the result into a single answer. That suits a
|
|
245
|
+
caller that cannot read several answers; every caller so far can, and the
|
|
246
|
+
orchestrator verifies against the code better than a merging model. A merge
|
|
247
|
+
also hides who said what, which is the evidence the bench runs on. Revisit
|
|
248
|
+
if a single-answer client needs the bench. (Revisited 2026-10-19 for chat
|
|
249
|
+
UIs: merged replies, opt-in, with every seat still called, recorded and
|
|
250
|
+
returned in `bench`, and the merge told to keep who said what.)
|
|
251
|
+
- **Collections replace `bench/panel`.** Every request names a collection;
|
|
252
|
+
the bench adds the explorers.
|
|
253
|
+
- **No single-model routes**, which also dropped the shadow worker and judge
|
|
254
|
+
model from the first draft: they learned from single-model traffic that no
|
|
255
|
+
longer passes through the bench.
|
|
256
|
+
- **Decision models** are onboarded into `config/decision-models.json` with
|
|
257
|
+
the question types they accept, and called through the endpoint they were
|
|
258
|
+
picked from. Option descriptions must be given, because bare names mislead
|
|
259
|
+
them (Mercury Decide called an enumeration `code` at 0.93).
|
|
260
|
+
- **Provider paths differ:** Inception and OpenRouter call the API
|
|
261
|
+
`decisions`, Ollama `systemone`. Open WebUI's passthrough appends the path to
|
|
262
|
+
the connection's base, so its URL carries `{path}` and onboarding records the
|
|
263
|
+
one that answered.
|
|
264
|
+
|
|
265
|
+
## Reviews so far
|
|
266
|
+
|
|
267
|
+
- `bench/panel` reviewed `gateway.py`: Mercury Decide picked `review` at 0.99;
|
|
268
|
+
two seats found real defects, two found nothing, two agreed on a streaming
|
|
269
|
+
hang that was false.
|
|
270
|
+
- `bench/core` reviewed the collections change: 3 members + 2 explorers, shape
|
|
271
|
+
`review` at 0.97; four seats found real defects (fixed), one came back empty,
|
|
272
|
+
and three asserted a duplicate-seat bug the code already prevents.
|
package/NOTICE
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
Benchwarmers, by Wendell Research
|
|
2
|
-
Copyright (c) 2026 Wendell Research. Licensed under the MIT License (LICENSE).
|
|
3
|
-
|
|
4
|
-
This repository also contains material from others, under their own licences:
|
|
5
|
-
|
|
6
|
-
Real-bug tasks (suite/tasks/rebench-*) are drawn from the nebius/SWE-rebench-V2 dataset
|
|
7
|
-
on Hugging Face (CC-BY-4.0). Each task holds code from the project it names,
|
|
8
|
-
under that project's licence:
|
|
9
|
-
|
|
10
|
-
matchms/matchms (Apache-2.0)
|
|
11
|
-
https://github.com/matchms/matchms/pull/809
|
|
12
|
-
more-itertools/more-itertools (MIT)
|
|
13
|
-
https://github.com/more-itertools/more-itertools/pull/1028
|
|
14
|
-
narwhals-dev/narwhals (MIT)
|
|
15
|
-
https://github.com/narwhals-dev/narwhals/pull/2864
|
|
16
|
-
QuantEcon/QuantEcon.py (MIT)
|
|
17
|
-
https://github.com/QuantEcon/QuantEcon.py/pull/769
|
|
18
|
-
scrapy/scrapy (BSD-3-Clause)
|
|
19
|
-
https://github.com/scrapy/scrapy/pull/6867
|
|
20
|
-
|
|
21
|
-
Fonts (ui/fonts): Chivo and Chivo Mono, Copyright The Chivo Project Authors
|
|
22
|
-
(https://github.com/Omnibus-Type/Chivo), under the SIL Open Font License 1.1
|
|
23
|
-
(ui/fonts/OFL-*.txt).
|
|
24
|
-
|
|
25
|
-
The Wendell Research mascot (ui/mascot.png, drawn at centre court in the
|
|
26
|
-
Hardwood theme) is a registered trademark of Wendell Research. It is not
|
|
27
|
-
covered by the MIT License: a fork may keep it in place unchanged, but should
|
|
28
|
-
not use it to name or brand another product.
|
|
1
|
+
Benchwarmers, by Wendell Research
|
|
2
|
+
Copyright (c) 2026 Wendell Research. Licensed under the MIT License (LICENSE).
|
|
3
|
+
|
|
4
|
+
This repository also contains material from others, under their own licences:
|
|
5
|
+
|
|
6
|
+
Real-bug tasks (suite/tasks/rebench-*) are drawn from the nebius/SWE-rebench-V2 dataset
|
|
7
|
+
on Hugging Face (CC-BY-4.0). Each task holds code from the project it names,
|
|
8
|
+
under that project's licence:
|
|
9
|
+
|
|
10
|
+
matchms/matchms (Apache-2.0)
|
|
11
|
+
https://github.com/matchms/matchms/pull/809
|
|
12
|
+
more-itertools/more-itertools (MIT)
|
|
13
|
+
https://github.com/more-itertools/more-itertools/pull/1028
|
|
14
|
+
narwhals-dev/narwhals (MIT)
|
|
15
|
+
https://github.com/narwhals-dev/narwhals/pull/2864
|
|
16
|
+
QuantEcon/QuantEcon.py (MIT)
|
|
17
|
+
https://github.com/QuantEcon/QuantEcon.py/pull/769
|
|
18
|
+
scrapy/scrapy (BSD-3-Clause)
|
|
19
|
+
https://github.com/scrapy/scrapy/pull/6867
|
|
20
|
+
|
|
21
|
+
Fonts (ui/fonts): Chivo and Chivo Mono, Copyright The Chivo Project Authors
|
|
22
|
+
(https://github.com/Omnibus-Type/Chivo), under the SIL Open Font License 1.1
|
|
23
|
+
(ui/fonts/OFL-*.txt).
|
|
24
|
+
|
|
25
|
+
The Wendell Research mascot (ui/mascot.png, drawn at centre court in the
|
|
26
|
+
Hardwood theme) is a registered trademark of Wendell Research. It is not
|
|
27
|
+
covered by the MIT License: a fork may keep it in place unchanged, but should
|
|
28
|
+
not use it to name or brand another product.
|