@wendellresearch-ai/benchwarmers 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/AGENTS.md +152 -147
  2. package/BENCHMARKS.md +262 -262
  3. package/GATEWAY.md +415 -272
  4. package/NOTICE +28 -28
  5. package/PRIVACY.md +46 -46
  6. package/README.md +408 -314
  7. package/SECURITY.md +83 -83
  8. package/TERMS.md +22 -22
  9. package/USAGE.md +186 -163
  10. package/VERSION +1 -1
  11. package/bin/benchwarmers.js +16 -74
  12. package/cli.py +674 -0
  13. package/examples/config/gateway.json +5 -5
  14. package/examples/config/model-roles.json +11 -11
  15. package/gateway.py +939 -755
  16. package/launcher.py +120 -0
  17. package/mcp_server.py +266 -266
  18. package/metrics/alerts.py +116 -116
  19. package/metrics/aliases.py +41 -41
  20. package/metrics/bench_collections.py +141 -141
  21. package/metrics/catalog.py +304 -295
  22. package/metrics/client.py +804 -394
  23. package/metrics/cliproxy.py +191 -191
  24. package/metrics/connect.py +691 -691
  25. package/metrics/cost.py +171 -152
  26. package/metrics/decide.py +227 -227
  27. package/metrics/discover.py +318 -318
  28. package/metrics/endpoints.py +436 -436
  29. package/metrics/formats.py +1021 -0
  30. package/metrics/guard.py +647 -592
  31. package/metrics/learn.py +234 -221
  32. package/metrics/ledger.py +148 -0
  33. package/metrics/litellm_hub.py +206 -206
  34. package/metrics/model_roles.py +94 -84
  35. package/metrics/offload.py +166 -166
  36. package/metrics/owui.py +205 -205
  37. package/metrics/pacing.py +248 -0
  38. package/metrics/paths.py +60 -60
  39. package/metrics/quota.py +383 -0
  40. package/metrics/route.py +581 -579
  41. package/metrics/scheduler.py +275 -275
  42. package/metrics/sources.py +167 -167
  43. package/metrics/wrong.py +144 -144
  44. package/package.json +69 -65
  45. package/perf/README.md +89 -89
  46. package/perf/run.py +284 -261
  47. package/scripts/auto_retire.py +367 -367
  48. package/scripts/check.py +113 -106
  49. package/scripts/check_deprecations.py +378 -378
  50. package/scripts/check_themes.py +103 -103
  51. package/scripts/init.py +124 -124
  52. package/scripts/list_served.py +78 -78
  53. package/scripts/notice.py +78 -78
  54. package/scripts/onboard.py +410 -331
  55. package/scripts/provider_tags.py +129 -129
  56. package/scripts/reprefix.py +136 -136
  57. package/scripts/sync_config.py +105 -105
  58. package/scripts/test_auth.py +255 -255
  59. package/scripts/test_cli.py +189 -0
  60. package/scripts/test_client.py +277 -233
  61. package/scripts/test_connect.py +222 -222
  62. package/scripts/test_first_run.py +180 -180
  63. package/scripts/test_formats.py +549 -0
  64. package/scripts/test_hubs.py +328 -328
  65. package/scripts/test_launcher.py +67 -0
  66. package/scripts/test_llm_log.py +85 -81
  67. package/scripts/test_mcp.py +178 -178
  68. package/scripts/test_merged.py +215 -214
  69. package/scripts/test_no_owui.py +291 -291
  70. package/scripts/test_owui.py +90 -90
  71. package/scripts/test_quota.py +334 -0
  72. package/scripts/test_serve_edit.py +453 -438
  73. package/scripts/test_server_json.py +79 -0
  74. package/scripts/test_verdict.py +102 -98
  75. package/scripts/test_vision.py +231 -228
  76. package/serve.py +3972 -3764
  77. package/suite/mine.py +154 -154
  78. package/suite/pace.py +145 -145
  79. package/suite/run.py +1046 -1042
  80. package/suite/seed.py +421 -421
  81. package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
  82. package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
  83. package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
  84. package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
  85. package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
  86. package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
  87. package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
  88. package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
  89. package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
  90. package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
  91. package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
  92. package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
  93. package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
  94. package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
  95. package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
  96. package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
  97. package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
  98. package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
  99. package/ui/bench.css +430 -425
  100. package/ui/bench.js +1757 -1576
  101. package/ui/themes/dark.css +35 -35
  102. package/ui/themes/hardwood.css +52 -52
  103. package/ui/themes/rainforest.css +32 -32
