@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.
Files changed (96) hide show
  1. package/AGENTS.md +147 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +272 -271
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +368 -275
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +170 -155
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -72
  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 -712
  16. package/launcher.py +120 -0
  17. package/mcp_server.py +266 -0
  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 -373
  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 -574
  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 -64
  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 -104
  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 -0
  62. package/scripts/test_mcp.py +178 -0
  63. package/scripts/test_merged.py +214 -193
  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 -386
  67. package/scripts/test_verdict.py +98 -98
  68. package/scripts/test_vision.py +228 -228
  69. package/serve.py +3904 -3662
  70. package/suite/mine.py +154 -154
  71. package/suite/pace.py +145 -145
  72. package/suite/run.py +1046 -1040
  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 -424
  93. package/ui/bench.js +1756 -1555
  94. package/ui/themes/dark.css +35 -35
  95. package/ui/themes/hardwood.css +52 -52
  96. package/ui/themes/rainforest.css +32 -32
@@ -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()
@@ -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()