@wendellresearch-ai/benchwarmers 0.5.6 → 0.6.0

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.
@@ -131,8 +131,9 @@ def load() -> dict[str, dict]:
131
131
  else's machine is a timeout with no explanation. Whatever needs an
132
132
  endpoint asks default() (base, key, models and yaml_block all do), and
133
133
  that refuses, so a job run before any endpoint exists says what to do."""
134
+ import locked
134
135
  try:
135
- data = json.loads(FILE.read_text(encoding="utf-8"))
136
+ data = json.loads(locked.read_text(FILE)) # inside a render, from its snapshot
136
137
  except FileNotFoundError:
137
138
  return {}
138
139
  except (OSError, ValueError) as exc:
@@ -147,11 +148,8 @@ def connections() -> dict:
147
148
  ties, names for bare-IP hosts, models.dev ids, providers whose free tier
148
149
  works only in their own client. {} when absent: a fresh
149
150
  install has no such facts, and none are assumed."""
150
- try:
151
- data = json.loads(CONNECTIONS.read_text(encoding="utf-8"))
152
- except (OSError, ValueError):
153
- return {}
154
- return data if isinstance(data, dict) else {}
151
+ import locked
152
+ return locked.read_json(CONNECTIONS) # inside a render, from its snapshot
155
153
 
156
154
 
157
155
  _default: tuple = (None, "")
package/metrics/guard.py CHANGED
@@ -146,9 +146,11 @@ def cors_origin(headers, port: int) -> str:
146
146
  return origin if origin.lower() in origins(port) else ""
147
147
 
148
148
 
149
- def check(headers, port: int, post: bool, max_body: int, cors: bool = False) -> int:
149
+ def check(headers, port: int, post: bool, max_body: int, cors: bool = False,
150
+ kind: str = "application/json") -> int:
150
151
  """Raise Refused, or return how many body bytes to read (0 for a GET).
151
- `cors`: a gateway route, where an allowed origin may be cross-site."""
152
+ `cors`: a gateway route, where an allowed origin may be cross-site.
153
+ `kind`: the body's type; never one a plain form can send (an import's zip)."""
152
154
  host = (headers.get("Host") or "").strip().lower()
153
155
  if host not in hosts(port):
154
156
  raise Refused(403, f"unknown Host {host or '(none)'!r}; set BENCHWARMERS_PUBLIC_URL"
@@ -161,8 +163,8 @@ def check(headers, port: int, post: bool, max_body: int, cors: bool = False) ->
161
163
  raise Refused(403, "cross-site requests are refused")
162
164
  if origin and origin not in allowed:
163
165
  raise Refused(403, f"Origin {origin!r} is not allowed; set BENCH_ALLOWED_ORIGINS to add one")
164
- if not (headers.get("Content-Type") or "").lower().startswith("application/json"):
165
- raise Refused(415, "send Content-Type: application/json")
166
+ if not (headers.get("Content-Type") or "").lower().startswith(kind):
167
+ raise Refused(415, f"send Content-Type: {kind}")
166
168
  try:
167
169
  length = int(headers.get("Content-Length") or 0)
168
170
  except ValueError:
package/metrics/ledger.py CHANGED
@@ -17,7 +17,9 @@ are what a provider's request limits count. `status` is ok, empty, error,
17
17
  timeout or refused (not sent: pacing, or the free-tier guard). Tokens are the
18
18
  provider's own figures from the reply's usage, absent when it gave none.
19
19
  `caller` is the script that made the call (gateway, perf/run, suite/run, ...)
20
- and `purpose` what the caller said it was for, when it said.
20
+ and `purpose` what the caller said it was for, when it said. A failed call
21
+ (status error) carries its `error`, start and end (client.clip), from
22
+ 2026-11-08: the uptime report classifies it (suite/run.uptime_class).
21
23
 
22
24
  A FILE A MONTH
23
25
  --------------
package/metrics/locked.py CHANGED
@@ -26,6 +26,7 @@ import json
26
26
  import os
27
27
  import pathlib
28
28
  import tempfile
29
+ import threading
29
30
  import time
30
31
 
31
32
  if os.name == "nt":
@@ -71,6 +72,7 @@ def write_atomic(path: pathlib.Path, text: str) -> None:
71
72
  writers never share it (a fixed `.part` name let one writer move another's
72
73
  half-written file into place)."""
73
74
  path = pathlib.Path(path)