@@ -1,90 +1,90 @@
1
- """metrics/owui.py against a fake Open WebUI: nothing is sent anywhere.
2
-
3
- python scripts/test_owui.py
4
-
5
- The config write must give back every key and every other connection exactly
6
- as read, add the id once and only where the connection lists ids, and the
7
- model record must come out public, tagged and with the sibling's logo.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import copy
13
- import pathlib
14
- import sys
15
-
16
- ROOT = pathlib.Path(__file__).resolve().parent.parent
17
- sys.path[:0] = [str(ROOT / "metrics"), str(ROOT / "suite")]
18
- import owui # noqa: E402
19
-
20
- CONFIG = {"ENABLE_OPENAI_API": True,
21
- "OPENAI_API_BASE_URLS": ["https://api.groq.com/openai/v1", "https://api.llm7.io/v1"],
22
- "OPENAI_API_KEYS": ["gsk_real_key_1", "llm7_real_key_2"],
23
- "OPENAI_API_CONFIGS": {"0": {"enable": True, "prefix_id": "", "model_ids": ["groq/compound"]},
24
- "1": {"enable": True, "prefix_id": "llm7", "model_ids": []}}}
25
- MODELS = [{"id": "groq/compound", "urlIdx": 0, "tags": [{"name": "groq-api"}]},
26
- {"id": "llm7.codestral-latest", "urlIdx": 1, "tags": [{"name": "llm7-api"}]}]
27
- RECORDS = [{"id": "groq/compound", "name": "groq/compound", "base_model_id": None, "params": {},
28
- "meta": {"profile_image_url": "data:image/webp;base64,LOGO", "capabilities": {"vision": True},
29
- "tags": [{"name": "groq-api"}]}}]
30
-
31
-
32
- def fake(state):
33
- def call(path, body=None):
34
- state["calls"].append((path, copy.deepcopy(body)))
35
- if path == "/api/models":
36
- return {"data": state["models"]}
37
- if path == "/openai/config":
38
- return copy.deepcopy(state["config"])
39
- if path == "/openai/config/update":
40
- state["config"] = copy.deepcopy(body)
41
- return body
42
- if path == "/api/v1/models/base":
43
- return copy.deepcopy(state["records"])
44
- if path in ("/api/v1/models/create", "/api/v1/models/model/update"):
45
- state["records"] = [r for r in state["records"] if r["id"] != body["id"]] + [body]
46
- state["models"] = state["models"] + [{"id": body["id"], "urlIdx": 0, "tags": body["meta"]["tags"]}]
47
- return body
48
- raise AssertionError(path)
49
- return call
50
-
51
-
52
- def run(connection, model, registered):
53
- state = {"config": copy.deepcopy(CONFIG), "models": list(MODELS), "records": copy.deepcopy(RECORDS), "calls": []}
54
- owui._call = fake(state)
55
- owui.endpoint = lambda: "openwebui"
56
- owui.endpoints.models = lambda name, timeout=30: {m["id"]: m for m in state["models"]}
57
- import route
58
- import run as suite_run
59
- suite_run._registered_names = lambda: registered
60
- route.connection_of = lambda alias: {"compound": "groq", "codestral": "llm7-api"}[alias]
61
- served, done = owui.add(connection, model)
62
- return state, served, done
63
-
64
-
65
- state, served, done = run("groq", "moonshotai/kimi-k2", {"compound": "groq/compound"})
66
- cfg = state["config"]
67
- assert served == "moonshotai/kimi-k2", served
68
- assert cfg["OPENAI_API_KEYS"] == CONFIG["OPENAI_API_KEYS"], "keys changed"
69
- assert cfg["OPENAI_API_BASE_URLS"] == CONFIG["OPENAI_API_BASE_URLS"]
70
- assert cfg["OPENAI_API_CONFIGS"]["1"] == CONFIG["OPENAI_API_CONFIGS"]["1"], "another connection changed"
71
- assert cfg["OPENAI_API_CONFIGS"]["0"]["model_ids"] == ["groq/compound", "moonshotai/kimi-k2"]
72
- rec = next(r for r in state["records"] if r["id"] == served)
73
- assert rec["access_grants"] == owui.PUBLIC
74
- assert rec["meta"]["tags"] == [{"name": "groq-api"}], rec["meta"]["tags"] # Open WebUI's tag, not the bench's name
75
- assert rec["meta"]["profile_image_url"] == "data:image/webp;base64,LOGO"
76
- assert any("logo" in d for d in done), done
77
-
78
- # An empty Model IDs list means "everything": nothing is written, and the prefix names the id.
79
- state, served, done = run("llm7-api", "mistral-small", {"codestral": "llm7.codestral-latest"})
80
- assert served == "llm7.mistral-small", served
81
- assert not any(p == "/openai/config/update" for p, _ in state["calls"]), "wrote a config it did not need to"
82
-
83
- # The same id already served by ANOTHER connection is refused, not doubled.
84
- MODELS.append({"id": "llm7.qwen", "urlIdx": 0, "tags": []})
85
- try:
86
- run("llm7-api", "qwen", {"codestral": "llm7.codestral-latest"})
87
- raise AssertionError("a collision with another connection was not refused")
88
- except ValueError as exc:
89
- assert "already serves" in str(exc), exc
90
- print("owui checks pass")
1
+ """metrics/owui.py against a fake Open WebUI: nothing is sent anywhere.
2
+
3
+ python scripts/test_owui.py
4
+
5
+ The config write must give back every key and every other connection exactly
6
+ as read, add the id once and only where the connection lists ids, and the
7
+ model record must come out public, tagged and with the sibling's logo.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import copy
13
+ import pathlib
14
+ import sys
15
+
16
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
17
+ sys.path[:0] = [str(ROOT / "metrics"), str(ROOT / "suite")]
18
+ import owui # noqa: E402
19
+
20
+ CONFIG = {"ENABLE_OPENAI_API": True,
21
+ "OPENAI_API_BASE_URLS": ["https://api.groq.com/openai/v1", "https://api.llm7.io/v1"],
22
+ "OPENAI_API_KEYS": ["gsk_real_key_1", "llm7_real_key_2"],
23
+ "OPENAI_API_CONFIGS": {"0": {"enable": True, "prefix_id": "", "model_ids": ["groq/compound"]},
24
+ "1": {"enable": True, "prefix_id": "llm7", "model_ids": []}}}
25
+ MODELS = [{"id": "groq/compound", "urlIdx": 0, "tags": [{"name": "groq-api"}]},
26
+ {"id": "llm7.codestral-latest", "urlIdx": 1, "tags": [{"name": "llm7-api"}]}]
27
+ RECORDS = [{"id": "groq/compound", "name": "groq/compound", "base_model_id": None, "params": {},
28
+ "meta": {"profile_image_url": "data:image/webp;base64,LOGO", "capabilities": {"vision": True},
29
+ "tags": [{"name": "groq-api"}]}}]
30
+
31
+
32
+ def fake(state):
33
+ def call(path, body=None):
34
+ state["calls"].append((path, copy.deepcopy(body)))
35
+ if path == "/api/models":
36
+ return {"data": state["models"]}
37
+ if path == "/openai/config":
38
+ return copy.deepcopy(state["config"])
39
+ if path == "/openai/config/update":
40
+ state["config"] = copy.deepcopy(body)
41
+ return body
42
+ if path == "/api/v1/models/base":
43
+ return copy.deepcopy(state["records"])
44
+ if path in ("/api/v1/models/create", "/api/v1/models/model/update"):
45
+ state["records"] = [r for r in state["records"] if r["id"] != body["id"]] + [body]
46
+ state["models"] = state["models"] + [{"id": body["id"], "urlIdx": 0, "tags": body["meta"]["tags"]}]
47
+ return body
48
+ raise AssertionError(path)
49
+ return call
50
+
51
+
52
+ def run(connection, model, registered):
53
+ state = {"config": copy.deepcopy(CONFIG), "models": list(MODELS), "records": copy.deepcopy(RECORDS), "calls": []}
54
+ owui._call = fake(state)
55
+ owui.endpoint = lambda: "openwebui"
56
+ owui.endpoints.models = lambda name, timeout=30: {m["id"]: m for m in state["models"]}
57
+ import route
58
+ import run as suite_run
59
+ suite_run._registered_names = lambda: registered
60
+ route.connection_of = lambda alias: {"compound": "groq", "codestral": "llm7-api"}[alias]
61
+ served, done = owui.add(connection, model)
62
+ return state, served, done
63
+
64
+
65
+ state, served, done = run("groq", "moonshotai/kimi-k2", {"compound": "groq/compound"})
66
+ cfg = state["config"]
67
+ assert served == "moonshotai/kimi-k2", served
68
+ assert cfg["OPENAI_API_KEYS"] == CONFIG["OPENAI_API_KEYS"], "keys changed"
69
+ assert cfg["OPENAI_API_BASE_URLS"] == CONFIG["OPENAI_API_BASE_URLS"]
70
+ assert cfg["OPENAI_API_CONFIGS"]["1"] == CONFIG["OPENAI_API_CONFIGS"]["1"], "another connection changed"
71
+ assert cfg["OPENAI_API_CONFIGS"]["0"]["model_ids"] == ["groq/compound", "moonshotai/kimi-k2"]
72
+ rec = next(r for r in state["records"] if r["id"] == served)
73
+ assert rec["access_grants"] == owui.PUBLIC
74
+ assert rec["meta"]["tags"] == [{"name": "groq-api"}], rec["meta"]["tags"] # Open WebUI's tag, not the bench's name
75
+ assert rec["meta"]["profile_image_url"] == "data:image/webp;base64,LOGO"
76
+ assert any("logo" in d for d in done), done
77
+
78
+ # An empty Model IDs list means "everything": nothing is written, and the prefix names the id.
79
+ state, served, done = run("llm7-api", "mistral-small", {"codestral": "llm7.codestral-latest"})
80
+ assert served == "llm7.mistral-small", served
81
+ assert not any(p == "/openai/config/update" for p, _ in state["calls"]), "wrote a config it did not need to"
82
+
83
+ # The same id already served by ANOTHER connection is refused, not doubled.
84
+ MODELS.append({"id": "llm7.qwen", "urlIdx": 0, "tags": []})
85
+ try:
86
+ run("llm7-api", "qwen", {"codestral": "llm7.codestral-latest"})
87
+ raise AssertionError("a collision with another connection was not refused")
88
+ except ValueError as exc:
89
+ assert "already serves" in str(exc), exc
90
+ print("owui checks pass")
@@ -0,0 +1,334 @@
1
+ """The built-in client's Phase 2b, on a fake server: streaming, the usage
2
+ ledger, quota records, pacing and the free-tier guard.
3
+
4
+ python scripts/test_quota.py
5
+
6
+ No model is called and nothing of the install's is read or written:
7
+ BENCHWARMERS_HOME and LLM_USER_PATH name temp folders before any module
8
+ reads them. Each scenario is one model name the fake server answers in its
9
+ own way; the pacing ones sit on endpoints of their own, so their counts do
10
+ not mix. Covered:
11
+
12
+ - a streamed answer: text and reasoning apart, each piece heard by on_delta,
13
+ usage from the final chunk, the first token's time; and its ledger line;
14
+ - an upstream that answers plain JSON to a stream request, and one that
15
+ refuses stream_options (asked again without);
16
+ - a reply naming another model: the call succeeds, the swap is noted;
17
+ - an empty answer cut off by its budget: tried once more with twice the budget;
18
+ - a stream that breaks off, and STOP between chunks;
19
+ - quota records from rate-limit headers, the next call waiting out the reset;
20
+ a Gemini 429 body's daily quota learned as a limit;
21
+ - an rpm limit across six threads at once and across two processes (calls in
22
+ flight count), a monthly cap (rpmo), and both refusals classed as capacity;
23
+ - the free-tier guard holding a model that went paid, until it is allowed;
24
+ - metrics/cost.py reading the ledger.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import multiprocessing
31
+ import os
32
+ import pathlib
33
+ import sys
34
+ import tempfile
35
+ import threading
36
+ import time
37
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
38
+
39
+ # A child process (the cross-process check) re-runs this module: it must find the parent's folder.
40
+ TMP = pathlib.Path(os.environ.get("BENCH_QUOTA_TMP") or tempfile.mkdtemp(prefix="bench-quota-"))
41
+ os.environ.update(BENCHWARMERS_HOME=str(TMP), LLM_USER_PATH=str(TMP / "llm"), BENCH_CLIENT="builtin",
42
+ BENCH_QUOTA_TMP=str(TMP))
43
+ os.environ.pop("BENCH_ALLOW_PAID", None)
44
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
45
+ sys.path[:0] = [str(ROOT / "metrics"), str(ROOT / "suite")]
46
+
47
+ import alerts # noqa: E402
48
+ import client # noqa: E402
49
+ import cost # noqa: E402
50
+ import learn # noqa: E402
51
+ import ledger # noqa: E402
52
+ import quota # noqa: E402
53
+ import run # noqa: E402
54
+
55
+ seen: dict[str, list[dict]] = {}
56
+ lock = threading.Lock()
57
+
58
+
59
+ class Fake(BaseHTTPRequestHandler):
60
+ def log_message(self, *_):
61
+ pass
62
+
63
+ def _send(self, code: int, body: bytes, ctype: str = "application/json", extra=None):
64
+ self.send_response(code)
65
+ self.send_header("Content-Type", ctype)
66
+ self.send_header("Content-Length", str(len(body)))
67
+ for k, v in (extra or {}).items():
68
+ self.send_header(k, v)
69
+ self.end_headers()
70
+ self.wfile.write(body)
71
+
72
+ def _stream(self, model: str, deltas: list[dict], finish: str = "stop", usage=None, served=None,
73
+ extra=None, gap: float = 0.0, close_early: bool = False):
74
+ self.send_response(200)
75
+ self.send_header("Content-Type", "text/event-stream")
76
+ for k, v in (extra or {}).items():
77
+ self.send_header(k, v)
78
+ self.end_headers()
79
+ head = {"id": "c1", "object": "chat.completion.chunk", "created": 0, "model": served or model}
80
+ for d in deltas:
81
+ self.wfile.write(f"data: {json.dumps({**head, 'choices': [{'index': 0, 'delta': d}]})}\n\n".encode())
82
+ self.wfile.flush()
83
+ time.sleep(gap)
84
+ if close_early:
85
+ return # HTTP/1.0: the connection closes, no finish, no [DONE]
86
+ tail = [{**head, "choices": [{"index": 0, "delta": {}, "finish_reason": finish}]},
87
+ {**head, "choices": [], "usage": usage or {"prompt_tokens": 7, "completion_tokens": 4,
88
+ "total_tokens": 11}}]
89
+ self.wfile.write(("".join(f"data: {json.dumps(c)}\n\n" for c in tail) + "data: [DONE]\n\n").encode())
90
+
91
+ def do_POST(self): # noqa: N802
92
+ req = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
93
+ model = req["model"]
94
+ with lock:
95
+ calls = seen.setdefault(model, [])
96
+ calls.append(req)
97
+ n = len(calls)
98
+ try:
99
+ if model == "plain" or (model == "nooptions" and "stream_options" in req):
100
+ if model == "nooptions":
101
+ return self._send(400, b'{"error": {"message": "Unrecognized request argument supplied:'
102
+ b' stream_options"}}')
103
+ return self._send(200, json.dumps({"model": model, "choices": [{"message": {
104
+ "content": "plain answer"}, "finish_reason": "stop"}],
105
+ "usage": {"prompt_tokens": 2, "completion_tokens": 2}}).encode())
106
+ if model == "overcap" and (req.get("max_tokens") or 0) >= 100:
107
+ return self._send(400, b'{"error": {"message": "max_tokens is too large: 128"}}')
108
+ if model in ("budget", "overcap") and (req.get("max_tokens") or 0) < 100:
109
+ return self._stream(model, [{"reasoning_content": "thinking and thinking"}], finish="length",
110
+ usage={"prompt_tokens": 5, "completion_tokens": req.get("max_tokens")})
111
+ if model == "ratelimited" and n <= 3:
112
+ return self._send(429, b'{"error": {"message": "slow down", "code": 429}}', extra={
113
+ "Retry-After": "0.1", "x-ratelimit-limit-requests": "3",
114
+ "x-ratelimit-remaining-requests": "0", "x-ratelimit-reset-requests": "2s"})
115
+ if model == "gemini":
116
+ return self._send(429, json.dumps([{"error": {"code": 429, "status": "RESOURCE_EXHAUSTED",
117
+ "message": "Quota exceeded", "details": [
118
+ {"@type": "type.googleapis.com/google.rpc.QuotaFailure", "violations": [
119
+ {"quotaId": "GenerateRequestsPerDayPerProjectPerModel-FreeTier",
120
+ "quotaValue": "50"}]},
121
+ {"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "0.1s"}]}}]).encode())
122
+ if model == "paced":
123
+ time.sleep(0.4) # long enough that the seats overlap
124
+ if model == "dribble":
125
+ return self._stream(model, [{"content": "x"}] * 25, gap=0.2)
126
+ if model == "breakoff":
127
+ return self._stream(model, [{"content": "half an answer"}], close_early=True)
128
+ return self._stream(model, [{"reasoning_content": "let me "}, {"reasoning_content": "see"},
129
+ {"content": "The answer "}, {"content": "is ready."}],
130
+ served="someone-else-7b" if model == "swapped" else None,
131
+ extra={"x-ratelimit-remaining-requests": "99", "x-ratelimit-limit-requests": "100",
132
+ "x-ratelimit-reset-requests": "6m0s"} if model == "ok" else None)
133
+ except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
134
+ pass
135
+
136
+
137
+ def setup(port: int) -> None:
138
+ base = f"http://127.0.0.1:{port}"
139
+ eps = {"fakeprov": f"{base}/v1", "pacedprov": f"{base}/p/v1", "procprov": f"{base}/q/v1",
140
+ "monthprov": f"{base}/m/v1"}
141
+ (TMP / "config").mkdir(parents=True, exist_ok=True)
142
+ (TMP / "llm").mkdir(parents=True, exist_ok=True)
143
+ (TMP / "config" / "endpoints.json").write_text(json.dumps(
144
+ {n: {"base": b, "key": "fake", "kind": "provider", **({"default": True} if n == "fakeprov" else {})}
145
+ for n, b in eps.items()}), encoding="utf-8")
146
+ where = {"paced": "pacedprov", "proc": "procprov", "month": "monthprov"}
147
+ yaml = []
148
+ for m in ("ok", "plain", "nooptions", "swapped", "budget", "overcap", "ratelimited", "gemini", "dribble", "breakoff",
149
+ "paced", "proc", "month", "paid"):
150
+ yaml += [f"- model_id: t-{m}", f" model_name: {'paced' if m in ('proc', 'month') else m}",
151
+ f' api_base: "{eps[where.get(m, "fakeprov")]}"', " api_key_name: fake"]
152
+ (TMP / "config" / "extra-openai-models.yaml").write_text("\n".join(yaml) + "\n", encoding="utf-8")
153
+ (TMP / "config" / "model-options.json").write_text('{"t-budget": {"max_tokens": 64}, "t-overcap": {"max_tokens": 64}}',
154
+ encoding="utf-8")
155
+ (TMP / "config" / "connection-limits.json").write_text(json.dumps(
156
+ {"pacedprov": {"rpm": 3}, "procprov": {"rpm": 4}, "monthprov": {"rpmo": 3}}), encoding="utf-8")
157
+ (TMP / "llm" / "keys.json").write_text('{"fake": "sk-test-not-a-key"}', encoding="utf-8")
158
+
159
+
160
+ def _child(n: int) -> None:
161
+ """One process's calls, for the cross-process pacing check."""
162
+ for _ in range(n):
163
+ client.chat("t-proc", "hi", timeout=5)
164
+
165
+
166
+ def main() -> int:
167
+ srv = ThreadingHTTPServer(("127.0.0.1", 0), Fake)
168
+ srv.daemon_threads = True
169
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
170
+ setup(srv.server_address[1])
171
+ failures = 0
172
+
173
+ def check(ok: bool, what: str, detail="") -> None:
174
+ nonlocal failures
175
+ print(("ok " if ok else "FAIL ") + what + ("" if ok else f"\n {detail}"))
176
+ failures += not ok
177
+
178
+ def last(alias: str) -> dict:
179
+ return [r for r in ledger.rows() if r["alias"] == alias][-1]
180
+
181
+ # Streaming: text and reasoning apart, every piece heard, usage from the last chunk.
182
+ heard = []
183
+ r = client.chat("t-ok", "Say something.", on_delta=lambda k, t: heard.append((k, t)), purpose="test")
184
+ req = seen["ok"][-1]
185
+ check(r["text"] == "The answer is ready." and r["reasoning"] == "let me see" and r["finish"] == "stop",
186
+ "a streamed answer, its reasoning kept apart", r)
187
+ check(req.get("stream") is True and req.get("stream_options") == {"include_usage": True},
188
+ "asked for a stream with usage", req)
189
+ check(heard == [("reasoning", "let me "), ("reasoning", "see"), ("text", "The answer "), ("text", "is ready.")],
190
+ "on_delta heard each piece in order", heard)
191
+ check(r["usage"].get("completion_tokens") == 4 and r["first_token_s"] is not None and r["attempts"] == 1,
192
+ "usage from the final chunk, first token timed", r)
193
+ row = last("t-ok")
194
+ check(row["status"] == "ok" and row["tokens_in"] == 7 and row["tokens_out"] == 4 and row["attempts"] == 1
195
+ and row["connection"] == "fakeprov" and row["caller"] == "scripts/test_quota" and row["purpose"] == "test",
196
+ "a ledger line with tokens, connection and caller", row)
197
+ recs = quota.live("fakeprov", "t-ok")
198
+ check(len(recs) == 1 and recs[0]["remaining"] == 99 and recs[0]["limit"] == 100,
199
+ "the reply's rate-limit headers kept as a quota record", recs)
200
+
201
+ # Upstreams that do not stream.
202
+ r = client.chat("t-plain", "hi")
203
+ check(r["text"] == "plain answer" and not r["error"], "plain JSON to a stream request is read as before", r)
204
+ r = client.chat("t-nooptions", "hi")
205
+ again = seen["nooptions"][-1]
206
+ check(r["text"] == "The answer is ready." and r["attempts"] == 2 and "stream_options" not in again
207
+ and again.get("stream") is True, "stream_options refused: asked once more without, still streamed", r)
208
+
209
+ # A silent swap: the call succeeds and is noted.
210
+ r = client.chat("t-swapped", "hi")
211
+ swap = (learn.learned().get("t-swapped") or {}).get("served_as") or {}
212
+ check(r["text"] and r["model"] == "someone-else-7b" and swap.get("served") == "someone-else-7b"
213
+ and last("t-swapped").get("swap") is True, "a swapped model is noted, the answer kept", (r, swap))
214
+
215
+ # An empty answer that ran out of budget: once more with twice the budget.
216
+ r = client.chat("t-budget", "hi")
217
+ tried = [c.get("max_tokens") for c in seen["budget"]]
218
+ check(r["text"] == "The answer is ready." and tried == [64, 128]
219
+ and r.get("budget_retry") == {"max_tokens": [64, 128], "answered": True}
220
+ and last("t-budget")["attempts"] == 2, "an empty reply cut by its budget is retried once, larger",
221
+ (tried, r))
222
+ r = client.chat("t-overcap", "hi")
223
+ check(r["text"] == "" and not r["error"] and r["budget_retry"]["answered"] is False
224
+ and "too large" in r["budget_retry"]["failed"] and run.failure_class(client.output(r, 30)) == "empty",
225
+ "a budget retry that fails leaves the empty answer, not an error", r)
226
+
227
+ # A stream that breaks off, and STOP between chunks.
228
+ r = client.chat("t-breakoff", "hi", timeout=10)
229
+ check("broke off before it finished" in r["error"] and run.failure_class(client.output(r, 10)) == "other",
230
+ "a stream that breaks off is an error, not a short answer", r["error"])
231
+ threading.Timer(0.6, client.STOP.set).start()
232
+ t = time.monotonic()
233
+ r = client.chat("t-dribble", "hi", timeout=30)
234
+ client.STOP.clear()
235
+ check(r["timed_out"] and time.monotonic() - t < 3, f"STOP ends a stream between chunks ({time.monotonic() - t:.1f}s)", r)
236
+
237
+ # Quota records: a 429's headers, then the next call waits for the reset.
238
+ r = client.chat("t-ratelimited", "hi", timeout=20)
239
+ # Read whole: the Retry-After record (0.1 s) has passed already, so live() leaves it out.
240
+ recs = (quota.locked.read_json(quota.FILE).get("fakeprov") or {}).get("t-ratelimited") or {}
241
+ check(r["status"] == 429 and r["attempts"] == 3 and recs.get("requests/unknown", {}).get("remaining") == 0
242
+ and "requests/retry-after" in recs, "a 429's rate-limit headers kept as quota records", (r, recs))
243
+ t = time.monotonic()
244
+ r = client.chat("t-ratelimited", "hi", timeout=20)
245
+ waited = time.monotonic() - t
246
+ check(r["text"] and r["attempts"] == 1 and 1.0 <= waited < 6,
247
+ f"the next call waited out the stated reset, then answered ({waited:.1f}s)", r)
248
+ r = client.chat("t-gemini", "hi", timeout=20)
249
+ rpd = ((learn.learned().get("t-gemini") or {}).get("limits") or {}).get("rpd") or {}
250
+ check(r["status"] == 429 and rpd.get("value") == 50 and rpd.get("where") == "quota",
251
+ "a Gemini 429 body's daily quota is learned as the model's rpd", rpd)
252
+
253
+ # Pacing across threads: rpm 3, six seats at once, the server slow enough that they overlap.
254
+ out: list[dict] = []
255
+ seats = [threading.Thread(target=lambda: out.append(client.chat("t-paced", "hi", timeout=5)))
256
+ for _ in range(6)]
257
+ for s in seats:
258
+ s.start()
259
+ for s in seats:
260
+ s.join()
261
+ answered = [x for x in out if x["text"]]
262
+ refused = [x for x in out if "rate limit (not sent" in x["error"]]
263
+ check(len(answered) == 3 and len(refused) == 3 and len(seen["paced"]) == 3,
264
+ f"rpm 3 across six threads: {len(answered)} sent, {len(refused)} refused", [x["error"][:80] for x in out])
265
+ if refused:
266
+ body = client.output(refused[0], 5)
267
+ check(run.failure_class(body) == "capacity" and run.outcome(body)[0] == "error"
268
+ and "try again in" in body and learn.parse(body).get("limits") is None,
269
+ "the refusal is a capacity failure, with a wait and no new learned limit", body)
270
+ check(last("t-paced")["status"] in ("refused", "ok") and
271
+ sum(1 for x in ledger.rows() if x["alias"] == "t-paced" and x["status"] == "refused") == 3,
272
+ "refused calls are in the ledger, sending nothing")
273
+
274
+ # Across processes: rpm 4 on procprov, two processes of three calls.
275
+ procs = [multiprocessing.Process(target=_child, args=(3,)) for _ in range(2)]
276
+ for p in procs:
277
+ p.start()
278
+ for p in procs:
279
+ p.join()
280
+ rows = [x for x in ledger.rows() if x["connection"] == "procprov"]
281
+ sent = sum(1 for x in rows if x["status"] == "ok")
282
+ check(all(p.exitcode == 0 for p in procs) and sent == 4 and len(rows) == 6,
283
+ f"rpm 4 across two processes: {sent} of {len(rows)} sent", rows)
284
+
285
+ # A monthly cap: three this month already, so the next waits for next month, which no deadline fits.
286
+ now = time.time()
287
+ for _ in range(3):
288
+ ledger.append({"t": now - 1, "alias": "t-month", "connection": "monthprov", "status": "ok", "attempts": 1})
289
+ r = client.chat("t-month", "hi", timeout=30)
290
+ check("rate limit (not sent" in r["error"] and "a calendar month" in r["error"] and "this month" in r["error"],
291
+ "rpmo 3: the fourth call this month is refused", r["error"])
292
+ check(client.chat("t-ok", "hi")["text"] != "", "another connection is not held by that cap")
293
+
294
+ # The free-tier guard: a model that went paid is held until allowed.
295
+ before = len(seen.get("paid", []))
296
+ alerts.raise_alert("model:paid:paid", "went paid", "t-paid is listed with a price now (models.dev)",
297
+ "check the provider", alias="t-paid")
298
+ r = client.chat("t-paid", "hi")
299
+ body = client.output(r, 30)
300
+ check("free-tier guard" in r["error"] and len(seen.get("paid", [])) == before
301
+ and run.failure_class(body) == "entitlement" and last("t-paid")["status"] == "refused",
302
+ "the free-tier guard holds a model that went paid, as entitlement", body)
303
+ (TMP / "config" / "connections.json").write_text('{"allow_paid": ["t-paid"]}', encoding="utf-8")
304
+ check(client.chat("t-paid", "hi")["text"] != "", "allow_paid lets it through")
305
+
306
+ # cost.py reads the ledger.
307
+ data = cost.collect()
308
+ ok = data.get("t-ok", {})
309
+ check(ok.get("calls", 0) >= 2 and ok.get("input", 0) >= 14 and ok.get("output", 0) >= 8,
310
+ "cost.py counts the ledger's calls and tokens", ok)
311
+
312
+ # The dashboard: the Endpoints dialog's limits take rpmo; the Quotas page shows records and the ledger.
313
+ sys.path.insert(0, str(ROOT))
314
+ import serve
315
+ saved = serve.save_connection_limits("monthprov", {"rpm": 20, "rpmo": 1000})
316
+ view = {c["name"]: c for c in serve.connections_view()["connections"]}
317
+ check(saved["ok"] and "rpmo" in serve.connections_view()["keys"]
318
+ and view.get("monthprov", {}).get("limits") == {"rpm": 20, "rpmo": 1000},
319
+ "the Endpoints dialog saves a monthly cap (rpmo)", (saved, view.get("monthprov")))
320
+ bad = serve.save_connection_limits("monthprov", {"rpmo": -1})
321
+ check(not bad["ok"] and serve.save_model_facts("t-ok", {"limits": {"rpmo": 5}}).get("ok") is not None
322
+ and "rpmo" not in ((serve.catalog.overrides().get("t-ok") or {}).get("limits") or {}),
323
+ "a bad rpmo is refused; a model's own limits have none", bad)
324
+ page = serve.quotas_page()
325
+ check("What providers said is left" in page and "fakeprov" in page and "What the bench sent" in page,
326
+ "the Quotas page shows the records and the ledger's counts")
327
+
328
+ srv.shutdown()
329
+ print("\nthe ledger, quotas and pacing hold" if not failures else f"\n{failures} FAILED")
330
+ return 1 if failures else 0
331
+
332
+
333
+ if __name__ == "__main__":
334
+ sys.exit(main())