@wendellresearch-ai/benchwarmers 0.6.2 → 0.6.4
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 +3 -1
- package/BENCHMARKS.md +1 -1
- package/README.md +2 -0
- package/USAGE.md +4 -6
- package/VERSION +1 -1
- package/package.json +1 -1
- package/scripts/check.py +1 -0
- package/scripts/clients_doc.py +98 -0
- package/scripts/provider_tags.py +19 -3
- package/scripts/test_no_owui.py +8 -0
- package/serve.py +130 -248
- package/ui/bench.css +37 -11
- package/ui/bench.js +355 -118
package/AGENTS.md
CHANGED
|
@@ -124,7 +124,9 @@ in `bench.skipped`. One the catalog knows nothing about sits once its model
|
|
|
124
124
|
popup ticks Vision / OCR.
|
|
125
125
|
After the person checks an answer against the real work, record a verdict
|
|
126
126
|
(dashboard, or `POST /bench/verdict`); verdicts choose the next panels. See
|
|
127
|
-
GATEWAY.md for the whole contract.
|
|
127
|
+
GATEWAY.md for the whole contract. To set an app up on the gateway (opencode,
|
|
128
|
+
VS Code, Codex, Zed, Continue, Cline, Kilo Code, Claude Code, goose, chat
|
|
129
|
+
apps), CLIENTS.md has each one's configuration, file by file.
|
|
128
130
|
|
|
129
131
|
## Beyond this machine
|
|
130
132
|
|
package/BENCHMARKS.md
CHANGED
|
@@ -70,7 +70,7 @@ earlier scores on the same tasks, over the last 30 days (or 60).
|
|
|
70
70
|
|
|
71
71
|
Whether the provider was there when the bench called it, from the calls the
|
|
72
72
|
bench makes anyway: one cell a day, by provider and then by model, over the
|
|
73
|
-
last 7 days (or 30).
|
|
73
|
+
last 7 days (or 30).
|
|
74
74
|
|
|
75
75
|
- **Down**: the provider was busy or overloaded (a 5xx, high demand, a shared
|
|
76
76
|
pool's 429), unreachable (a broken stream, no reply), timed out (the
|
package/README.md
CHANGED
|
@@ -419,6 +419,8 @@ collections and MCP server, with a Copy button: opencode, VS Code chat, Codex
|
|
|
419
419
|
CLI, Zed, Continue, Cline and Kilo Code, Claude Code, goose, and chat apps
|
|
420
420
|
(LobeHub, AnythingLLM, Cherry Studio, Chatbox, LibreChat). Where keys are
|
|
421
421
|
asked for, it makes one and fills it in, never into a file that may sync.
|
|
422
|
+
The same set-ups are in [CLIENTS.md](CLIENTS.md), with a fresh install's
|
|
423
|
+
address and collections, for reading here or handing to an agent.
|
|
422
424
|
|
|
423
425
|
<!-- The MCP Registry's proof that the PyPI package is this server (server.json):
|
|
424
426
|
mcp-name: io.github.WendellResearch-AI/benchwarmers -->
|
package/USAGE.md
CHANGED
|
@@ -25,15 +25,14 @@ coding agent doing it with you); the gateway's whole contract is in
|
|
|
25
25
|
|
|
26
26
|
| Group | Page | What it is for |
|
|
27
27
|
| --- | --- | --- |
|
|
28
|
-
| Observe | **
|
|
28
|
+
| Observe | **Benchmarks** | The home page. How models do on the suite and on real work: bugs found against false alarms, who is confidently wrong, each model against its own earlier scores, uptime, speed, and the decision models. Each report shares as an image or its table. |
|
|
29
29
|
| | **Action needed** | What will not fix itself, by the connection it comes from, with Retry and Retire on each model. |
|
|
30
|
-
| | **Benchmarks** | How models do on the suite and on real work: bugs found against false alarms, who is confidently wrong, each model against its own earlier scores, uptime, speed, and the decision models. Each report shares as an image or its table. |
|
|
31
30
|
| | **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. |
|
|
32
31
|
| | **Logs** | Gateway calls (who sat and how it went, never the prompt), changes made from the dashboard, the scheduler, and onboarding runs. |
|
|
33
32
|
| 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. A model server running on this machine, at its usual port or any other its process is listening on (LM Studio, Jan, llama.cpp, vLLM and the like), is offered under Found on this machine: Add opens the dialog with it filled in, to test and save. Also your own app registrations and rate limits per connection. |
|
|
34
|
-
| | **Models** | Every model on the bench, by the connection it comes from. |
|
|
33
|
+
| | **Models** | Every model on the bench, by the connection it comes from: what it holds, its reply time and its limits. One search above every table, each table folds, and Collapse all / Expand all. Click a name for its popup: role, alias, facts, options, retire. |
|
|
35
34
|
| | **Collections** | The named panels your apps ask for, and the gateway's settings. |
|
|
36
|
-
| | **Clients** | Each popular app's set-up (opencode, VS Code, Codex, Zed, Continue, Cline, Kilo Code, Claude Code, goose, chat apps), filled in from this install, with Copy and a gateway key made on the spot. |
|
|
35
|
+
| | **Clients** | Each popular app's set-up (opencode, VS Code, Codex, Zed, Continue, Cline, Kilo Code, Claude Code, goose, chat apps), filled in from this install, with Copy and a gateway key made on the spot. [CLIENTS.md](CLIENTS.md) has the same, for a fresh install. |
|
|
37
36
|
| | **Discover** | Models worth adding, from public catalogs, with the ways this install can reach each one. |
|
|
38
37
|
| Control | **Settings** | Theme, gateway keys, the scheduler, the model client, providers whose free models work only in their own app, export and import, how the install is reached (shown only), and Restart. |
|
|
39
38
|
| | **Info** | The source repository, the version (linking its release), Python, data folder and counts. |
|
|
@@ -52,8 +51,7 @@ clears the alert) and Retire. A group of two or more has Retry all and Retire
|
|
|
52
51
|
all; a connection held because every model on it fails says so above its
|
|
53
52
|
models, with any link the provider gave. Automatic retirements from the last
|
|
54
53
|
week are folded at the end, each with Restore. Its count sits beside it in the
|
|
55
|
-
sidebar
|
|
56
|
-
soon. An alert whose models have all been retired goes at once. A model that
|
|
54
|
+
sidebar. An alert whose models have all been retired goes at once. A model that
|
|
57
55
|
is out of quota is not counting down: days the bench held its calls back until
|
|
58
56
|
a stated reset, an account's shared free quota was spent, or the provider said
|
|
59
57
|
the model's free quota is used up, pause its clock, and while it is still out
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.4
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wendellresearch-ai/benchwarmers",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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
|
@@ -49,6 +49,7 @@ CHECKS = [
|
|
|
49
49
|
("scripts/onboard.py", ["--selftest"], False),
|
|
50
50
|
("scripts/export_check.py", ["--selftest"], False),
|
|
51
51
|
("scripts/notice.py", ["--check"], False),
|
|
52
|
+
("scripts/clients_doc.py", ["--check"], False), # CLIENTS.md is what the Clients page shows
|
|
52
53
|
("scripts/check_themes.py", [], False),
|
|
53
54
|
("scripts/test_owui.py", [], False),
|
|
54
55
|
("scripts/test_hubs.py", [], False),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Write CLIENTS.md from the dashboard's Clients page, so each app's set-up is in
|
|
2
|
+
the repository for people and agents to read, and never drifts from the page.
|
|
3
|
+
|
|
4
|
+
python scripts/clients_doc.py # write CLIENTS.md
|
|
5
|
+
python scripts/clients_doc.py --check # fail if CLIENTS.md is not what it would write (check.py)
|
|
6
|
+
|
|
7
|
+
One source: serve.client_setups, the function the Clients page draws. The page
|
|
8
|
+
fills in this install (its address, its collections, a key made on the spot);
|
|
9
|
+
here they are the defaults a fresh install has: the server at 127.0.0.1:8122,
|
|
10
|
+
collections `core` and `fast`, and every call needing a gateway key, read from
|
|
11
|
+
BENCHWARMERS_KEY. The page's HTML (code, links) becomes Markdown.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import html
|
|
17
|
+
import os
|
|
18
|
+
import pathlib
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
|
|
23
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
24
|
+
OUT = ROOT / "CLIENTS.md"
|
|
25
|
+
COLLECTIONS = [("core", None, False), ("fast", None, False)]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def md(text: str) -> str:
|
|
29
|
+
"""The page's inline HTML as Markdown: code, links, line breaks; any other tag dropped."""
|
|
30
|
+
text = re.sub(r"<code>(.*?)</code>", lambda m: "`" + html.unescape(m.group(1)) + "`", text)
|
|
31
|
+
text = re.sub(r"<a [^>]*href='([^']*)'[^>]*>(.*?)</a>", r"[\2](\1)", text)
|
|
32
|
+
text = re.sub(r"<br\s*/?>", " \n", text)
|
|
33
|
+
return html.unescape(re.sub(r"<[^>]+>", "", text))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fence(lang: str, text: str) -> str:
|
|
37
|
+
tick = "````" if "```" in text else "```"
|
|
38
|
+
return f"{tick}{lang}\n{text}\n{tick}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render() -> str:
|
|
42
|
+
# serve reads the data folder at import: an empty temp one, never the install's.
|
|
43
|
+
os.environ.setdefault("BENCHWARMERS_HOME", tempfile.mkdtemp(prefix="bench-clients-doc-"))
|
|
44
|
+
sys.path[:0] = [str(ROOT), str(ROOT / "metrics"), str(ROOT / "suite")]
|
|
45
|
+
import serve
|
|
46
|
+
root = serve.DEFAULT_SERVER
|
|
47
|
+
out = ["# Setting up clients",
|
|
48
|
+
"",
|
|
49
|
+
"Every app Benchwarmers has a ready set-up for, as the dashboard's **Clients** page shows it,"
|
|
50
|
+
" written out for people and for agents setting an app up. The page fills in your own install:"
|
|
51
|
+
" its address, your collections and a gateway key made on the spot. Here they are a fresh"
|
|
52
|
+
f" install's: the server at `{root}`, the collections `core` and `fast`, and a gateway key"
|
|
53
|
+
f" read from `{serve.KEY_ENV}`. Replace them with yours.",
|
|
54
|
+
"",
|
|
55
|
+
"Agents get a collection in route mode (one member with its tools, `bench/<collection>:route`);"
|
|
56
|
+
" chat windows a merged answer (`:merged`); the MCP server the panel itself, as ask and verdict"
|
|
57
|
+
f" tools. Gateway `{root}/v1` · MCP server `{root}/mcp`.",
|
|
58
|
+
"",
|
|
59
|
+
f"_Generated from `serve.client_setups` by `scripts/clients_doc.py`; each app's format follows its"
|
|
60
|
+
f" own docs as read on {serve.CLIENTS_READ}. Do not edit by hand: run the script._",
|
|
61
|
+
"",
|
|
62
|
+
"## The gateway key",
|
|
63
|
+
"",
|
|
64
|
+
"When every call needs a key (`BENCHWARMERS_REQUIRE_KEYS=1`, or an admin token), make one on"
|
|
65
|
+
" Settings or the Clients page. Most set-ups read it from"
|
|
66
|
+
f" `{serve.KEY_ENV}`:",
|
|
67
|
+
""]
|
|
68
|
+
for s in serve._env_snips(serve.KEY_ENV):
|
|
69
|
+
out += [f"**{s['label']}**", "", fence(s["lang"], s["text"]), ""]
|
|
70
|
+
setups = serve.client_setups(root, COLLECTIONS, True)
|
|
71
|
+
out += ["## Contents", ""] + [f"- [{c['name']}](#{c['id']})" for c in setups] + [""]
|
|
72
|
+
for c in setups:
|
|
73
|
+
out += [f"<a id='{c['id']}'></a>", "", f"## {c['name']}", "", md(c["gets"]), "",
|
|
74
|
+
f"**Where it goes:** {md(c['where'])}", ""]
|
|
75
|
+
for s in c["blocks"]:
|
|
76
|
+
out += [f"**{s['label']}**", "", fence(s["lang"], s["text"]), ""]
|
|
77
|
+
docs = ", ".join(f"[{t}]({u})" for u, t in c["docs"])
|
|
78
|
+
out += [f"**Watch for:** {md(c['watch'])}", "",
|
|
79
|
+
f"Written from {docs}, read {serve.CLIENTS_READ}"
|
|
80
|
+
+ (f": {md(c['docs_note'])}" if c.get("docs_note") else "."), ""]
|
|
81
|
+
return "\n".join(out).rstrip() + "\n"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main() -> int:
|
|
85
|
+
text = render()
|
|
86
|
+
if "--check" in sys.argv:
|
|
87
|
+
if not OUT.exists() or OUT.read_text(encoding="utf-8") != text:
|
|
88
|
+
print("CLIENTS.md is not what serve.client_setups gives: run python scripts/clients_doc.py")
|
|
89
|
+
return 1
|
|
90
|
+
print("CLIENTS.md matches the Clients page")
|
|
91
|
+
return 0
|
|
92
|
+
OUT.write_text(text, encoding="utf-8", newline="\n")
|
|
93
|
+
print(f"wrote {OUT.name}")
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
sys.exit(main())
|
package/scripts/provider_tags.py
CHANGED
|
@@ -68,13 +68,29 @@ def get(hub: str, path: str) -> dict:
|
|
|
68
68
|
return json.load(response)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def _host(url: str) -> str:
|
|
72
|
+
return (url or "").split("//", 1)[-1].split("/", 1)[0].split(":", 1)[0].lower()
|
|
73
|
+
|
|
74
|
+
|
|
71
75
|
def connection_names(hub: str) -> list[str]:
|
|
72
|
-
"""One short name per connection index. URLs only — never the keys.
|
|
76
|
+
"""One short name per connection index. URLs only — never the keys.
|
|
77
|
+
|
|
78
|
+
A provider reached both through the hub and on a direct endpoint of its
|
|
79
|
+
own (Cloudflare: chat through Open WebUI, decisions only direct) is ONE
|
|
80
|
+
connection: the hub's connection to that host takes the direct endpoint's
|
|
81
|
+
name, which is what its directly registered models are tagged (main). Any
|
|
82
|
+
provider, by host; other hubs (CLI Proxy API) are not direct endpoints."""
|
|
73
83
|
urls = get(hub, "/openai/config").get("OPENAI_API_BASE_URLS", [])
|
|
84
|
+
direct: dict[str, str] = {}
|
|
85
|
+
for name, entry in endpoints.load().items():
|
|
86
|
+
if name != hub and not endpoints.hub(name):
|
|
87
|
+
direct.setdefault(_host(entry.get("base", "")), name)
|
|
74
88
|
names = []
|
|
75
89
|
for url in urls:
|
|
76
|
-
host = url
|
|
77
|
-
if host.
|
|
90
|
+
host = _host(url)
|
|
91
|
+
if host in direct and host not in (endpoints.connections().get("hosts") or {}):
|
|
92
|
+
names.append(direct[host])
|
|
93
|
+
elif host.replace(".", "").isdigit():
|
|
78
94
|
# A BARE IP NAMES NOTHING, and guessing from it was worse than
|
|
79
95
|
# useless: a proxy labelled `lan-proxy` is described by where it
|
|
80
96
|
# sits rather than what it is, and Open WebUI does not expose
|
package/scripts/test_no_owui.py
CHANGED
|
@@ -236,6 +236,14 @@ def owner(fake: str) -> None:
|
|
|
236
236
|
assert tags["fast-1"] == "groq" and tags["llm7.codestral"] == "llm7-api" and tags["vendor/x:free"] == "openrouter", tags
|
|
237
237
|
assert "never-read" not in json.dumps(tags)
|
|
238
238
|
assert route.connection_of("fast-1") == "groq" and route.connection_of("x-free") == "openrouter"
|
|
239
|
+
# A provider also on a direct endpoint of its own is one connection: the hub's takes the endpoint's name.
|
|
240
|
+
real_load, both = endpoints.load, dict(endpoints.load())
|
|
241
|
+
both["groq-direct"] = {"base": "https://api.groq.com/openai/v1", "key": "groq-direct"}
|
|
242
|
+
endpoints.load = lambda *a, **k: both
|
|
243
|
+
try:
|
|
244
|
+
assert provider_tags.connection_names("openwebui") == ["groq-direct", "llm7-api"]
|
|
245
|
+
finally:
|
|
246
|
+
endpoints.load = real_load
|
|
239
247
|
# A decision model onboarded from the direct endpoint: in no registration, untagged, its `via` is its connection.
|
|
240
248
|
write("decision-models.json", {"dec-1": {"served": "vendor/decide:free", "endpoint": "openrouter", "via": "openrouter"},
|
|
241
249
|
"dec-hub": {"served": "hub-decide", "endpoint": "openwebui", "via": "openwebui"}})
|