@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.
Files changed (96) hide show
  1. package/AGENTS.md +147 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +272 -272
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +368 -314
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +170 -163
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -74
  12. package/cli.py +666 -0
  13. package/examples/config/gateway.json +5 -5
  14. package/examples/config/model-roles.json +11 -11
  15. package/gateway.py +755 -755
  16. package/launcher.py +120 -0
  17. package/mcp_server.py +266 -266
  18. package/metrics/alerts.py +116 -116
  19. package/metrics/aliases.py +41 -41
  20. package/metrics/bench_collections.py +141 -141
  21. package/metrics/catalog.py +295 -295
  22. package/metrics/client.py +394 -394
  23. package/metrics/cliproxy.py +191 -191
  24. package/metrics/connect.py +691 -691
  25. package/metrics/cost.py +152 -152
  26. package/metrics/decide.py +227 -227
  27. package/metrics/discover.py +318 -318
  28. package/metrics/endpoints.py +436 -436
  29. package/metrics/guard.py +617 -592
  30. package/metrics/learn.py +221 -221
  31. package/metrics/litellm_hub.py +206 -206
  32. package/metrics/model_roles.py +84 -84
  33. package/metrics/offload.py +166 -166
  34. package/metrics/owui.py +205 -205
  35. package/metrics/paths.py +60 -60
  36. package/metrics/route.py +579 -579
  37. package/metrics/scheduler.py +275 -275
  38. package/metrics/sources.py +167 -167
  39. package/metrics/wrong.py +144 -144
  40. package/package.json +68 -65
  41. package/perf/README.md +89 -89
  42. package/perf/run.py +261 -261
  43. package/scripts/auto_retire.py +367 -367
  44. package/scripts/check.py +108 -106
  45. package/scripts/check_deprecations.py +378 -378
  46. package/scripts/check_themes.py +103 -103
  47. package/scripts/init.py +124 -124
  48. package/scripts/list_served.py +78 -78
  49. package/scripts/notice.py +78 -78
  50. package/scripts/onboard.py +401 -331
  51. package/scripts/provider_tags.py +129 -129
  52. package/scripts/reprefix.py +136 -136
  53. package/scripts/sync_config.py +105 -105
  54. package/scripts/test_auth.py +255 -255
  55. package/scripts/test_cli.py +189 -0
  56. package/scripts/test_client.py +233 -233
  57. package/scripts/test_connect.py +222 -222
  58. package/scripts/test_first_run.py +180 -180
  59. package/scripts/test_hubs.py +328 -328
  60. package/scripts/test_launcher.py +67 -0
  61. package/scripts/test_llm_log.py +85 -81
  62. package/scripts/test_mcp.py +178 -178
  63. package/scripts/test_merged.py +214 -214
  64. package/scripts/test_no_owui.py +291 -291
  65. package/scripts/test_owui.py +90 -90
  66. package/scripts/test_serve_edit.py +448 -438
  67. package/scripts/test_verdict.py +98 -98
  68. package/scripts/test_vision.py +228 -228
  69. package/serve.py +3904 -3764
  70. package/suite/mine.py +154 -154
  71. package/suite/pace.py +145 -145
  72. package/suite/run.py +1046 -1042
  73. package/suite/seed.py +421 -421
  74. package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
  75. package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
  76. package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
  77. package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
  78. package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
  79. package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
  80. package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
  81. package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
  82. package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
  83. package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
  84. package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
  85. package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
  86. package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
  87. package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
  88. package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
  89. package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
  90. package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
  91. package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
  92. package/ui/bench.css +430 -425
  93. package/ui/bench.js +1756 -1576
  94. package/ui/themes/dark.css +35 -35
  95. package/ui/themes/hardwood.css +52 -52
  96. 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})")