75
+ _forget(path)
74
76
  path.parent.mkdir(parents=True, exist_ok=True)
75
77
  fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".part")
76
78
  try:
@@ -90,10 +92,85 @@ def write_atomic(path: pathlib.Path, text: str) -> None:
90
92
  os.remove(tmp)
91
93
 
92
94
 
95
+ _seen = threading.local()
96
+
97
+
98
+ @contextlib.contextmanager
99
+ def snapshot():
100
+ """Inside it, this thread reads each file from disk once and reuses the
101
+ text: one dashboard render asked for the same dozen config files 11,000
102
+ times, seconds of opens on Windows. Text, not the parsed object, so a
103
+ caller changing what it parsed changes nothing for the next one. Other
104
+ threads (the gateway, a POST) read the disk as before; a write from this
105
+ thread drops its copy."""
106
+ if getattr(_seen, "texts", None) is not None: # already inside one
107
+ yield
108
+ return
109
+ _seen.texts = {}
110
+ try:
111
+ yield
112
+ finally:
113
+ _seen.texts = None
114
+
115
+
116
+ def once(key, compute):
117
+ """compute(); inside snapshot(), its first answer for `key` (an OSError as
118
+ first raised). Only for answers no caller can change, such as a string."""
119
+ texts = getattr(_seen, "texts", None)
120
+ if texts is None:
121
+ return compute()
122
+ if key not in texts:
123
+ try:
124
+ texts[key] = compute()
125
+ except OSError as exc:
126
+ texts[key] = exc # missing stays missing, as the same error
127
+ if isinstance(texts[key], OSError):
128
+ raise texts[key]
129
+ return texts[key]
130
+
131
+
132
+ def read_text(path) -> str:
133
+ """The file's text (OSError if it cannot be read); inside snapshot(), as
134
+ first read."""
135
+ return once(str(path), lambda: pathlib.Path(path).read_text(encoding="utf-8"))
136
+
137
+
138
+ def appended(path, mark: dict) -> tuple[bool, list[str]]:
139
+ """(start over?, lines) for a log that is only ever appended to: the
140
+ whole lines added since `mark` last saw it. The first time, or when the
141
+ file was replaced or rewritten rather than appended to (shorter, or the
142
+ bytes before where it stopped differ), it is True and every line. A last
143
+ line still missing its newline waits for the next call. `mark` is the
144
+ caller's own dict, kept between calls; hold one lock around this call
145
+ and whatever folds the lines in."""
146
+ path = str(path)
147
+ at, before = (mark.get("at", 0), mark.get("before", b"")) if mark.get("path") == path else (0, b"")
148
+ try:
149
+ with open(path, "rb") as fh:
150
+ size = fh.seek(0, os.SEEK_END)
151
+ if at:
152
+ fh.seek(max(at - len(before), 0))
153
+ if size < at or fh.read(len(before)) != before:
154
+ at, before = 0, b""
155
+ fh.seek(at)
156
+ data = fh.read()
157
+ except OSError:
158
+ mark.clear()
159
+ return True, []
160
+ end = data.rfind(b"\n") + 1
161
+ mark.update(path=path, at=at + end, before=(before + data[:end])[-64:])
162
+ return at == 0, data[:end].decode("utf-8", "replace").splitlines()
163
+
164
+
165
+ def _forget(path) -> None:
166
+ if (texts := getattr(_seen, "texts", None)) is not None:
167
+ texts.pop(str(path), None)
168
+
169
+
93
170
  def read_json(path: pathlib.Path) -> dict:
94
171
  """The file's object, or {} when it is missing, unreadable or not an object."""
95
172
  try:
96
- data = json.loads(pathlib.Path(path).read_text(encoding="utf-8"))
173
+ data = json.loads(read_text(path))
97
174
  except (OSError, ValueError):
98
175
  return {}
99
176
  return data if isinstance(data, dict) else {}
