@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
|
@@ -1,438 +1,453 @@
|
|
|
1
|
-
"""Exercise serve.change_model against a COPY of the config, never the real one.
|
|
2
|
-
|
|
3
|
-
python scripts/test_serve_edit.py
|
|
4
|
-
|
|
5
|
-
Each rule the popup enforces is tried once and must refuse; then a role change
|
|
6
|
-
and a rename are applied to the copy and checked. The llm sync is not run:
|
|
7
|
-
serve.ROOT points at the copy, which has no scripts/sync_config.py, so the
|
|
8
|
-
sync reports that it could not run. Alerts and endpoints point at copies too,
|
|
9
|
-
because removing a model clears its alerts and a regressed endpoint check
|
|
10
|
-
would write the endpoint file.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import json
|
|
16
|
-
import os
|
|
17
|
-
import pathlib
|
|
18
|
-
import shutil
|
|
19
|
-
import sys
|
|
20
|
-
import tempfile
|
|
21
|
-
|
|
22
|
-
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
23
|
-
sys.path.insert(0, str(ROOT / "metrics"))
|
|
24
|
-
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
25
|
-
sys.path.insert(0, str(ROOT))
|
|
26
|
-
import serve # noqa: E402
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def main() -> int:
|
|
30
|
-
tmp = pathlib.Path(tempfile.mkdtemp(prefix="bench-edit-"))
|
|
31
|
-
cfg = tmp / "config"
|
|
32
|
-
cfg.mkdir()
|
|
33
|
-
for name in ("model-roles.json", "extra-openai-models.yaml",
|
|
34
|
-
"alias-history.json", "model-options.json", "provider-tags.json"):
|
|
35
|
-
shutil.copy(DATA / "config" / name, cfg / name)
|
|
36
|
-
(tmp / "scripts").mkdir()
|
|
37
|
-
(tmp / "metrics").mkdir()
|
|
38
|
-
shutil.copy(ROOT / "metrics" / "aliases.py", tmp / "metrics" / "aliases.py")
|
|
39
|
-
# point serve at the copy
|
|
40
|
-
serve.ROOT = tmp
|
|
41
|
-
serve.DATA = tmp # the data paths serve builds inline follow this one
|
|
42
|
-
serve.ROLES_PATH = cfg / "model-roles.json"
|
|
43
|
-
serve.YAML_PATH = cfg / "extra-openai-models.yaml"
|
|
44
|
-
serve.HISTORY_PATH = cfg / "alias-history.json"
|
|
45
|
-
serve.OPTIONS_PATH = cfg / "model-options.json"
|
|
46
|
-
serve.CHANGES_PATH = cfg / "changes.jsonl"
|
|
47
|
-
serve.RETIRED_PATH = cfg / "retired.json"
|
|
48
|
-
import aliases
|
|
49
|
-
aliases.HISTORY = cfg / "alias-history.json"
|
|
50
|
-
import alerts
|
|
51
|
-
shutil.copy(DATA / "metrics" / "alerts.json", tmp / "metrics" / "alerts.json")
|
|
52
|
-
alerts.FILE = tmp / "metrics" / "alerts.json"
|
|
53
|
-
# Decision models too: a rename or removal of one writes decide.FILE, and
|
|
54
|
-
# which model the rename picks depends on the data (sorted first).
|
|
55
|
-
import decide
|
|
56
|
-
if (DATA / "config" / "decision-models.json").exists():
|
|
57
|
-
shutil.copy(DATA / "config" / "decision-models.json", cfg / "decision-models.json")
|
|
58
|
-
decide.FILE = cfg / "decision-models.json"
|
|
59
|
-
shutil.copy(DATA / "config" / "endpoints.json", cfg / "endpoints.json")
|
|
60
|
-
serve.endpoints.FILE = cfg / "endpoints.json"
|
|
61
|
-
serve.endpoints.YAML = serve.YAML_PATH
|
|
62
|
-
# Collections: removing a model takes it out of them, and Restore puts it back.
|
|
63
|
-
serve.bench_collections.FILE = cfg / "collections.json"
|
|
64
|
-
serve.bench_collections.SETTINGS_FILE = cfg / "gateway.json"
|
|
65
|
-
real = {p: p.read_bytes() for p in (DATA / "metrics" / "alerts.json", DATA / "config" / "endpoints.json")}
|
|
66
|
-
real_collections = (DATA / "config" / "collections.json").read_bytes() \
|
|
67
|
-
if (DATA / "config" / "collections.json").exists() else None
|
|
68
|
-
|
|
69
|
-
d = serve.model_directory()
|
|
70
|
-
some = sorted(d)[0]
|
|
71
|
-
retired = next(iter(json.loads((cfg / "alias-history.json").read_text(encoding="utf-8"))))
|
|
72
|
-
other = next(a for a in d if a != some)
|
|
73
|
-
failures = 0
|
|
74
|
-
|
|
75
|
-
def expect_refused(label, **kw):
|
|
76
|
-
nonlocal failures
|
|
77
|
-
r = serve.change_model(**kw)
|
|
78
|
-
ok = (not r["ok"]) and r["errors"] and not r["changed"]
|
|
79
|
-
print(("ok " if ok else "FAIL ") + label + ("" if ok else f" -> {r}"))
|
|
80
|
-
failures += 0 if ok else 1
|
|
81
|
-
|
|
82
|
-
expect_refused("unknown alias", alias="no-such-model", new_alias="x", new_role="general")
|
|
83
|
-
expect_refused("nothing to change", alias=some, new_alias=some, new_role=d[some]["role"])
|
|
84
|
-
expect_refused("unknown role", alias=some, new_alias=some, new_role="wizard")
|
|
85
|
-
expect_refused("alias with a space", alias=some, new_alias="bad name", new_role=d[some]["role"])
|
|
86
|
-
expect_refused("alias taken", alias=some, new_alias=other, new_role=d[some]["role"])
|
|
87
|
-
expect_refused("retired alias reused", alias=some, new_alias=retired, new_role=d[some]["role"])
|
|
88
|
-
expect_refused("case-only collision", alias=some, new_alias=other.upper(), new_role=d[some]["role"])
|
|
89
|
-
|
|
90
|
-
# role change
|
|
91
|
-
new_role = "code" if d[some]["role"] != "code" else "general"
|
|
92
|
-
r = serve.change_model(some, some, new_role)
|
|
93
|
-
ok = r["ok"] and serve.model_directory()[some]["role"] == new_role
|
|
94
|
-
print(("ok " if ok else "FAIL ") + f"role change {some}: {d[some]['role']} -> {new_role}")
|
|
95
|
-
failures += 0 if ok else 1
|
|
96
|
-
|
|
97
|
-
# ad hoc: a flag, not a role — setting it leaves the role alone
|
|
98
|
-
expect_refused("ad_hoc not a bool", alias=some, new_alias=some, new_role=None,
|
|
99
|
-
new_ad_hoc="yes")
|
|
100
|
-
r = serve.change_model(some, some, None, new_ad_hoc=True)
|
|
101
|
-
ok = (r["ok"] and serve.model_directory()[some]["ad_hoc"] is True
|
|
102
|
-
and serve.model_directory()[some]["role"] == new_role)
|
|
103
|
-
print(("ok " if ok else "FAIL ") + f"ad hoc on {some}, role kept")
|
|
104
|
-
failures += 0 if ok else 1
|
|
105
|
-
expect_refused("ad hoc already set", alias=some, new_alias=some, new_role=None,
|
|
106
|
-
new_ad_hoc=True)
|
|
107
|
-
|
|
108
|
-
# rename
|
|
109
|
-
r = serve.change_model(some, some + "-renamed", None)
|
|
110
|
-
d2 = serve.model_directory()
|
|
111
|
-
hist = json.loads((cfg / "alias-history.json").read_text(encoding="utf-8"))
|
|
112
|
-
roles = json.loads((cfg / "model-roles.json").read_text(encoding="utf-8"))["models"]
|
|
113
|
-
held = json.loads((cfg / "model-roles.json").read_text(encoding="utf-8"))["ad_hoc"]
|
|
114
|
-
ok = (r["ok"] and some not in d2 and some + "-renamed" in d2
|
|
115
|
-
and some + "-renamed" in held and some not in held
|
|
116
|
-
and d2[some + "-renamed"]["served"] == d[some]["served"]
|
|
117
|
-
and hist.get(some) == some + "-renamed"
|
|
118
|
-
and roles.get(some + "-renamed") == new_role and some not in roles
|
|
119
|
-
and aliases.current(some) == some + "-renamed")
|
|
120
|
-
print(("ok " if ok else "FAIL ") + f"rename {some} -> {some}-renamed; sync: {r['sync'][:60]}")
|
|
121
|
-
failures += 0 if ok else 1
|
|
122
|
-
# and the old name can never come back
|
|
123
|
-
expect_refused("old name refused after rename", alias=some + "-renamed",
|
|
124
|
-
new_alias=some, new_role=None)
|
|
125
|
-
lines = (cfg / "changes.jsonl").read_text(encoding="utf-8").splitlines()
|
|
126
|
-
print(("ok " if len(lines) == 3 else "FAIL ") + f"{len(lines)} changes logged")
|
|
127
|
-
failures += 0 if len(lines) == 3 else 1
|
|
128
|
-
|
|
129
|
-
# removal
|
|
130
|
-
victim = some + "-renamed"
|
|
131
|
-
served = d2[victim]["served"]
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
and
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
and
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
view
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
serve.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
("onboard:
|
|
210
|
-
("onboard:
|
|
211
|
-
("onboard:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
shutil.which = lambda name:
|
|
295
|
-
|
|
296
|
-
shutil.which =
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
endpoints_mod.CONNECTIONS =
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
serve.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
and
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
print("
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1
|
+
"""Exercise serve.change_model against a COPY of the config, never the real one.
|
|
2
|
+
|
|
3
|
+
python scripts/test_serve_edit.py
|
|
4
|
+
|
|
5
|
+
Each rule the popup enforces is tried once and must refuse; then a role change
|
|
6
|
+
and a rename are applied to the copy and checked. The llm sync is not run:
|
|
7
|
+
serve.ROOT points at the copy, which has no scripts/sync_config.py, so the
|
|
8
|
+
sync reports that it could not run. Alerts and endpoints point at copies too,
|
|
9
|
+
because removing a model clears its alerts and a regressed endpoint check
|
|
10
|
+
would write the endpoint file.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import pathlib
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
|
|
22
|
+
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
23
|
+
sys.path.insert(0, str(ROOT / "metrics"))
|
|
24
|
+
from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
|
|
25
|
+
sys.path.insert(0, str(ROOT))
|
|
26
|
+
import serve # noqa: E402
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> int:
|
|
30
|
+
tmp = pathlib.Path(tempfile.mkdtemp(prefix="bench-edit-"))
|
|
31
|
+
cfg = tmp / "config"
|
|
32
|
+
cfg.mkdir()
|
|
33
|
+
for name in ("model-roles.json", "extra-openai-models.yaml",
|
|
34
|
+
"alias-history.json", "model-options.json", "provider-tags.json"):
|
|
35
|
+
shutil.copy(DATA / "config" / name, cfg / name)
|
|
36
|
+
(tmp / "scripts").mkdir()
|
|
37
|
+
(tmp / "metrics").mkdir()
|
|
38
|
+
shutil.copy(ROOT / "metrics" / "aliases.py", tmp / "metrics" / "aliases.py")
|
|
39
|
+
# point serve at the copy
|
|
40
|
+
serve.ROOT = tmp
|
|
41
|
+
serve.DATA = tmp # the data paths serve builds inline follow this one
|
|
42
|
+
serve.ROLES_PATH = cfg / "model-roles.json"
|
|
43
|
+
serve.YAML_PATH = cfg / "extra-openai-models.yaml"
|
|
44
|
+
serve.HISTORY_PATH = cfg / "alias-history.json"
|
|
45
|
+
serve.OPTIONS_PATH = cfg / "model-options.json"
|
|
46
|
+
serve.CHANGES_PATH = cfg / "changes.jsonl"
|
|
47
|
+
serve.RETIRED_PATH = cfg / "retired.json"
|
|
48
|
+
import aliases
|
|
49
|
+
aliases.HISTORY = cfg / "alias-history.json"
|
|
50
|
+
import alerts
|
|
51
|
+
shutil.copy(DATA / "metrics" / "alerts.json", tmp / "metrics" / "alerts.json")
|
|
52
|
+
alerts.FILE = tmp / "metrics" / "alerts.json"
|
|
53
|
+
# Decision models too: a rename or removal of one writes decide.FILE, and
|
|
54
|
+
# which model the rename picks depends on the data (sorted first).
|
|
55
|
+
import decide
|
|
56
|
+
if (DATA / "config" / "decision-models.json").exists():
|
|
57
|
+
shutil.copy(DATA / "config" / "decision-models.json", cfg / "decision-models.json")
|
|
58
|
+
decide.FILE = cfg / "decision-models.json"
|
|
59
|
+
shutil.copy(DATA / "config" / "endpoints.json", cfg / "endpoints.json")
|
|
60
|
+
serve.endpoints.FILE = cfg / "endpoints.json"
|
|
61
|
+
serve.endpoints.YAML = serve.YAML_PATH
|
|
62
|
+
# Collections: removing a model takes it out of them, and Restore puts it back.
|
|
63
|
+
serve.bench_collections.FILE = cfg / "collections.json"
|
|
64
|
+
serve.bench_collections.SETTINGS_FILE = cfg / "gateway.json"
|
|
65
|
+
real = {p: p.read_bytes() for p in (DATA / "metrics" / "alerts.json", DATA / "config" / "endpoints.json")}
|
|
66
|
+
real_collections = (DATA / "config" / "collections.json").read_bytes() \
|
|
67
|
+
if (DATA / "config" / "collections.json").exists() else None
|
|
68
|
+
|
|
69
|
+
d = serve.model_directory()
|
|
70
|
+
some = sorted(d)[0]
|
|
71
|
+
retired = next(iter(json.loads((cfg / "alias-history.json").read_text(encoding="utf-8"))))
|
|
72
|
+
other = next(a for a in d if a != some)
|
|
73
|
+
failures = 0
|
|
74
|
+
|
|
75
|
+
def expect_refused(label, **kw):
|
|
76
|
+
nonlocal failures
|
|
77
|
+
r = serve.change_model(**kw)
|
|
78
|
+
ok = (not r["ok"]) and r["errors"] and not r["changed"]
|
|
79
|
+
print(("ok " if ok else "FAIL ") + label + ("" if ok else f" -> {r}"))
|
|
80
|
+
failures += 0 if ok else 1
|
|
81
|
+
|
|
82
|
+
expect_refused("unknown alias", alias="no-such-model", new_alias="x", new_role="general")
|
|
83
|
+
expect_refused("nothing to change", alias=some, new_alias=some, new_role=d[some]["role"])
|
|
84
|
+
expect_refused("unknown role", alias=some, new_alias=some, new_role="wizard")
|
|
85
|
+
expect_refused("alias with a space", alias=some, new_alias="bad name", new_role=d[some]["role"])
|
|
86
|
+
expect_refused("alias taken", alias=some, new_alias=other, new_role=d[some]["role"])
|
|
87
|
+
expect_refused("retired alias reused", alias=some, new_alias=retired, new_role=d[some]["role"])
|
|
88
|
+
expect_refused("case-only collision", alias=some, new_alias=other.upper(), new_role=d[some]["role"])
|
|
89
|
+
|
|
90
|
+
# role change
|
|
91
|
+
new_role = "code" if d[some]["role"] != "code" else "general"
|
|
92
|
+
r = serve.change_model(some, some, new_role)
|
|
93
|
+
ok = r["ok"] and serve.model_directory()[some]["role"] == new_role
|
|
94
|
+
print(("ok " if ok else "FAIL ") + f"role change {some}: {d[some]['role']} -> {new_role}")
|
|
95
|
+
failures += 0 if ok else 1
|
|
96
|
+
|
|
97
|
+
# ad hoc: a flag, not a role — setting it leaves the role alone
|
|
98
|
+
expect_refused("ad_hoc not a bool", alias=some, new_alias=some, new_role=None,
|
|
99
|
+
new_ad_hoc="yes")
|
|
100
|
+
r = serve.change_model(some, some, None, new_ad_hoc=True)
|
|
101
|
+
ok = (r["ok"] and serve.model_directory()[some]["ad_hoc"] is True
|
|
102
|
+
and serve.model_directory()[some]["role"] == new_role)
|
|
103
|
+
print(("ok " if ok else "FAIL ") + f"ad hoc on {some}, role kept")
|
|
104
|
+
failures += 0 if ok else 1
|
|
105
|
+
expect_refused("ad hoc already set", alias=some, new_alias=some, new_role=None,
|
|
106
|
+
new_ad_hoc=True)
|
|
107
|
+
|
|
108
|
+
# rename
|
|
109
|
+
r = serve.change_model(some, some + "-renamed", None)
|
|
110
|
+
d2 = serve.model_directory()
|
|
111
|
+
hist = json.loads((cfg / "alias-history.json").read_text(encoding="utf-8"))
|
|
112
|
+
roles = json.loads((cfg / "model-roles.json").read_text(encoding="utf-8"))["models"]
|
|
113
|
+
held = json.loads((cfg / "model-roles.json").read_text(encoding="utf-8"))["ad_hoc"]
|
|
114
|
+
ok = (r["ok"] and some not in d2 and some + "-renamed" in d2
|
|
115
|
+
and some + "-renamed" in held and some not in held
|
|
116
|
+
and d2[some + "-renamed"]["served"] == d[some]["served"]
|
|
117
|
+
and hist.get(some) == some + "-renamed"
|
|
118
|
+
and roles.get(some + "-renamed") == new_role and some not in roles
|
|
119
|
+
and aliases.current(some) == some + "-renamed")
|
|
120
|
+
print(("ok " if ok else "FAIL ") + f"rename {some} -> {some}-renamed; sync: {r['sync'][:60]}")
|
|
121
|
+
failures += 0 if ok else 1
|
|
122
|
+
# and the old name can never come back
|
|
123
|
+
expect_refused("old name refused after rename", alias=some + "-renamed",
|
|
124
|
+
new_alias=some, new_role=None)
|
|
125
|
+
lines = (cfg / "changes.jsonl").read_text(encoding="utf-8").splitlines()
|
|
126
|
+
print(("ok " if len(lines) == 3 else "FAIL ") + f"{len(lines)} changes logged")
|
|
127
|
+
failures += 0 if len(lines) == 3 else 1
|
|
128
|
+
|
|
129
|
+
# removal
|
|
130
|
+
victim = some + "-renamed"
|
|
131
|
+
served = d2[victim]["served"]
|
|
132
|
+
r = serve.remove_model("no-such", "whatever it is")
|
|
133
|
+
ok = not r["ok"] and not r["changed"]
|
|
134
|
+
print(("ok " if ok else "FAIL ") + "remove: unknown alias"); failures += 0 if ok else 1
|
|
135
|
+
# In three collections: a member of two, the merger of one; a fourth without it.
|
|
136
|
+
colls = {"with-it": {"members": [other, victim], "merger": victim, "reply": "merged"},
|
|
137
|
+
"member": {"members": [victim, other], "merger": ""},
|
|
138
|
+
"doomed": {"members": [victim], "merger": ""},
|
|
139
|
+
"without": {"members": [other], "merger": other}}
|
|
140
|
+
(cfg / "collections.json").write_text(json.dumps(colls), encoding="utf-8")
|
|
141
|
+
logged = len(serve.CHANGES_PATH.read_text(encoding="utf-8").splitlines())
|
|
142
|
+
before = len(serve.registered())
|
|
143
|
+
r = serve.remove_model(victim, "retired by provider on 2026-09-15")
|
|
144
|
+
d3 = serve.model_directory()
|
|
145
|
+
retired = json.loads((cfg / "retired.json").read_text(encoding="utf-8"))
|
|
146
|
+
yaml = (cfg / "extra-openai-models.yaml").read_text(encoding="utf-8")
|
|
147
|
+
ok = (r["ok"] and victim not in d3 and len(serve.registered()) == before - 1
|
|
148
|
+
and retired[victim]["served"] == served and f"model_id: {victim}\n" not in yaml
|
|
149
|
+
and victim not in json.loads((cfg / "model-roles.json").read_text(encoding="utf-8"))["models"]
|
|
150
|
+
and victim not in json.loads((cfg / "model-roles.json").read_text(encoding="utf-8"))["ad_hoc"]
|
|
151
|
+
and served not in serve.registered())
|
|
152
|
+
print(("ok " if ok else "FAIL ") + f"removed {victim}; registry {before} -> {len(serve.registered())}")
|
|
153
|
+
failures += 0 if ok else 1
|
|
154
|
+
after = json.loads((cfg / "collections.json").read_text(encoding="utf-8"))
|
|
155
|
+
log = [json.loads(x) for x in serve.CHANGES_PATH.read_text(encoding="utf-8").splitlines()[logged:]]
|
|
156
|
+
ok = (after == {"with-it": {"members": [other], "merger": "", "reply": "merged"},
|
|
157
|
+
"member": {"members": [other], "merger": ""},
|
|
158
|
+
"doomed": {"members": [], "merger": ""},
|
|
159
|
+
"without": colls["without"]}
|
|
160
|
+
and retired[victim]["collections"] == {"with-it": ["members", "merger"], "member": ["members"],
|
|
161
|
+
"doomed": ["members"]}
|
|
162
|
+
and "config/collections.json" in r["changed"]
|
|
163
|
+
and sorted(e["field"] for e in log if e["field"].startswith("collection "))
|
|
164
|
+
== ["collection doomed", "collection member", "collection with-it"])
|
|
165
|
+
print(("ok " if ok else "FAIL ") + "removed from its collections, recorded and logged"
|
|
166
|
+
+ ("" if ok else f" -> {after} {retired[victim]} {r['changed']} {log}")); failures += 0 if ok else 1
|
|
167
|
+
expect_refused("retired name never reused", alias=other, new_alias=victim, new_role=None)
|
|
168
|
+
# The collection dialog's member search matches the served name too, so the view carries it.
|
|
169
|
+
view = serve.collections_view()
|
|
170
|
+
ok = (set(view["served"]) == set(view["models"]) and victim not in view["served"]
|
|
171
|
+
and view["served"].get(other) == d3[other]["served"])
|
|
172
|
+
print(("ok " if ok else "FAIL ") + "collection view: served name for every member choice"); failures += 0 if ok else 1
|
|
173
|
+
# restore: same alias back, out of retired.json, role and ad hoc kept, and
|
|
174
|
+
# back into the collections that still exist (one deleted, one changed since)
|
|
175
|
+
serve.delete_collection("doomed")
|
|
176
|
+
serve.bench_collections.FILE.write_text(json.dumps({**json.loads(serve.bench_collections.FILE.read_text(
|
|
177
|
+
encoding="utf-8")), "member": {"members": [other, "someone-new"], "merger": ""}}), encoding="utf-8")
|
|
178
|
+
r = serve.restore_model(victim)
|
|
179
|
+
retired = json.loads((cfg / "retired.json").read_text(encoding="utf-8"))
|
|
180
|
+
roles = json.loads((cfg / "model-roles.json").read_text(encoding="utf-8"))
|
|
181
|
+
ok = (r["ok"] and serve.registered().get(served) == victim and victim not in retired
|
|
182
|
+
and roles["models"].get(victim) == new_role and victim in roles["ad_hoc"])
|
|
183
|
+
print(("ok " if ok else "FAIL ") + f"restored {victim}" + ("" if ok else f" -> {r}"))
|
|
184
|
+
failures += 0 if ok else 1
|
|
185
|
+
back = json.loads((cfg / "collections.json").read_text(encoding="utf-8"))
|
|
186
|
+
ok = (back == {"with-it": {"members": [other, victim], "merger": victim, "reply": "merged"},
|
|
187
|
+
"member": {"members": [other, "someone-new", victim], "merger": ""},
|
|
188
|
+
"without": colls["without"]}
|
|
189
|
+
and "config/collections.json" in r["changed"])
|
|
190
|
+
print(("ok " if ok else "FAIL ") + "restored into the collections that still exist"
|
|
191
|
+
+ ("" if ok else f" -> {back} {r['changed']}")); failures += 0 if ok else 1
|
|
192
|
+
# One that was in no collection: collections.json untouched and not reported.
|
|
193
|
+
(cfg / "collections.json").write_text(json.dumps({"without": colls["without"]}), encoding="utf-8")
|
|
194
|
+
r = serve.remove_model(victim, "") # no reason given: the popup confirmed it
|
|
195
|
+
gone = json.loads((cfg / "retired.json").read_text(encoding="utf-8"))[victim]
|
|
196
|
+
ok = (r["ok"] and "config/collections.json" not in r["changed"]
|
|
197
|
+
and "collections" not in gone and gone["why"] == "retired by hand")
|
|
198
|
+
r2 = serve.restore_model(victim)
|
|
199
|
+
ok = ok and r2["ok"] and "config/collections.json" not in r2["changed"]
|
|
200
|
+
print(("ok " if ok else "FAIL ") + "in no collection: collections.json not touched"
|
|
201
|
+
+ ("" if ok else f" -> {r} {r2}")); failures += 0 if ok else 1
|
|
202
|
+
ok = not serve.restore_model(victim)["ok"]
|
|
203
|
+
print(("ok " if ok else "FAIL ") + "restore refused when not retired"); failures += 0 if ok else 1
|
|
204
|
+
serve.remove_model(victim, "retired again for the onboarding checks")
|
|
205
|
+
# onboarding refusals (nothing is started)
|
|
206
|
+
for label, kw in (("onboard: empty", dict(served="")),
|
|
207
|
+
("onboard: spaces", dict(served="vendor/model x")),
|
|
208
|
+
("onboard: already registered", dict(served=d3[other]["served"])),
|
|
209
|
+
("onboard: retired served model", dict(served=served)),
|
|
210
|
+
("onboard: shell characters", dict(served='m" & calc & "')),
|
|
211
|
+
("onboard: unknown endpoint", dict(served="vendor/m", endpoint="nope"))):
|
|
212
|
+
r = serve.start_onboard(**kw)
|
|
213
|
+
ok = not r["ok"] and "log" not in r
|
|
214
|
+
print(("ok " if ok else "FAIL ") + label + ("" if ok else f" -> {r}")); failures += 0 if ok else 1
|
|
215
|
+
print(("ok " if serve.onboard_status("../etc/passwd")["ok"] is False else "FAIL ") + "onboard log name guarded")
|
|
216
|
+
# How an onboarding stopped: shown for a registered model until it answers.
|
|
217
|
+
serve.ONBOARD_ISSUES_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
serve.ONBOARD_ISSUES_PATH.write_text(json.dumps({
|
|
219
|
+
other: {"at": "2026-10-22T16:43:34+02:00", "step": "availability probe", "failure": "capacity",
|
|
220
|
+
"detail": "429 free-models-per-day", "log": ""},
|
|
221
|
+
"gone-model": {"failure": "structural"}}), encoding="utf-8")
|
|
222
|
+
shown = serve.onboard_issues({other: ("throttled", "warn", "")})
|
|
223
|
+
ok = list(shown) == [other] and shown[other]["label"] == "throttled" and shown[other]["css"] == "warn"
|
|
224
|
+
ok = ok and not serve.onboard_issues({other: ("healthy", "ok", "")}) # answered since: gone
|
|
225
|
+
print(("ok " if ok else "FAIL ") + "onboard issues: registered and not answering only"
|
|
226
|
+
+ ("" if ok else f" -> {shown}")); failures += 0 if ok else 1
|
|
227
|
+
serve.ONBOARD_ISSUES_PATH.unlink()
|
|
228
|
+
# endpoints: refused before any network call or write
|
|
229
|
+
for label, args in (("endpoint: bad name", ("Bad Name", "https://x.example/v1", "")),
|
|
230
|
+
("endpoint: bad url", ("good", "ftp://x", "")),
|
|
231
|
+
("endpoint: default not removable", None)):
|
|
232
|
+
r = serve.save_endpoint(*args) if args else serve.remove_endpoint(serve.endpoints.default())
|
|
233
|
+
ok = not r["ok"]
|
|
234
|
+
print(("ok " if ok else "FAIL ") + label); failures += 0 if ok else 1
|
|
235
|
+
|
|
236
|
+
# Removing an endpoint with a model on it: refused unless asked, then the
|
|
237
|
+
# model retires with it, and Restore will not move it to another endpoint.
|
|
238
|
+
eps = json.loads((cfg / "endpoints.json").read_text(encoding="utf-8"))
|
|
239
|
+
eps["edit-test-ep"] = {"base": "https://edit-test.example/v1", "key": "edit-test-ep"}
|
|
240
|
+
(cfg / "endpoints.json").write_text(json.dumps(eps), encoding="utf-8")
|
|
241
|
+
serve.YAML_PATH.write_text(serve.read(serve.YAML_PATH).rstrip("\n") + "\n"
|
|
242
|
+
+ serve.endpoints.yaml_block("edit-test-m", "vendor/edit-test", "edit-test-ep"),
|
|
243
|
+
encoding="utf-8")
|
|
244
|
+
refused = serve.remove_endpoint("edit-test-ep")
|
|
245
|
+
r = serve.remove_endpoint("edit-test-ep", with_models=True)
|
|
246
|
+
back = serve.restore_model("edit-test-m")
|
|
247
|
+
ok = (not refused["ok"] and "1 model" in refused["errors"][0]
|
|
248
|
+
and r["ok"] and r["retired"] == ["edit-test-m"] and "edit-test-ep" not in serve.endpoints.load()
|
|
249
|
+
and "edit-test-m" in json.loads((cfg / "retired.json").read_text(encoding="utf-8"))
|
|
250
|
+
and "edit-test-m" not in serve.model_directory()
|
|
251
|
+
and not back["ok"] and "was removed" in back["errors"][0])
|
|
252
|
+
print(("ok " if ok else "FAIL ") + "endpoint removed with its model; restore refused without it"
|
|
253
|
+
+ ("" if ok else f" -> {refused} {r} {back}"))
|
|
254
|
+
failures += 0 if ok else 1
|
|
255
|
+
|
|
256
|
+
# Call options: max_tokens set, refused above the model's max output or as
|
|
257
|
+
# a fraction or for no model, then cleared; the llm CLI's copy is stubbed.
|
|
258
|
+
told, real_info = [], serve.catalog.info
|
|
259
|
+
serve._llm_option = lambda alias, name, value: told.append((alias, name, value)) or "stub"
|
|
260
|
+
serve.catalog.info = lambda a: {**real_info(a), "max_output": 50_000}
|
|
261
|
+
chat = next(a for a, m in sorted(serve.model_directory().items()) if m.get("kind") != "decision")
|
|
262
|
+
before = json.loads(serve.OPTIONS_PATH.read_text(encoding="utf-8"))
|
|
263
|
+
try:
|
|
264
|
+
put = serve.save_model_options(chat, 31_337)
|
|
265
|
+
mid = json.loads(serve.OPTIONS_PATH.read_text(encoding="utf-8"))
|
|
266
|
+
refused = [serve.save_model_options(chat, 60_000), serve.save_model_options(chat, 12.5),
|
|
267
|
+
serve.save_model_options("no-such-model", 100), serve.save_model_options(chat, True)]
|
|
268
|
+
gone = serve.save_model_options(chat, None)
|
|
269
|
+
after = json.loads(serve.OPTIONS_PATH.read_text(encoding="utf-8"))
|
|
270
|
+
finally:
|
|
271
|
+
serve.catalog.info = real_info
|
|
272
|
+
ok = (put["ok"] and mid[chat]["max_tokens"] == 31_337 and not any(r["ok"] for r in refused)
|
|
273
|
+
and gone["ok"] and "max_tokens" not in after.get(chat, {}) and after.get("_why") == before.get("_why")
|
|
274
|
+
and told == [(chat, "max_tokens", 31_337), (chat, "max_tokens", None)])
|
|
275
|
+
print(("ok " if ok else "FAIL ") + f"call options for {chat}" + ("" if ok else f" -> {put} {refused} {gone} {told}"))
|
|
276
|
+
failures += 0 if ok else 1
|
|
277
|
+
|
|
278
|
+
# The scheduler: a mode and the jobs turned off, nothing else written.
|
|
279
|
+
import scheduler
|
|
280
|
+
scheduler.CONFIG = cfg / "scheduler.json"
|
|
281
|
+
bad = [serve.save_scheduler("sometimes", {}), serve.save_scheduler("off", {"nope": True})]
|
|
282
|
+
good = serve.save_scheduler("log-only", {"perf": False, "suite": True})
|
|
283
|
+
conf = scheduler.settings()
|
|
284
|
+
ok = (not any(r["ok"] for r in bad) and good["ok"]
|
|
285
|
+
and conf == {"mode": "log-only", "jobs": {"perf": False}} and "perf" not in scheduler.enabled(conf))
|
|
286
|
+
print(("ok " if ok else "FAIL ") + "scheduler mode and jobs" + ("" if ok else f" -> {bad} {good} {conf}"))
|
|
287
|
+
failures += 0 if ok else 1
|
|
288
|
+
|
|
289
|
+
# The model client: chosen here unless BENCH_CLIENT is set; llm only where installed.
|
|
290
|
+
import client as model_client
|
|
291
|
+
model_client.CHOICE = cfg / "client.json"
|
|
292
|
+
env, real_which = os.environ.pop("BENCH_CLIENT", None), shutil.which
|
|
293
|
+
try:
|
|
294
|
+
shutil.which = lambda name: "/usr/bin/" + name # llm installed: the default is llm
|
|
295
|
+
first = model_client.chosen()
|
|
296
|
+
shutil.which = lambda name: None # not installed (npm, a container)
|
|
297
|
+
bare = model_client.chosen()
|
|
298
|
+
shutil.which = real_which
|
|
299
|
+
made = serve.save_client("builtin")
|
|
300
|
+
picked = (model_client.chosen(), model_client.backend())
|
|
301
|
+
os.environ["BENCH_CLIENT"] = "llm"
|
|
302
|
+
pinned = (model_client.chosen(), serve.save_client("builtin")["ok"])
|
|
303
|
+
del os.environ["BENCH_CLIENT"]
|
|
304
|
+
shutil.which = lambda name: None
|
|
305
|
+
no_llm = serve.save_client("llm")["ok"]
|
|
306
|
+
shutil.which = real_which
|
|
307
|
+
unknown = serve.save_client("curl")["ok"]
|
|
308
|
+
finally:
|
|
309
|
+
shutil.which = real_which
|
|
310
|
+
os.environ.pop("BENCH_CLIENT", None)
|
|
311
|
+
if env is not None:
|
|
312
|
+
os.environ["BENCH_CLIENT"] = env
|
|
313
|
+
ok = (first == ("llm", "default") and bare == ("builtin", "default") and made["ok"] and picked == (("builtin", "settings"), "builtin")
|
|
314
|
+
and pinned == (("llm", "environment"), False) and not no_llm and not unknown)
|
|
315
|
+
print(("ok " if ok else "FAIL ") + "model client choice" + ("" if ok else f" -> {first} {made} {picked} {pinned}"))
|
|
316
|
+
failures += 0 if ok else 1
|
|
317
|
+
|
|
318
|
+
# Free only in their own app: only what differs from the built-in is written; bad input refused.
|
|
319
|
+
import sources
|
|
320
|
+
real_conn, endpoints_mod = serve.endpoints.CONNECTIONS, serve.endpoints
|
|
321
|
+
endpoints_mod.CONNECTIONS = cfg / "connections.json"
|
|
322
|
+
try:
|
|
323
|
+
endpoints_mod.CONNECTIONS.write_text('{"paid": ["x"]}', encoding="utf-8")
|
|
324
|
+
off = serve.save_own_client_free({"opencode": False, "opencode-go": True})["ok"]
|
|
325
|
+
wrote = json.loads(endpoints_mod.CONNECTIONS.read_text(encoding="utf-8"))
|
|
326
|
+
seen_off = sources.free_in_own_client_only()["opencode"]
|
|
327
|
+
back = serve.save_own_client_free({"opencode": True, "opencode-go": True})["ok"]
|
|
328
|
+
cleared = json.loads(endpoints_mod.CONNECTIONS.read_text(encoding="utf-8"))
|
|
329
|
+
refused = (serve.save_own_client_free({"no-such-provider-x": True})["ok"],
|
|
330
|
+
serve.save_own_client_free({"opencode": "no"})["ok"], serve.save_own_client_free(["opencode"])["ok"])
|
|
331
|
+
card = serve.own_client_card()
|
|
332
|
+
finally:
|
|
333
|
+
endpoints_mod.CONNECTIONS = real_conn
|
|
334
|
+
ok = (off and wrote == {"paid": ["x"], "free_in_own_client_only": {"opencode": False}} and seen_off is False
|
|
335
|
+
and back and cleared == {"paid": ["x"]} and refused == (False, False, False)
|
|
336
|
+
and "value='opencode'" in card and "oc-save" in card)
|
|
337
|
+
print(("ok " if ok else "FAIL ") + "free only in their own app" + ("" if ok else f" -> {wrote} {cleared} {refused}"))
|
|
338
|
+
failures += 0 if ok else 1
|
|
339
|
+
|
|
340
|
+
# The onboarding queue, with onboard.py faked: one at a time, Stop skips the
|
|
341
|
+
# rest, each outcome read from its exit and log, Clear forgets a finished batch.
|
|
342
|
+
import threading
|
|
343
|
+
import time
|
|
344
|
+
hold = threading.Event()
|
|
345
|
+
runs = tmp / "runs"
|
|
346
|
+
runs.mkdir(exist_ok=True)
|
|
347
|
+
outcome = {"a": (0, " [1/5] served\n [3/5] synced\n"), "ok": (0, ""),
|
|
348
|
+
"quiet": (0, " STOPPING: it answered nothing.\n"),
|
|
349
|
+
"bad": (1, "bad is not served.\n")}
|
|
350
|
+
order = []
|
|
351
|
+
|
|
352
|
+
class Proc:
|
|
353
|
+
def __init__(self, served):
|
|
354
|
+
self.served = served
|
|
355
|
+
|
|
356
|
+
def wait(self):
|
|
357
|
+
if self.served == "a":
|
|
358
|
+
hold.wait(10)
|
|
359
|
+
return outcome[self.served][0]
|
|
360
|
+
|
|
361
|
+
modes = {}
|
|
362
|
+
|
|
363
|
+
def fake_spawn(served, endpoint="", who="ui", mode="full"):
|
|
364
|
+
order.append(served)
|
|
365
|
+
modes[served] = mode
|
|
366
|
+
(runs / f"onboard-{served}.log").write_text(f"onboarding {served} from e -> alias {served}-x\n"
|
|
367
|
+
+ outcome[served][1], encoding="utf-8")
|
|
368
|
+
return {"ok": True, "errors": [], "log": f"onboard-{served}.log"}, Proc(served)
|
|
369
|
+
|
|
370
|
+
def settle():
|
|
371
|
+
for _ in range(200):
|
|
372
|
+
if not serve.onboard_queue()["running"]:
|
|
373
|
+
return
|
|
374
|
+
time.sleep(0.05)
|
|
375
|
+
|
|
376
|
+
kept = (serve._spawn_onboard, serve._onboard_refusal, serve.DATA)
|
|
377
|
+
serve._spawn_onboard, serve._onboard_refusal, serve.DATA = fake_spawn, lambda s, e, m="full": [], tmp
|
|
378
|
+
try:
|
|
379
|
+
first = serve.queue_onboard([{"served": "a", "endpoint": "e"}, {"served": "ok", "endpoint": "e"}])
|
|
380
|
+
twice = serve.queue_onboard([{"served": "ok", "endpoint": "e"}])["ok"]
|
|
381
|
+
for _ in range(200): # Stop once `a` is under way, as a person would
|
|
382
|
+
if order:
|
|
383
|
+
break
|
|
384
|
+
time.sleep(0.05)
|
|
385
|
+
stop = serve.stop_onboard_queue()
|
|
386
|
+
stopping = stop["stopping"] and [i["state"] for i in stop["items"]] == ["running", "skipped"]
|
|
387
|
+
busy_clear = serve.clear_onboard_queue()["ok"]
|
|
388
|
+
after_stop = serve.queue_onboard([{"served": "quiet", "endpoint": "e"}])["ok"] # picked after Stop: runs
|
|
389
|
+
step = None # the running model's step, from its log
|
|
390
|
+
for _ in range(200):
|
|
391
|
+
run = [i for i in serve.onboard_queue()["items"] if i["state"] == "running"]
|
|
392
|
+
if run and run[0].get("step"):
|
|
393
|
+
step = run[0]["step"]
|
|
394
|
+
break
|
|
395
|
+
time.sleep(0.05)
|
|
396
|
+
hold.set()
|
|
397
|
+
settle()
|
|
398
|
+
stopped = {i["served"]: i["state"] for i in serve.onboard_queue()["items"]}
|
|
399
|
+
order.remove("quiet")
|
|
400
|
+
serve.queue_onboard([{"served": s, "endpoint": "e", "mode": m} for s, m in
|
|
401
|
+
(("ok", "full"), ("quiet", "quick"), ("bad", "quick-adhoc"))])
|
|
402
|
+
settle()
|
|
403
|
+
batch = {i["served"]: (i["state"], i["alias"], bool(i["note"])) for i in serve.onboard_queue()["items"]}
|
|
404
|
+
cleared = serve.clear_onboard_queue()["items"]
|
|
405
|
+
bad_input = (serve.queue_onboard([])["ok"], serve.queue_onboard("ok")["ok"],
|
|
406
|
+
serve.stop_onboard_queue()["ok"])
|
|
407
|
+
finally:
|
|
408
|
+
hold.set()
|
|
409
|
+
serve._spawn_onboard, serve._onboard_refusal, serve.DATA = kept
|
|
410
|
+
ok = (first["ok"] and not twice and stopping and not busy_clear and after_stop and step == 3
|
|
411
|
+
and stopped == {"a": "done", "ok": "skipped", "quiet": "no answer"} and order == ["a", "ok", "quiet", "bad"]
|
|
412
|
+
and batch == {"ok": ("done", "ok-x", False), "quiet": ("no answer", "quiet-x", True),
|
|
413
|
+
"bad": ("failed", "bad-x", True)}
|
|
414
|
+
and cleared == [] and bad_input == (False, False, False)
|
|
415
|
+
and modes == {"a": "full", "ok": "full", "quiet": "quick", "bad": "quick-adhoc"})
|
|
416
|
+
print(("ok " if ok else "FAIL ") + "onboarding queue" + ("" if ok else f" -> {stopped} {order} {batch} {modes}"))
|
|
417
|
+
failures += 0 if ok else 1
|
|
418
|
+
|
|
419
|
+
# Dismiss: one picker key or many in one write, a change-log line each;
|
|
420
|
+
# undo brings back only the ones named; nothing named is refused.
|
|
421
|
+
serve.DISMISSED_PATH = cfg / "dismissed.json"
|
|
422
|
+
seen = len(serve.CHANGES_PATH.read_text(encoding="utf-8").splitlines())
|
|
423
|
+
many, one = serve.dismiss(["vendor/a", "vendor/b@openrouter"]), serve.dismiss("vendor/c")
|
|
424
|
+
gone = set(json.loads(serve.DISMISSED_PATH.read_text(encoding="utf-8")))
|
|
425
|
+
back = serve.dismiss(["vendor/a", "vendor/c"], undo=True)
|
|
426
|
+
left = set(json.loads(serve.DISMISSED_PATH.read_text(encoding="utf-8")))
|
|
427
|
+
log = [(e["field"], e["new"]) for e in map(json.loads, serve.CHANGES_PATH.read_text(encoding="utf-8").splitlines()[seen:])]
|
|
428
|
+
refused = tuple(serve.dismiss(x)["ok"] for x in ([], " ", None, [3]))
|
|
429
|
+
ok = (many["ok"] and one["ok"] and back["ok"] and gone == {"vendor/a", "vendor/b@openrouter", "vendor/c"}
|
|
430
|
+
and left == {"vendor/b@openrouter"} and refused == (False, False, False, False)
|
|
431
|
+
and log == [("dismissed", "vendor/a"), ("dismissed", "vendor/b@openrouter"), ("dismissed", "vendor/c"),
|
|
432
|
+
("undismissed", "vendor/a"), ("undismissed", "vendor/c")])
|
|
433
|
+
print(("ok " if ok else "FAIL ") + "dismiss in bulk, and undo" + ("" if ok else f" -> {gone} {left} {log} {refused}"))
|
|
434
|
+
failures += 0 if ok else 1
|
|
435
|
+
|
|
436
|
+
# The owner's files are as they were. alerts.json can change under a
|
|
437
|
+
# running suite, so only what this test could do counts: the victim's
|
|
438
|
+
# alerts cleared from the real file.
|
|
439
|
+
real_alerts = DATA / "metrics" / "alerts.json"
|
|
440
|
+
victim_keys = {k for k in json.loads(real[real_alerts] or b"{}") if k.endswith(f":{served}")}
|
|
441
|
+
ok = (real[DATA / "config" / "endpoints.json"] == (DATA / "config" / "endpoints.json").read_bytes()
|
|
442
|
+
and victim_keys <= set(json.loads(real_alerts.read_bytes() or b"{}"))
|
|
443
|
+
and real_collections == ((DATA / "config" / "collections.json").read_bytes()
|
|
444
|
+
if (DATA / "config" / "collections.json").exists() else None))
|
|
445
|
+
print(("ok " if ok else "FAIL ") + "the real alerts, endpoints and collections were not touched")
|
|
446
|
+
failures += 0 if ok else 1
|
|
447
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
448
|
+
print("\nall rules hold" if not failures else f"\n{failures} FAILED")
|
|
449
|
+
return 1 if failures else 0
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if __name__ == "__main__":
|
|
453
|
+
sys.exit(main())
|