@wendellresearch-ai/benchwarmers 0.2.0 → 0.2.2

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 (103) hide show
  1. package/AGENTS.md +152 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +415 -272
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +408 -314
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +186 -163
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -74
  12. package/cli.py +674 -0
  13. package/examples/config/gateway.json +5 -5
  14. package/examples/config/model-roles.json +11 -11
  15. package/gateway.py +939 -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 +304 -295
  22. package/metrics/client.py +804 -394
  23. package/metrics/cliproxy.py +191 -191
  24. package/metrics/connect.py +691 -691
  25. package/metrics/cost.py +171 -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/formats.py +1021 -0
  30. package/metrics/guard.py +647 -592
  31. package/metrics/learn.py +234 -221
  32. package/metrics/ledger.py +148 -0
  33. package/metrics/litellm_hub.py +206 -206
  34. package/metrics/model_roles.py +94 -84
  35. package/metrics/offload.py +166 -166
  36. package/metrics/owui.py +205 -205
  37. package/metrics/pacing.py +248 -0
  38. package/metrics/paths.py +60 -60
  39. package/metrics/quota.py +383 -0
  40. package/metrics/route.py +581 -579
  41. package/metrics/scheduler.py +275 -275
  42. package/metrics/sources.py +167 -167
  43. package/metrics/wrong.py +144 -144
  44. package/package.json +69 -65
  45. package/perf/README.md +89 -89
  46. package/perf/run.py +284 -261
  47. package/scripts/auto_retire.py +367 -367
  48. package/scripts/check.py +113 -106
  49. package/scripts/check_deprecations.py +378 -378
  50. package/scripts/check_themes.py +103 -103
  51. package/scripts/init.py +124 -124
  52. package/scripts/list_served.py +78 -78
  53. package/scripts/notice.py +78 -78
  54. package/scripts/onboard.py +410 -331
  55. package/scripts/provider_tags.py +129 -129
  56. package/scripts/reprefix.py +136 -136
  57. package/scripts/sync_config.py +105 -105
  58. package/scripts/test_auth.py +255 -255
  59. package/scripts/test_cli.py +189 -0
  60. package/scripts/test_client.py +277 -233
  61. package/scripts/test_connect.py +222 -222
  62. package/scripts/test_first_run.py +180 -180
  63. package/scripts/test_formats.py +549 -0
  64. package/scripts/test_hubs.py +328 -328
  65. package/scripts/test_launcher.py +67 -0
  66. package/scripts/test_llm_log.py +85 -81
  67. package/scripts/test_mcp.py +178 -178
  68. package/scripts/test_merged.py +215 -214
  69. package/scripts/test_no_owui.py +291 -291
  70. package/scripts/test_owui.py +90 -90
  71. package/scripts/test_quota.py +334 -0
  72. package/scripts/test_serve_edit.py +453 -438
  73. package/scripts/test_server_json.py +79 -0
  74. package/scripts/test_verdict.py +102 -98
  75. package/scripts/test_vision.py +231 -228
  76. package/serve.py +3972 -3764
  77. package/suite/mine.py +154 -154
  78. package/suite/pace.py +145 -145
  79. package/suite/run.py +1046 -1042
  80. package/suite/seed.py +421 -421
  81. package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
  82. package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
  83. package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
  84. package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
  85. package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
  86. package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
  87. package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
  88. package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
  89. package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
  90. package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
  91. package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
  92. package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
  93. package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
  94. package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
  95. package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
  96. package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
  97. package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
  98. package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
  99. package/ui/bench.css +430 -425
  100. package/ui/bench.js +1757 -1576
  101. package/ui/themes/dark.css +35 -35
  102. package/ui/themes/hardwood.css +52 -52
  103. package/ui/themes/rainforest.css +32 -32
