@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
@@ -1,328 +1,328 @@
1
- """The LiteLLM and CLI Proxy API hub adapters against fake hubs: nothing is sent anywhere real.
2
-
3
- python scripts/test_hubs.py
4
-
5
- The fakes answer with the shapes each hub's own source produces (recorded
6
- below from LiteLLM 1.106.1 and CLI Proxy API 8.0.36, 2026-10-21; hosts and
7
- keys made up), and refuse a wrong key as the real ones do. Checked: a dry run
8
- writes nothing; a real add sends what it should and no key; LiteLLM without
9
- its database mode is refused with what to change, before or at /model/new;
10
- CLI Proxy API refuses with no management key before sending anything, never
11
- gets the API key on a management route, and stops when its list changes
12
- underneath; Discover offers each hub's connections and its Add goes through
13
- the adapter of the endpoint's hub. BENCHWARMERS_HOME and LLM_USER_PATH point
14
- at a temp folder before anything is imported.
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import copy
20
- import json
21
- import os
22
- import pathlib
23
- import shutil
24
- import sys
25
- import tempfile
26
- import threading
27
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
28
-
29
- ROOT = pathlib.Path(__file__).resolve().parent.parent
30
- TMP = pathlib.Path(tempfile.mkdtemp(prefix="bench-hubs-"))
31
- os.environ["BENCHWARMERS_HOME"] = str(TMP)
32
- os.environ["LLM_USER_PATH"] = str(TMP / "llm")
33
- (TMP / "config").mkdir()
34
- (TMP / "llm").mkdir()
35
- # Keys from the environment, as a container would have them. LiteLLM's API key
36
- # is its master key (no admin key: the fallback); CLI Proxy API's management
37
- # key is its own.
38
- os.environ.update(BENCHWARMERS_KEY_LITE="sk-master-test", BENCHWARMERS_KEY_CPA="cpa-api-test",
39
- BENCHWARMERS_KEY_CPA_ADMIN="mgmt-test", BENCHWARMERS_KEY_PLAIN="sk-master-test")
40
- sys.path[:0] = [str(ROOT), str(ROOT / "metrics"), str(ROOT / "suite"), str(ROOT / "scripts")]
41
-
42
- # LiteLLM 1.106.1: GET /model/info (api_key removed, secrets masked), the readiness details,
43
- # POST /model/new's stored row, and its errors as ProxyException bodies.
44
- LITE_DEPLOYMENTS = [
45
- {"model_name": "llama-3.3-70b",
46
- "litellm_params": {"model": "groq/llama-3.3-70b-versatile", "litellm_credential_name": "groq-cred",
47
- "max_retries": 2, "rpm": 30, "input_cost_per_token": 5.9e-07},
48
- "model_info": {"id": "9b1c7e", "db_model": True, "direct_access": True, "access_via_team_ids": []}},
49
- {"model_name": "mistral-small",
50
- "litellm_params": {"model": "openai/mistral-small-latest", "api_base": "https://api.mistral.example/v1",
51
- "extra_headers": {"Authorization": "Bear*****************9f2c"}, "timeout": 60},
52
- "model_info": {"id": "41aa02", "db_model": False}}]
53
- LITE_READY = {"status": "healthy", "db": "connected", "cache": None, "litellm_version": "1.106.1",
54
- "success_callbacks": [], "use_aiohttp_transport": True, "log_level": "WARNING",
55
- "is_detailed_debug": False, "show_no_redis_warning": False}
56
- LITE_ERRORS = {
57
- "no-db": "{'error': \"No DB Connected. Here's how to do it - https://docs.litellm.ai/docs/proxy/virtual_keys\"}",
58
- "no-store": "{'error': \"Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature.\"}"}
59
- # CLI Proxy API 8.0.36: GET /v0/management/openai-compatibility (upstream keys included, as it sends them).
60
- CPA_PROVIDERS = [
61
- {"name": "openrouter", "disabled": False, "base-url": "https://openrouter.example/api/v1",
62
- "api-key-entries": [{"api-key": "sk-or-upstream-never-sent", "auth-index": "a1"}],
63
- "models": [{"name": "vendor/old:free", "alias": "old-free"}], "auth-index": "c3"},
64
- {"name": "team", "disabled": False, "prefix": "team", "base-url": "https://llm.team.example/v1",
65
- "api-key-entries": [{"api-key": "team-upstream-never-sent"}], "models": [{"name": "m1", "alias": "m1"}]}]
66
-
67
- S: dict = {}
68
- SEEN: list[tuple[str, str, dict | None]] = []
69
-
70
-
71
- def reset(**over) -> None:
72
- S.clear()
73
- S.update(lite=copy.deepcopy(LITE_DEPLOYMENTS), ready=dict(LITE_READY), store=True,
74
- cpa=copy.deepcopy(CPA_PROVIDERS), version="8.0.36", race=False)
75
- S.update(over)
76
- SEEN.clear()
77
-
78
-
79
- def writes() -> list[str]:
80
- return [p for m, p, _b in SEEN if m in ("POST", "PATCH", "PUT", "DELETE")]
81
-
82
-
83
- class Fake(BaseHTTPRequestHandler):
84
- def _route(self, method): # noqa: C901
85
- n = int(self.headers.get("Content-Length") or 0)
86
- body = json.loads(self.rfile.read(n)) if n else None
87
- SEEN.append((method, self.path, body))
88
- auth = self.headers.get("Authorization", "")
89
- if self.path.startswith("/lite/"):
90
- if auth != "Bearer sk-master-test":
91
- return self._send({"error": {"message": "Authentication Error, Invalid proxy server token passed.",
92
- "type": "auth_error", "param": "None", "code": "401"}}, 401)
93
- path = self.path[len("/lite"):]
94
- if path == "/health/readiness/details":
95
- return self._send(S["ready"])
96
- if path == "/model/info":
97
- return self._send({"data": S["lite"]})
98
- if path == "/v1/models":
99
- return self._send({"data": [{"id": d["model_name"], "object": "model"} for d in S["lite"]],
100
- "object": "list"})
101
- if path == "/model/new" and method == "POST":
102
- if S["ready"]["db"] != "connected" or not S["store"]:
103
- msg = LITE_ERRORS["no-db" if S["ready"]["db"] != "connected" else "no-store"]
104
- return self._send({"error": {"message": msg, "type": "auth_error", "param": "None",
105
- "code": "500"}}, 500)
106
- S["lite"].append({**body, "model_info": {**body["model_info"], "id": "77e0d1", "db_model": True}})
107
- return self._send({"model_id": "77e0d1", "model_name": body["model_name"],
108
- "litellm_params": "gAAAAB-encrypted-row", "model_info": body["model_info"],
109
- "created_by": "default_user_id"})
110
- if self.path.startswith("/cpa/"):
111
- path = self.path[len("/cpa"):]
112
- if path == "/v1/models":
113
- if auth != "Bearer cpa-api-test":
114
- return self._send({"error": "invalid api key"}, 401)
115
- ids = [(f"{e['prefix']}/" if e.get("prefix") else "") + (m.get("alias") or m["name"])
116
- for e in S["cpa"] for m in e["models"]]
117
- return self._send({"data": [{"id": i, "object": "model"} for i in ids], "object": "list"})
118
- if path == "/v0/management/openai-compatibility":
119
- if auth != "Bearer mgmt-test":
120
- return self._send({"error": "invalid management key"}, 401)
121
- if method == "GET":
122
- return self._send({"openai-compatibility": S["cpa"]}, headers={"X-CPA-VERSION": S["version"]})
123
- if method == "PATCH":
124
- entry = next((e for e in S["cpa"] if e["name"] == body["name"]), None)
125
- if entry is None:
126
- return self._send({"error": "item not found"}, 404)
127
- entry["models"] = body["value"]["models"] # replaced whole, as the real one does
128
- if S["race"]: # someone else's stale write lands after ours
129
- entry["models"] = entry["models"][:-1]
130
- return self._send({"status": "ok"}, headers={"X-CPA-VERSION": S["version"]})
131
- self._send({"detail": "Not Found"}, 404)
132
-
133
- def do_GET(self): # noqa: N802
134
- self._route("GET")
135
-
136
- def do_POST(self): # noqa: N802
137
- self._route("POST")
138
-
139
- def do_PATCH(self): # noqa: N802
140
- self._route("PATCH")
141
-
142
- def _send(self, data, status=200, headers=None):
143
- raw = json.dumps(data).encode()
144
- self.send_response(status)
145
- self.send_header("Content-Type", "application/json")
146
- for k, v in (headers or {}).items():
147
- self.send_header(k, v)
148
- self.send_header("Content-Length", str(len(raw)))
149
- self.end_headers()
150
- self.wfile.write(raw)
151
-
152
- def log_message(self, *_a):
153
- pass
154
-
155
-
156
- def refused(fn, *args) -> str:
157
- try:
158
- fn(*args)
159
- except ValueError as exc:
160
- return str(exc)
161
- raise AssertionError(f"{fn.__module__}.{fn.__name__}{args} was not refused")
162
-
163
-
164
- def litellm() -> None:
165
- import litellm_hub as lite
166
- reset()
167
- p = lite.plan("groq", "moonshotai/kimi-k2") # dry run
168
- assert writes() == [], writes()
169
- assert p["served"] == "moonshotai/kimi-k2" and p["deployment"] == "create" and p["version_note"] == "", p
170
- assert p["litellm_params"] == {"model": "groq/moonshotai/kimi-k2", "litellm_credential_name": "groq-cred",
171
- "max_retries": 2}, p["litellm_params"] # no price, no rpm, no key
172
- assert p["database_mode"] is True and "groq-cred" in p["key"], p
173
- # A masked header is never sent back; the api_base host is the connection.
174
- m = lite.plan("api.mistral.example", "mistral-large")
175
- assert m["litellm_params"] == {"model": "openai/mistral-large", "api_base": "https://api.mistral.example/v1",
176
- "timeout": 60}, m["litellm_params"]
177
- assert m["not_copied"] == ["extra_headers"], m # said, not silently dropped
178
- assert set(lite.upstreams()) == {"groq", "api.mistral.example"}
179
- assert "already serves" in refused(lite.plan, "groq", "mistral-small") # one name, two upstreams
180
-
181
- served, done = lite.add("groq", "moonshotai/kimi-k2") # a real add
182
- sent = [b for meth, path, b in SEEN if path == "/lite/model/new"]
183
- assert served == "moonshotai/kimi-k2" and len(sent) == 1, SEEN
184
- assert sent[0] == {"model_name": served, "litellm_params": p["litellm_params"],
185
- "model_info": {"tags": ["groq"]}}, sent[0]
186
- assert "api_key" not in json.dumps(sent[0]) and "gAAAAB" not in " ".join(done), done
187
- assert any("77e0d1" in d for d in done), done
188
- assert lite.add("groq", "moonshotai/kimi-k2")[1][-1].endswith("nothing added") # twice: no second deployment
189
-
190
- # No database: refused by the dry run, before /model/new.
191
- reset(ready={**LITE_READY, "db": "Not connected"})
192
- assert "DATABASE_URL" in refused(lite.add, "groq", "x") and writes() == [], writes()
193
- # A database, but models kept in config.yaml: /model/new refuses, nothing is added.
194
- reset(store=False)
195
- assert "STORE_MODEL_IN_DB=True" in refused(lite.add, "groq", "x") and len(S["lite"]) == 2
196
- # Another release is said, not refused.
197
- reset(ready={**LITE_READY, "litellm_version": "1.107.2"})
198
- assert "1.107.2" in lite.plan("groq", "x")["version_note"]
199
-
200
-
201
- def cliproxy() -> None:
202
- import cliproxy as cpa
203
- reset()
204
- p = cpa.plan("openrouter", "vendor/new:free") # dry run
205
- assert writes() == [] and p["add_to_list"] and p["served"] == "vendor/new:free" and not p["version_note"], p
206
- assert cpa.plan("team", "m2")["served"] == "team/m2" # a prefix names the served id
207
- assert cpa.plan("openrouter", "old-free")["add_to_list"] is False # listed by its alias already
208
- assert cpa.plan("team", "m1")["add_to_list"] is False # its own list: not a collision
209
- assert "already serves" in refused(cpa.plan, "openrouter", "m1") # team serves a bare m1 too
210
- assert cpa.upstreams() == {"openrouter": ("https://openrouter.example/api/v1", ""),
211
- "team": ("https://llm.team.example/v1", "team")}
212
-
213
- served, done = cpa.add("openrouter", "vendor/new:free") # a real add
214
- patches = [b for meth, _p, b in SEEN if meth == "PATCH"]
215
- assert served == "vendor/new:free" and len(patches) == 1, SEEN
216
- assert patches[0] == {"name": "openrouter", "value": {"models": [
217
- {"name": "vendor/old:free", "alias": "old-free"}, {"name": "vendor/new:free", "alias": "vendor/new:free"}]}}
218
- assert "never-sent" not in json.dumps(patches[0]) # no upstream key goes back
219
- assert S["cpa"][1] == CPA_PROVIDERS[1], "another provider changed"
220
- gets = [i for i, (meth, path, _b) in enumerate(SEEN) if meth == "GET" and "management" in path]
221
- at = next(i for i, (meth, _p, _b) in enumerate(SEEN) if meth == "PATCH")
222
- assert any(i < at for i in gets) and any(i > at for i in gets), "not read just before and after"
223
- assert done[-1].startswith("added vendor/new:free"), done # the fake took only the management key
224
- SEEN.clear() # listed already: the fresh read decides
225
- assert cpa.add("openrouter", "old-free")[1][0].endswith("nothing changed") and writes() == []
226
-
227
- # A provider gone by the PATCH is said as that, not as the management API being off.
228
- assert "left CLI Proxy API" in refused(cpa._call, "PATCH", {"name": "gone", "value": {"models": []}})
229
- # The list changes underneath: stop and say so.
230
- reset(race=True)
231
- assert "changed while this wrote it" in refused(cpa.add, "openrouter", "vendor/new:free")
232
- # No management key stored: nothing is sent at all.
233
- reset()
234
- saved = os.environ.pop("BENCHWARMERS_KEY_CPA_ADMIN")
235
- try:
236
- msg = refused(cpa.plan, "openrouter", "x")
237
- assert "BENCHWARMERS_KEY_CPA_ADMIN" in msg and SEEN == [], (msg, SEEN)
238
- finally:
239
- os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = saved
240
- # A wrong management key: said once, with the ban in mind, never retried.
241
- os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = "wrong"
242
- try:
243
- assert "30 minutes" in refused(cpa.plan, "openrouter", "x") and len(SEEN) == 1, SEEN
244
- finally:
245
- os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = saved
246
- reset(version="v8.1.0")
247
- assert "8.1.0" in cpa.plan("openrouter", "x")["version_note"]
248
-
249
-
250
- def discover_add() -> None:
251
- """Discover's routes and Add: each hub through its own adapter."""
252
- import discover
253
- import endpoints
254
- import owui
255
- import serve
256
- reset()
257
- rows = [{"provider": "groq", "provider_name": "Groq", "model": "vendor/new", "connection": None, "api": None},
258
- {"provider": "openrouter", "provider_name": "OpenRouter", "model": "vendor/x:free",
259
- "connection": "openrouter", "api": "https://openrouter.example/api/v1/"},
260
- {"provider": "mistral", "provider_name": "Mistral", "model": "mistral-large", "connection": None,
261
- "api": "https://api.mistral.example/v1"},
262
- {"provider": "nobody", "provider_name": "Nobody", "model": "n", "connection": None, "api": None}]
263
- routes, errors = discover.hub_routes(rows)
264
- assert errors == [] and writes() == [], (errors, writes())
265
- got = [[(x["hub"], x["connection"], x["endpoint"]) for x in r] for r in routes]
266
- assert got == [[("litellm", "groq", "lite")], [("cliproxy", "openrouter", "cpa")],
267
- [("litellm", "api.mistral.example", "lite")], []], got
268
- lite_steps, cpa_steps = routes[0][0]["steps"][0], routes[1][0]["steps"][0]
269
- assert "tags:" in lite_steps and "logo" not in lite_steps, lite_steps # only what each hub takes
270
- assert "tags" not in cpa_steps and "logo" not in cpa_steps and "access" not in cpa_steps, cpa_steps
271
- assert routes[1][0]["on_bench_as"] == "x-free", routes[1][0] # registered on cpa below
272
- # A hub that cannot be asked is said, and the rest still offered.
273
- os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = ""
274
- try:
275
- routes, errors = discover.hub_routes(rows[:2])
276
- assert len(errors) == 1 and "CLI Proxy API" in errors[0] and [len(r) for r in routes] == [1, 0], errors
277
- finally:
278
- os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = "mgmt-test"
279
-
280
- onboarded = []
281
- serve.queue_onboard = lambda items, who="ui": onboarded.append((items[0]["served"], items[0]["endpoint"])) or {
282
- "ok": True, "errors": [], "items": []}
283
- owui.add = lambda c, m: (m, [f"owui added {m} on {c}"])
284
- r = serve.discover_add("groq", "vendor/new", endpoint="lite")
285
- assert r["ok"] and "/lite/model/new" in writes() and onboarded[-1] == ("vendor/new", "lite"), r
286
- r = serve.discover_add("openrouter", "vendor/y:free", endpoint="cpa")
287
- assert r["ok"] and onboarded[-1] == ("vendor/y:free", "cpa"), r
288
- assert [m for m, p, _b in SEEN if "management" in p and m == "PATCH"] == ["PATCH"]
289
- r = serve.discover_add("groq-api", "vendor/z") # no endpoint: Open WebUI, as before
290
- assert r["steps"] == ["owui added vendor/z on groq-api"] and onboarded[-1] == ("vendor/z", "owui"), r
291
- assert serve.discover_add("groq-api", "vendor/z", endpoint="owui")["ok"]
292
- assert "not a hub" in serve.discover_add("groq", "v/z", endpoint="plain")["errors"][0]
293
- changes = [json.loads(line)["field"] for line in serve.CHANGES_PATH.read_text(encoding="utf-8").splitlines()]
294
- assert {"litellm-add", "cliproxy-add", "openwebui-add"} <= set(changes), changes
295
-
296
- # The admin key from the Endpoints form: kept as <key>-admin, never in the reply.
297
- r = serve.save_endpoint("lite", endpoints.load()["lite"]["base"], "", admin="sk-master-test")
298
- assert r["ok"] and "lite-admin" in r["note"] and "sk-master" not in json.dumps(r), r
299
- assert endpoints._stored()["lite-admin"] == "sk-master-test"
300
- assert serve.save_endpoint("plain", endpoints.load()["plain"]["base"], "", admin="nope")["ok"]
301
- assert "plain-admin" not in endpoints._stored(), "a provider got an admin key"
302
-
303
-
304
- def main() -> int:
305
- server = ThreadingHTTPServer(("127.0.0.1", 0), Fake)
306
- threading.Thread(target=server.serve_forever, daemon=True).start()
307
- fake = f"http://127.0.0.1:{server.server_port}"
308
- (TMP / "config" / "endpoints.json").write_text(json.dumps({
309
- "owui": {"base": fake + "/api", "key": "owui", "default": True}, # kind inferred, as the owner's
310
- "lite": {"base": fake + "/lite/v1", "key": "lite", "kind": "hub", "hub": "litellm"},
311
- "cpa": {"base": fake + "/cpa/v1", "key": "cpa", "kind": "hub", "hub": "cliproxy"},
312
- "plain": {"base": fake + "/lite/v1", "key": "plain", "kind": "provider"}}), encoding="utf-8")
313
- import endpoints
314
- (TMP / "config" / "extra-openai-models.yaml").write_text(endpoints.yaml_block("x-free", "vendor/x:free", "cpa"),
315
- encoding="utf-8")
316
- try:
317
- litellm()
318
- cliproxy()
319
- discover_add()
320
- finally:
321
- server.shutdown()
322
- shutil.rmtree(TMP, ignore_errors=True)
323
- print("hub adapter checks pass")
324
- return 0
325
-
326
-
327
- if __name__ == "__main__":
328
- sys.exit(main())
1
+ """The LiteLLM and CLI Proxy API hub adapters against fake hubs: nothing is sent anywhere real.
2
+
3
+ python scripts/test_hubs.py
4
+
5
+ The fakes answer with the shapes each hub's own source produces (recorded
6
+ below from LiteLLM 1.106.1 and CLI Proxy API 8.0.36, 2026-10-21; hosts and
7
+ keys made up), and refuse a wrong key as the real ones do. Checked: a dry run
8
+ writes nothing; a real add sends what it should and no key; LiteLLM without
9
+ its database mode is refused with what to change, before or at /model/new;
10
+ CLI Proxy API refuses with no management key before sending anything, never
11
+ gets the API key on a management route, and stops when its list changes
12
+ underneath; Discover offers each hub's connections and its Add goes through
13
+ the adapter of the endpoint's hub. BENCHWARMERS_HOME and LLM_USER_PATH point
14
+ at a temp folder before anything is imported.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import copy
20
+ import json
21
+ import os
22
+ import pathlib
23
+ import shutil
24
+ import sys
25
+ import tempfile
26
+ import threading
27
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
28
+
29
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
30
+ TMP = pathlib.Path(tempfile.mkdtemp(prefix="bench-hubs-"))
31
+ os.environ["BENCHWARMERS_HOME"] = str(TMP)
32
+ os.environ["LLM_USER_PATH"] = str(TMP / "llm")
33
+ (TMP / "config").mkdir()
34
+ (TMP / "llm").mkdir()
35
+ # Keys from the environment, as a container would have them. LiteLLM's API key
36
+ # is its master key (no admin key: the fallback); CLI Proxy API's management
37
+ # key is its own.
38
+ os.environ.update(BENCHWARMERS_KEY_LITE="sk-master-test", BENCHWARMERS_KEY_CPA="cpa-api-test",
39
+ BENCHWARMERS_KEY_CPA_ADMIN="mgmt-test", BENCHWARMERS_KEY_PLAIN="sk-master-test")
40
+ sys.path[:0] = [str(ROOT), str(ROOT / "metrics"), str(ROOT / "suite"), str(ROOT / "scripts")]
41
+
42
+ # LiteLLM 1.106.1: GET /model/info (api_key removed, secrets masked), the readiness details,
43
+ # POST /model/new's stored row, and its errors as ProxyException bodies.
44
+ LITE_DEPLOYMENTS = [
45
+ {"model_name": "llama-3.3-70b",
46
+ "litellm_params": {"model": "groq/llama-3.3-70b-versatile", "litellm_credential_name": "groq-cred",
47
+ "max_retries": 2, "rpm": 30, "input_cost_per_token": 5.9e-07},
48
+ "model_info": {"id": "9b1c7e", "db_model": True, "direct_access": True, "access_via_team_ids": []}},
49
+ {"model_name": "mistral-small",
50
+ "litellm_params": {"model": "openai/mistral-small-latest", "api_base": "https://api.mistral.example/v1",
51
+ "extra_headers": {"Authorization": "Bear*****************9f2c"}, "timeout": 60},
52
+ "model_info": {"id": "41aa02", "db_model": False}}]
53
+ LITE_READY = {"status": "healthy", "db": "connected", "cache": None, "litellm_version": "1.106.1",
54
+ "success_callbacks": [], "use_aiohttp_transport": True, "log_level": "WARNING",
55
+ "is_detailed_debug": False, "show_no_redis_warning": False}
56
+ LITE_ERRORS = {
57
+ "no-db": "{'error': \"No DB Connected. Here's how to do it - https://docs.litellm.ai/docs/proxy/virtual_keys\"}",
58
+ "no-store": "{'error': \"Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature.\"}"}
59
+ # CLI Proxy API 8.0.36: GET /v0/management/openai-compatibility (upstream keys included, as it sends them).
60
+ CPA_PROVIDERS = [
61
+ {"name": "openrouter", "disabled": False, "base-url": "https://openrouter.example/api/v1",
62
+ "api-key-entries": [{"api-key": "sk-or-upstream-never-sent", "auth-index": "a1"}],
63
+ "models": [{"name": "vendor/old:free", "alias": "old-free"}], "auth-index": "c3"},
64
+ {"name": "team", "disabled": False, "prefix": "team", "base-url": "https://llm.team.example/v1",
65
+ "api-key-entries": [{"api-key": "team-upstream-never-sent"}], "models": [{"name": "m1", "alias": "m1"}]}]
66
+
67
+ S: dict = {}
68
+ SEEN: list[tuple[str, str, dict | None]] = []
69
+
70
+
71
+ def reset(**over) -> None:
72
+ S.clear()
73
+ S.update(lite=copy.deepcopy(LITE_DEPLOYMENTS), ready=dict(LITE_READY), store=True,
74
+ cpa=copy.deepcopy(CPA_PROVIDERS), version="8.0.36", race=False)
75
+ S.update(over)
76
+ SEEN.clear()
77
+
78
+
79
+ def writes() -> list[str]:
80
+ return [p for m, p, _b in SEEN if m in ("POST", "PATCH", "PUT", "DELETE")]
81
+
82
+
83
+ class Fake(BaseHTTPRequestHandler):
84
+ def _route(self, method): # noqa: C901
85
+ n = int(self.headers.get("Content-Length") or 0)
86
+ body = json.loads(self.rfile.read(n)) if n else None
87
+ SEEN.append((method, self.path, body))
88
+ auth = self.headers.get("Authorization", "")
89
+ if self.path.startswith("/lite/"):
90
+ if auth != "Bearer sk-master-test":
91
+ return self._send({"error": {"message": "Authentication Error, Invalid proxy server token passed.",
92
+ "type": "auth_error", "param": "None", "code": "401"}}, 401)
93
+ path = self.path[len("/lite"):]
94
+ if path == "/health/readiness/details":
95
+ return self._send(S["ready"])
96
+ if path == "/model/info":
97
+ return self._send({"data": S["lite"]})
98
+ if path == "/v1/models":
99
+ return self._send({"data": [{"id": d["model_name"], "object": "model"} for d in S["lite"]],
100
+ "object": "list"})
101
+ if path == "/model/new" and method == "POST":
102
+ if S["ready"]["db"] != "connected" or not S["store"]:
103
+ msg = LITE_ERRORS["no-db" if S["ready"]["db"] != "connected" else "no-store"]
104
+ return self._send({"error": {"message": msg, "type": "auth_error", "param": "None",
105
+ "code": "500"}}, 500)
106
+ S["lite"].append({**body, "model_info": {**body["model_info"], "id": "77e0d1", "db_model": True}})
107
+ return self._send({"model_id": "77e0d1", "model_name": body["model_name"],
108
+ "litellm_params": "gAAAAB-encrypted-row", "model_info": body["model_info"],
109
+ "created_by": "default_user_id"})
110
+ if self.path.startswith("/cpa/"):
111
+ path = self.path[len("/cpa"):]
112
+ if path == "/v1/models":
113
+ if auth != "Bearer cpa-api-test":
114
+ return self._send({"error": "invalid api key"}, 401)
115
+ ids = [(f"{e['prefix']}/" if e.get("prefix") else "") + (m.get("alias") or m["name"])
116
+ for e in S["cpa"] for m in e["models"]]
117
+ return self._send({"data": [{"id": i, "object": "model"} for i in ids], "object": "list"})
118
+ if path == "/v0/management/openai-compatibility":
119
+ if auth != "Bearer mgmt-test":
120
+ return self._send({"error": "invalid management key"}, 401)
121
+ if method == "GET":
122
+ return self._send({"openai-compatibility": S["cpa"]}, headers={"X-CPA-VERSION": S["version"]})
123
+ if method == "PATCH":
124
+ entry = next((e for e in S["cpa"] if e["name"] == body["name"]), None)
125
+ if entry is None:
126
+ return self._send({"error": "item not found"}, 404)
127
+ entry["models"] = body["value"]["models"] # replaced whole, as the real one does
128
+ if S["race"]: # someone else's stale write lands after ours
129
+ entry["models"] = entry["models"][:-1]
130
+ return self._send({"status": "ok"}, headers={"X-CPA-VERSION": S["version"]})
131
+ self._send({"detail": "Not Found"}, 404)
132
+
133
+ def do_GET(self): # noqa: N802
134
+ self._route("GET")
135
+
136
+ def do_POST(self): # noqa: N802
137
+ self._route("POST")
138
+
139
+ def do_PATCH(self): # noqa: N802
140
+ self._route("PATCH")
141
+
142
+ def _send(self, data, status=200, headers=None):
143
+ raw = json.dumps(data).encode()
144
+ self.send_response(status)
145
+ self.send_header("Content-Type", "application/json")
146
+ for k, v in (headers or {}).items():
147
+ self.send_header(k, v)
148
+ self.send_header("Content-Length", str(len(raw)))
149
+ self.end_headers()
150
+ self.wfile.write(raw)
151
+
152
+ def log_message(self, *_a):
153
+ pass
154
+
155
+
156
+ def refused(fn, *args) -> str:
157
+ try:
158
+ fn(*args)
159
+ except ValueError as exc:
160
+ return str(exc)
161
+ raise AssertionError(f"{fn.__module__}.{fn.__name__}{args} was not refused")
162
+
163
+
164
+ def litellm() -> None:
165
+ import litellm_hub as lite
166
+ reset()
167
+ p = lite.plan("groq", "moonshotai/kimi-k2") # dry run
168
+ assert writes() == [], writes()
169
+ assert p["served"] == "moonshotai/kimi-k2" and p["deployment"] == "create" and p["version_note"] == "", p
170
+ assert p["litellm_params"] == {"model": "groq/moonshotai/kimi-k2", "litellm_credential_name": "groq-cred",
171
+ "max_retries": 2}, p["litellm_params"] # no price, no rpm, no key
172
+ assert p["database_mode"] is True and "groq-cred" in p["key"], p
173
+ # A masked header is never sent back; the api_base host is the connection.
174
+ m = lite.plan("api.mistral.example", "mistral-large")
175
+ assert m["litellm_params"] == {"model": "openai/mistral-large", "api_base": "https://api.mistral.example/v1",
176
+ "timeout": 60}, m["litellm_params"]
177
+ assert m["not_copied"] == ["extra_headers"], m # said, not silently dropped
178
+ assert set(lite.upstreams()) == {"groq", "api.mistral.example"}
179
+ assert "already serves" in refused(lite.plan, "groq", "mistral-small") # one name, two upstreams
180
+
181
+ served, done = lite.add("groq", "moonshotai/kimi-k2") # a real add
182
+ sent = [b for meth, path, b in SEEN if path == "/lite/model/new"]
183
+ assert served == "moonshotai/kimi-k2" and len(sent) == 1, SEEN
184
+ assert sent[0] == {"model_name": served, "litellm_params": p["litellm_params"],
185
+ "model_info": {"tags": ["groq"]}}, sent[0]
186
+ assert "api_key" not in json.dumps(sent[0]) and "gAAAAB" not in " ".join(done), done
187
+ assert any("77e0d1" in d for d in done), done
188
+ assert lite.add("groq", "moonshotai/kimi-k2")[1][-1].endswith("nothing added") # twice: no second deployment
189
+
190
+ # No database: refused by the dry run, before /model/new.
191
+ reset(ready={**LITE_READY, "db": "Not connected"})
192
+ assert "DATABASE_URL" in refused(lite.add, "groq", "x") and writes() == [], writes()
193
+ # A database, but models kept in config.yaml: /model/new refuses, nothing is added.
194
+ reset(store=False)
195
+ assert "STORE_MODEL_IN_DB=True" in refused(lite.add, "groq", "x") and len(S["lite"]) == 2
196
+ # Another release is said, not refused.
197
+ reset(ready={**LITE_READY, "litellm_version": "1.107.2"})
198
+ assert "1.107.2" in lite.plan("groq", "x")["version_note"]
199
+
200
+
201
+ def cliproxy() -> None:
202
+ import cliproxy as cpa
203
+ reset()
204
+ p = cpa.plan("openrouter", "vendor/new:free") # dry run
205
+ assert writes() == [] and p["add_to_list"] and p["served"] == "vendor/new:free" and not p["version_note"], p
206
+ assert cpa.plan("team", "m2")["served"] == "team/m2" # a prefix names the served id
207
+ assert cpa.plan("openrouter", "old-free")["add_to_list"] is False # listed by its alias already
208
+ assert cpa.plan("team", "m1")["add_to_list"] is False # its own list: not a collision
209
+ assert "already serves" in refused(cpa.plan, "openrouter", "m1") # team serves a bare m1 too
210
+ assert cpa.upstreams() == {"openrouter": ("https://openrouter.example/api/v1", ""),
211
+ "team": ("https://llm.team.example/v1", "team")}
212
+
213
+ served, done = cpa.add("openrouter", "vendor/new:free") # a real add
214
+ patches = [b for meth, _p, b in SEEN if meth == "PATCH"]
215
+ assert served == "vendor/new:free" and len(patches) == 1, SEEN
216
+ assert patches[0] == {"name": "openrouter", "value": {"models": [
217
+ {"name": "vendor/old:free", "alias": "old-free"}, {"name": "vendor/new:free", "alias": "vendor/new:free"}]}}
218
+ assert "never-sent" not in json.dumps(patches[0]) # no upstream key goes back
219
+ assert S["cpa"][1] == CPA_PROVIDERS[1], "another provider changed"
220
+ gets = [i for i, (meth, path, _b) in enumerate(SEEN) if meth == "GET" and "management" in path]
221
+ at = next(i for i, (meth, _p, _b) in enumerate(SEEN) if meth == "PATCH")
222
+ assert any(i < at for i in gets) and any(i > at for i in gets), "not read just before and after"
223
+ assert done[-1].startswith("added vendor/new:free"), done # the fake took only the management key
224
+ SEEN.clear() # listed already: the fresh read decides
225
+ assert cpa.add("openrouter", "old-free")[1][0].endswith("nothing changed") and writes() == []
226
+
227
+ # A provider gone by the PATCH is said as that, not as the management API being off.
228
+ assert "left CLI Proxy API" in refused(cpa._call, "PATCH", {"name": "gone", "value": {"models": []}})
229
+ # The list changes underneath: stop and say so.
230
+ reset(race=True)
231
+ assert "changed while this wrote it" in refused(cpa.add, "openrouter", "vendor/new:free")
232
+ # No management key stored: nothing is sent at all.
233
+ reset()
234
+ saved = os.environ.pop("BENCHWARMERS_KEY_CPA_ADMIN")
235
+ try:
236
+ msg = refused(cpa.plan, "openrouter", "x")
237
+ assert "BENCHWARMERS_KEY_CPA_ADMIN" in msg and SEEN == [], (msg, SEEN)
238
+ finally:
239
+ os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = saved
240
+ # A wrong management key: said once, with the ban in mind, never retried.
241
+ os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = "wrong"
242
+ try:
243
+ assert "30 minutes" in refused(cpa.plan, "openrouter", "x") and len(SEEN) == 1, SEEN
244
+ finally:
245
+ os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = saved
246
+ reset(version="v8.1.0")
247
+ assert "8.1.0" in cpa.plan("openrouter", "x")["version_note"]
248
+
249
+
250
+ def discover_add() -> None:
251
+ """Discover's routes and Add: each hub through its own adapter."""
252
+ import discover
253
+ import endpoints
254
+ import owui
255
+ import serve
256
+ reset()
257
+ rows = [{"provider": "groq", "provider_name": "Groq", "model": "vendor/new", "connection": None, "api": None},
258
+ {"provider": "openrouter", "provider_name": "OpenRouter", "model": "vendor/x:free",
259
+ "connection": "openrouter", "api": "https://openrouter.example/api/v1/"},
260
+ {"provider": "mistral", "provider_name": "Mistral", "model": "mistral-large", "connection": None,
261
+ "api": "https://api.mistral.example/v1"},
262
+ {"provider": "nobody", "provider_name": "Nobody", "model": "n", "connection": None, "api": None}]
263
+ routes, errors = discover.hub_routes(rows)
264
+ assert errors == [] and writes() == [], (errors, writes())
265
+ got = [[(x["hub"], x["connection"], x["endpoint"]) for x in r] for r in routes]
266
+ assert got == [[("litellm", "groq", "lite")], [("cliproxy", "openrouter", "cpa")],
267
+ [("litellm", "api.mistral.example", "lite")], []], got
268
+ lite_steps, cpa_steps = routes[0][0]["steps"][0], routes[1][0]["steps"][0]
269
+ assert "tags:" in lite_steps and "logo" not in lite_steps, lite_steps # only what each hub takes
270
+ assert "tags" not in cpa_steps and "logo" not in cpa_steps and "access" not in cpa_steps, cpa_steps
271
+ assert routes[1][0]["on_bench_as"] == "x-free", routes[1][0] # registered on cpa below
272
+ # A hub that cannot be asked is said, and the rest still offered.
273
+ os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = ""
274
+ try:
275
+ routes, errors = discover.hub_routes(rows[:2])
276
+ assert len(errors) == 1 and "CLI Proxy API" in errors[0] and [len(r) for r in routes] == [1, 0], errors
277
+ finally:
278
+ os.environ["BENCHWARMERS_KEY_CPA_ADMIN"] = "mgmt-test"
279
+
280
+ onboarded = []
281
+ serve.queue_onboard = lambda items, who="ui": onboarded.append((items[0]["served"], items[0]["endpoint"])) or {
282
+ "ok": True, "errors": [], "items": []}
283
+ owui.add = lambda c, m: (m, [f"owui added {m} on {c}"])
284
+ r = serve.discover_add("groq", "vendor/new", endpoint="lite")
285
+ assert r["ok"] and "/lite/model/new" in writes() and onboarded[-1] == ("vendor/new", "lite"), r
286
+ r = serve.discover_add("openrouter", "vendor/y:free", endpoint="cpa")
287
+ assert r["ok"] and onboarded[-1] == ("vendor/y:free", "cpa"), r
288
+ assert [m for m, p, _b in SEEN if "management" in p and m == "PATCH"] == ["PATCH"]
289
+ r = serve.discover_add("groq-api", "vendor/z") # no endpoint: Open WebUI, as before
290
+ assert r["steps"] == ["owui added vendor/z on groq-api"] and onboarded[-1] == ("vendor/z", "owui"), r
291
+ assert serve.discover_add("groq-api", "vendor/z", endpoint="owui")["ok"]
292
+ assert "not a hub" in serve.discover_add("groq", "v/z", endpoint="plain")["errors"][0]
293
+ changes = [json.loads(line)["field"] for line in serve.CHANGES_PATH.read_text(encoding="utf-8").splitlines()]
294
+ assert {"litellm-add", "cliproxy-add", "openwebui-add"} <= set(changes), changes
295
+
296
+ # The admin key from the Endpoints form: kept as <key>-admin, never in the reply.
297
+ r = serve.save_endpoint("lite", endpoints.load()["lite"]["base"], "", admin="sk-master-test")
298
+ assert r["ok"] and "lite-admin" in r["note"] and "sk-master" not in json.dumps(r), r
299
+ assert endpoints._stored()["lite-admin"] == "sk-master-test"
300
+ assert serve.save_endpoint("plain", endpoints.load()["plain"]["base"], "", admin="nope")["ok"]
301
+ assert "plain-admin" not in endpoints._stored(), "a provider got an admin key"
302
+
303
+
304
+ def main() -> int:
305
+ server = ThreadingHTTPServer(("127.0.0.1", 0), Fake)
306
+ threading.Thread(target=server.serve_forever, daemon=True).start()
307
+ fake = f"http://127.0.0.1:{server.server_port}"
308
+ (TMP / "config" / "endpoints.json").write_text(json.dumps({
309
+ "owui": {"base": fake + "/api", "key": "owui", "default": True}, # kind inferred, as the owner's
310
+ "lite": {"base": fake + "/lite/v1", "key": "lite", "kind": "hub", "hub": "litellm"},
311
+ "cpa": {"base": fake + "/cpa/v1", "key": "cpa", "kind": "hub", "hub": "cliproxy"},
312
+ "plain": {"base": fake + "/lite/v1", "key": "plain", "kind": "provider"}}), encoding="utf-8")
313
+ import endpoints
314
+ (TMP / "config" / "extra-openai-models.yaml").write_text(endpoints.yaml_block("x-free", "vendor/x:free", "cpa"),
315
+ encoding="utf-8")
316
+ try:
317
+ litellm()
318
+ cliproxy()
319
+ discover_add()
320
+ finally:
321
+ server.shutdown()
322
+ shutil.rmtree(TMP, ignore_errors=True)
323
+ print("hub adapter checks pass")
324
+ return 0
325
+
326
+
327
+ if __name__ == "__main__":
328
+ sys.exit(main())