@wendellresearch-ai/benchwarmers 0.2.0 → 0.2.1
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 +147 -147
- package/BENCHMARKS.md +262 -262
- package/GATEWAY.md +272 -272
- package/NOTICE +28 -28
- package/PRIVACY.md +46 -46
- package/README.md +368 -314
- package/SECURITY.md +83 -83
- package/TERMS.md +22 -22
- package/USAGE.md +170 -163
- package/VERSION +1 -1
- package/bin/benchwarmers.js +16 -74
- package/cli.py +666 -0
- package/examples/config/gateway.json +5 -5
- package/examples/config/model-roles.json +11 -11
- package/gateway.py +755 -755
- package/launcher.py +120 -0
- package/mcp_server.py +266 -266
- package/metrics/alerts.py +116 -116
- package/metrics/aliases.py +41 -41
- package/metrics/bench_collections.py +141 -141
- package/metrics/catalog.py +295 -295
- package/metrics/client.py +394 -394
- package/metrics/cliproxy.py +191 -191
- package/metrics/connect.py +691 -691
- package/metrics/cost.py +152 -152
- package/metrics/decide.py +227 -227
- package/metrics/discover.py +318 -318
- package/metrics/endpoints.py +436 -436
- package/metrics/guard.py +617 -592
- package/metrics/learn.py +221 -221
- package/metrics/litellm_hub.py +206 -206
- package/metrics/model_roles.py +84 -84
- package/metrics/offload.py +166 -166
- package/metrics/owui.py +205 -205
- package/metrics/paths.py +60 -60
- package/metrics/route.py +579 -579
- package/metrics/scheduler.py +275 -275
- package/metrics/sources.py +167 -167
- package/metrics/wrong.py +144 -144
- package/package.json +68 -65
- package/perf/README.md +89 -89
- package/perf/run.py +261 -261
- package/scripts/auto_retire.py +367 -367
- package/scripts/check.py +108 -106
- package/scripts/check_deprecations.py +378 -378
- package/scripts/check_themes.py +103 -103
- package/scripts/init.py +124 -124
- package/scripts/list_served.py +78 -78
- package/scripts/notice.py +78 -78
- package/scripts/onboard.py +401 -331
- package/scripts/provider_tags.py +129 -129
- package/scripts/reprefix.py +136 -136
- package/scripts/sync_config.py +105 -105
- package/scripts/test_auth.py +255 -255
- package/scripts/test_cli.py +189 -0
- package/scripts/test_client.py +233 -233
- package/scripts/test_connect.py +222 -222
- package/scripts/test_first_run.py +180 -180
- package/scripts/test_hubs.py +328 -328
- package/scripts/test_launcher.py +67 -0
- package/scripts/test_llm_log.py +85 -81
- package/scripts/test_mcp.py +178 -178
- package/scripts/test_merged.py +214 -214
- package/scripts/test_no_owui.py +291 -291
- package/scripts/test_owui.py +90 -90
- package/scripts/test_serve_edit.py +448 -438
- package/scripts/test_verdict.py +98 -98
- package/scripts/test_vision.py +228 -228
- package/serve.py +3904 -3764
- package/suite/mine.py +154 -154
- package/suite/pace.py +145 -145
- package/suite/run.py +1046 -1042
- package/suite/seed.py +421 -421
- package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
- package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
- package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
- package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
- package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
- package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
- package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
- package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
- package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
- package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
- package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
- package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
- package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
- package/ui/bench.css +430 -425
- package/ui/bench.js +1756 -1576
- package/ui/themes/dark.css +35 -35
- package/ui/themes/hardwood.css +52 -52
- package/ui/themes/rainforest.css +32 -32
package/metrics/owui.py
CHANGED
|
@@ -1,205 +1,205 @@
|
|
|
1
|
-
"""The Open WebUI hub adapter: add a model as it is otherwise done by hand, then hand it to onboarding.
|
|
2
|
-
|
|
3
|
-
python metrics/owui.py <connection> <model id> # what would change, changes nothing
|
|
4
|
-
|
|
5
|
-
One of three hub adapters (litellm_hub.py, cliproxy.py), each exposing
|
|
6
|
-
SUPPORTS, CHECKED, endpoint(), plan() and add(); serve.discover_add picks one
|
|
7
|
-
by the endpoint's hub (endpoints.adapter).
|
|
8
|
-
|
|
9
|
-
THE HAND STEPS (2026-10-18)
|
|
10
|
-
---------------------------
|
|
11
|
-
1. Edit the connection: add the model to its Model IDs list (advanced
|
|
12
|
-
settings). Only when the list is in use; an empty list means Open WebUI
|
|
13
|
-
already shows everything the provider serves.
|
|
14
|
-
2. Find the model in Admin > Models: make it public, tag it with the
|
|
15
|
-
connection's name (`nvidia-api`, `groq`, ...), give it the provider logo.
|
|
16
|
-
|
|
17
|
-
The logo, the capability switches and the default features are copied from
|
|
18
|
-
a model already on the same connection, because every model on a connection
|
|
19
|
-
carries the same ones. A connection is found by its tag: the Open WebUI
|
|
20
|
-
connection (`urlIdx`) its tagged models are served from. Then
|
|
21
|
-
`scripts/onboard.py` registers, probes and runs the suite as for any model.
|
|
22
|
-
|
|
23
|
-
Admin calls with the endpoint's admin key (endpoints.admin_key: an admin's
|
|
24
|
-
API key, which is the endpoint's own key unless one is stored apart); keys
|
|
25
|
-
are never printed, and the config is written back exactly as read, plus one
|
|
26
|
-
id. The endpoint is whichever is a hub of type openwebui (`endpoints.find_hub`);
|
|
27
|
-
an install without one has no Open WebUI to add to, and `endpoint()` says so.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
from __future__ import annotations
|
|
31
|
-
|
|
32
|
-
import collections
|
|
33
|
-
import json
|
|
34
|
-
import pathlib
|
|
35
|
-
import sys
|
|
36
|
-
import time
|
|
37
|
-
import urllib.request
|
|
38
|
-
|
|
39
|
-
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
40
|
-
sys.path.insert(0, str(ROOT / "metrics"))
|
|
41
|
-
sys.path.insert(0, str(ROOT / "suite"))
|
|
42
|
-
import endpoints # noqa: E402
|
|
43
|
-
|
|
44
|
-
PUBLIC = [{"principal_type": "user", "principal_id": "*", "permission": "read"}]
|
|
45
|
-
COPIED = ("profile_image_url", "capabilities", "defaultFeatureIds")
|
|
46
|
-
# What this hub takes (RELEASE-PLAN section 2, "Adding a model to a hub"); the
|
|
47
|
-
# dashboard says only what is here. "" where the hub has no such thing.
|
|
48
|
-
SUPPORTS = {"allow_list": "Model IDs list", "tags": "the connection's tag",
|
|
49
|
-
"logo": "copied from the connection's other models (PNG, JPEG, GIF or WebP)",
|
|
50
|
-
"access": "public: every signed-in user",
|
|
51
|
-
"link": "/?settings=admin:models (v0.11+, the Models tab; up to v0.10 /admin/settings/models?id=<id>)"}
|
|
52
|
-
CHECKED = ("0.12",) # 0.12.3, 2026-10-21: the routes below and /api/version
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def endpoint() -> str:
|
|
56
|
-
"""The Open WebUI hub's endpoint name. ValueError when there is none: the
|
|
57
|
-
dashboard shows it as the reason, and nothing has been sent anywhere."""
|
|
58
|
-
name = endpoints.find_hub("openwebui")
|
|
59
|
-
if not name:
|
|
60
|
-
raise ValueError("no endpoint is an Open WebUI hub; add it on the Endpoints page as kind hub, Open WebUI")
|
|
61
|
-
return name
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _call(path: str, body: dict | None = None):
|
|
65
|
-
name = endpoint()
|
|
66
|
-
root = endpoints.base(name).removesuffix("/api")
|
|
67
|
-
req = urllib.request.Request(root + path, data=None if body is None else json.dumps(body).encode(),
|
|
68
|
-
method="GET" if body is None else "POST",
|
|
69
|
-
headers={"Authorization": f"Bearer {endpoints.admin_key(name)}",
|
|
70
|
-
"Content-Type": "application/json", "Accept": "application/json"})
|
|
71
|
-
with urllib.request.urlopen(req, timeout=60) as r:
|
|
72
|
-
return json.loads(r.read() or b"null")
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def version() -> tuple[str, str]:
|
|
76
|
-
"""(release, endpoints.version_note's sentence). Advice only: a hub that
|
|
77
|
-
will not say is still added to."""
|
|
78
|
-
try:
|
|
79
|
-
v = str((_call("/api/version") or {}).get("version") or "")
|
|
80
|
-
except Exception: # noqa: BLE001
|
|
81
|
-
v = ""
|
|
82
|
-
return v, endpoints.version_note("openwebui", v, CHECKED)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def connections() -> dict[str, tuple[int, str]]:
|
|
86
|
-
"""Connection name -> (Open WebUI connection index, the tag its models carry).
|
|
87
|
-
|
|
88
|
-
The bench names a connection from config/provider-tags.json (`groq`,
|
|
89
|
-
`mistral`), Open WebUI's tags differ (`groq-api`, `mistral-studio`), so
|
|
90
|
-
the two meet at the index the bench's own registered models are served
|
|
91
|
-
from. Open WebUI's tag names are accepted as well."""
|
|
92
|
-
import route
|
|
93
|
-
from run import _registered_names
|
|
94
|
-
data = _call("/api/models").get("data", [])
|
|
95
|
-
by_id = {m["id"]: m for m in data}
|
|
96
|
-
tag_at: dict[int, collections.Counter] = collections.defaultdict(collections.Counter)
|
|
97
|
-
for m in data:
|
|
98
|
-
for t in m.get("tags") or []:
|
|
99
|
-
if m.get("urlIdx") is not None:
|
|
100
|
-
tag_at[m["urlIdx"]][t["name"]] += 1
|
|
101
|
-
at: dict[str, collections.Counter] = collections.defaultdict(collections.Counter)
|
|
102
|
-
for alias, served in _registered_names().items():
|
|
103
|
-
m = by_id.get(served)
|
|
104
|
-
if m and m.get("urlIdx") is not None:
|
|
105
|
-
at[route.connection_of(alias)][m["urlIdx"]] += 1
|
|
106
|
-
out = {}
|
|
107
|
-
for name, c in at.items():
|
|
108
|
-
idx = c.most_common(1)[0][0]
|
|
109
|
-
tags = tag_at.get(idx) # .get: indexing a defaultdict would add an empty one
|
|
110
|
-
out[name] = (idx, tags.most_common(1)[0][0] if tags else name)
|
|
111
|
-
for idx, c in tag_at.items():
|
|
112
|
-
tag = c.most_common(1)[0][0]
|
|
113
|
-
out.setdefault(tag, (idx, tag))
|
|
114
|
-
return out
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def plan(connection: str, model_id: str) -> dict:
|
|
118
|
-
"""What adding would change. Raises ValueError when it cannot be done here."""
|
|
119
|
-
release, note = version()
|
|
120
|
-
found = connections().get(connection)
|
|
121
|
-
if found is None:
|
|
122
|
-
raise ValueError(f"no Open WebUI connection is known as {connection!r}: none of the bench's"
|
|
123
|
-
" models is served from it and no Open WebUI model carries that tag")
|
|
124
|
-
idx, tag = found
|
|
125
|
-
cfg = _call("/openai/config")
|
|
126
|
-
if idx >= len(cfg.get("OPENAI_API_BASE_URLS") or []):
|
|
127
|
-
raise ValueError(f"Open WebUI's connections changed while this ran; try again")
|
|
128
|
-
conn = (cfg.get("OPENAI_API_CONFIGS") or {}).get(str(idx), {})
|
|
129
|
-
if conn.get("enable") is False:
|
|
130
|
-
raise ValueError(f"the {connection} connection is switched off in Open WebUI")
|
|
131
|
-
prefix = conn.get("prefix_id") or ""
|
|
132
|
-
served = f"{prefix}.{model_id}" if prefix else model_id
|
|
133
|
-
# One id, two connections: Open WebUI would serve only one of them.
|
|
134
|
-
elsewhere = next((m.get("urlIdx") for m in _call("/api/models").get("data", [])
|
|
135
|
-
if m.get("id") == served and m.get("urlIdx") not in (None, idx)), None)
|
|
136
|
-
if elsewhere is not None:
|
|
137
|
-
raise ValueError(f"Open WebUI already serves {served} from connection {elsewhere}; give the"
|
|
138
|
-
f" {connection} connection a prefix in Open WebUI so the ids differ")
|
|
139
|
-
listed = conn.get("model_ids") or []
|
|
140
|
-
records = {m["id"]: m for m in _call("/api/v1/models/base")}
|
|
141
|
-
sibling = next((m for m in records.values()
|
|
142
|
-
if any(t.get("name") == tag for t in (m.get("meta") or {}).get("tags") or [])
|
|
143
|
-
and (m.get("meta") or {}).get("profile_image_url")), None)
|
|
144
|
-
return {"connection": connection, "tag": tag, "index": idx, "base_url": cfg["OPENAI_API_BASE_URLS"][idx],
|
|
145
|
-
"model_id": model_id, "served": served,
|
|
146
|
-
"add_to_list": bool(listed) and model_id not in listed,
|
|
147
|
-
"record": "update" if served in records else "create",
|
|
148
|
-
"copy_from": sibling["id"] if sibling else None, "version": release, "version_note": note}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def add(connection: str, model_id: str) -> tuple[str, list[str]]:
|
|
152
|
-
"""Do it. (served id, what was done)."""
|
|
153
|
-
p = plan(connection, model_id)
|
|
154
|
-
done = [p["version_note"]] if p["version_note"] else []
|
|
155
|
-
if p["add_to_list"]:
|
|
156
|
-
cfg = _call("/openai/config")
|
|
157
|
-
# The write sends every key back as read. 0.12.3 returns them whole; a
|
|
158
|
-
# version that masks them would have this overwrite real keys.
|
|
159
|
-
if any(isinstance(k, str) and ch in k for k in cfg.get("OPENAI_API_KEYS", []) for ch in "*•"):
|
|
160
|
-
raise ValueError("Open WebUI returned masked API keys; writing the config back would"
|
|
161
|
-
" replace them, so add the model ID to the connection by hand")
|
|
162
|
-
# Decided again on the fresh read: the list may have changed since plan().
|
|
163
|
-
# ponytail: read-modify-write; an edit made in Open WebUI in the same second is lost.
|
|
164
|
-
ids = cfg["OPENAI_API_CONFIGS"][str(p["index"])].setdefault("model_ids", [])
|
|
165
|
-
if ids and model_id not in ids:
|
|
166
|
-
ids.append(model_id)
|
|
167
|
-
_call("/openai/config/update", cfg)
|
|
168
|
-
done.append(f"added {model_id} to the {connection} connection's Model IDs")
|
|
169
|
-
records = {m["id"]: m for m in _call("/api/v1/models/base")}
|
|
170
|
-
old = records.get(p["served"]) or {}
|
|
171
|
-
meta = dict(old.get("meta") or {})
|
|
172
|
-
sib = (records.get(p["copy_from"]) or {}).get("meta") or {}
|
|
173
|
-
for k in COPIED:
|
|
174
|
-
if sib.get(k) and not meta.get(k):
|
|
175
|
-
meta[k] = sib[k]
|
|
176
|
-
tags = [t for t in meta.get("tags") or [] if t.get("name") != p["tag"]]
|
|
177
|
-
meta["tags"] = tags + [{"name": p["tag"]}]
|
|
178
|
-
form = {"id": p["served"], "name": old.get("name") or p["served"], "base_model_id": old.get("base_model_id"),
|
|
179
|
-
"meta": meta, "params": old.get("params") or {}, "access_grants": PUBLIC, "is_active": True}
|
|
180
|
-
try:
|
|
181
|
-
_call("/api/v1/models/model/update" if old else "/api/v1/models/create", form)
|
|
182
|
-
except Exception as exc: # noqa: BLE001
|
|
183
|
-
raise ValueError("; ".join(done + [f"then the model record failed ({exc}); finish it in"
|
|
184
|
-
" Admin > Models"])) from exc
|
|
185
|
-
done.append(f"{'updated' if old else 'created'} its model record: public, tagged {p['tag']}"
|
|
186
|
-
+ (f", logo and switches from {p['copy_from']}" if p["copy_from"] else ", no logo found to copy"))
|
|
187
|
-
# Open WebUI rebuilds its model list on request; wait until it serves the new id.
|
|
188
|
-
for _ in range(10):
|
|
189
|
-
if p["served"] in endpoints.models(endpoint()):
|
|
190
|
-
break
|
|
191
|
-
time.sleep(2)
|
|
192
|
-
else:
|
|
193
|
-
done.append(f"Open WebUI does not list {p['served']} yet; onboarding will say if it never does")
|
|
194
|
-
return p["served"], done
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def main() -> None:
|
|
198
|
-
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
199
|
-
if len(sys.argv) != 3:
|
|
200
|
-
sys.exit(__doc__)
|
|
201
|
-
print(json.dumps(plan(sys.argv[1], sys.argv[2]), indent=2))
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if __name__ == "__main__":
|
|
205
|
-
main()
|
|
1
|
+
"""The Open WebUI hub adapter: add a model as it is otherwise done by hand, then hand it to onboarding.
|
|
2
|
+
|
|
3
|
+
python metrics/owui.py <connection> <model id> # what would change, changes nothing
|
|
4
|
+
|
|
5
|
+
One of three hub adapters (litellm_hub.py, cliproxy.py), each exposing
|
|
6
|
+
SUPPORTS, CHECKED, endpoint(), plan() and add(); serve.discover_add picks one
|
|
7
|
+
by the endpoint's hub (endpoints.adapter).
|
|
8
|
+
|
|
9
|
+
THE HAND STEPS (2026-10-18)
|
|
10
|
+
---------------------------
|
|
11
|
+
1. Edit the connection: add the model to its Model IDs list (advanced
|
|
12
|
+
settings). Only when the list is in use; an empty list means Open WebUI
|
|
13
|
+
already shows everything the provider serves.
|
|
14
|
+
2. Find the model in Admin > Models: make it public, tag it with the
|
|
15
|
+
connection's name (`nvidia-api`, `groq`, ...), give it the provider logo.
|
|
16
|
+
|
|
17
|
+
The logo, the capability switches and the default features are copied from
|
|
18
|
+
a model already on the same connection, because every model on a connection
|
|
19
|
+
carries the same ones. A connection is found by its tag: the Open WebUI
|
|
20
|
+
connection (`urlIdx`) its tagged models are served from. Then
|
|
21
|
+
`scripts/onboard.py` registers, probes and runs the suite as for any model.
|
|
22
|
+
|
|
23
|
+
Admin calls with the endpoint's admin key (endpoints.admin_key: an admin's
|
|
24
|
+
API key, which is the endpoint's own key unless one is stored apart); keys
|
|
25
|
+
are never printed, and the config is written back exactly as read, plus one
|
|
26
|
+
id. The endpoint is whichever is a hub of type openwebui (`endpoints.find_hub`);
|
|
27
|
+
an install without one has no Open WebUI to add to, and `endpoint()` says so.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import collections
|
|
33
|
+
import json
|
|
34
|
+
import pathlib
|
|
35
|
+
import sys
|
|
36
|
+
import time
|
|
37
|
+
import urllib.request
|
|
38
|
+
|
|
39
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
40
|
+
sys.path.insert(0, str(ROOT / "metrics"))
|
|
41
|
+
sys.path.insert(0, str(ROOT / "suite"))
|
|
42
|
+
import endpoints # noqa: E402
|
|
43
|
+
|
|
44
|
+
PUBLIC = [{"principal_type": "user", "principal_id": "*", "permission": "read"}]
|
|
45
|
+
COPIED = ("profile_image_url", "capabilities", "defaultFeatureIds")
|
|
46
|
+
# What this hub takes (RELEASE-PLAN section 2, "Adding a model to a hub"); the
|
|
47
|
+
# dashboard says only what is here. "" where the hub has no such thing.
|
|
48
|
+
SUPPORTS = {"allow_list": "Model IDs list", "tags": "the connection's tag",
|
|
49
|
+
"logo": "copied from the connection's other models (PNG, JPEG, GIF or WebP)",
|
|
50
|
+
"access": "public: every signed-in user",
|
|
51
|
+
"link": "/?settings=admin:models (v0.11+, the Models tab; up to v0.10 /admin/settings/models?id=<id>)"}
|
|
52
|
+
CHECKED = ("0.12",) # 0.12.3, 2026-10-21: the routes below and /api/version
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def endpoint() -> str:
|
|
56
|
+
"""The Open WebUI hub's endpoint name. ValueError when there is none: the
|
|
57
|
+
dashboard shows it as the reason, and nothing has been sent anywhere."""
|
|
58
|
+
name = endpoints.find_hub("openwebui")
|
|
59
|
+
if not name:
|
|
60
|
+
raise ValueError("no endpoint is an Open WebUI hub; add it on the Endpoints page as kind hub, Open WebUI")
|
|
61
|
+
return name
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _call(path: str, body: dict | None = None):
|
|
65
|
+
name = endpoint()
|
|
66
|
+
root = endpoints.base(name).removesuffix("/api")
|
|
67
|
+
req = urllib.request.Request(root + path, data=None if body is None else json.dumps(body).encode(),
|
|
68
|
+
method="GET" if body is None else "POST",
|
|
69
|
+
headers={"Authorization": f"Bearer {endpoints.admin_key(name)}",
|
|
70
|
+
"Content-Type": "application/json", "Accept": "application/json"})
|
|
71
|
+
with urllib.request.urlopen(req, timeout=60) as r:
|
|
72
|
+
return json.loads(r.read() or b"null")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def version() -> tuple[str, str]:
|
|
76
|
+
"""(release, endpoints.version_note's sentence). Advice only: a hub that
|
|
77
|
+
will not say is still added to."""
|
|
78
|
+
try:
|
|
79
|
+
v = str((_call("/api/version") or {}).get("version") or "")
|
|
80
|
+
except Exception: # noqa: BLE001
|
|
81
|
+
v = ""
|
|
82
|
+
return v, endpoints.version_note("openwebui", v, CHECKED)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def connections() -> dict[str, tuple[int, str]]:
|
|
86
|
+
"""Connection name -> (Open WebUI connection index, the tag its models carry).
|
|
87
|
+
|
|
88
|
+
The bench names a connection from config/provider-tags.json (`groq`,
|
|
89
|
+
`mistral`), Open WebUI's tags differ (`groq-api`, `mistral-studio`), so
|
|
90
|
+
the two meet at the index the bench's own registered models are served
|
|
91
|
+
from. Open WebUI's tag names are accepted as well."""
|
|
92
|
+
import route
|
|
93
|
+
from run import _registered_names
|
|
94
|
+
data = _call("/api/models").get("data", [])
|
|
95
|
+
by_id = {m["id"]: m for m in data}
|
|
96
|
+
tag_at: dict[int, collections.Counter] = collections.defaultdict(collections.Counter)
|
|
97
|
+
for m in data:
|
|
98
|
+
for t in m.get("tags") or []:
|
|
99
|
+
if m.get("urlIdx") is not None:
|
|
100
|
+
tag_at[m["urlIdx"]][t["name"]] += 1
|
|
101
|
+
at: dict[str, collections.Counter] = collections.defaultdict(collections.Counter)
|
|
102
|
+
for alias, served in _registered_names().items():
|
|
103
|
+
m = by_id.get(served)
|
|
104
|
+
if m and m.get("urlIdx") is not None:
|
|
105
|
+
at[route.connection_of(alias)][m["urlIdx"]] += 1
|
|
106
|
+
out = {}
|
|
107
|
+
for name, c in at.items():
|
|
108
|
+
idx = c.most_common(1)[0][0]
|
|
109
|
+
tags = tag_at.get(idx) # .get: indexing a defaultdict would add an empty one
|
|
110
|
+
out[name] = (idx, tags.most_common(1)[0][0] if tags else name)
|
|
111
|
+
for idx, c in tag_at.items():
|
|
112
|
+
tag = c.most_common(1)[0][0]
|
|
113
|
+
out.setdefault(tag, (idx, tag))
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def plan(connection: str, model_id: str) -> dict:
|
|
118
|
+
"""What adding would change. Raises ValueError when it cannot be done here."""
|
|
119
|
+
release, note = version()
|
|
120
|
+
found = connections().get(connection)
|
|
121
|
+
if found is None:
|
|
122
|
+
raise ValueError(f"no Open WebUI connection is known as {connection!r}: none of the bench's"
|
|
123
|
+
" models is served from it and no Open WebUI model carries that tag")
|
|
124
|
+
idx, tag = found
|
|
125
|
+
cfg = _call("/openai/config")
|
|
126
|
+
if idx >= len(cfg.get("OPENAI_API_BASE_URLS") or []):
|
|
127
|
+
raise ValueError(f"Open WebUI's connections changed while this ran; try again")
|
|
128
|
+
conn = (cfg.get("OPENAI_API_CONFIGS") or {}).get(str(idx), {})
|
|
129
|
+
if conn.get("enable") is False:
|
|
130
|
+
raise ValueError(f"the {connection} connection is switched off in Open WebUI")
|
|
131
|
+
prefix = conn.get("prefix_id") or ""
|
|
132
|
+
served = f"{prefix}.{model_id}" if prefix else model_id
|
|
133
|
+
# One id, two connections: Open WebUI would serve only one of them.
|
|
134
|
+
elsewhere = next((m.get("urlIdx") for m in _call("/api/models").get("data", [])
|
|
135
|
+
if m.get("id") == served and m.get("urlIdx") not in (None, idx)), None)
|
|
136
|
+
if elsewhere is not None:
|
|
137
|
+
raise ValueError(f"Open WebUI already serves {served} from connection {elsewhere}; give the"
|
|
138
|
+
f" {connection} connection a prefix in Open WebUI so the ids differ")
|
|
139
|
+
listed = conn.get("model_ids") or []
|
|
140
|
+
records = {m["id"]: m for m in _call("/api/v1/models/base")}
|
|
141
|
+
sibling = next((m for m in records.values()
|
|
142
|
+
if any(t.get("name") == tag for t in (m.get("meta") or {}).get("tags") or [])
|
|
143
|
+
and (m.get("meta") or {}).get("profile_image_url")), None)
|
|
144
|
+
return {"connection": connection, "tag": tag, "index": idx, "base_url": cfg["OPENAI_API_BASE_URLS"][idx],
|
|
145
|
+
"model_id": model_id, "served": served,
|
|
146
|
+
"add_to_list": bool(listed) and model_id not in listed,
|
|
147
|
+
"record": "update" if served in records else "create",
|
|
148
|
+
"copy_from": sibling["id"] if sibling else None, "version": release, "version_note": note}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def add(connection: str, model_id: str) -> tuple[str, list[str]]:
|
|
152
|
+
"""Do it. (served id, what was done)."""
|
|
153
|
+
p = plan(connection, model_id)
|
|
154
|
+
done = [p["version_note"]] if p["version_note"] else []
|
|
155
|
+
if p["add_to_list"]:
|
|
156
|
+
cfg = _call("/openai/config")
|
|
157
|
+
# The write sends every key back as read. 0.12.3 returns them whole; a
|
|
158
|
+
# version that masks them would have this overwrite real keys.
|
|
159
|
+
if any(isinstance(k, str) and ch in k for k in cfg.get("OPENAI_API_KEYS", []) for ch in "*•"):
|
|
160
|
+
raise ValueError("Open WebUI returned masked API keys; writing the config back would"
|
|
161
|
+
" replace them, so add the model ID to the connection by hand")
|
|
162
|
+
# Decided again on the fresh read: the list may have changed since plan().
|
|
163
|
+
# ponytail: read-modify-write; an edit made in Open WebUI in the same second is lost.
|
|
164
|
+
ids = cfg["OPENAI_API_CONFIGS"][str(p["index"])].setdefault("model_ids", [])
|
|
165
|
+
if ids and model_id not in ids:
|
|
166
|
+
ids.append(model_id)
|
|
167
|
+
_call("/openai/config/update", cfg)
|
|
168
|
+
done.append(f"added {model_id} to the {connection} connection's Model IDs")
|
|
169
|
+
records = {m["id"]: m for m in _call("/api/v1/models/base")}
|
|
170
|
+
old = records.get(p["served"]) or {}
|
|
171
|
+
meta = dict(old.get("meta") or {})
|
|
172
|
+
sib = (records.get(p["copy_from"]) or {}).get("meta") or {}
|
|
173
|
+
for k in COPIED:
|
|
174
|
+
if sib.get(k) and not meta.get(k):
|
|
175
|
+
meta[k] = sib[k]
|
|
176
|
+
tags = [t for t in meta.get("tags") or [] if t.get("name") != p["tag"]]
|
|
177
|
+
meta["tags"] = tags + [{"name": p["tag"]}]
|
|
178
|
+
form = {"id": p["served"], "name": old.get("name") or p["served"], "base_model_id": old.get("base_model_id"),
|
|
179
|
+
"meta": meta, "params": old.get("params") or {}, "access_grants": PUBLIC, "is_active": True}
|
|
180
|
+
try:
|
|
181
|
+
_call("/api/v1/models/model/update" if old else "/api/v1/models/create", form)
|
|
182
|
+
except Exception as exc: # noqa: BLE001
|
|
183
|
+
raise ValueError("; ".join(done + [f"then the model record failed ({exc}); finish it in"
|
|
184
|
+
" Admin > Models"])) from exc
|
|
185
|
+
done.append(f"{'updated' if old else 'created'} its model record: public, tagged {p['tag']}"
|
|
186
|
+
+ (f", logo and switches from {p['copy_from']}" if p["copy_from"] else ", no logo found to copy"))
|
|
187
|
+
# Open WebUI rebuilds its model list on request; wait until it serves the new id.
|
|
188
|
+
for _ in range(10):
|
|
189
|
+
if p["served"] in endpoints.models(endpoint()):
|
|
190
|
+
break
|
|
191
|
+
time.sleep(2)
|
|
192
|
+
else:
|
|
193
|
+
done.append(f"Open WebUI does not list {p['served']} yet; onboarding will say if it never does")
|
|
194
|
+
return p["served"], done
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def main() -> None:
|
|
198
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
199
|
+
if len(sys.argv) != 3:
|
|
200
|
+
sys.exit(__doc__)
|
|
201
|
+
print(json.dumps(plan(sys.argv[1], sys.argv[2]), indent=2))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
main()
|
package/metrics/paths.py
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
"""Where this install keeps its data: one answer for every module.
|
|
2
|
-
|
|
3
|
-
python metrics/paths.py # the data folder in use, and why
|
|
4
|
-
|
|
5
|
-
RELEASE-PLAN.md Phase 1, code and data apart. Everything that is an install's
|
|
6
|
-
own state (config/, metrics/*.json*, perf/history.csv, runs/, suite/tasks/ with
|
|
7
|
-
the saved answers) is read and written under DATA, never beside the code, so
|
|
8
|
-
the same code can serve the public repository and the owner's instance.
|
|
9
|
-
|
|
10
|
-
DATA is, in order:
|
|
11
|
-
1. BENCHWARMERS_HOME, when set;
|
|
12
|
-
2. the folder named in `.benchwarmers-home` at the top of the checkout (one
|
|
13
|
-
line; gitignored). The owner's instance points at C:\\benchwarmers-data
|
|
14
|
-
this way, which every process started from this checkout sees (the dashboard,
|
|
15
|
-
the scheduled tasks, the suite), without an environment variable;
|
|
16
|
-
3. the checkout itself, the layout before Phase 1.
|
|
17
|
-
ponytail: (3) keeps a fresh checkout working until `benchwarmers init`
|
|
18
|
-
exists (Phase 4); then the default becomes the OS's per-user data folder.
|
|
19
|
-
|
|
20
|
-
Inside DATA the layout is the checkout's own (`config/endpoints.json`,
|
|
21
|
-
`metrics/offloads.jsonl`, `suite/tasks/<id>/output/`), so moving an install's
|
|
22
|
-
data is a copy, not a rewrite.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
from __future__ import annotations
|
|
26
|
-
|
|
27
|
-
import os
|
|
28
|
-
import pathlib
|
|
29
|
-
|
|
30
|
-
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
31
|
-
POINTER = ROOT / ".benchwarmers-home"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def home() -> tuple[pathlib.Path, str]:
|
|
35
|
-
"""(the data folder, how it was chosen)."""
|
|
36
|
-
if env := os.environ.get("BENCHWARMERS_HOME", "").strip():
|
|
37
|
-
return pathlib.Path(env).expanduser(), "BENCHWARMERS_HOME"
|
|
38
|
-
try:
|
|
39
|
-
named = POINTER.read_text(encoding="utf-8").strip()
|
|
40
|
-
except OSError:
|
|
41
|
-
named = ""
|
|
42
|
-
if named:
|
|
43
|
-
return pathlib.Path(named).expanduser(), f"{POINTER.name}"
|
|
44
|
-
return ROOT, "the checkout (no BENCHWARMERS_HOME or .benchwarmers-home)"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
DATA, WHY = home()
|
|
48
|
-
LAYOUT = ("config", "metrics", "perf", "runs", "suite/tasks")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def make(folder: pathlib.Path = DATA) -> None:
|
|
52
|
-
"""The layout, empty. Writers that append (the change log, onboarding's log
|
|
53
|
-
and notes, the perf history) expect their folder to exist, which on a fresh
|
|
54
|
-
install it does not; serve.py and scripts/init.py make it once instead."""
|
|
55
|
-
for sub in LAYOUT:
|
|
56
|
-
(folder / sub).mkdir(parents=True, exist_ok=True)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if __name__ == "__main__":
|
|
60
|
-
print(f"{DATA} ({WHY})")
|
|
1
|
+
"""Where this install keeps its data: one answer for every module.
|
|
2
|
+
|
|
3
|
+
python metrics/paths.py # the data folder in use, and why
|
|
4
|
+
|
|
5
|
+
RELEASE-PLAN.md Phase 1, code and data apart. Everything that is an install's
|
|
6
|
+
own state (config/, metrics/*.json*, perf/history.csv, runs/, suite/tasks/ with
|
|
7
|
+
the saved answers) is read and written under DATA, never beside the code, so
|
|
8
|
+
the same code can serve the public repository and the owner's instance.
|
|
9
|
+
|
|
10
|
+
DATA is, in order:
|
|
11
|
+
1. BENCHWARMERS_HOME, when set;
|
|
12
|
+
2. the folder named in `.benchwarmers-home` at the top of the checkout (one
|
|
13
|
+
line; gitignored). The owner's instance points at C:\\benchwarmers-data
|
|
14
|
+
this way, which every process started from this checkout sees (the dashboard,
|
|
15
|
+
the scheduled tasks, the suite), without an environment variable;
|
|
16
|
+
3. the checkout itself, the layout before Phase 1.
|
|
17
|
+
ponytail: (3) keeps a fresh checkout working until `benchwarmers init`
|
|
18
|
+
exists (Phase 4); then the default becomes the OS's per-user data folder.
|
|
19
|
+
|
|
20
|
+
Inside DATA the layout is the checkout's own (`config/endpoints.json`,
|
|
21
|
+
`metrics/offloads.jsonl`, `suite/tasks/<id>/output/`), so moving an install's
|
|
22
|
+
data is a copy, not a rewrite.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import pathlib
|
|
29
|
+
|
|
30
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
31
|
+
POINTER = ROOT / ".benchwarmers-home"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def home() -> tuple[pathlib.Path, str]:
|
|
35
|
+
"""(the data folder, how it was chosen)."""
|
|
36
|
+
if env := os.environ.get("BENCHWARMERS_HOME", "").strip():
|
|
37
|
+
return pathlib.Path(env).expanduser(), "BENCHWARMERS_HOME"
|
|
38
|
+
try:
|
|
39
|
+
named = POINTER.read_text(encoding="utf-8").strip()
|
|
40
|
+
except OSError:
|
|
41
|
+
named = ""
|
|
42
|
+
if named:
|
|
43
|
+
return pathlib.Path(named).expanduser(), f"{POINTER.name}"
|
|
44
|
+
return ROOT, "the checkout (no BENCHWARMERS_HOME or .benchwarmers-home)"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
DATA, WHY = home()
|
|
48
|
+
LAYOUT = ("config", "metrics", "perf", "runs", "suite/tasks")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def make(folder: pathlib.Path = DATA) -> None:
|
|
52
|
+
"""The layout, empty. Writers that append (the change log, onboarding's log
|
|
53
|
+
and notes, the perf history) expect their folder to exist, which on a fresh
|
|
54
|
+
install it does not; serve.py and scripts/init.py make it once instead."""
|
|
55
|
+
for sub in LAYOUT:
|
|
56
|
+
(folder / sub).mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
print(f"{DATA} ({WHY})")
|