@wendellresearch-ai/benchwarmers 0.1.1 → 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 -271
- package/NOTICE +28 -28
- package/PRIVACY.md +46 -46
- package/README.md +368 -275
- package/SECURITY.md +83 -83
- package/TERMS.md +22 -22
- package/USAGE.md +170 -155
- package/VERSION +1 -1
- package/bin/benchwarmers.js +16 -72
- package/cli.py +666 -0
- package/examples/config/gateway.json +5 -5
- package/examples/config/model-roles.json +11 -11
- package/gateway.py +755 -712
- package/launcher.py +120 -0
- package/mcp_server.py +266 -0
- 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 -373
- 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 -574
- package/metrics/scheduler.py +275 -275
- package/metrics/sources.py +167 -167
- package/metrics/wrong.py +144 -144
- package/package.json +68 -64
- package/perf/README.md +89 -89
- package/perf/run.py +261 -261
- package/scripts/auto_retire.py +367 -367
- package/scripts/check.py +108 -104
- 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 -0
- package/scripts/test_mcp.py +178 -0
- package/scripts/test_merged.py +214 -193
- package/scripts/test_no_owui.py +291 -291
- package/scripts/test_owui.py +90 -90
- package/scripts/test_serve_edit.py +448 -386
- package/scripts/test_verdict.py +98 -98
- package/scripts/test_vision.py +228 -228
- package/serve.py +3904 -3662
- package/suite/mine.py +154 -154
- package/suite/pace.py +145 -145
- package/suite/run.py +1046 -1040
- 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 -424
- package/ui/bench.js +1756 -1555
- package/ui/themes/dark.css +35 -35
- package/ui/themes/hardwood.css +52 -52
- package/ui/themes/rainforest.css +32 -32
package/scripts/provider_tags.py
CHANGED
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
"""Record which connection each model arrives through.
|
|
2
|
-
|
|
3
|
-
python scripts/provider_tags.py
|
|
4
|
-
|
|
5
|
-
Open WebUI fronts several upstreams, and which one a model comes through
|
|
6
|
-
decides things the model itself does not: whether it shares a rate limit with
|
|
7
|
-
another model, whether it is a free tier, and who to ask when it disappears.
|
|
8
|
-
`kimi-k3` and `gemini-3.8-flash-high` are different vendors but the same
|
|
9
|
-
connection, so they cap together.
|
|
10
|
-
|
|
11
|
-
Writes `config/provider-tags.json` — a plain map of model id to a short tag —
|
|
12
|
-
which the dashboard reads. A file rather than a live call so the dashboard
|
|
13
|
-
stays a view of the repository and works with the network down.
|
|
14
|
-
|
|
15
|
-
**THE KEYS ARE NEVER READ.** `/openai/config` returns `OPENAI_API_KEYS`
|
|
16
|
-
alongside the base URLs, in plaintext. This takes `OPENAI_API_BASE_URLS` and
|
|
17
|
-
the host name off each URL, and nothing else, so no secret can reach a file
|
|
18
|
-
that is committed. Worth knowing that endpoint behaves that way.
|
|
19
|
-
|
|
20
|
-
A REFINEMENT, NOT A PREREQUISITE. Only an Open WebUI hub (endpoints.find_hub)
|
|
21
|
-
has connections to name; without one this writes nothing and exits cleanly,
|
|
22
|
-
and every model's connection is the endpoint it is registered on
|
|
23
|
-
(route.connection_of), which needs no file at all.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
import json
|
|
29
|
-
import pathlib
|
|
30
|
-
import sys
|
|
31
|
-
import urllib.request
|
|
32
|
-
|
|
33
|
-
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
34
|
-
sys.path.insert(0, str(REPO / "metrics"))
|
|
35
|
-
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
36
|
-
OUT = DATA / "config" / "provider-tags.json"
|
|
37
|
-
import endpoints # noqa: E402
|
|
38
|
-
TIMEOUT = 30
|
|
39
|
-
|
|
40
|
-
# Host -> the name a person would use for it. Anything unlisted falls back to
|
|
41
|
-
# its own hostname, which is still more use than an index.
|
|
42
|
-
# Connections on a local network are named by hand, because the API will not
|
|
43
|
-
# say: `hosts` in config/connections.json. The suffix that follows (e.g.
|
|
44
|
-
# `/vendor-a`) comes from the MODEL's own `owned_by` and is worth keeping: one
|
|
45
|
-
# proxy fronts several upstreams, so `my-proxy/vendor-a` and `my-proxy/vendor-b`
|
|
46
|
-
# share a host and a rate limit while being entirely different vendors.
|
|
47
|
-
|
|
48
|
-
NAMES = {
|
|
49
|
-
"ollama.com": "ollama-cloud",
|
|
50
|
-
"openrouter.ai": "openrouter",
|
|
51
|
-
"api.groq.com": "groq",
|
|
52
|
-
"api.cerebras.ai": "cerebras",
|
|
53
|
-
"integrate.api.nvidia.com": "nvidia-api",
|
|
54
|
-
"api.mistral.ai": "mistral",
|
|
55
|
-
"api.llm7.io": "llm7-api",
|
|
56
|
-
"api.openai.com": "openai",
|
|
57
|
-
"api.anthropic.com": "anthropic",
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def get(hub: str, path: str) -> dict:
|
|
62
|
-
"""GET off the Open WebUI server's root, where `/openai/config` and
|
|
63
|
-
`/api/models` both hang, with the hub's own key."""
|
|
64
|
-
request = urllib.request.Request(
|
|
65
|
-
endpoints.base(hub).removesuffix("/api") + path,
|
|
66
|
-
headers={"Authorization": f"Bearer {endpoints.key(hub)}"})
|
|
67
|
-
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
|
|
68
|
-
return json.load(response)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def connection_names(hub: str) -> list[str]:
|
|
72
|
-
"""One short name per connection index. URLs only — never the keys."""
|
|
73
|
-
urls = get(hub, "/openai/config").get("OPENAI_API_BASE_URLS", [])
|
|
74
|
-
names = []
|
|
75
|
-
for url in urls:
|
|
76
|
-
host = url.split("//", 1)[-1].split("/", 1)[0].split(":", 1)[0]
|
|
77
|
-
if host.replace(".", "").isdigit():
|
|
78
|
-
# A BARE IP NAMES NOTHING, and guessing from it was worse than
|
|
79
|
-
# useless: a proxy labelled `lan-proxy` is described by where it
|
|
80
|
-
# sits rather than what it is, and Open WebUI does not expose
|
|
81
|
-
# connection names over the API to find that out —
|
|
82
|
-
# `OPENAI_API_CONFIGS` has a `tags` field and it is empty. So it
|
|
83
|
-
# is written down in config/connections.json `hosts`.
|
|
84
|
-
names.append((endpoints.connections().get("hosts") or {}).get(host, "lan-proxy"))
|
|
85
|
-
else:
|
|
86
|
-
names.append(NAMES.get(host, host))
|
|
87
|
-
return names
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def main() -> None:
|
|
91
|
-
hub = endpoints.find_hub("openwebui")
|
|
92
|
-
if not hub:
|
|
93
|
-
print("no Open WebUI hub: nothing to tag; each model's connection is its endpoint")
|
|
94
|
-
return
|
|
95
|
-
names = connection_names(hub)
|
|
96
|
-
tags = {}
|
|
97
|
-
for entry in get(hub, "/api/models").get("data", []):
|
|
98
|
-
model_id = entry.get("id")
|
|
99
|
-
if not model_id:
|
|
100
|
-
continue
|
|
101
|
-
if entry.get("connection_type") == "local":
|
|
102
|
-
tags[model_id] = "local"
|
|
103
|
-
continue
|
|
104
|
-
index = entry.get("urlIdx")
|
|
105
|
-
tag = names[index] if isinstance(index, int) and index < len(names) else "?"
|
|
106
|
-
# The upstream owner, when it says something the connection does not.
|
|
107
|
-
owner = (entry.get("openai") or {}).get("owned_by")
|
|
108
|
-
if owner and owner not in ("openai", tag) and tag != "?":
|
|
109
|
-
tag = f"{tag}/{owner}"
|
|
110
|
-
tags[model_id] = tag
|
|
111
|
-
|
|
112
|
-
# Models registered on another endpoint arrive through THAT endpoint,
|
|
113
|
-
# whatever Open WebUI would have called them.
|
|
114
|
-
for r in endpoints.registry():
|
|
115
|
-
if r["endpoint"] not in ("?", hub):
|
|
116
|
-
tags[r["served"]] = r["endpoint"]
|
|
117
|
-
|
|
118
|
-
OUT.write_text(json.dumps(tags, indent=2, sort_keys=True) + "\n",
|
|
119
|
-
encoding="utf-8")
|
|
120
|
-
print(f"wrote {OUT} ({len(tags)} models)")
|
|
121
|
-
seen: dict[str, int] = {}
|
|
122
|
-
for tag in tags.values():
|
|
123
|
-
seen[tag] = seen.get(tag, 0) + 1
|
|
124
|
-
for tag, count in sorted(seen.items(), key=lambda kv: -kv[1]):
|
|
125
|
-
print(f" {tag:24} {count}")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if __name__ == "__main__":
|
|
129
|
-
main()
|
|
1
|
+
"""Record which connection each model arrives through.
|
|
2
|
+
|
|
3
|
+
python scripts/provider_tags.py
|
|
4
|
+
|
|
5
|
+
Open WebUI fronts several upstreams, and which one a model comes through
|
|
6
|
+
decides things the model itself does not: whether it shares a rate limit with
|
|
7
|
+
another model, whether it is a free tier, and who to ask when it disappears.
|
|
8
|
+
`kimi-k3` and `gemini-3.8-flash-high` are different vendors but the same
|
|
9
|
+
connection, so they cap together.
|
|
10
|
+
|
|
11
|
+
Writes `config/provider-tags.json` — a plain map of model id to a short tag —
|
|
12
|
+
which the dashboard reads. A file rather than a live call so the dashboard
|
|
13
|
+
stays a view of the repository and works with the network down.
|
|
14
|
+
|
|
15
|
+
**THE KEYS ARE NEVER READ.** `/openai/config` returns `OPENAI_API_KEYS`
|
|
16
|
+
alongside the base URLs, in plaintext. This takes `OPENAI_API_BASE_URLS` and
|
|
17
|
+
the host name off each URL, and nothing else, so no secret can reach a file
|
|
18
|
+
that is committed. Worth knowing that endpoint behaves that way.
|
|
19
|
+
|
|
20
|
+
A REFINEMENT, NOT A PREREQUISITE. Only an Open WebUI hub (endpoints.find_hub)
|
|
21
|
+
has connections to name; without one this writes nothing and exits cleanly,
|
|
22
|
+
and every model's connection is the endpoint it is registered on
|
|
23
|
+
(route.connection_of), which needs no file at all.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import pathlib
|
|
30
|
+
import sys
|
|
31
|
+
import urllib.request
|
|
32
|
+
|
|
33
|
+
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
34
|
+
sys.path.insert(0, str(REPO / "metrics"))
|
|
35
|
+
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
36
|
+
OUT = DATA / "config" / "provider-tags.json"
|
|
37
|
+
import endpoints # noqa: E402
|
|
38
|
+
TIMEOUT = 30
|
|
39
|
+
|
|
40
|
+
# Host -> the name a person would use for it. Anything unlisted falls back to
|
|
41
|
+
# its own hostname, which is still more use than an index.
|
|
42
|
+
# Connections on a local network are named by hand, because the API will not
|
|
43
|
+
# say: `hosts` in config/connections.json. The suffix that follows (e.g.
|
|
44
|
+
# `/vendor-a`) comes from the MODEL's own `owned_by` and is worth keeping: one
|
|
45
|
+
# proxy fronts several upstreams, so `my-proxy/vendor-a` and `my-proxy/vendor-b`
|
|
46
|
+
# share a host and a rate limit while being entirely different vendors.
|
|
47
|
+
|
|
48
|
+
NAMES = {
|
|
49
|
+
"ollama.com": "ollama-cloud",
|
|
50
|
+
"openrouter.ai": "openrouter",
|
|
51
|
+
"api.groq.com": "groq",
|
|
52
|
+
"api.cerebras.ai": "cerebras",
|
|
53
|
+
"integrate.api.nvidia.com": "nvidia-api",
|
|
54
|
+
"api.mistral.ai": "mistral",
|
|
55
|
+
"api.llm7.io": "llm7-api",
|
|
56
|
+
"api.openai.com": "openai",
|
|
57
|
+
"api.anthropic.com": "anthropic",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get(hub: str, path: str) -> dict:
|
|
62
|
+
"""GET off the Open WebUI server's root, where `/openai/config` and
|
|
63
|
+
`/api/models` both hang, with the hub's own key."""
|
|
64
|
+
request = urllib.request.Request(
|
|
65
|
+
endpoints.base(hub).removesuffix("/api") + path,
|
|
66
|
+
headers={"Authorization": f"Bearer {endpoints.key(hub)}"})
|
|
67
|
+
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
|
|
68
|
+
return json.load(response)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def connection_names(hub: str) -> list[str]:
|
|
72
|
+
"""One short name per connection index. URLs only — never the keys."""
|
|
73
|
+
urls = get(hub, "/openai/config").get("OPENAI_API_BASE_URLS", [])
|
|
74
|
+
names = []
|
|
75
|
+
for url in urls:
|
|
76
|
+
host = url.split("//", 1)[-1].split("/", 1)[0].split(":", 1)[0]
|
|
77
|
+
if host.replace(".", "").isdigit():
|
|
78
|
+
# A BARE IP NAMES NOTHING, and guessing from it was worse than
|
|
79
|
+
# useless: a proxy labelled `lan-proxy` is described by where it
|
|
80
|
+
# sits rather than what it is, and Open WebUI does not expose
|
|
81
|
+
# connection names over the API to find that out —
|
|
82
|
+
# `OPENAI_API_CONFIGS` has a `tags` field and it is empty. So it
|
|
83
|
+
# is written down in config/connections.json `hosts`.
|
|
84
|
+
names.append((endpoints.connections().get("hosts") or {}).get(host, "lan-proxy"))
|
|
85
|
+
else:
|
|
86
|
+
names.append(NAMES.get(host, host))
|
|
87
|
+
return names
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> None:
|
|
91
|
+
hub = endpoints.find_hub("openwebui")
|
|
92
|
+
if not hub:
|
|
93
|
+
print("no Open WebUI hub: nothing to tag; each model's connection is its endpoint")
|
|
94
|
+
return
|
|
95
|
+
names = connection_names(hub)
|
|
96
|
+
tags = {}
|
|
97
|
+
for entry in get(hub, "/api/models").get("data", []):
|
|
98
|
+
model_id = entry.get("id")
|
|
99
|
+
if not model_id:
|
|
100
|
+
continue
|
|
101
|
+
if entry.get("connection_type") == "local":
|
|
102
|
+
tags[model_id] = "local"
|
|
103
|
+
continue
|
|
104
|
+
index = entry.get("urlIdx")
|
|
105
|
+
tag = names[index] if isinstance(index, int) and index < len(names) else "?"
|
|
106
|
+
# The upstream owner, when it says something the connection does not.
|
|
107
|
+
owner = (entry.get("openai") or {}).get("owned_by")
|
|
108
|
+
if owner and owner not in ("openai", tag) and tag != "?":
|
|
109
|
+
tag = f"{tag}/{owner}"
|
|
110
|
+
tags[model_id] = tag
|
|
111
|
+
|
|
112
|
+
# Models registered on another endpoint arrive through THAT endpoint,
|
|
113
|
+
# whatever Open WebUI would have called them.
|
|
114
|
+
for r in endpoints.registry():
|
|
115
|
+
if r["endpoint"] not in ("?", hub):
|
|
116
|
+
tags[r["served"]] = r["endpoint"]
|
|
117
|
+
|
|
118
|
+
OUT.write_text(json.dumps(tags, indent=2, sort_keys=True) + "\n",
|
|
119
|
+
encoding="utf-8")
|
|
120
|
+
print(f"wrote {OUT} ({len(tags)} models)")
|
|
121
|
+
seen: dict[str, int] = {}
|
|
122
|
+
for tag in tags.values():
|
|
123
|
+
seen[tag] = seen.get(tag, 0) + 1
|
|
124
|
+
for tag, count in sorted(seen.items(), key=lambda kv: -kv[1]):
|
|
125
|
+
print(f" {tag:24} {count}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
package/scripts/reprefix.py
CHANGED
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
"""Repoint a connection's models after Open WebUI gains or changes a prefix.
|
|
2
|
-
|
|
3
|
-
python scripts/reprefix.py llm7-api --prefix "llm7."
|
|
4
|
-
python scripts/reprefix.py llm7-api --prefix "llm7." --apply
|
|
5
|
-
|
|
6
|
-
Open WebUI can prefix a connection's model ids, which is the fix for two
|
|
7
|
-
connections serving the same name — `codestral-latest` exists on both mistral
|
|
8
|
-
and llm7, and Open WebUI showed only one of them until llm7 got a prefix.
|
|
9
|
-
|
|
10
|
-
**THE RENAME IS CHEAP BECAUSE NOTHING HERE IS KEYED BY THE SERVED NAME.**
|
|
11
|
-
Every score, every perf row, every offload and every floor result is keyed by
|
|
12
|
-
the ALIAS. So repointing a connection is one line per model in the yaml, and
|
|
13
|
-
all history survives untouched — which is the answer to "will I have to redo
|
|
14
|
-
everything connected to the models". No.
|
|
15
|
-
|
|
16
|
-
Two things do need care, and this handles both:
|
|
17
|
-
|
|
18
|
-
1. **The availability snapshot.** It records served names, so after a prefix
|
|
19
|
-
the old ones simply vanish and the daily check reports every model on the
|
|
20
|
-
connection as retired. The snapshot is migrated rather than left to raise a
|
|
21
|
-
page of false alarms.
|
|
22
|
-
2. **Aliases stay put.** It is tempting to rename `minimax-m2.7` to
|
|
23
|
-
`llm7-minimax-m2.7` for tidiness. Do not: that orphans its perf history and
|
|
24
|
-
its suite outputs, and the router would treat it as a model it has never
|
|
25
|
-
seen. The alias is the identity; the served name is just an address.
|
|
26
|
-
|
|
27
|
-
Dry-run by default. `--apply` writes. Prefixes are an Open WebUI feature, so
|
|
28
|
-
without an Open WebUI hub (endpoints.find_hub) this refuses before reading
|
|
29
|
-
anything.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
from __future__ import annotations
|
|
33
|
-
|
|
34
|
-
import argparse
|
|
35
|
-
import json
|
|
36
|
-
import pathlib
|
|
37
|
-
import re
|
|
38
|
-
import sys
|
|
39
|
-
|
|
40
|
-
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
41
|
-
sys.path.insert(0, str(ROOT / "metrics"))
|
|
42
|
-
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
43
|
-
YAML = DATA / "config" / "extra-openai-models.yaml"
|
|
44
|
-
SNAPSHOT = DATA / "config" / "served-snapshot.json"
|
|
45
|
-
TAGS = DATA / "config" / "provider-tags.json"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def registered() -> list[tuple[str, str]]:
|
|
49
|
-
"""(alias, served_name) in file order."""
|
|
50
|
-
out, current = [], None
|
|
51
|
-
for line in YAML.read_text(encoding="utf-8").splitlines():
|
|
52
|
-
if (m := re.match(r"\s*-?\s*model_id:\s*(.+)", line)):
|
|
53
|
-
current = m.group(1).strip().strip("\"'")
|
|
54
|
-
elif (m := re.match(r"\s*model_name:\s*(.+)", line)):
|
|
55
|
-
out.append((current, m.group(1).strip().strip("\"'")))
|
|
56
|
-
return out
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def main() -> None:
|
|
60
|
-
ap = argparse.ArgumentParser(description=__doc__)
|
|
61
|
-
ap.add_argument("connection", help="tag as shown by provider_tags.py")
|
|
62
|
-
ap.add_argument("--prefix", required=True,
|
|
63
|
-
help='what Open WebUI now prepends, e.g. "llm7."')
|
|
64
|
-
ap.add_argument("--apply", action="store_true", help="write the changes")
|
|
65
|
-
args = ap.parse_args()
|
|
66
|
-
import endpoints
|
|
67
|
-
hub = endpoints.find_hub("openwebui")
|
|
68
|
-
if not hub:
|
|
69
|
-
sys.exit("no endpoint is an Open WebUI hub, and only Open WebUI prefixes connections:"
|
|
70
|
-
" nothing to repoint")
|
|
71
|
-
|
|
72
|
-
tags = json.loads(TAGS.read_text(encoding="utf-8"))
|
|
73
|
-
served_now = set(tags)
|
|
74
|
-
|
|
75
|
-
changes = []
|
|
76
|
-
for alias, served in registered():
|
|
77
|
-
if tags.get(served, "").split("/")[0] == args.connection:
|
|
78
|
-
continue # already correct
|
|
79
|
-
candidate = args.prefix + served
|
|
80
|
-
# Only repoint when the PREFIXED name is what the endpoint now serves
|
|
81
|
-
# and the bare one is not. Anything else is a different problem and
|
|
82
|
-
# guessing at it would break a working registration.
|
|
83
|
-
if candidate in served_now and served not in served_now:
|
|
84
|
-
changes.append((alias, served, candidate))
|
|
85
|
-
|
|
86
|
-
if not changes:
|
|
87
|
-
print(f"Nothing to repoint for {args.connection}.")
|
|
88
|
-
print("Either the prefix is already applied, or the endpoint does not "
|
|
89
|
-
"serve the prefixed names.")
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
print(f"{len(changes)} registration(s) to repoint "
|
|
93
|
-
f"({'applying' if args.apply else 'dry run'}):\n")
|
|
94
|
-
for alias, old, new in changes:
|
|
95
|
-
print(f" {alias:<32} {old} -> {new}")
|
|
96
|
-
print("\nAliases are NOT changed, so every score, perf row, offload and "
|
|
97
|
-
"floor result\nkeeps pointing at the same model. That is why this "
|
|
98
|
-
"is cheap.")
|
|
99
|
-
|
|
100
|
-
if not args.apply:
|
|
101
|
-
print("\nRe-run with --apply to write.")
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
text = YAML.read_text(encoding="utf-8")
|
|
105
|
-
for _alias, old, new in changes:
|
|
106
|
-
quoted_new = f'"{new}"' if any(c in new for c in ":/") else new
|
|
107
|
-
text = re.sub(rf"^( model_name: ){re.escape(old)}\"?$",
|
|
108
|
-
rf"\g<1>{quoted_new}", text, count=1, flags=re.M)
|
|
109
|
-
text = re.sub(rf'^( model_name: )"{re.escape(old)}"$',
|
|
110
|
-
rf"\g<1>{quoted_new}", text, count=1, flags=re.M)
|
|
111
|
-
YAML.write_text(text, encoding="utf-8")
|
|
112
|
-
|
|
113
|
-
# The snapshot records served NAMES, so without this the daily check
|
|
114
|
-
# reports the whole connection as retired tomorrow morning.
|
|
115
|
-
if SNAPSHOT.exists():
|
|
116
|
-
snap = json.loads(SNAPSHOT.read_text(encoding="utf-8"))
|
|
117
|
-
moved = 0
|
|
118
|
-
for _alias, old, new in changes:
|
|
119
|
-
# Keyed as the snapshot keys the hub's models: bare when it is the default.
|
|
120
|
-
old, new = endpoints.snapshot_key(old, hub), endpoints.snapshot_key(new, hub)
|
|
121
|
-
if old in snap.get("last_seen", {}):
|
|
122
|
-
snap["last_seen"][new] = snap["last_seen"].pop(old)
|
|
123
|
-
moved += 1
|
|
124
|
-
snap.get("first_missing", {}).pop(old, None)
|
|
125
|
-
SNAPSHOT.write_text(json.dumps(snap, indent=2, sort_keys=True) + "\n",
|
|
126
|
-
encoding="utf-8")
|
|
127
|
-
print(f"\nmigrated {moved} snapshot entr"
|
|
128
|
-
f"{'ies' if moved != 1 else 'y'} so the daily check does not "
|
|
129
|
-
f"report them retired")
|
|
130
|
-
|
|
131
|
-
print("\nNow run: python scripts/sync_config.py && "
|
|
132
|
-
"python scripts/provider_tags.py")
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if __name__ == "__main__":
|
|
136
|
-
main()
|
|
1
|
+
"""Repoint a connection's models after Open WebUI gains or changes a prefix.
|
|
2
|
+
|
|
3
|
+
python scripts/reprefix.py llm7-api --prefix "llm7."
|
|
4
|
+
python scripts/reprefix.py llm7-api --prefix "llm7." --apply
|
|
5
|
+
|
|
6
|
+
Open WebUI can prefix a connection's model ids, which is the fix for two
|
|
7
|
+
connections serving the same name — `codestral-latest` exists on both mistral
|
|
8
|
+
and llm7, and Open WebUI showed only one of them until llm7 got a prefix.
|
|
9
|
+
|
|
10
|
+
**THE RENAME IS CHEAP BECAUSE NOTHING HERE IS KEYED BY THE SERVED NAME.**
|
|
11
|
+
Every score, every perf row, every offload and every floor result is keyed by
|
|
12
|
+
the ALIAS. So repointing a connection is one line per model in the yaml, and
|
|
13
|
+
all history survives untouched — which is the answer to "will I have to redo
|
|
14
|
+
everything connected to the models". No.
|
|
15
|
+
|
|
16
|
+
Two things do need care, and this handles both:
|
|
17
|
+
|
|
18
|
+
1. **The availability snapshot.** It records served names, so after a prefix
|
|
19
|
+
the old ones simply vanish and the daily check reports every model on the
|
|
20
|
+
connection as retired. The snapshot is migrated rather than left to raise a
|
|
21
|
+
page of false alarms.
|
|
22
|
+
2. **Aliases stay put.** It is tempting to rename `minimax-m2.7` to
|
|
23
|
+
`llm7-minimax-m2.7` for tidiness. Do not: that orphans its perf history and
|
|
24
|
+
its suite outputs, and the router would treat it as a model it has never
|
|
25
|
+
seen. The alias is the identity; the served name is just an address.
|
|
26
|
+
|
|
27
|
+
Dry-run by default. `--apply` writes. Prefixes are an Open WebUI feature, so
|
|
28
|
+
without an Open WebUI hub (endpoints.find_hub) this refuses before reading
|
|
29
|
+
anything.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import json
|
|
36
|
+
import pathlib
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
41
|
+
sys.path.insert(0, str(ROOT / "metrics"))
|
|
42
|
+
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
43
|
+
YAML = DATA / "config" / "extra-openai-models.yaml"
|
|
44
|
+
SNAPSHOT = DATA / "config" / "served-snapshot.json"
|
|
45
|
+
TAGS = DATA / "config" / "provider-tags.json"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def registered() -> list[tuple[str, str]]:
|
|
49
|
+
"""(alias, served_name) in file order."""
|
|
50
|
+
out, current = [], None
|
|
51
|
+
for line in YAML.read_text(encoding="utf-8").splitlines():
|
|
52
|
+
if (m := re.match(r"\s*-?\s*model_id:\s*(.+)", line)):
|
|
53
|
+
current = m.group(1).strip().strip("\"'")
|
|
54
|
+
elif (m := re.match(r"\s*model_name:\s*(.+)", line)):
|
|
55
|
+
out.append((current, m.group(1).strip().strip("\"'")))
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> None:
|
|
60
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
61
|
+
ap.add_argument("connection", help="tag as shown by provider_tags.py")
|
|
62
|
+
ap.add_argument("--prefix", required=True,
|
|
63
|
+
help='what Open WebUI now prepends, e.g. "llm7."')
|
|
64
|
+
ap.add_argument("--apply", action="store_true", help="write the changes")
|
|
65
|
+
args = ap.parse_args()
|
|
66
|
+
import endpoints
|
|
67
|
+
hub = endpoints.find_hub("openwebui")
|
|
68
|
+
if not hub:
|
|
69
|
+
sys.exit("no endpoint is an Open WebUI hub, and only Open WebUI prefixes connections:"
|
|
70
|
+
" nothing to repoint")
|
|
71
|
+
|
|
72
|
+
tags = json.loads(TAGS.read_text(encoding="utf-8"))
|
|
73
|
+
served_now = set(tags)
|
|
74
|
+
|
|
75
|
+
changes = []
|
|
76
|
+
for alias, served in registered():
|
|
77
|
+
if tags.get(served, "").split("/")[0] == args.connection:
|
|
78
|
+
continue # already correct
|
|
79
|
+
candidate = args.prefix + served
|
|
80
|
+
# Only repoint when the PREFIXED name is what the endpoint now serves
|
|
81
|
+
# and the bare one is not. Anything else is a different problem and
|
|
82
|
+
# guessing at it would break a working registration.
|
|
83
|
+
if candidate in served_now and served not in served_now:
|
|
84
|
+
changes.append((alias, served, candidate))
|
|
85
|
+
|
|
86
|
+
if not changes:
|
|
87
|
+
print(f"Nothing to repoint for {args.connection}.")
|
|
88
|
+
print("Either the prefix is already applied, or the endpoint does not "
|
|
89
|
+
"serve the prefixed names.")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
print(f"{len(changes)} registration(s) to repoint "
|
|
93
|
+
f"({'applying' if args.apply else 'dry run'}):\n")
|
|
94
|
+
for alias, old, new in changes:
|
|
95
|
+
print(f" {alias:<32} {old} -> {new}")
|
|
96
|
+
print("\nAliases are NOT changed, so every score, perf row, offload and "
|
|
97
|
+
"floor result\nkeeps pointing at the same model. That is why this "
|
|
98
|
+
"is cheap.")
|
|
99
|
+
|
|
100
|
+
if not args.apply:
|
|
101
|
+
print("\nRe-run with --apply to write.")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
text = YAML.read_text(encoding="utf-8")
|
|
105
|
+
for _alias, old, new in changes:
|
|
106
|
+
quoted_new = f'"{new}"' if any(c in new for c in ":/") else new
|
|
107
|
+
text = re.sub(rf"^( model_name: ){re.escape(old)}\"?$",
|
|
108
|
+
rf"\g<1>{quoted_new}", text, count=1, flags=re.M)
|
|
109
|
+
text = re.sub(rf'^( model_name: )"{re.escape(old)}"$',
|
|
110
|
+
rf"\g<1>{quoted_new}", text, count=1, flags=re.M)
|
|
111
|
+
YAML.write_text(text, encoding="utf-8")
|
|
112
|
+
|
|
113
|
+
# The snapshot records served NAMES, so without this the daily check
|
|
114
|
+
# reports the whole connection as retired tomorrow morning.
|
|
115
|
+
if SNAPSHOT.exists():
|
|
116
|
+
snap = json.loads(SNAPSHOT.read_text(encoding="utf-8"))
|
|
117
|
+
moved = 0
|
|
118
|
+
for _alias, old, new in changes:
|
|
119
|
+
# Keyed as the snapshot keys the hub's models: bare when it is the default.
|
|
120
|
+
old, new = endpoints.snapshot_key(old, hub), endpoints.snapshot_key(new, hub)
|
|
121
|
+
if old in snap.get("last_seen", {}):
|
|
122
|
+
snap["last_seen"][new] = snap["last_seen"].pop(old)
|
|
123
|
+
moved += 1
|
|
124
|
+
snap.get("first_missing", {}).pop(old, None)
|
|
125
|
+
SNAPSHOT.write_text(json.dumps(snap, indent=2, sort_keys=True) + "\n",
|
|
126
|
+
encoding="utf-8")
|
|
127
|
+
print(f"\nmigrated {moved} snapshot entr"
|
|
128
|
+
f"{'ies' if moved != 1 else 'y'} so the daily check does not "
|
|
129
|
+
f"report them retired")
|
|
130
|
+
|
|
131
|
+
print("\nNow run: python scripts/sync_config.py && "
|
|
132
|
+
"python scripts/provider_tags.py")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|