@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.
- package/AGENTS.md +152 -147
- package/BENCHMARKS.md +262 -262
- package/GATEWAY.md +415 -272
- package/NOTICE +28 -28
- package/PRIVACY.md +46 -46
- package/README.md +408 -314
- package/SECURITY.md +83 -83
- package/TERMS.md +22 -22
- package/USAGE.md +186 -163
- package/VERSION +1 -1
- package/bin/benchwarmers.js +16 -74
- package/cli.py +674 -0
- package/examples/config/gateway.json +5 -5
- package/examples/config/model-roles.json +11 -11
- package/gateway.py +939 -755
- package/launcher.py +120 -0
- package/mcp_server.py +266 -266
- package/metrics/alerts.py +116 -116
- package/metrics/aliases.py +41 -41
- package/metrics/bench_collections.py +141 -141
- package/metrics/catalog.py +304 -295
- package/metrics/client.py +804 -394
- package/metrics/cliproxy.py +191 -191
- package/metrics/connect.py +691 -691
- package/metrics/cost.py +171 -152
- package/metrics/decide.py +227 -227
- package/metrics/discover.py +318 -318
- package/metrics/endpoints.py +436 -436
- package/metrics/formats.py +1021 -0
- package/metrics/guard.py +647 -592
- package/metrics/learn.py +234 -221
- package/metrics/ledger.py +148 -0
- package/metrics/litellm_hub.py +206 -206
- package/metrics/model_roles.py +94 -84
- package/metrics/offload.py +166 -166
- package/metrics/owui.py +205 -205
- package/metrics/pacing.py +248 -0
- package/metrics/paths.py +60 -60
- package/metrics/quota.py +383 -0
- package/metrics/route.py +581 -579
- package/metrics/scheduler.py +275 -275
- package/metrics/sources.py +167 -167
- package/metrics/wrong.py +144 -144
- package/package.json +69 -65
- package/perf/README.md +89 -89
- package/perf/run.py +284 -261
- package/scripts/auto_retire.py +367 -367
- package/scripts/check.py +113 -106
- package/scripts/check_deprecations.py +378 -378
- package/scripts/check_themes.py +103 -103
- package/scripts/init.py +124 -124
- package/scripts/list_served.py +78 -78
- package/scripts/notice.py +78 -78
- package/scripts/onboard.py +410 -331
- package/scripts/provider_tags.py +129 -129
- package/scripts/reprefix.py +136 -136
- package/scripts/sync_config.py +105 -105
- package/scripts/test_auth.py +255 -255
- package/scripts/test_cli.py +189 -0
- package/scripts/test_client.py +277 -233
- package/scripts/test_connect.py +222 -222
- package/scripts/test_first_run.py +180 -180
- package/scripts/test_formats.py +549 -0
- package/scripts/test_hubs.py +328 -328
- package/scripts/test_launcher.py +67 -0
- package/scripts/test_llm_log.py +85 -81
- package/scripts/test_mcp.py +178 -178
- package/scripts/test_merged.py +215 -214
- package/scripts/test_no_owui.py +291 -291
- package/scripts/test_owui.py +90 -90
- package/scripts/test_quota.py +334 -0
- package/scripts/test_serve_edit.py +453 -438
- package/scripts/test_server_json.py +79 -0
- package/scripts/test_verdict.py +102 -98
- package/scripts/test_vision.py +231 -228
- package/serve.py +3972 -3764
- package/suite/mine.py +154 -154
- package/suite/pace.py +145 -145
- package/suite/run.py +1046 -1042
- package/suite/seed.py +421 -421
- package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
- package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
- package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
- package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
- package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
- package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
- package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
- package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
- package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
- package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
- package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
- package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
- package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
- package/ui/bench.css +430 -425
- package/ui/bench.js +1757 -1576
- package/ui/themes/dark.css +35 -35
- package/ui/themes/hardwood.css +52 -52
- package/ui/themes/rainforest.css +32 -32
package/scripts/test_hubs.py
CHANGED
|
@@ -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())
|