@wendellresearch-ai/benchwarmers 0.3.3 → 0.5.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/GATEWAY.md +9 -5
- package/PRIVACY.md +9 -3
- package/README.md +31 -8
- package/USAGE.md +81 -15
- package/VERSION +1 -1
- package/cli.py +17 -5
- package/gateway.py +34 -2
- package/launcher.py +2 -1
- package/metrics/aliases.py +24 -3
- package/metrics/discover.py +68 -18
- package/metrics/endpoints.py +36 -4
- package/metrics/model_roles.py +3 -1
- package/metrics/pacing.py +23 -3
- package/metrics/quotaview.py +460 -0
- package/metrics/route.py +3 -2
- package/metrics/scheduler.py +2 -1
- package/metrics/sources.py +29 -6
- package/metrics/wrong.py +3 -1
- package/package.json +1 -1
- package/scripts/check.py +3 -0
- package/scripts/onboard.py +12 -1
- package/scripts/test_hubs.py +10 -5
- package/scripts/test_no_owui.py +14 -2
- package/scripts/test_quota.py +12 -2
- package/scripts/test_serve_edit.py +58 -5
- package/scripts/test_server_json.py +22 -2
- package/serve.py +570 -128
- 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.css +71 -6
- package/ui/bench.js +543 -49
package/GATEWAY.md
CHANGED
|
@@ -389,7 +389,7 @@ the panels' verdicts. A verdict on a conversation is optional
|
|
|
389
389
|
(`POST /bench/verdict` with that call id); the offload alone already counts
|
|
390
390
|
toward rotation. The Gateway card lists route conversations in their own
|
|
391
391
|
table (when, who asked, the first message, who answered with ✓/✗, turns,
|
|
392
|
-
tool calls, failovers) and none of them
|
|
392
|
+
tool calls, failovers) and none of them counts as a verdict owed.
|
|
393
393
|
|
|
394
394
|
**Shadows (later, opt-in).** A shadow seat would answer the same turn in the
|
|
395
395
|
background for evaluation only, after the caller has its answer. The place
|
|
@@ -515,9 +515,9 @@ key limited to its collection and a rate, or keep the key on the app's server.
|
|
|
515
515
|
| Shape picker | Optional decision model (above). |
|
|
516
516
|
| Call log | `runs/gateway/calls.jsonl`, gitignored: every seat's answer, status, timing and call id. |
|
|
517
517
|
| 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. |
|
|
518
|
-
| Collections tab | The collections and their form,
|
|
519
|
-
| Catalog | `metrics/catalog.py`: context window, output limit, rate limits and capabilities per model, refreshed daily
|
|
520
|
-
| Discover tab | `metrics/discover.py`: models.dev
|
|
518
|
+
| Collections tab | The collections, each drawn as its panel sits, and their form (one row per model, a chip per provider). Then the gateway's recent requests: every seat's status and verdict, how long the caller waited (Time), and how many verdicts are owed. Clicking a seat opens its answer and the verdict form. |
|
|
519
|
+
| Catalog | `metrics/catalog.py`: context window, output limit, rate limits and capabilities per model, refreshed daily by the built-in scheduler; a free model whose price goes above zero raises a "went paid" alert. |
|
|
520
|
+
| Discover tab | `metrics/discover.py`: FreeLLMAPI (leading) and models.dev merged into one list of models worth adding, free first, FreeLLMAPI's smartest 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"). |
|
|
521
521
|
| Merged replies | Opt-in: one merger answer as `content`, every seat still in `bench` (The reply). |
|
|
522
522
|
| MCP server | `mcp_server.py`: the same three routes as MCP tools for agents, `list_collections`, `ask` and `verdict`. Two transports for one `answer()`: `POST /mcp` (or `/v1/mcp`) on either port, streamable HTTP with a JSON reply per request, its tools calling the server's own routes over loopback with the caller's credential and Host; and stdio (`benchwarmers mcp`), a caller like any other over HTTP (`BENCHWARMERS_URL`, `BENCHWARMERS_KEY`). The Claude Code plugin in `plugin/` connects to `/mcp` and adds a command, a skill and a `Stop` hook (`plugin/hooks/hooks.json`, an `mcp_tool` hook) that calls `pending_verdicts` with the session's `transcript_path`: the call ids in the transcript (Claude Code keeps no `Mcp-Session-Id`, so the transcript is the record of whose panels they were), of which the call log says which are answered seats with no verdict (`POST /bench/pending`), returned once as `additionalContext`. A server on another machine cannot read the transcript, so there the hook says nothing. A tool call answers as SSE with progress notifications, which reset Claude Code's 300-second idle limit. |
|
|
523
523
|
|
|
@@ -529,7 +529,11 @@ key limited to its collection and a rate, or keep the key on the app's server.
|
|
|
529
529
|
- **Every seat is named** in the response, including ones that failed or came
|
|
530
530
|
back empty, because who answered is itself evidence about the roster.
|
|
531
531
|
- **Free models only**, so rate limits are the constraint; a failed seat is
|
|
532
|
-
reported and logged, not retried into the limit.
|
|
532
|
+
reported and logged, not retried into the limit. A model the bench already
|
|
533
|
+
knows has no room (a limit or quota it has counted, `metrics/pacing.py`) is
|
|
534
|
+
not seated at all: an explorer seat goes to a model with room now, a member
|
|
535
|
+
is named as not asked when its wait would outlast the seat, and a request
|
|
536
|
+
with no seat left gets a 429.
|
|
533
537
|
- **Privacy:** prompts go only to providers the bench already uses.
|
|
534
538
|
- **Bind to 127.0.0.1**, as the dashboard does. Elsewhere only with an admin
|
|
535
539
|
token set, and then every call carries a gateway key made on the
|
package/PRIVACY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Privacy policy
|
|
2
2
|
|
|
3
|
-
*Effective
|
|
3
|
+
*Effective 29 October 2026.*
|
|
4
4
|
|
|
5
5
|
Benchwarmers runs on your own machine or server. It has no account, no
|
|
6
6
|
telemetry and no server of its own, and nothing you do with it reaches
|
|
@@ -13,10 +13,16 @@ Wendell Research.
|
|
|
13
13
|
That includes what the gateway relays for your apps, and the calls the
|
|
14
14
|
probes, the suite and onboarding make. Each endpoint's own privacy policy
|
|
15
15
|
applies to what it receives; some free tiers keep prompts or train on them.
|
|
16
|
-
- **Catalog reads**, carrying nothing of yours:
|
|
17
|
-
(at most once a day) and OpenRouter's public
|
|
16
|
+
- **Catalog reads**, carrying nothing of yours: FreeLLMAPI (its feed and its
|
|
17
|
+
site's sitemap) and models.dev (at most once a day) and OpenRouter's public
|
|
18
|
+
model list.
|
|
18
19
|
- **Sign-ins**, only when you choose one: the provider's own sign-in page, in
|
|
19
20
|
your browser, and the provider's token endpoint.
|
|
21
|
+
- **Quota reads**, only when you press Refresh on Quota management, carrying
|
|
22
|
+
no prompt:
|
|
23
|
+
- OpenRouter's key status, asked with your key.
|
|
24
|
+
- For CLI Proxy API's sign-ins, through the proxy, the quota endpoint of the
|
|
25
|
+
provider each sign-in belongs to (Google's, ChatGPT's).
|
|
20
26
|
- **Charts**: the dashboard's page loads Observable Plot from jsDelivr, at a
|
|
21
27
|
pinned version, in your browser.
|
|
22
28
|
- **Real-bug task seeding**, only when you run `suite/rebench.py`: dataset rows
|
package/README.md
CHANGED
|
@@ -11,7 +11,13 @@
|
|
|
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
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
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>
|
|
14
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>
|
|
15
21
|
<br>
|
|
16
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>
|
|
17
23
|
<a href="GATEWAY.md#other-api-formats"><img src="https://img.shields.io/badge/API-Anthropic_Messages-111111" alt="Anthropic Messages API"></a>
|
|
@@ -154,8 +160,11 @@ dependencies either way: the app is the Python standard library.
|
|
|
154
160
|
4. **Onboard a few models** from it ("Onboard a model"): tick one or several;
|
|
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
|
-
to the scheduled passes;
|
|
158
|
-
calling the model on its own, for a metered or trial
|
|
163
|
+
to the scheduled passes; **+ ad hoc** (with Full or Quick) keeps the bench
|
|
164
|
+
from ever calling the model on its own afterwards, for a metered or trial
|
|
165
|
+
key. Tick **Vision
|
|
166
|
+
model** for one that reads images and little else: it sits only for
|
|
167
|
+
questions about images, and is tested on the vision tasks, not the code ones.
|
|
159
168
|
|
|
160
169
|
5. **Make a collection** on the Collections page (the models a panel always
|
|
161
170
|
includes, and what it is for). The dashboard answers the gateway's routes on
|
|
@@ -186,10 +195,12 @@ dependencies either way: the app is the Python standard library.
|
|
|
186
195
|
(requests a minute, day or month; tokens a minute or day). They are kept per
|
|
187
196
|
connection, since models behind one key share them, and enforced: a call
|
|
188
197
|
waits when the wait fits, or is refused before it goes out. Every call lands
|
|
189
|
-
in a monthly usage ledger with its tokens
|
|
190
|
-
left
|
|
191
|
-
-
|
|
192
|
-
|
|
198
|
+
in a monthly usage ledger with its tokens. Quota management has a tile per
|
|
199
|
+
provider showing what is left, when it resets and who is resting. OpenRouter
|
|
200
|
+
and CLI Proxy API's sign-ins are read live, at no cost to quota.
|
|
201
|
+
- **Discover.** FreeLLMAPI's free catalog, with models.dev for the details
|
|
202
|
+
and paid models it lacks, merged into one list of models worth adding,
|
|
203
|
+
smartest first, each with the routes this install has to it.
|
|
193
204
|
|
|
194
205
|
## Asking a panel
|
|
195
206
|
|
|
@@ -240,6 +251,7 @@ without it: ask and judge, and run the bench itself. It is a client of a
|
|
|
240
251
|
running server, so the same commands work against one on another machine.
|
|
241
252
|
|
|
242
253
|
bench status what is running, and where
|
|
254
|
+
bench restart start it again on the code now on disk
|
|
243
255
|
bench collections what you can ask
|
|
244
256
|
bench ask core "Where can this return a stale value?" -f cache.py
|
|
245
257
|
git diff | bench ask core "Review this diff" a pipe is the prompt
|
|
@@ -309,6 +321,13 @@ Benchwarmers running, the hook shows a hook-error notice at the end of each
|
|
|
309
321
|
turn, as `/mcp` shows the server as failed; start the server, or disable the
|
|
310
322
|
plugin for that session.
|
|
311
323
|
|
|
324
|
+
**Cursor and Antigravity plugins.** The same skill, with the stdio server
|
|
325
|
+
below (so they need Node and Python 3.11 or later): Cursor's is
|
|
326
|
+
[on cursor.directory](https://cursor.directory/plugins/benchwarmers), and
|
|
327
|
+
Antigravity's installs from the repository:
|
|
328
|
+
|
|
329
|
+
agy plugin install https://github.com/WendellResearch-AI/benchwarmers
|
|
330
|
+
|
|
312
331
|
**Any other MCP client** that speaks HTTP:
|
|
313
332
|
|
|
314
333
|
```json
|
|
@@ -417,9 +436,13 @@ reaches Wendell Research. What it sends, and where:
|
|
|
417
436
|
a hub such as Open WebUI, or a local runtime. That includes the probes, the
|
|
418
437
|
suite and onboarding, which spend calls on your keys. Add only endpoints you
|
|
419
438
|
are content to send that work to.
|
|
420
|
-
- **Catalog reads**, no key and nothing of yours:
|
|
421
|
-
once a day, and OpenRouter's public
|
|
439
|
+
- **Catalog reads**, no key and nothing of yours: FreeLLMAPI (its feed and
|
|
440
|
+
its site's sitemap) and models.dev once a day, and OpenRouter's public
|
|
441
|
+
model list.
|
|
422
442
|
- **Sign-ins**, only when you choose one: the provider's own sign-in page.
|
|
443
|
+
- **Quota reads**, only when you press Refresh on Quota management, with no
|
|
444
|
+
prompt: OpenRouter's key status, with your key. CLI Proxy API asks each
|
|
445
|
+
sign-in's own provider for its quota.
|
|
423
446
|
- **Charts**: the dashboard's browser page loads Observable Plot from jsDelivr
|
|
424
447
|
at a pinned version.
|
|
425
448
|
- **Seeding real-bug tasks**, only when you run `suite/rebench.py`: the
|
package/USAGE.md
CHANGED
|
@@ -14,6 +14,7 @@ coding agent doing it with you); the gateway's whole contract is in
|
|
|
14
14
|
- [Connecting an app](#connecting-an-app)
|
|
15
15
|
- [Recording verdicts](#recording-verdicts)
|
|
16
16
|
- [Discover: finding models to add](#discover-finding-models-to-add)
|
|
17
|
+
- [Quota management](#quota-management)
|
|
17
18
|
- [The scheduler](#the-scheduler)
|
|
18
19
|
- [Your data folder](#your-data-folder)
|
|
19
20
|
|
|
@@ -25,18 +26,25 @@ coding agent doing it with you); the gateway's whole contract is in
|
|
|
25
26
|
| --- | --- | --- |
|
|
26
27
|
| Observe | **At a glance** | Every model on the bench: whether it works right now, its role, and its connection. Click a name for its popup: role, alias, facts, options, retire. |
|
|
27
28
|
| | **Benchmarks** | How models do on the suite and on real work: bugs found against false alarms, who is confidently wrong, and speed. |
|
|
28
|
-
| | **Quota management** |
|
|
29
|
+
| | **Quota management** | A tile per provider: what is left, when it resets, and which of its models are resting. Then the week's resets, and what the bench sent. |
|
|
29
30
|
| | **Logs** | Gateway calls (who sat and how it went, never the prompt), changes made from the dashboard, the scheduler, and onboarding runs. |
|
|
30
31
|
| Gateway | **Endpoints** | Where models are served from. Add an endpoint (a provider's sign-in, or any OpenAI-compatible API key), change a key, and see what each one serves. Also your own app registrations and rate limits per connection. |
|
|
31
32
|
| | **Models** | Every model on the bench, by the connection it comes from. |
|
|
32
33
|
| | **Collections** | The named panels your apps ask for, and the gateway's settings. |
|
|
33
34
|
| | **Discover** | Models worth adding, from public catalogs, with the ways this install can reach each one. |
|
|
34
|
-
| Control | **Settings** | Theme, gateway keys, the scheduler, the model client, providers whose free models work only in their own app,
|
|
35
|
+
| Control | **Settings** | Theme, gateway keys, the scheduler, the model client, providers whose free models work only in their own app, how the install is reached (shown only), and Restart. |
|
|
35
36
|
| | **Info** | Version, Python, data folder and counts. |
|
|
36
37
|
|
|
37
38
|
"Action needed", above the pages, lists what will not fix itself, such as a
|
|
38
|
-
model about to be retired,
|
|
39
|
-
folds it away until something new
|
|
39
|
+
model about to be retired, naming each model and where it comes from, with
|
|
40
|
+
Retry, Open and Retire now beside it. Hide folds it away until something new
|
|
41
|
+
turns up; an alert whose models have all been retired goes at once.
|
|
42
|
+
|
|
43
|
+
**Restart** (Settings, Server and access), or `bench restart`, starts the
|
|
44
|
+
dashboard again on the code now on disk, after an update. It waits for
|
|
45
|
+
onboarding, a scheduled job and gateway calls in progress (`--force` cuts them
|
|
46
|
+
off), and Settings says when newer code is on disk. It needs no service
|
|
47
|
+
manager: `serve.py` restarts its own dashboard.
|
|
40
48
|
|
|
41
49
|
## Onboarding models
|
|
42
50
|
|
|
@@ -44,20 +52,31 @@ folds it away until something new turns up.
|
|
|
44
52
|
not on the bench yet. Search, and tick one or several: the ticks survive a new
|
|
45
53
|
search, so you can gather models from different connections in one go.
|
|
46
54
|
Dismiss hides one you never want offered (an image model, say); the dismissed
|
|
47
|
-
list brings it back.
|
|
55
|
+
list brings it back. Each endpoint's list is kept an hour, with its age and a
|
|
56
|
+
Refresh, and a model already queued or onboarding is not offered again.
|
|
48
57
|
|
|
49
58
|
Each model is registered, probed with four short calls, and, if it answers,
|
|
50
59
|
run through the suite. **Onboard as**, under the list, chooses how much that
|
|
51
60
|
spends:
|
|
52
61
|
|
|
53
62
|
- **Full**: the probe, then the suite now.
|
|
63
|
+
- **Full + ad hoc**: the probe and the suite now, once, so routing has scores
|
|
64
|
+
for it; after that the bench never calls the model on its own (no explorer
|
|
65
|
+
seat, no scheduled runs). A collection that lists it still calls it.
|
|
54
66
|
- **Quick**: the probe now; the scheduled suite passes fill the suite in, six
|
|
55
67
|
tasks a run. The same calls, spread out.
|
|
56
|
-
- **Quick + ad hoc**: the probe only, and
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
- **Quick + ad hoc**: the probe only, and then ad hoc as above. The one that
|
|
69
|
+
spends least, for a metered, capped or trial key; its suite stays empty,
|
|
70
|
+
since the scheduled passes leave ad hoc models alone.
|
|
71
|
+
|
|
72
|
+
**Vision model**, the box beside it, gives the model role **vision** as it is
|
|
73
|
+
registered: it sits only for requests that carry images, and its suite is the
|
|
74
|
+
vision tasks (photos of documents, charts, labels and screens with an answer
|
|
75
|
+
key) rather than the text and code ones. Full runs them now, Quick through the
|
|
76
|
+
scheduled passes. For a model that reads images and little else; a model that
|
|
77
|
+
does everything and also takes images stays general and gets both.
|
|
78
|
+
|
|
79
|
+
`bench onboard <served> [--quick] [--ad-hoc] [--vision]` does the same from a terminal.
|
|
61
80
|
Onboarding takes minutes and spends calls, so models run **one at a time**,
|
|
62
81
|
in a queue the dashboard keeps:
|
|
63
82
|
|
|
@@ -84,6 +103,15 @@ everything else well stays `general`.
|
|
|
84
103
|
A collection is a named panel: the models it always includes, the shape of
|
|
85
104
|
work it is for (review, design, enumeration, ...), and how its reply comes
|
|
86
105
|
back. On the **Collections** page, **New collection**, then pick the members.
|
|
106
|
+
Each collection is drawn as its panel sits: a seat per member, the explorer
|
|
107
|
+
seats dashed, and the merger after an arrow.
|
|
108
|
+
|
|
109
|
+
In the popup, the panel's seats are on top, and the member list has one row
|
|
110
|
+
per model with its context, vision and tools, and price. A model served by
|
|
111
|
+
several providers is still one row, with a chip per provider: tick the chip
|
|
112
|
+
of the route you want. Ticking two is allowed, and the count says so. The tags
|
|
113
|
+
grey out the models that do not fit them, as before.
|
|
114
|
+
|
|
87
115
|
Each call also seats explorers, the models unused longest, up to the gateway's
|
|
88
116
|
limits (Gateway settings, on the same page: how many calls a request may make,
|
|
89
117
|
and how many of them explore).
|
|
@@ -184,17 +212,55 @@ are often wrong together.
|
|
|
184
212
|
|
|
185
213
|
## Discover: finding models to add
|
|
186
214
|
|
|
187
|
-
**Discover**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
215
|
+
**Discover** leads with FreeLLMAPI's catalog of free tiers and fills in from
|
|
216
|
+
models.dev, which alone has prices, paid models and providers' base URLs. One
|
|
217
|
+
list: free models first, then by reach, and within each reach FreeLLMAPI's
|
|
218
|
+
smartest first (the **Rank** column), then what it does not rank, what is new
|
|
219
|
+
this week first and badged. A row opens its popup: what
|
|
220
|
+
the model takes and does, its limits and monthly budget, FreeLLMAPI's known
|
|
221
|
+
quirks for it, links to its freellmapi.co page and models.dev, and every route
|
|
222
|
+
this install has to it, each with an **Add via this**. Adding through a hub (Open WebUI, LiteLLM, CLI Proxy
|
|
191
223
|
API) puts it in that hub too; adding direct registers it on its own endpoint
|
|
192
|
-
only. Either way the onboarding joins the same queue
|
|
224
|
+
only. Either way the onboarding joins the same queue, with the same
|
|
225
|
+
**Onboard as** and **Vision model** choice as the Onboard dialog, asked in the
|
|
226
|
+
confirm before anything is added.
|
|
193
227
|
|
|
194
228
|
Some providers' free models work only inside their own app; Discover leaves
|
|
195
229
|
those out and keeps their paid models. Settings, **Free only in their own
|
|
196
230
|
app**, switches a provider either way or adds one.
|
|
197
231
|
|
|
232
|
+
## Quota management
|
|
233
|
+
|
|
234
|
+
One tile per provider a bench model arrives through, shaped by how that
|
|
235
|
+
provider limits:
|
|
236
|
+
- **Windows**: CLI Proxy API's sign-ins have a 5-hour and a weekly limit per
|
|
237
|
+
model group. Other providers have the windows their rate-limit headers state.
|
|
238
|
+
- **Pool**: OpenRouter's free requests a day, shared by its models, with a bar
|
|
239
|
+
per key.
|
|
240
|
+
- **Each model's last day**: shown where the provider states nothing.
|
|
241
|
+
|
|
242
|
+
Each tile shows when its limits reset and which of its models are resting
|
|
243
|
+
after a rate limit. **Models** lists the provider's bench models, whether each
|
|
244
|
+
is free to call now, and its last 24 hours of calls.
|
|
245
|
+
|
|
246
|
+
**Refresh never spends quota.** On OpenRouter it asks the key what is left. On
|
|
247
|
+
CLI Proxy API's sign-ins it asks the proxy for each one's quota, as its own
|
|
248
|
+
console does; this needs CLI Proxy API on the Endpoints page as a hub, with
|
|
249
|
+
its management key. Every other tile re-reads what the provider last said in
|
|
250
|
+
its replies to the bench. **Check now** sends one 16-token request to read the
|
|
251
|
+
reply's limit headers. It is offered only where the provider has sent them
|
|
252
|
+
before.
|
|
253
|
+
|
|
254
|
+
Tiles sort with the ones that are out first, then the soonest reset. Drag a
|
|
255
|
+
tile, or move it with the arrow keys, and the order is kept; **Sort** brings
|
|
256
|
+
the default order back.
|
|
257
|
+
|
|
258
|
+
Under the tiles are:
|
|
259
|
+
- the next 7 days' resets, and when each resting model is back
|
|
260
|
+
- **What the bench sent**: requests per provider this month and in the last 24
|
|
261
|
+
hours, as a chart with its table
|
|
262
|
+
- the known limits, folded away
|
|
263
|
+
|
|
198
264
|
## The scheduler
|
|
199
265
|
|
|
200
266
|
The recurring jobs (the morning availability probe, the catalog refresh, the
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.5.0
|
package/cli.py
CHANGED
|
@@ -258,10 +258,9 @@ 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
|
-
if args.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
items = [{"served": s, "endpoint": args.endpoint or "", "mode": mode} for s in args.served]
|
|
261
|
+
mode = ("quick" if args.quick else "full") + ("-adhoc" if args.ad_hoc else "")
|
|
262
|
+
items = [{"served": s, "endpoint": args.endpoint or "", "mode": mode, "vision": args.vision}
|
|
263
|
+
for s in args.served]
|
|
265
264
|
reply = call("POST", "/api/onboard-queue", {"items": items})
|
|
266
265
|
if args.wait:
|
|
267
266
|
while reply.get("running"):
|
|
@@ -384,6 +383,13 @@ def cmd_verdict(args) -> None:
|
|
|
384
383
|
f" {len(body['real'])} real, {len(body['false'])} false"))
|
|
385
384
|
|
|
386
385
|
|
|
386
|
+
def cmd_restart(args) -> None:
|
|
387
|
+
"""serve.restart: refused while work is in progress, unless --force."""
|
|
388
|
+
reply = call("POST", "/api/restart", {"force": args.force})
|
|
389
|
+
out(args, reply, lambda r: done(r, "restarting on the code now on disk; back in a few seconds"
|
|
390
|
+
+ (f" (cut off: {'; '.join(r['cut'])})" if r.get("cut") else "")))
|
|
391
|
+
|
|
392
|
+
|
|
387
393
|
def cmd_endpoints(args) -> None:
|
|
388
394
|
reply = call("GET", "/api/endpoints")
|
|
389
395
|
out(args, reply, lambda r: table(r["endpoints"], ["name", "base", "kind", "hub", "default", "key_set", "models"],
|
|
@@ -546,6 +552,9 @@ def parser() -> argparse.ArgumentParser:
|
|
|
546
552
|
return act
|
|
547
553
|
|
|
548
554
|
cmd("status", cmd_status, "what is running, and where")
|
|
555
|
+
s = cmd("restart", cmd_restart, "start the dashboard again on the code now on disk")
|
|
556
|
+
s.add_argument("--force", action="store_true",
|
|
557
|
+
help="even while onboarding, a scheduled job or gateway calls are in progress (cuts them off)")
|
|
549
558
|
|
|
550
559
|
s = cmd("ask", cmd_ask, "ask a collection: every seat answers")
|
|
551
560
|
s.add_argument("collection", help="a collection, e.g. core (bench collections)")
|
|
@@ -597,7 +606,10 @@ def parser() -> argparse.ArgumentParser:
|
|
|
597
606
|
s.add_argument("--quick", action="store_true",
|
|
598
607
|
help="probe only; the scheduled suite passes fill the suite in later")
|
|
599
608
|
s.add_argument("--ad-hoc", action="store_true",
|
|
600
|
-
help="
|
|
609
|
+
help="the bench never calls it on its own afterwards (metered, capped or trial keys);"
|
|
610
|
+
" without --quick the suite still runs once now")
|
|
611
|
+
s.add_argument("--vision", action="store_true",
|
|
612
|
+
help="role vision: a seat only for questions about images, and its suite is the vision tasks")
|
|
601
613
|
|
|
602
614
|
s = cmd("dismiss", cmd_dismiss, "stop offering served models as candidates")
|
|
603
615
|
s.add_argument("served", nargs="+"), s.add_argument("--endpoint"), s.add_argument("--undo", action="store_true")
|
package/gateway.py
CHANGED
|
@@ -330,6 +330,22 @@ def retired_why(entry: dict) -> str:
|
|
|
330
330
|
" (Restore in its model popup brings it back)")
|
|
331
331
|
|
|
332
332
|
|
|
333
|
+
def refused_now(aliases, est_tokens: int, within: float) -> dict[str, str]:
|
|
334
|
+
"""alias -> why, for models whose known rate limits or quota leave no room
|
|
335
|
+
for a call within `within` seconds (metrics/pacing.held): asked, they
|
|
336
|
+
would only bring back the bench's own refusal, so they are not seated."""
|
|
337
|
+
import pacing
|
|
338
|
+
out = {}
|
|
339
|
+
for a in aliases:
|
|
340
|
+
try:
|
|
341
|
+
h = pacing.held(a, route.connection_of(a), est_tokens)
|
|
342
|
+
except Exception: # noqa: BLE001 -- a broken limit file is not a refusal
|
|
343
|
+
continue
|
|
344
|
+
if h and h[0] > within:
|
|
345
|
+
out[a] = f"held by a rate limit: {h[1]}; room in {pacing._wait_text(h[0])}"
|
|
346
|
+
return out
|
|
347
|
+
|
|
348
|
+
|
|
333
349
|
def retiring() -> set[str]:
|
|
334
350
|
"""Aliases not to spend an explorer seat on: those on a retiring alert
|
|
335
351
|
(failing their probes; 2026-10-16 both explorers of a bench/core call were
|
|
@@ -407,6 +423,10 @@ class TooLarge(ValueError):
|
|
|
407
423
|
"""The prompt does not fit a member's context window (HTTP 413)."""
|
|
408
424
|
|
|
409
425
|
|
|
426
|
+
class AllHeld(LookupError):
|
|
427
|
+
"""Every seat is held by a known rate limit (HTTP 429): try again later."""
|
|
428
|
+
|
|
429
|
+
|
|
410
430
|
def estimate_tokens(text: str) -> int:
|
|
411
431
|
"""Characters / 4 plus 10%: tokenizers differ by family, so this errs high.
|
|
412
432
|
The windows it is compared with are corrected by learned 'maximum context
|
|
@@ -590,6 +610,13 @@ def run_collection(name: str, messages: list[dict], nominated: list[str] | None
|
|
|
590
610
|
f" windows) in bench/{name}. Trim it under {min(small.values()):,} tokens,"
|
|
591
611
|
" or choose a collection whose members have larger windows"
|
|
592
612
|
" (GET /v1/models lists each collection's context_window).")
|
|
613
|
+
# A MODEL THE BENCH KNOWS WILL BE REFUSED IS NOT ASKED: a member or a
|
|
614
|
+
# `with` whose limits have no room before its seat would give up, named
|
|
615
|
+
# like the others; it would only come back as the refusal.
|
|
616
|
+
held = refused_now([*sitting, *nominated], needed, SEAT_TIMEOUT)
|
|
617
|
+
skipped += [{"seat": "member" if a in sitting else "with", "alias": a, "why": why} for a, why in held.items()]
|
|
618
|
+
sitting = [a for a in sitting if a not in held]
|
|
619
|
+
nominated = [a for a in nominated if a not in held]
|
|
593
620
|
if pictures and not collection.get("shape"):
|
|
594
621
|
shape, how = "vision", "the request carries images"
|
|
595
622
|
elif not n_explorers:
|
|
@@ -603,6 +630,8 @@ def run_collection(name: str, messages: list[dict], nominated: list[str] | None
|
|
|
603
630
|
# `with` in the request body is route.py's --with: an explorer seat.
|
|
604
631
|
from run import _registered_names
|
|
605
632
|
avoid = retiring() | set(too_small(needed, _registered_names())) # explorers must fit too
|
|
633
|
+
# and have room now: an explorer seat can go to a model without a wait.
|
|
634
|
+
avoid |= set(refused_now(_registered_names(), needed, 0))
|
|
606
635
|
if pictures:
|
|
607
636
|
avoid |= {a for a in _registered_names() if cannot_see(a)} # and see
|
|
608
637
|
p = route.panel(shape, large=len(whole) > LARGE, nominated=nominated or None,
|
|
@@ -615,8 +644,9 @@ def run_collection(name: str, messages: list[dict], nominated: list[str] | None
|
|
|
615
644
|
+ ", and no model that takes images was free for an explorer seat. Add one to the"
|
|
616
645
|
" collection, or call one whose together.capabilities on GET /v1/models lists vision.")
|
|
617
646
|
if not seats:
|
|
618
|
-
raise
|
|
619
|
-
|
|
647
|
+
raise (AllHeld if held else LookupError)(
|
|
648
|
+
f"collection {name!r} has no members and no explorer could be seated"
|
|
649
|
+
+ "".join(f"; {k['alias']} not asked: {k['why']}" for k in skipped))
|
|
620
650
|
panel_id = f"p{dt.datetime.now():%Y%m%d-%H%M%S}-{secrets.token_hex(2)}"
|
|
621
651
|
model = bench_collections.PREFIX + name
|
|
622
652
|
task = " ".join(prompt.split())[:90] + f" ({model}, {shape})"
|
|
@@ -718,6 +748,8 @@ def refusal(exc: Exception) -> tuple[int, str]:
|
|
|
718
748
|
return 413, str(exc)
|
|
719
749
|
if isinstance(exc, ValueError):
|
|
720
750
|
return 400, str(exc)
|
|
751
|
+
if isinstance(exc, AllHeld):
|
|
752
|
+
return 429, str(exc)
|
|
721
753
|
if isinstance(exc, LookupError):
|
|
722
754
|
return 503, str(exc)
|
|
723
755
|
return 500, f"the panel failed ({type(exc).__name__}: {exc})"
|
package/launcher.py
CHANGED
|
@@ -22,7 +22,7 @@ ROOT = Path(__file__).resolve().parent # the app: a checkout, th
|
|
|
22
22
|
SERVERS = {"serve": "serve.py", "gateway": "gateway.py", "mcp": "mcp_server.py",
|
|
23
23
|
"init": str(Path("scripts") / "init.py")}
|
|
24
24
|
# Everything but running a server is cli.py, a client of a running server.
|
|
25
|
-
CLIENT = ("status", "ask", "verdict", "models", "model", "candidates", "onboard", "dismiss",
|
|
25
|
+
CLIENT = ("status", "restart", "ask", "verdict", "models", "model", "candidates", "onboard", "dismiss",
|
|
26
26
|
"collections", "collection", "endpoints", "endpoint", "keys", "key", "settings")
|
|
27
27
|
|
|
28
28
|
|
|
@@ -57,6 +57,7 @@ def usage() -> str:
|
|
|
57
57
|
"",
|
|
58
58
|
"Using it (a client of a running server, BENCHWARMERS_URL; --help on any):",
|
|
59
59
|
" status what is running, and where",
|
|
60
|
+
" restart [--force] start the dashboard again on the code now on disk",
|
|
60
61
|
" ask <collection> [prompt] ask a collection; -f FILE adds a file, a pipe reads stdin",
|
|
61
62
|
" verdict <call> --useful|--not-useful [--real ...] [--false ...]",
|
|
62
63
|
" models | model <action> list, rename, role, retire, restore, max-tokens, ...",
|
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:
|