@wendellresearch-ai/benchwarmers 0.2.1 → 0.2.2

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 CHANGED
@@ -105,6 +105,9 @@ Any OpenAI-compatible client asks for the model `bench/<collection>`:
105
105
  -d '{"model": "bench/<collection>", "messages": [{"role": "user", "content": "..."}]}'
106
106
 
107
107
  Every seat's answer comes back, plus a `bench` field with each seat's call id.
108
+ Clients of the Anthropic, Ollama, Gemini and Responses APIs work too, with
109
+ the server's address as their base URL (GATEWAY.md, "Other API formats");
110
+ requests carrying tools are refused, since a panel answers with text.
108
111
  For a chat window that shows one answer, ask for `bench/<collection>:merged`
109
112
  (or set the collection's reply to merged): a merger model writes one answer
110
113
  saying where the seats agreed and disagreed, and every seat is still in
@@ -133,7 +136,9 @@ browser app on another origin also needs `BENCH_ALLOWED_ORIGINS`. Check the
133
136
  result on Settings, Server and access, which shows each of these as it stands
134
137
  (never a secret's value). In Docker: put the token and the `BENCHWARMERS_KEY_<NAME>` variables in
135
138
  a `.env` beside `compose.yaml`, then `docker compose up -d`; the dashboard and
136
- gateway share one port and the data folder is a volume.
139
+ gateway share one port and the data folder is a volume. For a hosting platform
140
+ (Fly.io, Railway, Render) or a VPS with Caddy, follow [HOSTING.md](HOSTING.md)
141
+ and its templates under `deploy/`.
137
142
 
138
143
  ## When something looks wrong
139
144
 
package/GATEWAY.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Status: **built** (2026-10-16): `python gateway.py`, then point an OpenAI client
4
4
  at `http://127.0.0.1:8123/v1` with a collection as the model, e.g. `bench/core`.
5
+ Since 2026-10-23 it also speaks Anthropic Messages, OpenAI Responses, Ollama,
6
+ Gemini and OpenAI's legacy completions, and streams each seat as it finishes
7
+ (Other API formats, Streaming).
5
8
  Diagram: `.archify/architecture-bench-gateway-20261016-182500/bench-gateway.html`.
6
9
 
7
10
  ## The idea in one paragraph
@@ -101,8 +104,15 @@ exactly as sent:
101
104
  plan) gets no images yet.
102
105
  - **A vision collection** is members that take images; `GET /v1/models` lists
103
106
  `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.
107
+ request on a collection with no shape takes the shape `vision` (general,
108
+ light and vision models), so verdicts on image work are counted apart.
109
+ - **Vision specialists.** A model whose role is `vision` (set in its popup) is
110
+ good at reading images and weak at text-only work, such as an OCR or
111
+ document model. Only the `vision` shape admits it, and as a collection
112
+ member it sits only when the request carries images; on a text request it
113
+ is listed in `bench.skipped` and its seat goes to the next member or an
114
+ explorer. Named in `with`, it sits anyway. A model that reads images and
115
+ does everything else well stays `general`.
106
116
  - **Size.** The body cap is 20 MB: a photo as a data URL is 4/3 of its file,
107
117
  and 20 MB is the tightest whole-request limit among the big providers
108
118
  (Gemini's for inline data). Above it the gateway answers 413 and says to
@@ -126,7 +136,7 @@ exactly as sent:
126
136
  `choices[0].message.content` is markdown: one section per seat with its call
127
137
  id, status and time. Beside it, a `bench` field carries the same seats as data,
128
138
  so a caller need not parse the markdown (OpenAI clients ignore unknown fields;
129
- a streaming client gets it on the first chunk):
139
+ a streaming client gets it on the last chunk, the one with the finish_reason):
130
140
 
131
141
  ```json
132
142
  "bench": {"panel": "p20261017-...", "collection": "bench/core", "shape": "review",
@@ -149,14 +159,146 @@ fits. It gets its own call id (`<panel>.merger`, in `bench.merger`, the
149
159
  offload log and the Gateway card) so a verdict can judge the merge; `bench`
150
160
  still carries every seat. If no merger has the context window for the
151
161
  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.
162
+ one-line note (`bench.note`). A streaming client gets the merged answer in one
163
+ piece once the merge is done (Streaming).
154
164
 
155
165
  A verdict is `POST /bench/verdict` with `{"call", "useful": true|false, "note",
156
166
  "real": [...], "false": [...]}`, `offload.py verdict <call id>`, or the verdict
157
167
  dialog in the dashboard's Collections tab, under Gateway (click a seat). Sending the same verdict
158
168
  twice writes nothing.
159
169
 
170
+ ## Streaming
171
+
172
+ With `"stream": true` (or the format's own way of asking, below):
173
+
174
+ - **The 200 goes at once**: as soon as the panel is seated, and at the
175
+ latest after a second (the shape picker can take longer). A refusal raised
176
+ before then (too large for a member, an image no seat can see, no seat
177
+ free) keeps its status and the format's error body, as without streaming.
178
+ - **Keep-alives** go every 10 seconds while the seats think, in the format's
179
+ own form: an SSE comment for the OpenAI formats, Anthropic's `ping`, a
180
+ chunk with no text for Ollama and Gemini (their readers have no comments).
181
+ A client or proxy with an idle timeout keeps the connection.
182
+ - **Every answer** (`reply` all): the heading, then each seat's section the
183
+ moment that seat finishes, then the verdict lines. The pieces are the same
184
+ as the non-streamed reply's, with the seats in the order they finished
185
+ rather than seat order.
186
+ - **Merged**: the merged answer in one piece when the merge is done (or
187
+ every answer with its note, if the merge fails). Token by token waits for
188
+ the built-in client's streaming.
189
+ - **An error after the 200** goes into the stream in the format's own error
190
+ shape (the OpenAI SDKs' `{"error": ...}` chunk, Anthropic's `error` event,
191
+ Responses' `error` and `response.failed`, Ollama's `{"error": ...}` line,
192
+ Gemini's error object), so a client raises rather than hangs.
193
+ - **The `bench` field** rides on the last event where the format has room
194
+ (the table below). It is in the JSON on the wire; an SDK that parses into
195
+ fixed types may drop it (the Anthropic SDK's accumulated final message and
196
+ the `ollama` library's replies do; read the raw event, or the call ids,
197
+ which every reply's text carries). A caller that hangs up does not stop
198
+ the panel: every seat still answers and is recorded.
199
+
200
+ ## Other API formats
201
+
202
+ Each is a thin adapter around the same panel (`metrics/formats.py`): the
203
+ request becomes OpenAI-style messages, the panel runs exactly as on
204
+ `/v1/chat/completions` (collections, explorers, size check, images, keys,
205
+ scope, rate caps, CORS, the call log), and the reply goes back in the
206
+ format's own shape. The model field is the collection: `bench/core`, or
207
+ `bench/core:merged` for one merged answer. Clients that pick from a list
208
+ (`/api/tags`, `/v1beta/models`, `/v1/models`) see every collection in both
209
+ reply modes.
210
+
211
+ | Format | Routes | Streams with | `bench` field | Known clients |
212
+ |---|---|---|---|---|
213
+ | OpenAI chat completions | `POST /v1/chat/completions`, `GET /v1/models` | `"stream": true` (SSE) | in the reply; the last chunk | OpenAI SDKs, Open WebUI's OpenAI connection, most chat apps |
214
+ | OpenAI Responses | `POST /v1/responses` | `"stream": true` (typed events) | in the response; `response.completed` | OpenAI SDKs' `responses.create`, Responses-only clients |
215
+ | OpenAI legacy completions | `POST /v1/completions` | `"stream": true` (SSE) | in the reply; the last chunk | older tools that send a `prompt` |
216
+ | Anthropic Messages | `POST /v1/messages`, `POST /v1/messages/count_tokens` | `"stream": true` (SSE events) | in the message; `message_delta` | Anthropic SDKs with `base_url`, tools built on them |
217
+ | Ollama | `POST /api/chat`, `POST /api/generate`, `GET /api/tags`, `GET /api/version`, `POST /api/show` | on unless `"stream": false` (NDJSON) | in the reply; the `done` line | Open WebUI's Ollama connection, the `ollama` libraries, Ollama-only apps |
218
+ | Gemini | `POST /v1beta/models/<collection>:generateContent`, `:streamGenerateContent` (`?alt=sse`, else a JSON array), `:countTokens`, `GET /v1beta/models` | the stream route | **headers**: `X-Bench-Panel`, `X-Bench-Seats` (call, seat, alias, status); none when streamed | Google GenAI SDKs with `base_url`, Gemini REST |
219
+
220
+ The base URL is the server itself (`http://127.0.0.1:8122` on the dashboard's
221
+ port, `:8123` for `gateway.py`): the Anthropic SDK and Google's add their own
222
+ `/v1/...` and `/v1beta/...`, Ollama clients their `/api/...`; OpenAI clients
223
+ take `.../v1`.
224
+
225
+ ```bash
226
+ curl http://127.0.0.1:8122/v1/messages -H "Content-Type: application/json" \
227
+ -d '{"model": "bench/core", "max_tokens": 1024, "messages": [{"role": "user", "content": "Where can this return a stale value? ..."}]}'
228
+ curl http://127.0.0.1:8122/v1/responses -H "Content-Type: application/json" \
229
+ -d '{"model": "bench/core", "input": "Where can this return a stale value? ..."}'
230
+ curl http://127.0.0.1:8122/v1/completions -H "Content-Type: application/json" \
231
+ -d '{"model": "bench/core", "prompt": "Where can this return a stale value? ..."}'
232
+ curl http://127.0.0.1:8122/api/chat -H "Content-Type: application/json" \
233
+ -d '{"model": "bench/core", "stream": false, "messages": [{"role": "user", "content": "Where can this return a stale value? ..."}]}'
234
+ curl "http://127.0.0.1:8122/v1beta/models/bench/core:streamGenerateContent?alt=sse" -H "Content-Type: application/json" \
235
+ -d '{"contents": [{"parts": [{"text": "Where can this return a stale value? ..."}]}]}'
236
+ ```
237
+
238
+ Every POST needs `Content-Type: application/json`, as on chat completions
239
+ (it is what keeps a web page's plain form out).
240
+
241
+ ```python
242
+ import anthropic
243
+ bench = anthropic.Anthropic(base_url="http://127.0.0.1:8122", api_key="local") # or a gateway key
244
+ with bench.messages.stream(model="bench/core", max_tokens=1024,
245
+ messages=[{"role": "user", "content": "..."}]) as s:
246
+ for text in s.text_stream:
247
+ print(text, end="") # each seat's section as that seat finishes
248
+
249
+ from google import genai
250
+ from google.genai import types
251
+ gem = genai.Client(api_key="local", http_options=types.HttpOptions(base_url="http://127.0.0.1:8122"))
252
+ print(gem.models.generate_content(model="bench/core:merged", contents="...").text)
253
+ ```
254
+
255
+ In **Open WebUI**, add `http://127.0.0.1:8122` as an Ollama connection (or
256
+ `.../v1` as an OpenAI one): the collections appear as models.
257
+
258
+ **Keys.** Where keys are required, a gateway key goes as `Authorization:
259
+ Bearer`, or as `x-api-key` (the Anthropic SDK's `api_key`), `x-goog-api-key`
260
+ (the Google SDKs' `api_key`), or `?key=` on a Gemini route only (Google's REST
261
+ examples). The first one sent is judged. The access log never records a
262
+ query string or a key; neither does http.server's own error line.
263
+
264
+ **Images** in each format's shape become `image_url` parts, so only seats that
265
+ can see sit (Images): Anthropic `image` blocks (base64 or url), Responses
266
+ `input_image`, Ollama's `images` (base64, its type read from its first
267
+ bytes), Gemini `inlineData` and `fileData` at an http(s) URL.
268
+
269
+ **Refused, not dropped** (400, with why), because a client waiting for what
270
+ it asked would hang or loop on a panel's text:
271
+
272
+ - **Tools and agents are not supported yet.** A request carrying tools, or a
273
+ conversation holding tool calls or tool results, in any format (chat
274
+ completions included), is refused with a message saying a route mode is
275
+ coming: one member proxied with tools and streaming, for agent clients
276
+ such as Claude Code, Codex or opencode, with background shadow seats as an
277
+ opt-in after that. An empty `tools` list is no tools.
278
+ - a PDF, audio or other non-image part; an image as an uploaded file id
279
+ (Anthropic `file`, Responses `file_id`, Gemini's Files API);
280
+ - `previous_response_id` or a stored `conversation` on Responses: nothing is
281
+ stored (`store` is answered `false`), so send the whole conversation;
282
+ - Ollama's `context` (a model's own token state): use `/api/chat`;
283
+ - more than one prompt on legacy completions.
284
+
285
+ **Ignored**: sampling options (`max_tokens`, `temperature`, `top_p`, `stop`
286
+ and their native names) are read but each seat keeps its own call options;
287
+ structured-output settings (Ollama `format`, Gemini `responseSchema`,
288
+ Responses `text.format`), since a panel answers in markdown; a model's own
289
+ reasoning from an earlier turn (Anthropic thinking blocks, Responses
290
+ reasoning items, Gemini thought parts).
291
+
292
+ **Counts and durations.** `usage` and the token-count routes
293
+ (`/v1/messages/count_tokens`, Gemini `:countTokens`) are the size check's
294
+ estimate, characters / 4 plus 10% and 2,500 a picture, with no model called.
295
+ Ollama's durations are real. `/api/version` reports the Ollama API version
296
+ whose shapes these follow (0.12.0), so clients send what it reads.
297
+
298
+ **On the dashboard's port**, Ollama's five `/api/` routes are the gateway's
299
+ (a gateway key, never the login cookie); the dashboard's own `/api/...`
300
+ routes never use those names.
301
+
160
302
  ## The decision model's job (optional)
161
303
 
162
304
  A collection's members are fixed; only the explorer seats are chosen per
@@ -201,7 +343,8 @@ key limited to its collection and a rate, or keep the key on the app's server.
201
343
 
202
344
  | Part | Job |
203
345
  |---|---|
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. |
346
+ | 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, streamed seat by seat when asked. |
347
+ | Formats | `metrics/formats.py`: Anthropic Messages, Responses, legacy completions, Ollama and Gemini, each translated to and from OpenAI messages (Other API formats); the outbound half is for the built-in client to call those APIs natively, not wired yet. |
205
348
  | Collections | `metrics/bench_collections.py` reads the two config files and computes explorer seats. |
206
349
  | Seat router | `route.panel(members=…, explorers=n)` fills explorer seats by staleness for the shape. |
207
350
  | Shape picker | Optional decision model (above). |
package/README.md CHANGED
@@ -76,7 +76,8 @@ verdicts decide who sits on the next panel.
76
76
  - [Licence](#licence)
77
77
 
78
78
  Day-to-day use (the dashboard's pages, connecting apps, onboarding in bulk) is
79
- in [USAGE.md](USAGE.md). A coding agent setting it up for someone should start
79
+ in [USAGE.md](USAGE.md). Running it on a server or a hosting platform is in
80
+ [HOSTING.md](HOSTING.md). A coding agent setting it up for someone should start
80
81
  with [AGENTS.md](AGENTS.md).
81
82
 
82
83
  ## Try it
@@ -189,6 +190,27 @@ seat is still in `bench`. Record a verdict per call once you have checked it:
189
190
  or from the dashboard. See [GATEWAY.md](GATEWAY.md) for the full contract:
190
191
  images, prompt sizes, who may call, and the reply's shape.
191
192
 
193
+ **Other clients.** The same panel answers in other APIs' shapes, so a client
194
+ that speaks one of them works with the server's address as its base URL:
195
+
196
+ | Format | Route | For |
197
+ |---|---|---|
198
+ | OpenAI chat completions | `POST /v1/chat/completions` | OpenAI SDKs, most chat apps |
199
+ | OpenAI Responses | `POST /v1/responses` | `responses.create` clients (nothing is stored) |
200
+ | OpenAI legacy completions | `POST /v1/completions` | tools that send a `prompt` |
201
+ | Anthropic Messages | `POST /v1/messages` | Anthropic SDKs with `base_url` |
202
+ | Ollama | `POST /api/chat`, `/api/generate`, `GET /api/tags` | Open WebUI's Ollama connection, Ollama apps |
203
+ | Gemini | `POST /v1beta/models/bench/<collection>:generateContent` | Google GenAI SDKs with `base_url` |
204
+
205
+ curl http://127.0.0.1:8122/v1/messages -H "Content-Type: application/json" \
206
+ -d '{"model": "bench/<collection>", "max_tokens": 1024, "messages": [{"role": "user", "content": "..."}]}'
207
+
208
+ Each streams when asked: the reply starts at once, and each seat's answer
209
+ arrives as that seat finishes. Agent and tool use is not supported yet: a
210
+ request carrying tools is refused with a 400 rather than answered without
211
+ them. GATEWAY.md, "Other API formats", has a curl for each and what is
212
+ refused or ignored.
213
+
192
214
  ## From the command line
193
215
 
194
216
  `bench` (or `benchwarmers`, the same program) does what the dashboard does,
@@ -272,6 +294,9 @@ On native Windows, a client that cannot start `npx` itself needs
272
294
  | `BENCHWARMERS_KEY` | a gateway key from Settings, sent as `Authorization: Bearer`; needed for a server on another machine, or where keys are required |
273
295
  | `BENCHWARMERS_TIMEOUT` | seconds to wait for a panel, default 900 |
274
296
 
297
+ <!-- The MCP Registry's proof that the PyPI package is this server (server.json):
298
+ mcp-name: io.github.WendellResearch-AI/benchwarmers -->
299
+
275
300
  ## The suite
276
301
 
277
302
  Models are scored on review work: find the faulty line, or say whether a
@@ -302,6 +327,18 @@ built-in client makes every call). Put the secrets in a `.env` file beside
302
327
  then `docker compose up -d` and open http://127.0.0.1:8122. The compose file
303
328
  publishes on this machine only; see its comments for a proxy.
304
329
 
330
+ **Hosted, always on:** [HOSTING.md](HOSTING.md) compares Fly.io, Railway,
331
+ Render and a VPS with Caddy (cost, sleep, disks, HTTPS), with a template for
332
+ each under [deploy/](deploy/).
333
+
334
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/WendellResearch-AI/benchwarmers)
335
+
336
+ Render is one click: a paid instance with a 1 GB disk and a generated admin
337
+ token. Fly.io is four commands from a checkout (`cp deploy/fly.toml .`,
338
+ `fly launch --copy-config --no-deploy`, `fly secrets set
339
+ BENCHWARMERS_ADMIN_TOKEN=...`, `fly deploy`). Railway is a few steps in its CLI
340
+ or browser, in [HOSTING.md](HOSTING.md#railway).
341
+
305
342
  ## What leaves your machine
306
343
 
307
344
  Benchwarmers has no account, no telemetry and no server of its own; nothing
@@ -325,7 +362,10 @@ detail.
325
362
 
326
363
  ## Limits
327
364
 
328
- - **Early.** v0.1: no installer yet, no streaming replies, and the LiteLLM and
365
+ - **No tools yet.** A panel answers with text, not a tool call, so agents
366
+ that need tool use (Claude Code, Codex, opencode as callers) wait for a
367
+ route mode; requests with tools are refused, not trimmed.
368
+ - **Early.** v0.1: no installer yet, merged replies stream in one piece, and the LiteLLM and
329
369
  CLI Proxy API hub adapters are built from those projects' source, not yet
330
370
  run against a real hub.
331
371
  - **Calls cost.** Probes, onboarding and the suite run on your keys and free
package/USAGE.md CHANGED
@@ -104,6 +104,22 @@ Images go in as OpenAI `image_url` parts; only models that take images sit,
104
104
  and the rest are listed in `bench.skipped`. [GATEWAY.md](GATEWAY.md) has the
105
105
  limits and the reply's full shape.
106
106
 
107
+ **A client that speaks another API** works too, with the server's address as
108
+ its base URL and a collection as its model:
109
+
110
+ | Client | Base URL | Notes |
111
+ | --- | --- | --- |
112
+ | Open WebUI, as an **Ollama** connection | `http://127.0.0.1:8122` | collections listed from `/api/tags`; `bench/<collection>:merged` reads best in a chat window |
113
+ | **Anthropic** SDK (`anthropic.Anthropic(base_url=...)`) | `http://127.0.0.1:8122` | `/v1/messages`; a gateway key goes in `api_key` |
114
+ | **Google GenAI** SDK (`http_options=HttpOptions(base_url=...)`) | `http://127.0.0.1:8122` | `generate_content(model="bench/<collection>")`; a key in `api_key` |
115
+ | OpenAI **Responses** clients | `http://127.0.0.1:8122/v1` | `responses.create`; nothing is stored, so send the whole conversation |
116
+ | Older tools on `/v1/completions` | `http://127.0.0.1:8122/v1` | one `prompt` |
117
+
118
+ Asked to stream, each answers at once and sends each seat's answer as it
119
+ finishes. Agents that need tools (Claude Code, Codex, opencode as callers)
120
+ are not supported yet: a request with tools is refused with a 400 saying so.
121
+ GATEWAY.md, "Other API formats", has a curl for each.
122
+
107
123
  **A coding agent** connects over MCP instead: the Claude Code plugin
108
124
  (`/plugin marketplace add WendellResearch-AI/benchwarmers`, then
109
125
  `/plugin install benchwarmers@benchwarmers`), or `benchwarmers mcp` in any MCP
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.2.2
package/cli.py CHANGED
@@ -258,7 +258,10 @@ def cmd_onboard(args) -> None:
258
258
  reply = call("GET", "/api/onboard-queue") if verb == "status" else \
259
259
  call("POST", f"/api/onboard-queue/{verb}", {})
260
260
  else:
261
- items = [{"served": s, "endpoint": args.endpoint or ""} for s in args.served]
261
+ if args.ad_hoc and not args.quick:
262
+ sys.exit("--ad-hoc goes with --quick: an ad hoc model gets no suite run of its own")
263
+ mode = "quick-adhoc" if args.ad_hoc else "quick" if args.quick else "full"
264
+ items = [{"served": s, "endpoint": args.endpoint or "", "mode": mode} for s in args.served]
262
265
  reply = call("POST", "/api/onboard-queue", {"items": items})
263
266
  if args.wait:
264
267
  while reply.get("running"):
@@ -482,6 +485,7 @@ keeps only the answers. Then judge each seat you checked:
482
485
  bench candidates --new
483
486
  bench onboard minimax/minimax-m3.1-flash:free --wait
484
487
  bench onboard a/one:free b/two:free --endpoint openrouter
488
+ bench onboard command-a-plus-05-2026 --endpoint cohere --quick --ad-hoc # a trial key
485
489
  bench onboard status | stop | clear""",
486
490
  "collection": """examples:
487
491
  bench collection save reviewers --members a,b,c --description "adversarial review" --tags review
@@ -590,6 +594,10 @@ def parser() -> argparse.ArgumentParser:
590
594
  s.add_argument("served", nargs="+", help="served names, as `bench candidates` lists them")
591
595
  s.add_argument("--endpoint", help="the endpoint they are on; the default if omitted")
592
596
  s.add_argument("--wait", action="store_true", help="stay until the queue finishes")
597
+ s.add_argument("--quick", action="store_true",
598
+ help="probe only; the scheduled suite passes fill the suite in later")
599
+ s.add_argument("--ad-hoc", action="store_true",
600
+ help="with --quick: the bench never calls it on its own (metered, capped or trial keys)")
593
601
 
594
602
  s = cmd("dismiss", cmd_dismiss, "stop offering served models as candidates")
595
603
  s.add_argument("served", nargs="+"), s.add_argument("--endpoint"), s.add_argument("--undo", action="store_true")