@@ -105,6 +182,7 @@ def update(path: pathlib.Path, change, **dump):
105
182
  json.dumps (indent defaults to 2)."""
106
183
  dump.setdefault("indent", 2)
107
184
  with lock(path):
185
+ _forget(path) # under the lock, the disk's copy
108
186
  data = read_json(path)
109
187
  before = json.dumps(data, sort_keys=True)
110
188
  result = change(data)
@@ -135,6 +213,29 @@ def _selftest() -> None:
135
213
  assert data == {"t": 200, "p": 150}, data
136
214
  assert update(f, lambda d: "same") == "same" # no change, no write, result passed back
137
215
  assert not [p for p in f.parent.iterdir() if p.suffix == ".part"], "temp file left behind"
216
+ # A snapshot reads each file once; its own writes, and leaving it, read the disk again.
217
+ g = pathlib.Path(tmp) / "g.json"
218
+ with snapshot():
219
+ assert read_json(g) == {} # missing stays missing
220
+ g.write_text('{"a": 1}', encoding="utf-8")
221
+ assert read_json(g) == {}
222
+ write_atomic(g, '{"a": 2}')
223
+ assert read_json(g) == {"a": 2}
224
+ g.write_text('{"a": 3}', encoding="utf-8")
225
+ read_json(g)["a"] = 9 # changing what it parsed changes nothing
226
+ assert read_json(g) == {"a": 2}
227
+ assert read_json(g) == {"a": 3}
228
+ # A log read in pieces: only what was added, a half-written line waits,
229
+ # and a rewrite starts over.
230
+ log, mark = pathlib.Path(tmp) / "l.jsonl", {}
231
+ assert appended(log, mark) == (True, [])
232
+ log.write_bytes(b"a\nb\nc")
233
+ assert appended(log, mark) == (True, ["a", "b"])
234
+ with log.open("ab") as fh:
235
+ fh.write(b"\nd\n")
236
+ assert appended(log, mark) == (False, ["c", "d"]) and appended(log, mark) == (False, [])
237
+ log.write_bytes(b"x\ny\nz\nw\n") # longer, but not what was there
238
+ assert appended(log, mark) == (True, ["x", "y", "z", "w"])
138
239
  print("ok")
139
240
 
140
241
 
@@ -29,34 +29,51 @@ import collections
29
29
  import datetime as dt
30
30
  import json
31
31
  import pathlib
32
+ import threading
32
33
 
33
34
  ROOT = pathlib.Path(__file__).resolve().parent.parent
34
35
  from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
35
36
  LOG = DATA / "metrics" / "offloads.jsonl"
36
37
 
37
38
 
39
+ _lock = threading.Lock()
40
+ _seen: dict = {"mark": {}, "renames": None, "out": [], "by_call": {}}
41
+
42
+
38
43
  def entries() -> list[dict]:
39
44
  """Every offload, oldest first. An entry with a `call` id (written by the
40
45
  bench gateway) is superseded by a later one with the same id: the gateway
41
- records each seat unjudged as it answers, and the verdict comes after."""
42
- if not LOG.exists():
43
- return []
44
- out, by_call = [], {}
45
- from aliases import current
46
- for line in LOG.read_text(encoding="utf-8").splitlines():
47
- if line.strip():
48
- try:
49
- e = json.loads(line)
50
- except ValueError:
51
- continue
52
- e["model"] = current(e.get("model", "")) # a renamed model keeps its record
53
- if e.get("call") in by_call:
54
- out[by_call[e["call"]]] = e
55
- continue
56
- if e.get("call"):
57
- by_call[e["call"]] = len(out)
58
- out.append(e)
59
- return out
46
+ records each seat unjudged as it answers, and the verdict comes after.
47
+
48
+ Only the lines added since the last call are read (all of them again
49
+ after a rename): one dashboard render asked seven times, a log of 44,000
50
+ lines took most of a second each, and the gateway adds to it on every
51
+ call. A new list each call; the entries in it are shared, so never change
52
+ one."""
53
+ import aliases
54
+ import locked
55
+ with _lock:
56
+ s = _seen
57
+ if s["renames"] is not aliases.history(): # a rename re-reads the history as a new dict
58
+ s.update(mark={}, renames=aliases.history())
59
+ fresh, lines = locked.appended(LOG, s["mark"])
60
+ if fresh:
61
+ s.update(out=[], by_call={})
62
+ out, by_call = s["out"], s["by_call"]
63
+ for line in lines:
64
+ if line.strip():
65
+ try:
66
+ e = json.loads(line)
67
+ except ValueError:
68
+ continue
69
+ e["model"] = aliases.current(e.get("model", ""), s["renames"]) # a renamed model keeps its record
70
+ if e.get("call") in by_call:
71
+ out[by_call[e["call"]]] = e
72
+ continue
73
+ if e.get("call"):
74
+ by_call[e["call"]] = len(out)
75
+ out.append(e)
76
+ return list(out)
60
77
 
61
78
 
