@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 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). At a glance has each model's 7 days too.
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 | **At a glance** | Every model on the bench: whether it works right now, its uptime over 7 days, its role, and its connection. Click a name for its popup: role, alias, facts, options, retire. |
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, and At a glance opens on one line saying what is waiting and how
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.2
1
+ 0.6.4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendellresearch-ai/benchwarmers",
3
- "version": "0.6.2",
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())
@@ -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.split("//", 1)[-1].split("/", 1)[0].split(":", 1)[0]
77
- if host.replace(".", "").isdigit():
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
@@ -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"}})