@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
package/metrics/cost.py CHANGED
@@ -1,152 +1,171 @@
1
- """What each model actually costs us, from the `llm` CLI's own log.
2
-
3
- python metrics/cost.py
4
-
5
- **No new instrumentation.** The `llm` CLI has been logging every call all
6
- along — 382 turns with token counts were already on disk before this file
7
- existed. This reads them.
8
-
9
- WHY REASONING TOKENS MATTER SEPARATELY
10
- --------------------------------------
11
- They are billed like output and are invisible in the answer. One
12
- `gemini-3.8-flash-high` call spent **6,524** of them on a prompt whose reply
13
- was a few lines; a trivial one spent 123 of 134 total tokens thinking. A model
14
- that answers correctly at four times the token cost of another is a different
15
- proposition for bulk work, and nothing else in this repo could see that.
16
-
17
- WHAT THIS IS NOT
18
- ----------------
19
- Money. Most of what we call is free or flat-rate, and inventing a price per
20
- model would be a number nobody could check. Tokens are the honest unit: they
21
- are what we spend, they are what a provider meters, and a rate can be applied
22
- later by whoever knows it.
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import collections
28
- import json
29
- import pathlib
30
- import sqlite3
31
-
32
- ROOT = pathlib.Path(__file__).resolve().parent.parent
33
- from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
34
- from endpoints import LLM_DIR # noqa: E402
35
- LOGS = LLM_DIR / "logs.db"
36
- OUT = DATA / "metrics" / "cost.json"
37
-
38
-
39
- def alias_history() -> dict[str, str]:
40
- path = DATA / "config" / "alias-history.json"
41
- if not path.exists():
42
- return {}
43
- return json.loads(path.read_text(encoding="utf-8"))
44
-
45
-
46
- LEDGER = DATA / "metrics" / "benchmark-calls.json"
47
-
48
-
49
- def note_benchmark_calls(model: str, calls: int) -> None:
50
- """Record that N of a model's logged calls were a BENCHMARK, not work.
51
-
52
- Without this the call counter measures how much a model was BENCHMARKED
53
- rather than how much it was relied on, and says the opposite of what it
54
- appears to. `deepseek-v4-pro-0813` shows 214 calls, of which roughly 200
55
- are floor runs that failed — it looks like the most-used model here and it
56
- is nothing of the sort.
57
-
58
- Each runner reports its own total, because each runner is the only thing
59
- that knows. Offload calls are then the remainder.
60
- """
61
- try:
62
- data = json.loads(LEDGER.read_text(encoding="utf-8")) if LEDGER.exists() else {}
63
- except ValueError:
64
- data = {}
65
- data[model] = data.get(model, 0) + calls
66
- LEDGER.parent.mkdir(parents=True, exist_ok=True)
67
- LEDGER.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n",
68
- encoding="utf-8")
69
-
70
-
71
- def benchmark_calls() -> dict[str, int]:
72
- if not LEDGER.exists():
73
- return {}
74
- try:
75
- return json.loads(LEDGER.read_text(encoding="utf-8"))
76
- except ValueError:
77
- return {}
78
-
79
-
80
- def collect() -> dict:
81
- """Per-model totals, with old aliases folded into their current name."""
82
- if not LOGS.exists():
83
- return {}
84
- rename = alias_history()
85
- db = sqlite3.connect(f"file:{LOGS}?mode=ro", uri=True)
86
- db.row_factory = sqlite3.Row
87
-
88
- agg: dict[str, dict] = collections.defaultdict(
89
- lambda: {"calls": 0, "input": 0, "output": 0, "reasoning": 0,
90
- "duration_ms": 0, "with_reasoning_data": 0})
91
- for row in db.execute(
92
- "SELECT model, input_tokens, output_tokens, token_details,"
93
- " duration_ms FROM turns"):
94
- # Turns predating the versioned rename carry the old alias.
95
- model = rename.get(row["model"], row["model"])
96
- a = agg[model]
97
- a["calls"] += 1
98
- a["input"] += row["input_tokens"] or 0
99
- a["output"] += row["output_tokens"] or 0
100
- a["duration_ms"] += row["duration_ms"] or 0
101
- try:
102
- details = json.loads(row["token_details"] or "{}") or {}
103
- except ValueError:
104
- details = {}
105
- reasoning = ((details.get("completion_tokens_details") or {})
106
- .get("reasoning_tokens"))
107
- if reasoning is not None:
108
- a["reasoning"] += reasoning
109
- a["with_reasoning_data"] += 1
110
-
111
- bench = benchmark_calls()
112
- out = {}
113
- for model, a in agg.items():
114
- billed = a["output"] + a["reasoning"]
115
- out[model] = {
116
- **a,
117
- # The number that decides bulk work: how much of what we pay for
118
- # never appears in the answer.
119
- "reasoning_share": (round(a["reasoning"] / billed, 3)
120
- if billed else None),
121
- "tokens_per_call": (round((a["input"] + billed) / a["calls"])
122
- if a["calls"] else 0),
123
- # Benchmark calls ONLY. There was an `offload_calls` here too,
124
- # derived as calls minus benchmarks, and it was wrong in both
125
- # directions: the ledger began the day it was written, so every
126
- # earlier suite run counted as work. Real offloads are recorded
127
- # deliberately in metrics/offload.py, because subtracting one
128
- # imperfect number from another gives a third nobody can check.
129
- "benchmark_calls": bench.get(model, 0),
130
- }
131
- return out
132
-
133
-
134
- def main() -> None:
135
- data = collect()
136
- OUT.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n",
137
- encoding="utf-8")
138
- rows = sorted(data.items(), key=lambda kv: -kv[1]["tokens_per_call"])
139
- print(f"{'model':<30} {'calls':>5} {'tok/call':>9} {'reasoning share':>16}")
140
- for model, a in rows:
141
- share = ("—" if a["reasoning_share"] is None
142
- else f"{a['reasoning_share']:.1%}")
143
- note = "" if a["with_reasoning_data"] else " (no reasoning data)"
144
- print(f"{model:<30} {a['calls']:>5} {a['tokens_per_call']:>9,} "
145
- f"{share:>16}{note}")
146
- print(f"\nwrote {OUT}")
147
- print("Tokens, not money: most of what we call is free or flat-rate, and "
148
- "a made-up price is a number nobody can check.")
149
-
150
-
151
- if __name__ == "__main__":
152
- main()
1
+ """What each model actually costs us, from the bench's usage ledger and the `llm` CLI's log.
2
+
3
+ python metrics/cost.py
4
+
5
+ TWO SOURCES, NO CALL IN BOTH. The built-in client writes a line for every
6
+ call to the usage ledger (metrics/ledger.py, RELEASE-PLAN Phase 2b); calls
7
+ made through the `llm` backend, and every call before the switch, are in
8
+ llm's own logs.db, which it has kept all along (382 turns with token counts
9
+ were on disk before this file existed). The built-in client never writes
10
+ logs.db and `llm` never writes the ledger, so the two are added up.
11
+
12
+ WHY REASONING TOKENS MATTER SEPARATELY
13
+ --------------------------------------
14
+ They are billed like output and are invisible in the answer. One
15
+ `gemini-3.8-flash-high` call spent **6,524** of them on a prompt whose reply
16
+ was a few lines; a trivial one spent 123 of 134 total tokens thinking. A model
17
+ that answers correctly at four times the token cost of another is a different
18
+ proposition for bulk work, and nothing else in this repo could see that.
19
+
20
+ WHAT THIS IS NOT
21
+ ----------------
22
+ Money. Most of what we call is free or flat-rate, and inventing a price per
23
+ model would be a number nobody could check. Tokens are the honest unit: they
24
+ are what we spend, they are what a provider meters, and a rate can be applied
25
+ later by whoever knows it.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import collections
31
+ import json
32
+ import pathlib
33
+ import sqlite3
34
+
35
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
36
+ from paths import DATA # noqa: E402 -- the install's data folder (metrics/paths.py)
37
+ from endpoints import LLM_DIR # noqa: E402
38
+ LOGS = LLM_DIR / "logs.db"
39
+ OUT = DATA / "metrics" / "cost.json"
40
+
41
+
42
+ def alias_history() -> dict[str, str]:
43
+ path = DATA / "config" / "alias-history.json"
44
+ if not path.exists():
45
+ return {}
46
+ return json.loads(path.read_text(encoding="utf-8"))
47
+
48
+
49
+ LEDGER = DATA / "metrics" / "benchmark-calls.json"
50
+
51
+
52
+ def note_benchmark_calls(model: str, calls: int) -> None:
53
+ """Record that N of a model's logged calls were a BENCHMARK, not work.
54
+
55
+ Without this the call counter measures how much a model was BENCHMARKED
56
+ rather than how much it was relied on, and says the opposite of what it
57
+ appears to. `deepseek-v4-pro-0813` shows 214 calls, of which roughly 200
58
+ are floor runs that failed — it looks like the most-used model here and it
59
+ is nothing of the sort.
60
+
61
+ Each runner reports its own total, because each runner is the only thing
62
+ that knows. Offload calls are then the remainder.
63
+ """
64
+ try:
65
+ data = json.loads(LEDGER.read_text(encoding="utf-8")) if LEDGER.exists() else {}
66
+ except ValueError:
67
+ data = {}
68
+ data[model] = data.get(model, 0) + calls
69
+ LEDGER.parent.mkdir(parents=True, exist_ok=True)
70
+ LEDGER.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n",
71
+ encoding="utf-8")
72
+
73
+
74
+ def benchmark_calls() -> dict[str, int]:
75
+ if not LEDGER.exists():
76
+ return {}
77
+ try:
78
+ return json.loads(LEDGER.read_text(encoding="utf-8"))
79
+ except ValueError:
80
+ return {}
81
+
82
+
83
+ def _llm_turns():
84
+ """(model, input, output, reasoning or None, duration_ms) per turn in llm's log."""
85
+ if not LOGS.exists():
86
+ return
87
+ db = sqlite3.connect(f"file:{LOGS}?mode=ro", uri=True)
88
+ db.row_factory = sqlite3.Row
89
+ for row in db.execute(
90
+ "SELECT model, input_tokens, output_tokens, token_details,"
91
+ " duration_ms FROM turns"):
92
+ try:
93
+ details = json.loads(row["token_details"] or "{}") or {}
94
+ except ValueError:
95
+ details = {}
96
+ reasoning = ((details.get("completion_tokens_details") or {})
97
+ .get("reasoning_tokens"))
98
+ yield (row["model"], row["input_tokens"], row["output_tokens"], reasoning,
99
+ row["duration_ms"])
100
+
101
+
102
+ def _ledger_calls():
103
+ """The same, per answered call in the usage ledger. Refused and failed
104
+ calls spent nothing, as llm's log (which keeps only answers) counts."""
105
+ import ledger
106
+ for row in ledger.rows():
107
+ if row.get("status") in ("ok", "empty"):
108
+ yield (row.get("alias") or "?", row.get("tokens_in"), row.get("tokens_out"),
109
+ row.get("reasoning"), round((row.get("seconds") or 0) * 1000))
110
+
111
+
112
+ def collect() -> dict:
113
+ """Per-model totals, with old aliases folded into their current name."""
114
+ rename = alias_history()
115
+ agg: dict[str, dict] = collections.defaultdict(
116
+ lambda: {"calls": 0, "input": 0, "output": 0, "reasoning": 0,
117
+ "duration_ms": 0, "with_reasoning_data": 0})
118
+ for name, tokens_in, tokens_out, reasoning, ms in (*_llm_turns(), *_ledger_calls()):
119
+ # Turns predating the versioned rename carry the old alias.
120
+ model = rename.get(name, name)
121
+ a = agg[model]
122
+ a["calls"] += 1
123
+ a["input"] += tokens_in or 0
124
+ a["output"] += tokens_out or 0
125
+ a["duration_ms"] += ms or 0
126
+ if reasoning is not None:
127
+ a["reasoning"] += reasoning
128
+ a["with_reasoning_data"] += 1
129
+
130
+ bench = benchmark_calls()
131
+ out = {}
132
+ for model, a in agg.items():
133
+ billed = a["output"] + a["reasoning"]
134
+ out[model] = {
135
+ **a,
136
+ # The number that decides bulk work: how much of what we pay for
137
+ # never appears in the answer.
138
+ "reasoning_share": (round(a["reasoning"] / billed, 3)
139
+ if billed else None),
140
+ "tokens_per_call": (round((a["input"] + billed) / a["calls"])
141
+ if a["calls"] else 0),
142
+ # Benchmark calls ONLY. There was an `offload_calls` here too,
143
+ # derived as calls minus benchmarks, and it was wrong in both
144
+ # directions: the ledger began the day it was written, so every
145
+ # earlier suite run counted as work. Real offloads are recorded
146
+ # deliberately in metrics/offload.py, because subtracting one
147
+ # imperfect number from another gives a third nobody can check.
148
+ "benchmark_calls": bench.get(model, 0),
149
+ }
150
+ return out
151
+
152
+
153
+ def main() -> None:
154
+ data = collect()
155
+ OUT.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n",
156
+ encoding="utf-8")
157
+ rows = sorted(data.items(), key=lambda kv: -kv[1]["tokens_per_call"])
158
+ print(f"{'model':<30} {'calls':>5} {'tok/call':>9} {'reasoning share':>16}")
159
+ for model, a in rows:
160
+ share = ("—" if a["reasoning_share"] is None
161
+ else f"{a['reasoning_share']:.1%}")
162
+ note = "" if a["with_reasoning_data"] else " (no reasoning data)"
163
+ print(f"{model:<30} {a['calls']:>5} {a['tokens_per_call']:>9,} "
164
+ f"{share:>16}{note}")
165
+ print(f"\nwrote {OUT}")
166
+ print("Tokens, not money: most of what we call is free or flat-rate, and "
167
+ "a made-up price is a number nobody can check.")
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()