@@ -1,191 +1,191 @@
1
- """The CLI Proxy API hub adapter: a model added to one of its openai-compatibility providers.
2
-
3
- python metrics/cliproxy.py <provider> <model id> # what would change, changes nothing
4
-
5
- CLI Proxy API reaches OpenAI-compatible upstreams through the
6
- `openai-compatibility` list in its config.yaml: a provider each, with a name,
7
- a base-url, its upstream keys and a `models` list of {name, alias}. Only
8
- listed models are served, as the alias (else the name), and also as
9
- `<prefix>/<alias>` when the provider has a prefix. A provider is a
10
- CONNECTION here, by its name. Adding a model is one PATCH of that provider's
11
- `models` through the management API, which saves config.yaml and reloads
12
- itself. The PATCH carries the models list and nothing else, so no upstream
13
- key is ever sent back (the GET does return them; nothing here prints one).
14
-
15
- The list is REPLACED WHOLE, so it is read, merged and written: read again
16
- just before the PATCH and read back after. Anything in the list sent that is
17
- missing afterwards means something else wrote it meanwhile, and this stops
18
- and says so. ponytail: an edit landing between that last read and the PATCH
19
- is still lost, the read-modify-write ceiling owui.py notes; CLI Proxy API has
20
- no version field to make the write conditional.
21
-
22
- The management key is not one of its API keys: it is the endpoint's admin key
23
- (endpoints.admin_key, with no fallback to the API key). CLI Proxy API bans an
24
- address for 30 minutes after 5 refused keys, so nothing here retries a
25
- refusal, and without a stored key nothing is sent at all.
26
-
27
- Checked against CLI Proxy API 8.0.36's source (2026-10-21):
28
- internal/api/server_management.go (GET and PATCH
29
- /v0/management/openai-compatibility), internal/api/handlers/management/
30
- config_lists.go (PATCH {"name", "value": {"models": [...]}} replaces only the
31
- fields given; the reply is {"status": "ok"}), handler.go (Authorization:
32
- Bearer or X-Management-Key; the X-CPA-VERSION header; 401 for a wrong key,
33
- 403 "remote management disabled" from another machine unless
34
- remote-management.allow-remote is true), server_management.go again (every
35
- management route 404s while remote-management.secret-key is unset).
36
- """
37
-
38
- from __future__ import annotations
39
-
40
- import json
41
- import pathlib
42
- import sys
43
- import time
44
- import urllib.error
45
- import urllib.request
46
-
47
- ROOT = pathlib.Path(__file__).resolve().parent.parent
48
- sys.path.insert(0, str(ROOT / "metrics"))
49
- import endpoints # noqa: E402
50
-
51
- # What this hub takes (RELEASE-PLAN section 2, "Adding a model to a hub"); "" where it has none.
52
- SUPPORTS = {"allow_list": "models list", "tags": "", "logo": "", "access": "",
53
- "link": "/management.html#/ai-providers"}
54
- CHECKED = ("8.0",)
55
- PATH = "/v0/management/openai-compatibility"
56
-
57
-
58
- def endpoint() -> str:
59
- """The CLI Proxy API hub's endpoint name; ValueError when there is none."""
60
- name = endpoints.find_hub("cliproxy")
61
- if not name:
62
- raise ValueError("no endpoint is a CLI Proxy API hub; add it on the Endpoints page as kind hub,"
63
- " CLI Proxy API")
64
- return name
65
-
66
-
67
- def _call(method: str = "GET", body: dict | None = None) -> tuple[dict, str]:
68
- """(reply, X-CPA-VERSION) from the openai-compatibility route."""
69
- name = endpoint()
70
- secret = endpoints.admin_key(name, fallback=False)
71
- if not secret:
72
- env = endpoints.ENV_KEY + endpoints.admin_name(endpoints.load()[name]).upper().replace("-", "_")
73
- raise ValueError(f"CLI Proxy API's management key is not stored: give it as the admin key on the"
74
- f" Endpoints page (Change key on {name}), or set {env}")
75
- req = urllib.request.Request(endpoints.base(name).removesuffix("/v1") + PATH, method=method,
76
- data=None if body is None else json.dumps(body).encode(),
77
- headers={"Authorization": f"Bearer {secret}", "Content-Type": "application/json",
78
- "Accept": "application/json"})
79
- try:
80
- with urllib.request.urlopen(req, timeout=15 if body is None else 60) as r:
81
- return json.loads(r.read() or b"null") or {}, r.headers.get("X-CPA-VERSION", "")
82
- except urllib.error.HTTPError as exc:
83
- raise ValueError(_refusal(exc)) from None
84
-
85
-
86
- def _refusal(exc: urllib.error.HTTPError) -> str:
87
- """What to change, from a refused management call."""
88
- try:
89
- said = str(json.loads(exc.read()).get("error") or "")[:200]
90
- except (ValueError, AttributeError):
91
- said = ""
92
- if exc.code == 404 and said == "item not found": # the PATCH's: no provider of that name now
93
- return "the provider left CLI Proxy API while this ran; nothing changed"
94
- if exc.code == 404:
95
- return ("CLI Proxy API's management API is off: set remote-management.secret-key in its config.yaml"
96
- " (every management route answers 404 without one)")
97
- if exc.code == 403 and "remote management disabled" in said:
98
- return ("CLI Proxy API refuses management from another machine: set remote-management.allow-remote:"
99
- " true in its config.yaml")
100
- if exc.code == 401:
101
- return (f"CLI Proxy API refused the management key ({said}); it bans an address for 30 minutes after"
102
- " 5 refusals, so check the key before trying again")
103
- return f"CLI Proxy API answered {exc.code}: {said}"
104
-
105
-
106
- def _providers() -> tuple[list[dict], str]:
107
- data, release = _call()
108
- return data.get("openai-compatibility") or [], release
109
-
110
-
111
- def _named(entries: list[dict], connection: str) -> dict | None:
112
- return next((e for e in entries if e.get("name") == connection), None)
113
-
114
-
115
- def _alias(m: dict) -> str:
116
- """What a listed model is served as: its alias, else its name."""
117
- return str(m.get("alias") or m.get("name") or "").strip()
118
-
119
-
120
- def upstreams() -> dict[str, tuple[str, str]]:
121
- """Provider -> (base URL, the prefix its served ids take, "" for none).
122
- What Discover matches a provider against (discover.hub_routes)."""
123
- return {e["name"]: (str(e.get("base-url") or "").rstrip("/"), str(e.get("prefix") or ""))
124
- for e in _providers()[0] if e.get("name") and not e.get("disabled")}
125
-
126
-
127
- def plan(connection: str, model_id: str) -> dict:
128
- """What adding would change. Writes nothing; ValueError when it cannot be done here."""
129
- entries, release = _providers()
130
- entry = _named(entries, connection)
131
- if entry is None:
132
- raise ValueError(f"CLI Proxy API has no openai-compatibility provider named {connection!r}")
133
- if entry.get("disabled"):
134
- raise ValueError(f"the {connection} provider is switched off in CLI Proxy API")
135
- prefix = str(entry.get("prefix") or "")
136
- # One id on two providers: CLI Proxy API would share its calls between them.
137
- # A prefixed provider serves the bare alias too (unless force-model-prefix),
138
- # so prefixes are not compared. ponytail: an OAuth model of the same id is not.
139
- elsewhere = next((e.get("name") for e in entries if e is not entry and not e.get("disabled")
140
- and any(_alias(m) == model_id for m in e.get("models") or [])), None)
141
- if elsewhere:
142
- raise ValueError(f"CLI Proxy API's {elsewhere} provider already serves {model_id}; listing it on"
143
- f" {connection} too would share its calls between them")
144
- models = entry.get("models") or []
145
- return {"connection": connection, "base_url": entry.get("base-url"), "model_id": model_id,
146
- "served": f"{prefix}/{model_id}" if prefix else model_id,
147
- "add_to_list": not any(_alias(m) == model_id for m in models), "listed": len(models),
148
- "version": release, "version_note": endpoints.version_note("cliproxy", release, CHECKED)}
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
- entry = _named(_providers()[0], connection) # again, just before the write: it decides, not plan()
156
- if entry is None:
157
- raise ValueError(f"the {connection} provider left CLI Proxy API while this ran; nothing changed")
158
- sent = list(entry.get("models") or [])
159
- if any(_alias(m) == model_id for m in sent):
160
- done.append(f"the {connection} provider already lists {model_id}; nothing changed")
161
- else:
162
- sent.append({"name": model_id, "alias": model_id})
163
- _call("PATCH", {"name": connection, "value": {"models": sent}})
164
- back = (_named(_providers()[0], connection) or {}).get("models") or []
165
- have = {(m.get("name"), m.get("alias")) for m in back}
166
- lost = [_alias(m) for m in sent if (m.get("name"), m.get("alias")) not in have]
167
- if lost:
168
- raise ValueError(f"CLI Proxy API's {connection} list changed while this wrote it: {', '.join(lost)}"
169
- " missing from it now, so something else edited it at the same moment; look at its"
170
- f" providers page ({endpoints.base(endpoint()).removesuffix('/v1')}{SUPPORTS['link']})"
171
- " before adding again")
172
- done.append(f"added {model_id} to the {connection} provider's models ({len(sent)} listed);"
173
- " CLI Proxy API reloads by itself")
174
- for _ in range(10): # the reload runs after the reply
175
- if p["served"] in endpoints.models(endpoint()):
176
- break
177
- time.sleep(2)
178
- else:
179
- done.append(f"CLI Proxy API does not list {p['served']} yet; onboarding will say if it never does")
180
- return p["served"], done
181
-
182
-
183
- def main() -> None:
184
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
185
- if len(sys.argv) != 3:
186
- sys.exit(__doc__)
187
- print(json.dumps(plan(sys.argv[1], sys.argv[2]), indent=2))
188
-
189
-
190
- if __name__ == "__main__":
191
- main()
1
+ """The CLI Proxy API hub adapter: a model added to one of its openai-compatibility providers.
2
+
3
+ python metrics/cliproxy.py <provider> <model id> # what would change, changes nothing
4
+
5
+ CLI Proxy API reaches OpenAI-compatible upstreams through the
6
+ `openai-compatibility` list in its config.yaml: a provider each, with a name,
7
+ a base-url, its upstream keys and a `models` list of {name, alias}. Only
8
+ listed models are served, as the alias (else the name), and also as
9
+ `<prefix>/<alias>` when the provider has a prefix. A provider is a
10
+ CONNECTION here, by its name. Adding a model is one PATCH of that provider's
11
+ `models` through the management API, which saves config.yaml and reloads
12
+ itself. The PATCH carries the models list and nothing else, so no upstream
13
+ key is ever sent back (the GET does return them; nothing here prints one).
14
+
15
+ The list is REPLACED WHOLE, so it is read, merged and written: read again
16
+ just before the PATCH and read back after. Anything in the list sent that is
17
+ missing afterwards means something else wrote it meanwhile, and this stops
18
+ and says so. ponytail: an edit landing between that last read and the PATCH
19
+ is still lost, the read-modify-write ceiling owui.py notes; CLI Proxy API has
20
+ no version field to make the write conditional.
21
+
22
+ The management key is not one of its API keys: it is the endpoint's admin key
23
+ (endpoints.admin_key, with no fallback to the API key). CLI Proxy API bans an
24
+ address for 30 minutes after 5 refused keys, so nothing here retries a
25
+ refusal, and without a stored key nothing is sent at all.
26
+
27
+ Checked against CLI Proxy API 8.0.36's source (2026-10-21):
28
+ internal/api/server_management.go (GET and PATCH
29
+ /v0/management/openai-compatibility), internal/api/handlers/management/
30
+ config_lists.go (PATCH {"name", "value": {"models": [...]}} replaces only the
31
+ fields given; the reply is {"status": "ok"}), handler.go (Authorization:
32
+ Bearer or X-Management-Key; the X-CPA-VERSION header; 401 for a wrong key,
33
+ 403 "remote management disabled" from another machine unless
34
+ remote-management.allow-remote is true), server_management.go again (every
35
+ management route 404s while remote-management.secret-key is unset).
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import json
41
+ import pathlib
42
+ import sys
43
+ import time
44
+ import urllib.error
45
+ import urllib.request
46
+
47
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
48
+ sys.path.insert(0, str(ROOT / "metrics"))
49
+ import endpoints # noqa: E402
50
+
51
+ # What this hub takes (RELEASE-PLAN section 2, "Adding a model to a hub"); "" where it has none.
52
+ SUPPORTS = {"allow_list": "models list", "tags": "", "logo": "", "access": "",
53
+ "link": "/management.html#/ai-providers"}
54
+ CHECKED = ("8.0",)
55
+ PATH = "/v0/management/openai-compatibility"
56
+
57
+
58
+ def endpoint() -> str:
59
+ """The CLI Proxy API hub's endpoint name; ValueError when there is none."""
60
+ name = endpoints.find_hub("cliproxy")
61
+ if not name:
62
+ raise ValueError("no endpoint is a CLI Proxy API hub; add it on the Endpoints page as kind hub,"
63
+ " CLI Proxy API")
64
+ return name
65
+
66
+
67
+ def _call(method: str = "GET", body: dict | None = None) -> tuple[dict, str]:
68
+ """(reply, X-CPA-VERSION) from the openai-compatibility route."""
69
+ name = endpoint()
70
+ secret = endpoints.admin_key(name, fallback=False)
71
+ if not secret:
72
+ env = endpoints.ENV_KEY + endpoints.admin_name(endpoints.load()[name]).upper().replace("-", "_")
73
+ raise ValueError(f"CLI Proxy API's management key is not stored: give it as the admin key on the"
74
+ f" Endpoints page (Change key on {name}), or set {env}")
75
+ req = urllib.request.Request(endpoints.base(name).removesuffix("/v1") + PATH, method=method,
76
+ data=None if body is None else json.dumps(body).encode(),
77
+ headers={"Authorization": f"Bearer {secret}", "Content-Type": "application/json",
78
+ "Accept": "application/json"})
79
+ try:
80
+ with urllib.request.urlopen(req, timeout=15 if body is None else 60) as r:
81
+ return json.loads(r.read() or b"null") or {}, r.headers.get("X-CPA-VERSION", "")
82
+ except urllib.error.HTTPError as exc:
83
+ raise ValueError(_refusal(exc)) from None
84
+
85
+
86
+ def _refusal(exc: urllib.error.HTTPError) -> str:
87
+ """What to change, from a refused management call."""
88
+ try:
89
+ said = str(json.loads(exc.read()).get("error") or "")[:200]
90
+ except (ValueError, AttributeError):
91
+ said = ""
92
+ if exc.code == 404 and said == "item not found": # the PATCH's: no provider of that name now
93
+ return "the provider left CLI Proxy API while this ran; nothing changed"
94
+ if exc.code == 404:
95
+ return ("CLI Proxy API's management API is off: set remote-management.secret-key in its config.yaml"
96
+ " (every management route answers 404 without one)")
97
+ if exc.code == 403 and "remote management disabled" in said:
98
+ return ("CLI Proxy API refuses management from another machine: set remote-management.allow-remote:"
99
+ " true in its config.yaml")
100
+ if exc.code == 401:
101
+ return (f"CLI Proxy API refused the management key ({said}); it bans an address for 30 minutes after"
102
+ " 5 refusals, so check the key before trying again")
103
+ return f"CLI Proxy API answered {exc.code}: {said}"
104
+
105
+
106
+ def _providers() -> tuple[list[dict], str]:
107
+ data, release = _call()
108
+ return data.get("openai-compatibility") or [], release
109
+
110
+
111
+ def _named(entries: list[dict], connection: str) -> dict | None:
112
+ return next((e for e in entries if e.get("name") == connection), None)
113
+
114
+
115
+ def _alias(m: dict) -> str:
116
+ """What a listed model is served as: its alias, else its name."""
117
+ return str(m.get("alias") or m.get("name") or "").strip()
118
+
119
+
120
+ def upstreams() -> dict[str, tuple[str, str]]:
121
+ """Provider -> (base URL, the prefix its served ids take, "" for none).
122
+ What Discover matches a provider against (discover.hub_routes)."""
123
+ return {e["name"]: (str(e.get("base-url") or "").rstrip("/"), str(e.get("prefix") or ""))
124
+ for e in _providers()[0] if e.get("name") and not e.get("disabled")}
125
+
126
+
127
+ def plan(connection: str, model_id: str) -> dict:
128
+ """What adding would change. Writes nothing; ValueError when it cannot be done here."""
129
+ entries, release = _providers()
130
+ entry = _named(entries, connection)
131
+ if entry is None:
132
+ raise ValueError(f"CLI Proxy API has no openai-compatibility provider named {connection!r}")
133
+ if entry.get("disabled"):
134
+ raise ValueError(f"the {connection} provider is switched off in CLI Proxy API")
135
+ prefix = str(entry.get("prefix") or "")
136
+ # One id on two providers: CLI Proxy API would share its calls between them.
137
+ # A prefixed provider serves the bare alias too (unless force-model-prefix),
138
+ # so prefixes are not compared. ponytail: an OAuth model of the same id is not.
139
+ elsewhere = next((e.get("name") for e in entries if e is not entry and not e.get("disabled")
140
+ and any(_alias(m) == model_id for m in e.get("models") or [])), None)
141
+ if elsewhere:
142
+ raise ValueError(f"CLI Proxy API's {elsewhere} provider already serves {model_id}; listing it on"
143
+ f" {connection} too would share its calls between them")
144
+ models = entry.get("models") or []
145
+ return {"connection": connection, "base_url": entry.get("base-url"), "model_id": model_id,
146
+ "served": f"{prefix}/{model_id}" if prefix else model_id,
147
+ "add_to_list": not any(_alias(m) == model_id for m in models), "listed": len(models),
148
+ "version": release, "version_note": endpoints.version_note("cliproxy", release, CHECKED)}
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
+ entry = _named(_providers()[0], connection) # again, just before the write: it decides, not plan()
156
+ if entry is None:
157
+ raise ValueError(f"the {connection} provider left CLI Proxy API while this ran; nothing changed")
158
+ sent = list(entry.get("models") or [])
159
+ if any(_alias(m) == model_id for m in sent):
160
+ done.append(f"the {connection} provider already lists {model_id}; nothing changed")
161
+ else:
162
+ sent.append({"name": model_id, "alias": model_id})
163
+ _call("PATCH", {"name": connection, "value": {"models": sent}})
164
+ back = (_named(_providers()[0], connection) or {}).get("models") or []
165
+ have = {(m.get("name"), m.get("alias")) for m in back}
166
+ lost = [_alias(m) for m in sent if (m.get("name"), m.get("alias")) not in have]
167
+ if lost:
168
+ raise ValueError(f"CLI Proxy API's {connection} list changed while this wrote it: {', '.join(lost)}"
169
+ " missing from it now, so something else edited it at the same moment; look at its"
170
+ f" providers page ({endpoints.base(endpoint()).removesuffix('/v1')}{SUPPORTS['link']})"
171
+ " before adding again")
172
+ done.append(f"added {model_id} to the {connection} provider's models ({len(sent)} listed);"
173
+ " CLI Proxy API reloads by itself")
174
+ for _ in range(10): # the reload runs after the reply
175
+ if p["served"] in endpoints.models(endpoint()):
176
+ break
177
+ time.sleep(2)
178
+ else:
179
+ done.append(f"CLI Proxy API does not list {p['served']} yet; onboarding will say if it never does")
180
+ return p["served"], done
181
+
182
+
183
+ def main() -> None:
184
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
185
+ if len(sys.argv) != 3:
186
+ sys.exit(__doc__)
187
+ print(json.dumps(plan(sys.argv[1], sys.argv[2]), indent=2))
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()