62
79
  def add(model: str, task: str, useful: bool | None, note: str, call: str | None = None,
package/metrics/route.py CHANGED
@@ -101,8 +101,9 @@ from aliases import current as current_alias # noqa: E402
101
101
 
102
102
 
103
103
  def _json(path: pathlib.Path) -> dict:
104
+ import locked
104
105
  try:
105
- return json.loads(path.read_text(encoding="utf-8"))
106
+ return json.loads(locked.read_text(path)) # inside a render, from its snapshot
106
107
  except (OSError, ValueError):
107
108
  return {}
108
109
 
@@ -135,7 +136,14 @@ def full_tag_of(alias: str) -> str:
135
136
  name, else "?": a hub fronts many connections, and an untagged model
136
137
  behind it (Open WebUI stopped listing it) is not on a connection called
137
138
  `openwebui` with every other untagged one.
139
+
140
+ Worked out once per alias in a dashboard render, which asked 2,600 times.
138
141
  """
142
+ import locked
143
+ return locked.once(("full_tag_of", alias), lambda: _full_tag_of(alias))
144
+
145
+
146
+ def _full_tag_of(alias: str) -> str:
139
147
  from run import _registered_names
140
148
  import endpoints
141
149
  # Tags are keyed by the served name. A decision model is not in the llm
@@ -6,7 +6,8 @@
6
6
  RELEASE-PLAN.md Phase 3. The checks used to exist only as Windows scheduled
7
7
  tasks (scripts/schedule.ps1), which macOS, Linux and Docker do not have. serve.py
8
8
  starts this in a background thread; the jobs below are the same seven, at the
9
- same times, with the same arguments (schedule.ps1 explains each choice).
9
+ same times, with the same arguments (schedule.ps1 explains each choice), and
10
+ since 2026-11-08 the decision benchmark, which has no Windows task.
10
11
 
11
12
  MODE, from config/scheduler.json:
12
13
  absent / "off" nothing runs. A fresh install spends no model calls until
@@ -55,6 +56,11 @@ JOBS = {
55
56
  "perf": (["perf/run.py", "-n", "3", "--timeout", "90", "--client", "builtin"], [(0, "07:00")]),
56
57
  "capabilities": (["metrics/probe_caps.py", "--gap", "30"], [(6, "07:00")]),
57
58
  "suite": (["suite/run.py", "--next"], [(None, t) for t in ("03:00", "09:00", "15:00", "21:00")]),
59
+ # Not in schedule.ps1 (2026-11-08). One question per model per set, 30 s
60
+ # apart round-robin: most decision models are OpenRouter `:free`, whose
61
+ # daily budget the suite and the probe share (metrics/decide.py).
62
+ "decisions": (["metrics/decide.py", "bench", "--per", "1", "--gap", "30", "--new-errors", "3"],
63
+ [(None, "04:30")]),
58
64
  }
59
65
  # Each mode as the Settings page names and explains it (the docstring above).
60
66
  MODES = {"off": ("Off", "Nothing runs. A fresh install spends no model calls until someone opts in."),
@@ -77,6 +83,9 @@ ABOUT = {
77
83
  " per model, spaced out.",
78
84
  "suite": "Scores one paid model on the review suite every six hours: up to six tasks it has not"
79
85
  " answered yet. These scores are what the router ranks by.",
86
+ "decisions": "Measures the decision models: one labelled question per model on each set (error classes,"
87
+ " collections, verdicts), 30 s apart, stopping a model at its first failure; and asks"
88
+ " about up to three new unrecognised error shapes. The gateway's picks ask the best one.",
80
89
  }
81
90
 
82
91
 
@@ -213,6 +222,7 @@ def _selftest() -> None:
213
222
  assert next_due(JOBS["perf"][1], mon) == dt.datetime(2026, 10, 19, 7, 0)
214
223
  assert next_due(JOBS["perf"][1], mon.replace(hour=8)) == dt.datetime(2026, 10, 26, 7, 0)
215
224
  assert next_due(JOBS["capabilities"][1], mon) == dt.datetime(2026, 10, 25, 7, 0)
225
+ assert next_due(JOBS["decisions"][1], mon) == dt.datetime(2026, 10, 19, 4, 30) and set(ABOUT) == set(JOBS)
216
226
  conf = {"mode": "log-only", "jobs": {n: n == "availability" for n in JOBS}}
217
227
  state: dict = {}
218
228
  global _log