@wendellresearch-ai/benchwarmers 0.3.2 → 0.4.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/README.md +19 -4
- package/USAGE.md +8 -1
- package/VERSION +1 -1
- package/cli.py +4 -1
- package/metrics/aliases.py +24 -3
- package/metrics/endpoints.py +36 -4
- package/metrics/model_roles.py +3 -1
- package/metrics/route.py +3 -2
- package/metrics/wrong.py +3 -1
- package/package.json +1 -1
- package/scripts/check.py +2 -0
- package/scripts/onboard.py +12 -1
- package/scripts/test_no_owui.py +5 -2
- package/scripts/test_serve_edit.py +4 -4
- package/scripts/test_server_json.py +22 -2
- package/serve.py +286 -29
- package/suite/README.md +33 -0
- package/suite/pace.py +8 -4
- package/suite/run.py +144 -16
- package/suite/tasks/vision-bars/fixtures/bars.png +0 -0
- package/suite/tasks/vision-bars/task.json +40 -0
- package/suite/tasks/vision-invoice/fixtures/invoice.png +0 -0
- package/suite/tasks/vision-invoice/task.json +40 -0
- package/suite/tasks/vision-label/fixtures/label.png +0 -0
- package/suite/tasks/vision-label/task.json +52 -0
- package/suite/tasks/vision-receipt/fixtures/receipt.png +0 -0
- package/suite/tasks/vision-receipt/task.json +44 -0
- package/suite/tasks/vision-screen/fixtures/screen.png +0 -0
- package/suite/tasks/vision-screen/task.json +48 -0
- package/suite/tasks/vision-shapes/fixtures/shapes.png +0 -0
- package/suite/tasks/vision-shapes/task.json +40 -0
- package/suite/tasks/vision-table/fixtures/table.png +0 -0
- package/suite/tasks/vision-table/task.json +40 -0
- package/suite/test_vision_tasks.py +201 -0
- package/suite/vision_keys.py +74 -0
- package/suite/vision_sample.py +232 -0
- package/suite/vision_tasks.py +338 -0
- package/ui/bench.js +87 -10
package/README.md
CHANGED
|
@@ -6,18 +6,24 @@
|
|
|
6
6
|
</h1>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="https://github.com/WendellResearch-AI/benchwarmers/releases"><img src="https://img.shields.io/github/v/release/WendellResearch-AI/benchwarmers?color=
|
|
9
|
+
<a href="https://github.com/WendellResearch-AI/benchwarmers/releases/latest"><img src="https://img.shields.io/github/v/release/WendellResearch-AI/benchwarmers?logo=github&color=181717" alt="Latest release"></a>
|
|
10
10
|
<a href="https://www.npmjs.com/package/@wendellresearch-ai/benchwarmers"><img src="https://img.shields.io/npm/v/@wendellresearch-ai/benchwarmers?color=cb3837&logo=npm" alt="npm"></a>
|
|
11
11
|
<a href="https://pypi.org/project/benchwarmers/"><img src="https://img.shields.io/pypi/v/benchwarmers?color=3775a9&logo=pypi&logoColor=white" alt="PyPI"></a>
|
|
12
|
-
<a href="https://github.com/WendellResearch-AI/benchwarmers/actions/workflows/check.yml"><img src="https://
|
|
12
|
+
<a href="https://github.com/WendellResearch-AI/benchwarmers/actions/workflows/check.yml"><img src="https://img.shields.io/github/actions/workflow/status/WendellResearch-AI/benchwarmers/check.yml?branch=main&logo=github&label=CI%20tests" alt="CI tests"></a>
|
|
13
|
+
<a href="https://registry.modelcontextprotocol.io/?q=benchwarmers"><img src="https://img.shields.io/badge/MCP_Registry-111111?logo=modelcontextprotocol&logoColor=white" alt="In the official MCP Registry"></a>
|
|
14
|
+
<a href="https://smithery.ai/skills/wendellresearch-ai/benchwarmers"><img src="https://img.shields.io/badge/Smithery-FF5601?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxODggMjExIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNNDEuNzAxMSA3Ny40OEgwVjEwMC4yNjRDMCAxMTguMTcxIDE0LjUyNzUgMTMyLjY5OCAzMi40MzQyIDEzMi42OThINTUuMjE4M1Y5MC45OTcxQzU1LjIxODMgODMuNTQxOCA0OS4xNTY1IDc3LjQ4IDQxLjcwMTEgNzcuNDhaIi8%2BPHBhdGggZD0iTTY2LjA4NzkgOTAuOTk3MVYxMzIuNjk4SDEwNy43ODlDMTE1LjI0NCAxMzIuNjk4IDEyMS4zMDYgMTI2LjYzNiAxMjEuMzA2IDExOS4xODFWNzcuNDhINzkuNjA1MUM3Mi4xNDk3IDc3LjQ4IDY2LjA4NzkgODMuNTQxOCA2Ni4wODc5IDkwLjk5NzFaIi8%2BPHBhdGggZD0iTTE1NC45OTUgNzcuNDhIMTMyLjIxVjExOS4xODFDMTMyLjIxIDEyNi42MzYgMTM4LjI3MiAxMzIuNjk4IDE0NS43MjggMTMyLjY5OEgxODcuNDI5VjEwNi4xNTJDMTg3LjQyOSA4OC4yNDQ5IDE3Mi45MDEgNzcuNTE0OCAxNTQuOTk1IDc3LjUxNDhWNzcuNDhaIi8%2BPHBhdGggZD0iTTAgNDUuOTUxNFY2Ni42MTAzSDQxLjcwMTFDNDkuMTU2NSA2Ni42MTAzIDU1LjIxODMgNjAuNTQ4NSA1NS4yMTgzIDUzLjA5MzJWMEMyNS44NDk4IDAuMjA5MDI4IDAuMDM0ODM4IDE0LjY2NjggMCA0NS45NTE0WiIvPjxwYXRoIGQ9Ik02Ni4wODc5IDUzLjA5M0M2Ni4wODc5IDYwLjU0ODMgNzIuMTQ5NyA2Ni42MTAxIDc5LjYwNTEgNjYuNjEwMUgxMjEuMzA2VjExLjQ2MTVDMTAwLjkyNiA4LjM2MDkyIDkwLjMwMDMgMS43NDE2OSA2Ni4wODc5IDAuMjQzNjUyVjUzLjA1ODFWNTMuMDkzWiIvPjxwYXRoIGQ9Ik0xNDQuMDkgMTMuMDYzOUMxMzkuNzcgMTMuMDYzOSAxMzUuODM0IDEyLjkyNDYgMTMyLjIxIDEyLjY4MDdWNjYuNjFIMTU0Ljk5NUMxNzIuOTAxIDY2LjYxIDE4Ny40MjkgNTIuMDgyNSAxODcuNDI5IDM0LjE3NThWMC4xNzM4MjhDMTc3LjI5MSA4LjExNjkgMTYyLjkwMyAxMy4wNjM5IDE0NC4wOSAxMy4wNjM5WiIvPjxwYXRoIGQ9Ik0wIDE3Ni4wMDJWMjEwLjAwNEMxMC4xMzc5IDIwMi4wNjEgMjQuNTI2IDE5Ny4xMTQgNDMuMzM4NSAxOTcuMTE0QzQ3LjY1ODQgMTk3LjExNCA1MS41OTUxIDE5Ny4yNTMgNTUuMjE4MyAxOTcuNTMyVjE0My42MDNIMzIuNDM0MkMxNC41Mjc1IDE0My42MDMgMCAxNTguMTMgMCAxNzYuMDM3TDAgMTc2LjAwMloiLz48cGF0aCBkPSJNMTA3Ljc4OSAxNDMuNjAzSDY2LjA4NzlWMTk4LjcxNkM4Ni40NjgxIDIwMS44MTcgOTcuMDkzOCAyMDguNDM2IDEyMS4zMDYgMjA5LjkzNFYxNTcuMTJDMTIxLjMwNiAxNDkuNjY0IDExNS4yNDQgMTQzLjYwMyAxMDcuNzg5IDE0My42MDNaIi8%2BPHBhdGggZD0iTTEzMi4yMSAxNTcuMDg1VjIxMC4xNzhDMTYxLjU0NCAyMDkuOTY5IDE4Ny4zMjQgMTk1LjUxMSAxODcuNDI5IDE2NC4yOTZWMTQzLjU2OEgxNDUuNzI4QzEzOC4yNzIgMTQzLjU2OCAxMzIuMjEgMTQ5LjYzIDEzMi4yMSAxNTcuMDg1WiIvPjwvc3ZnPg%3D%3D" alt="The benchwarmers skill on Smithery"></a>
|
|
15
|
+
<a href="https://skills.sh/wendellresearch-ai/benchwarmers/benchwarmers"><img src="https://img.shields.io/badge/skills.sh-111111" alt="On skills.sh"></a>
|
|
16
|
+
<a href="https://glama.ai/mcp/servers/WendellResearch-AI/benchwarmers"><img src="https://glama.ai/mcp/servers/WendellResearch-AI/benchwarmers/badges/score.svg" alt="Benchwarmers MCP server, quality and maintenance score on Glama"></a>
|
|
17
|
+
<a href="https://cursor.directory/plugins/benchwarmers"><img src="https://img.shields.io/badge/cursor.directory-111111?logo=cursor&logoColor=white" alt="On cursor.directory"></a>
|
|
18
|
+
<a href="#from-claude-code-and-other-agents"><img src="https://img.shields.io/badge/Antigravity-plugin-3186FF?labelColor=111111&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjEyIDE3IDg2IDgwIj48cGF0aCBkPSJNODkuNyA5My43YzQuNjcgMy41IDExLjY3IDEuMTcgNS4yNS01LjI1Qzc1LjcgNjkuNzggNzkuNzggMTguNDUgNTUuODcgMTguNDVTMzYuMDMgNjkuNzggMTYuNzggODguNDVjLTcgNyAuNTggOC43NSA1LjI1IDUuMjVDNDAuMTIgODEuNDUgMzguOTUgNTkuODYgNTUuODcgNTkuODZTNzEuNjIgODEuNDUgODkuNyA5My43WiIgZmlsbD0iIzMxODZGRiIvPjxtYXNrIGlkPSJtIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIxMyIgeT0iMTgiIHdpZHRoPSI4NSIgaGVpZ2h0PSI3OCI%2BPHBhdGggZD0iTTg5LjcgOTMuN2M0LjY3IDMuNSAxMS42NyAxLjE3IDUuMjUtNS4yNUM3NS43IDY5Ljc4IDc5Ljc4IDE4LjQ1IDU1Ljg3IDE4LjQ1UzM2LjAzIDY5Ljc4IDE2Ljc4IDg4LjQ1Yy03IDcgLjU4IDguNzUgNS4yNSA1LjI1QzQwLjEyIDgxLjQ1IDM4Ljk1IDU5Ljg2IDU1Ljg3IDU5Ljg2UzcxLjYyIDgxLjQ1IDg5LjcgOTMuN1oiIGZpbGw9IiNmZmYiLz48L21hc2s%2BPGcgbWFzaz0idXJsKCNtKSI%2BPGcgZmlsdGVyPSJ1cmwoI2EpIj48ZWxsaXBzZSBjeD0iOTYuNDkiIGN5PSIzNS4xMiIgcng9IjI5LjUiIHJ5PSIzMC4xNSIgdHJhbnNmb3JtPSJyb3RhdGUoNzYuOTIgOTYuNDkgMzUuMTIpIiBmaWxsPSIjRkM0MTNEIi8%2BPC9nPjxnIGZpbHRlcj0idXJsKCNiKSI%2BPGVsbGlwc2UgY3g9IjkuMDMiIGN5PSI0MS42NiIgcng9IjMwLjgzIiByeT0iMzkuOTQiIHRyYW5zZm9ybT0icm90YXRlKDc0LjEzIDkuMDMgNDEuNjYpIiBmaWxsPSIjMDBCOTVDIi8%2BPC9nPjxnIGZpbHRlcj0idXJsKCNjKSI%2BPGVsbGlwc2UgY3g9Ijc1Ljc1IiBjeT0iMTA0LjgyIiByeD0iMjkuMDIiIHJ5PSIyNy45NCIgdHJhbnNmb3JtPSJyb3RhdGUoNzYuOTIgNzUuNzUgMTA0LjgyKSIgZmlsbD0iIzMxODZGRiIvPjwvZz48ZyBmaWx0ZXI9InVybCgjZCkiPjxlbGxpcHNlIGN4PSIzMy41NyIgY3k9IjM1LjQiIHJ4PSIzMy41NyIgcnk9IjM1LjQiIHRyYW5zZm9ybT0ibWF0cml4KC0uNDEgLjkxMiAtLjkxMiAtLjQxIDEwMS4yNSAtMTUuMTcpIiBmaWxsPSIjRkJCQzA0Ii8%2BPC9nPjwvZz48ZGVmcz48ZmlsdGVyIGlkPSJhIiB4PSIyOCIgeT0iLTMzIiB3aWR0aD0iMTM3IiBoZWlnaHQ9IjEzNiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIxOC44Ii8%2BPC9maWx0ZXI%2BPGZpbHRlciBpZD0iYiIgeD0iLTYzIiB5PSItMjMiIHdpZHRoPSIxNDQiIGhlaWdodD0iMTI5IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjE2Ii8%2BPC9maWx0ZXI%2BPGZpbHRlciBpZD0iYyIgeD0iMTYiIHk9IjQ0IiB3aWR0aD0iMTE5IiBoZWlnaHQ9IjEyMSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIxNS4yIi8%2BPC9maWx0ZXI%2BPGZpbHRlciBpZD0iZCIgeD0iLTgiIHk9Ii02MSIgd2lkdGg9IjEyNyIgaGVpZ2h0PSIxMjQiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI%2BPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMTMuOCIvPjwvZmlsdGVyPjwvZGVmcz48L3N2Zz4%3D" alt="Antigravity plugin"></a>
|
|
13
19
|
<a href="#docker-and-hosting"><img src="https://img.shields.io/badge/Docker-compose-2496ED?logo=docker&logoColor=white" alt="Runs in Docker"></a>
|
|
20
|
+
<a href="https://render.com/deploy?repo=https://github.com/WendellResearch-AI/benchwarmers"><img src="https://img.shields.io/badge/Render-deploy-8A05FF?logo=render&logoColor=white" alt="Deploy to Render in one click"></a>
|
|
14
21
|
<br>
|
|
15
22
|
<a href="GATEWAY.md#other-api-formats"><img src="https://img.shields.io/badge/API-OpenAI_%7C_Responses-111111" alt="OpenAI chat completions and Responses API"></a>
|
|
16
23
|
<a href="GATEWAY.md#other-api-formats"><img src="https://img.shields.io/badge/API-Anthropic_Messages-111111" alt="Anthropic Messages API"></a>
|
|
17
24
|
<a href="GATEWAY.md#other-api-formats"><img src="https://img.shields.io/badge/API-Gemini-111111" alt="Gemini API"></a>
|
|
18
25
|
<a href="GATEWAY.md#other-api-formats"><img src="https://img.shields.io/badge/API-Ollama-111111" alt="Ollama API"></a>
|
|
19
26
|
<a href="GATEWAY.md#route-mode-agent-clients"><img src="https://img.shields.io/badge/agents-tool_calling-111111" alt="Agent clients with tool calling, through route mode"></a>
|
|
20
|
-
<a href="https://registry.modelcontextprotocol.io/v0/servers?search=io.github.WendellResearch-AI/benchwarmers"><img src="https://img.shields.io/badge/MCP-Registry-111111" alt="MCP server in the official MCP Registry"></a>
|
|
21
27
|
<br>
|
|
22
28
|
<a href="#try-it"><img src="https://img.shields.io/badge/Python-3.11_%7C_3.12_%7C_3.13-3776AB?logo=python&logoColor=white" alt="Python 3.11 to 3.13"></a>
|
|
23
29
|
<a href="#try-it"><img src="https://img.shields.io/badge/core-stdlib_only-2ea44f" alt="The core needs only the Python standard library"></a>
|
|
@@ -155,7 +161,9 @@ dependencies either way: the app is the Python standard library.
|
|
|
155
161
|
each is registered, probed and run through the suite, one at a time, with
|
|
156
162
|
the progress beside the button. **Quick** probes now and leaves the suite
|
|
157
163
|
to the scheduled passes; **Quick + ad hoc** also keeps the bench from ever
|
|
158
|
-
calling the model on its own, for a metered or trial key.
|
|
164
|
+
calling the model on its own, for a metered or trial key. Tick **Vision
|
|
165
|
+
model** for one that reads images and little else: it sits only for
|
|
166
|
+
questions about images, and is tested on the vision tasks, not the code ones.
|
|
159
167
|
|
|
160
168
|
5. **Make a collection** on the Collections page (the models a panel always
|
|
161
169
|
includes, and what it is for). The dashboard answers the gateway's routes on
|
|
@@ -309,6 +317,13 @@ Benchwarmers running, the hook shows a hook-error notice at the end of each
|
|
|
309
317
|
turn, as `/mcp` shows the server as failed; start the server, or disable the
|
|
310
318
|
plugin for that session.
|
|
311
319
|
|
|
320
|
+
**Cursor and Antigravity plugins.** The same skill, with the stdio server
|
|
321
|
+
below (so they need Node and Python 3.11 or later): Cursor's is
|
|
322
|
+
[on cursor.directory](https://cursor.directory/plugins/benchwarmers), and
|
|
323
|
+
Antigravity's installs from the repository:
|
|
324
|
+
|
|
325
|
+
agy plugin install https://github.com/WendellResearch-AI/benchwarmers
|
|
326
|
+
|
|
312
327
|
**Any other MCP client** that speaks HTTP:
|
|
313
328
|
|
|
314
329
|
```json
|
package/USAGE.md
CHANGED
|
@@ -57,7 +57,14 @@ spends:
|
|
|
57
57
|
its own (no explorer seat, no scheduled runs). The one that saves calls, for
|
|
58
58
|
a metered, capped or trial key. A collection that lists it still calls it.
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
**Vision model**, the box beside it, gives the model role **vision** as it is
|
|
61
|
+
registered: it sits only for requests that carry images, and its suite is the
|
|
62
|
+
vision tasks (photos of documents, charts, labels and screens with an answer
|
|
63
|
+
key) rather than the text and code ones. Full runs them now, Quick through the
|
|
64
|
+
scheduled passes. For a model that reads images and little else; a model that
|
|
65
|
+
does everything and also takes images stays general and gets both.
|
|
66
|
+
|
|
67
|
+
`bench onboard <served> [--quick [--ad-hoc]] [--vision]` does the same from a terminal.
|
|
61
68
|
Onboarding takes minutes and spends calls, so models run **one at a time**,
|
|
62
69
|
in a queue the dashboard keeps:
|
|
63
70
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.0
|
package/cli.py
CHANGED
|
@@ -261,7 +261,8 @@ def cmd_onboard(args) -> None:
|
|
|
261
261
|
if args.ad_hoc and not args.quick:
|
|
262
262
|
sys.exit("--ad-hoc goes with --quick: an ad hoc model gets no suite run of its own")
|
|
263
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
|
|
264
|
+
items = [{"served": s, "endpoint": args.endpoint or "", "mode": mode, "vision": args.vision}
|
|
265
|
+
for s in args.served]
|
|
265
266
|
reply = call("POST", "/api/onboard-queue", {"items": items})
|
|
266
267
|
if args.wait:
|
|
267
268
|
while reply.get("running"):
|
|
@@ -598,6 +599,8 @@ def parser() -> argparse.ArgumentParser:
|
|
|
598
599
|
help="probe only; the scheduled suite passes fill the suite in later")
|
|
599
600
|
s.add_argument("--ad-hoc", action="store_true",
|
|
600
601
|
help="with --quick: the bench never calls it on its own (metered, capped or trial keys)")
|
|
602
|
+
s.add_argument("--vision", action="store_true",
|
|
603
|
+
help="role vision: a seat only for questions about images, and its suite is the vision tasks")
|
|
601
604
|
|
|
602
605
|
s = cmd("dismiss", cmd_dismiss, "stop offering served models as candidates")
|
|
603
606
|
s.add_argument("served", nargs="+"), s.add_argument("--endpoint"), s.add_argument("--undo", action="store_true")
|
package/metrics/aliases.py
CHANGED
|
@@ -16,17 +16,38 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
18
|
import pathlib
|
|
19
|
+
import time
|
|
19
20
|
|
|
20
21
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
21
22
|
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
22
23
|
HISTORY = DATA / "config" / "alias-history.json"
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
_cache: tuple = (None, {}, 0.0)
|
|
27
|
+
|
|
28
|
+
|
|
25
29
|
def history() -> dict[str, str]:
|
|
30
|
+
"""The file, read again only when it changes, and looked at no more than
|
|
31
|
+
once a second: one dashboard render asks tens of thousands of times, and
|
|
32
|
+
even a stat each time was seconds on Windows. A rename written here
|
|
33
|
+
(serve.rename) calls forget(), so it shows at once."""
|
|
34
|
+
global _cache
|
|
35
|
+
seen, now = _cache, time.monotonic() # one read: forget() may swap it on another thread
|
|
36
|
+
if seen[0] and seen[0][0] == str(HISTORY) and now - seen[2] < 1:
|
|
37
|
+
return seen[1]
|
|
26
38
|
try:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
st = HISTORY.stat()
|
|
40
|
+
key = (str(HISTORY), st.st_mtime_ns, st.st_size)
|
|
41
|
+
table = seen[1] if seen[0] == key else json.loads(HISTORY.read_text(encoding="utf-8"))
|
|
42
|
+
except (OSError, ValueError): # none yet, or half-written: no renames, for a second
|
|
43
|
+
key, table = (str(HISTORY), None, None), {}
|
|
44
|
+
_cache = (key, table, now)
|
|
45
|
+
return table
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def forget() -> None:
|
|
49
|
+
global _cache
|
|
50
|
+
_cache = (None, {}, 0.0)
|
|
30
51
|
|
|
31
52
|
|
|
32
53
|
def current(alias: str, table: dict[str, str] | None = None) -> str:
|
package/metrics/endpoints.py
CHANGED
|
@@ -154,13 +154,29 @@ def connections() -> dict:
|
|
|
154
154
|
return data if isinstance(data, dict) else {}
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
_default: tuple = (None, "")
|
|
158
|
+
|
|
159
|
+
|
|
157
160
|
def default() -> str:
|
|
161
|
+
"""The default endpoint's name, read again only when endpoints.json
|
|
162
|
+
changes: snapshot_key asks once per model, hundreds of times a page."""
|
|
163
|
+
global _default
|
|
164
|
+
try:
|
|
165
|
+
st = FILE.stat()
|
|
166
|
+
key = (str(FILE), st.st_mtime_ns, st.st_size)
|
|
167
|
+
except OSError:
|
|
168
|
+
key = None
|
|
169
|
+
seen = _default
|
|
170
|
+
if key and seen[0] == key:
|
|
171
|
+
return seen[1]
|
|
158
172
|
data = load()
|
|
159
173
|
if not data:
|
|
160
174
|
raise RuntimeError(f"no endpoints configured: add one from the dashboard (python serve.py), run"
|
|
161
175
|
f" scripts/init.py --endpoint NAME BASE_URL, or create {FILE}"
|
|
162
176
|
' like {"name": {"base": "https://.../v1", "key": "name", "default": true}}')
|
|
163
|
-
|
|
177
|
+
name = next((n for n, e in data.items() if e.get("default")), next(iter(data)))
|
|
178
|
+
_default = (key, name)
|
|
179
|
+
return name
|
|
164
180
|
|
|
165
181
|
|
|
166
182
|
def kind(name: str, data: dict | None = None) -> str:
|
|
@@ -322,13 +338,29 @@ def endpoint_name(reg: dict, data: dict | None = None) -> str | None:
|
|
|
322
338
|
return next((n for n in same if data[n].get("key") == reg.get("api_key_name")), same[0] if same else None)
|
|
323
339
|
|
|
324
340
|
|
|
341
|
+
_parsed: tuple = (None, {})
|
|
342
|
+
|
|
343
|
+
|
|
325
344
|
def registrations() -> dict[str, dict]:
|
|
326
345
|
"""alias -> its yaml entry as written by yaml_block: model_name, api_base,
|
|
327
|
-
api_key_name and a `headers` dict. Not a YAML parser; the file is ours.
|
|
346
|
+
api_key_name and a `headers` dict. Not a YAML parser; the file is ours.
|
|
347
|
+
|
|
348
|
+
Parsed again only when the file changes (a dashboard render asks hundreds
|
|
349
|
+
of times); every caller gets its own copies to change."""
|
|
350
|
+
global _parsed
|
|
351
|
+
try:
|
|
352
|
+
st = YAML.stat()
|
|
353
|
+
except OSError: # a fresh install has registered nothing yet
|
|
354
|
+
return {}
|
|
355
|
+
key = (str(YAML), st.st_mtime_ns, st.st_size)
|
|
356
|
+
if _parsed[0] != key:
|
|
357
|
+
_parsed = (key, _parse_registrations())
|
|
358
|
+
return {a: dict(r, headers=dict(r["headers"])) for a, r in _parsed[1].items()}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _parse_registrations() -> dict[str, dict]:
|
|
328
362
|
out: dict[str, dict] = {}
|
|
329
363
|
current = None
|
|
330
|
-
if not YAML.exists(): # a fresh install has registered nothing yet
|
|
331
|
-
return out
|
|
332
364
|
for line in YAML.read_text(encoding="utf-8").splitlines():
|
|
333
365
|
if (m := re.match(r"\s*-\s*model_id:\s*(.+)", line)):
|
|
334
366
|
current = out.setdefault(m.group(1).strip().strip("\"'"), {"headers": {}})
|
package/metrics/model_roles.py
CHANGED
|
@@ -20,7 +20,9 @@ roles: general reasoning / do-it-all: eligible for every shape
|
|
|
20
20
|
charts), weak at text-only work: admitted by the vision shape
|
|
21
21
|
alone, and a collection member with this role sits only when
|
|
22
22
|
the request carries images (gateway.py). A model that reads
|
|
23
|
-
images AND does everything else stays general.
|
|
23
|
+
images AND does everything else stays general. `bench onboard
|
|
24
|
+
--vision` (the picker's Vision model box) onboards one this
|
|
25
|
+
way; its suite is the vision tasks alone (suite/run.applies).
|
|
24
26
|
decision typed answers with probabilities through a Decisions API, not
|
|
25
27
|
chat; no shape admits them (config/decision-models.json)
|
|
26
28
|
|
package/metrics/route.py
CHANGED
|
@@ -250,8 +250,9 @@ def suite_recall() -> dict[str, tuple[int, int, int]]:
|
|
|
250
250
|
# Claim-verification tasks are a true/false verdict, where a guess
|
|
251
251
|
# scores half: folding them into finding-recall would blur the one
|
|
252
252
|
# number that picks the primary. They are reported on their own, as
|
|
253
|
-
# are seeded and real-bug (rebench) FAULT/NO FAULT tasks
|
|
254
|
-
|
|
253
|
+
# are seeded and real-bug (rebench) FAULT/NO FAULT tasks, and reading
|
|
254
|
+
# images (vision), which is not what a text panel is picked for.
|
|
255
|
+
if spec.get("shape") in ("claim", "seeded", "rebench", "vision") or spec.get("draft"):
|
|
255
256
|
continue
|
|
256
257
|
for answer in sorted((task_dir.parent / "output").glob("*.md")):
|
|
257
258
|
alias = current_alias(answer.stem) # a renamed model keeps its record
|
package/metrics/wrong.py
CHANGED
|
@@ -84,7 +84,9 @@ def table() -> dict[str, dict]:
|
|
|
84
84
|
"hunt": dict.fromkeys(HUNT, 0)})
|
|
85
85
|
for path in sorted(TASKS.glob("*/task.json")):
|
|
86
86
|
spec = json.loads(path.read_text(encoding="utf-8"))
|
|
87
|
-
|
|
87
|
+
# Reading images has its own record (serve.vision_rows): a trap taken
|
|
88
|
+
# there is a picture misread, not code.
|
|
89
|
+
if spec.get("draft") or spec.get("shape") == "vision":
|
|
88
90
|
continue
|
|
89
91
|
for answer in sorted((path.parent / "output").glob("*.md")):
|
|
90
92
|
text = answer.read_text(encoding="utf-8", errors="replace")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wendellresearch-ai/benchwarmers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Self-hosted panel gateway for LLMs: ask a collection of models at once, keep every answer side by side, rotate explorer models in, and let your verdicts pick the next panel.",
|
|
5
5
|
"mcpName": "io.github.WendellResearch-AI/benchwarmers",
|
|
6
6
|
"license": "MIT",
|
package/scripts/check.py
CHANGED
|
@@ -73,6 +73,8 @@ CHECKS = [
|
|
|
73
73
|
("suite/test_seed.py", [], False),
|
|
74
74
|
("suite/test_rebench.py", [], False),
|
|
75
75
|
("suite/test_pick.py", [], False),
|
|
76
|
+
("suite/test_vision_tasks.py", [], False),
|
|
77
|
+
("suite/vision_keys.py", [], False),
|
|
76
78
|
("scripts/test_client.py", [], True), # runs the llm CLI against a fake server
|
|
77
79
|
]
|
|
78
80
|
|
package/scripts/onboard.py
CHANGED
|
@@ -235,6 +235,9 @@ def main() -> None:
|
|
|
235
235
|
ap.add_argument("--ad-hoc", action="store_true",
|
|
236
236
|
help="mark it ad hoc as it is registered: the bench never calls it on its own"
|
|
237
237
|
" (for a metered, capped or trial key)")
|
|
238
|
+
ap.add_argument("--vision", action="store_true",
|
|
239
|
+
help="role vision as it is registered: it sits only for requests that carry"
|
|
240
|
+
" images, and its suite is the vision tasks (suite/run.applies)")
|
|
238
241
|
args = ap.parse_args()
|
|
239
242
|
|
|
240
243
|
name = args.served_name
|
|
@@ -296,6 +299,12 @@ def main() -> None:
|
|
|
296
299
|
lambda roles: roles.__setitem__("ad_hoc", sorted({*roles.get("ad_hoc", []), alias})),
|
|
297
300
|
ensure_ascii=False)
|
|
298
301
|
print(" ad hoc: the bench never calls it on its own")
|
|
302
|
+
if args.vision:
|
|
303
|
+
# Before the probe too: a text panel or a suite pass in between would seat it.
|
|
304
|
+
locked.update(DATA / "config" / "model-roles.json",
|
|
305
|
+
lambda roles: roles.setdefault("models", {}).__setitem__(alias, "vision"),
|
|
306
|
+
ensure_ascii=False)
|
|
307
|
+
print(" role vision: a seat only for requests that carry images")
|
|
299
308
|
run("scripts/sync_config.py", timeout=300)
|
|
300
309
|
# Open WebUI's tags name the connection behind the hub; without a hub the
|
|
301
310
|
# endpoint is the connection, and route.connection_of needs no tags.
|
|
@@ -359,10 +368,12 @@ def main() -> None:
|
|
|
359
368
|
record_issue(alias, None)
|
|
360
369
|
|
|
361
370
|
# 5. the tier that decides routing
|
|
371
|
+
# A vision model's suite is the vision tasks alone (suite/run.applies picks
|
|
372
|
+
# them by its role); a general model that takes images gets both.
|
|
362
373
|
if args.skip_suite:
|
|
363
374
|
print(" [5/5] suite skipped by request")
|
|
364
375
|
else:
|
|
365
|
-
print(" [5/5] running the internal suite...")
|
|
376
|
+
print(" [5/5] running the internal suite" + (" (its vision tasks)..." if args.vision else "..."))
|
|
366
377
|
try:
|
|
367
378
|
suite = run("suite/run.py", "--model", alias, "--timeout", "300",
|
|
368
379
|
timeout=3600)
|
package/scripts/test_no_owui.py
CHANGED
|
@@ -241,12 +241,15 @@ def owner(fake: str) -> None:
|
|
|
241
241
|
OWUI_MODELS.append({"id": "just-added", "urlIdx": 1})
|
|
242
242
|
real_names = provider_tags.connection_names
|
|
243
243
|
try:
|
|
244
|
-
|
|
244
|
+
# The picker's list is kept a few minutes (serve.LISTED_FOR); Refresh asks again.
|
|
245
|
+
kept = serve.onboard_candidates()
|
|
246
|
+
assert "just-added" not in {x["served"] for x in kept["candidates"]}, kept
|
|
247
|
+
live = {x["served"]: x["group"] for x in serve.onboard_candidates(fresh=True)["candidates"]}
|
|
245
248
|
|
|
246
249
|
def refused(hub):
|
|
247
250
|
raise OSError("401")
|
|
248
251
|
provider_tags.connection_names = refused
|
|
249
|
-
c = serve.onboard_candidates()
|
|
252
|
+
c = serve.onboard_candidates(fresh=True)
|
|
250
253
|
finally:
|
|
251
254
|
provider_tags.connection_names = real_names
|
|
252
255
|
OWUI_MODELS.pop()
|
|
@@ -360,9 +360,9 @@ def main() -> int:
|
|
|
360
360
|
|
|
361
361
|
modes = {}
|
|
362
362
|
|
|
363
|
-
def fake_spawn(served, endpoint="", who="ui", mode="full"):
|
|
363
|
+
def fake_spawn(served, endpoint="", who="ui", mode="full", vision=False):
|
|
364
364
|
order.append(served)
|
|
365
|
-
modes[served] = mode
|
|
365
|
+
modes[served] = mode + (" vision" if vision else "")
|
|
366
366
|
(runs / f"onboard-{served}.log").write_text(f"onboarding {served} from e -> alias {served}-x\n"
|
|
367
367
|
+ outcome[served][1], encoding="utf-8")
|
|
368
368
|
return {"ok": True, "errors": [], "log": f"onboard-{served}.log"}, Proc(served)
|
|
@@ -397,7 +397,7 @@ def main() -> int:
|
|
|
397
397
|
settle()
|
|
398
398
|
stopped = {i["served"]: i["state"] for i in serve.onboard_queue()["items"]}
|
|
399
399
|
order.remove("quiet")
|
|
400
|
-
serve.queue_onboard([{"served": s, "endpoint": "e", "mode": m} for s, m in
|
|
400
|
+
serve.queue_onboard([{"served": s, "endpoint": "e", "mode": m, "vision": s == "quiet"} for s, m in
|
|
401
401
|
(("ok", "full"), ("quiet", "quick"), ("bad", "quick-adhoc"))])
|
|
402
402
|
settle()
|
|
403
403
|
batch = {i["served"]: (i["state"], i["alias"], bool(i["note"])) for i in serve.onboard_queue()["items"]}
|
|
@@ -412,7 +412,7 @@ def main() -> int:
|
|
|
412
412
|
and batch == {"ok": ("done", "ok-x", False), "quiet": ("no answer", "quiet-x", True),
|
|
413
413
|
"bad": ("failed", "bad-x", True)}
|
|
414
414
|
and cleared == [] and bad_input == (False, False, False)
|
|
415
|
-
and modes == {"a": "full", "ok": "full", "quiet": "quick", "bad": "quick-adhoc"})
|
|
415
|
+
and modes == {"a": "full", "ok": "full", "quiet": "quick vision", "bad": "quick-adhoc"})
|
|
416
416
|
print(("ok " if ok else "FAIL ") + "onboarding queue" + ("" if ok else f" -> {stopped} {order} {batch} {modes}"))
|
|
417
417
|
failures += 0 if ok else 1
|
|
418
418
|
|
|
@@ -11,7 +11,10 @@ repository URL spells it, since the match is case-sensitive); every version is
|
|
|
11
11
|
VERSION; package.json carries the name as mcpName (npm's proof) and the README
|
|
12
12
|
PyPI shows carries `mcp-name: <name>` (PyPI's); the identifiers are the real
|
|
13
13
|
package names; the arguments start the MCP server through launcher.py; the
|
|
14
|
-
environment variables are the ones mcp_server.py reads.
|
|
14
|
+
environment variables are the ones mcp_server.py reads. The root's mcp.json,
|
|
15
|
+
mcp_config.json (Antigravity's name for it), plugin.json, .cursor-plugin and
|
|
16
|
+
skills/ (cursor.directory, Glama and Antigravity read the repo root, not
|
|
17
|
+
plugin/) start the same server and carry the plugin's skill. Standard library
|
|
15
18
|
only: the full schema is validated by mcp-publisher in the release itself.
|
|
16
19
|
"""
|
|
17
20
|
|
|
@@ -71,7 +74,24 @@ def main() -> int:
|
|
|
71
74
|
assert set(names) == read and len(names) == len(read), f"{kind}: lists {names}, mcp_server.py reads {read}"
|
|
72
75
|
for e in p["environmentVariables"]:
|
|
73
76
|
assert e.get("isSecret", False) == (e["name"] == "BENCHWARMERS_KEY"), e
|
|
74
|
-
|
|
77
|
+
|
|
78
|
+
# The repo root as cursor.directory and Glama read it: mcp.json starts the npm
|
|
79
|
+
# package's server, and skills/ is the Claude Code plugin's skill, word for word.
|
|
80
|
+
root_mcp = json.loads((ROOT / "mcp.json").read_text(encoding="utf-8"))["mcpServers"]["benchwarmers"]
|
|
81
|
+
assert root_mcp["command"] == "npx" and root_mcp["args"][:2] == ["-y", npm["name"]], root_mcp
|
|
82
|
+
assert launcher.plan(root_mcp["args"][2:]).get("script") == "mcp_server.py", root_mcp["args"]
|
|
83
|
+
agy = json.loads((ROOT / "mcp_config.json").read_text(encoding="utf-8"))["mcpServers"]["benchwarmers"]
|
|
84
|
+
assert agy == root_mcp, f"mcp_config.json (Antigravity's) {agy} differs from mcp.json {root_mcp}"
|
|
85
|
+
assert json.loads((ROOT / "plugin.json").read_text(encoding="utf-8"))["name"] == "benchwarmers"
|
|
86
|
+
cursor = json.loads((ROOT / ".cursor-plugin" / "plugin.json").read_text(encoding="utf-8"))
|
|
87
|
+
assert (cursor["skills"], cursor["mcpServers"]) == ("./skills/", "./mcp.json"), cursor
|
|
88
|
+
lf = lambda p: p.read_text(encoding="utf-8").replace("\r\n", "\n") # noqa: E731
|
|
89
|
+
copies = sorted(p.name for p in (ROOT / "skills").iterdir())
|
|
90
|
+
assert copies == sorted(p.name for p in (ROOT / "plugin" / "skills").iterdir()), copies
|
|
91
|
+
for skill in copies:
|
|
92
|
+
assert lf(ROOT / "skills" / skill / "SKILL.md") == lf(ROOT / "plugin" / "skills" / skill / "SKILL.md"), \
|
|
93
|
+
f"skills/{skill}/SKILL.md differs from plugin/skills/{skill}/SKILL.md: copy one over the other"
|
|
94
|
+
print(f"server.json ok: {name} {version}, npm and pypi; mcp.json, .cursor-plugin and skills/ match")
|
|
75
95
|
return 0
|
|
76
96
|
|
|
77
97
|
|