benchkit-cli 0.1.0__py3-none-any.whl

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 (149) hide show
  1. benchkit/__init__.py +1 -0
  2. benchkit/_pi_proxy.py +219 -0
  3. benchkit/answer_key_guard.ts +49 -0
  4. benchkit/artifacts.py +87 -0
  5. benchkit/benchmarks/__init__.py +105 -0
  6. benchkit/benchmarks/aider_polyglot.py +405 -0
  7. benchkit/benchmarks/arc.py +52 -0
  8. benchkit/benchmarks/banking77.py +149 -0
  9. benchkit/benchmarks/base.py +19 -0
  10. benchkit/benchmarks/boolq.py +49 -0
  11. benchkit/benchmarks/chartqa.py +133 -0
  12. benchkit/benchmarks/code_verifier.py +77 -0
  13. benchkit/benchmarks/downloads.py +166 -0
  14. benchkit/benchmarks/evalplus.py +288 -0
  15. benchkit/benchmarks/git_surgery.py +485 -0
  16. benchkit/benchmarks/gpqa.py +56 -0
  17. benchkit/benchmarks/gsm8k.py +97 -0
  18. benchkit/benchmarks/hellaswag.py +49 -0
  19. benchkit/benchmarks/humaneval.py +103 -0
  20. benchkit/benchmarks/ifeval.py +56 -0
  21. benchkit/benchmarks/ifeval_instructions.py +477 -0
  22. benchkit/benchmarks/mbpp.py +60 -0
  23. benchkit/benchmarks/mc_arena.py +479 -0
  24. benchkit/benchmarks/mcq.py +94 -0
  25. benchkit/benchmarks/medfailbench.py +537 -0
  26. benchkit/benchmarks/mmlu.py +54 -0
  27. benchkit/benchmarks/mmlu_pro.py +62 -0
  28. benchkit/benchmarks/mmvp.py +115 -0
  29. benchkit/benchmarks/ocrbench.py +96 -0
  30. benchkit/benchmarks/openbookqa.py +52 -0
  31. benchkit/benchmarks/patcheval.py +929 -0
  32. benchkit/benchmarks/piqa.py +52 -0
  33. benchkit/benchmarks/ruler.py +172 -0
  34. benchkit/benchmarks/ruler_generator.py +535 -0
  35. benchkit/benchmarks/sanity.py +174 -0
  36. benchkit/benchmarks/treejs_arena.py +268 -0
  37. benchkit/benchmarks/truthfulqa.py +52 -0
  38. benchkit/benchmarks/utils.py +25 -0
  39. benchkit/benchmarks/vision.py +230 -0
  40. benchkit/benchmarks/vstar.py +108 -0
  41. benchkit/benchmarks/winogrande.py +52 -0
  42. benchkit/benchmarks/xstest.py +116 -0
  43. benchkit/browser.py +429 -0
  44. benchkit/catalog.py +121 -0
  45. benchkit/cli.py +1000 -0
  46. benchkit/client.py +1413 -0
  47. benchkit/controls.py +87 -0
  48. benchkit/datasets/MMLU_LICENSE.txt +21 -0
  49. benchkit/datasets/README.md +142 -0
  50. benchkit/datasets/arc_challenge.jsonl +1172 -0
  51. benchkit/datasets/banking77.jsonl +3080 -0
  52. benchkit/datasets/banking77_intents.json +79 -0
  53. benchkit/datasets/boolq.jsonl +3270 -0
  54. benchkit/datasets/code_verifier.jsonl +500 -0
  55. benchkit/datasets/gpqa_diamond.jsonl +198 -0
  56. benchkit/datasets/gsm8k.jsonl +1319 -0
  57. benchkit/datasets/hellaswag.jsonl +1000 -0
  58. benchkit/datasets/humaneval.jsonl +164 -0
  59. benchkit/datasets/ifeval.jsonl +541 -0
  60. benchkit/datasets/mbpp.jsonl +500 -0
  61. benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
  62. benchkit/datasets/medfailbench_hard30.jsonl +30 -0
  63. benchkit/datasets/medfailbench_hard30.md +13 -0
  64. benchkit/datasets/openbookqa.jsonl +500 -0
  65. benchkit/datasets/piqa.jsonl +1838 -0
  66. benchkit/datasets/sanity_code.jsonl +6 -0
  67. benchkit/datasets/truthfulqa.jsonl +817 -0
  68. benchkit/datasets/winogrande.jsonl +1267 -0
  69. benchkit/datasets/xstest.csv +451 -0
  70. benchkit/decision.py +194 -0
  71. benchkit/demo.py +451 -0
  72. benchkit/dockerfiles.py +247 -0
  73. benchkit/engine.py +1270 -0
  74. benchkit/evaluation.py +195 -0
  75. benchkit/events.py +141 -0
  76. benchkit/executor.py +96 -0
  77. benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
  78. benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
  79. benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
  80. benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
  81. benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
  82. benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
  83. benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
  84. benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
  85. benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
  86. benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
  87. benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
  88. benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
  89. benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
  90. benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
  91. benchkit/git_surgery/secret-in-history/README.md +27 -0
  92. benchkit/git_surgery/secret-in-history/setup.sh +107 -0
  93. benchkit/git_surgery/secret-in-history/verify.sh +106 -0
  94. benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
  95. benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
  96. benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
  97. benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
  98. benchkit/history.py +474 -0
  99. benchkit/jobs.py +332 -0
  100. benchkit/leakage.py +51 -0
  101. benchkit/looping.py +873 -0
  102. benchkit/mc_render.py +274 -0
  103. benchkit/mc_viewer/dist/atlas.png +0 -0
  104. benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
  105. benchkit/mc_viewer/dist/version.json +4 -0
  106. benchkit/mc_viewer/dist/viewer.html +23 -0
  107. benchkit/mc_viewer/dist/viewer.js +2 -0
  108. benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
  109. benchkit/mc_viewer/dist/worker.js +2 -0
  110. benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
  111. benchkit/metrics.py +100 -0
  112. benchkit/ollama.py +60 -0
  113. benchkit/perf.py +353 -0
  114. benchkit/perf_report.py +207 -0
  115. benchkit/perturbations.py +208 -0
  116. benchkit/pi_agent.py +476 -0
  117. benchkit/pi_package/package-lock.json +1827 -0
  118. benchkit/pi_package/package.json +8 -0
  119. benchkit/report.py +142 -0
  120. benchkit/report_markdown.py +645 -0
  121. benchkit/results.py +688 -0
  122. benchkit/retry.py +163 -0
  123. benchkit/runner.py +769 -0
  124. benchkit/sandbox.py +1167 -0
  125. benchkit/template_check.py +291 -0
  126. benchkit/templates/arena.html +504 -0
  127. benchkit/templates/history.html +58 -0
  128. benchkit/templates/perf.html +509 -0
  129. benchkit/templates/report.html +769 -0
  130. benchkit/tui/__init__.py +5 -0
  131. benchkit/tui/app.py +87 -0
  132. benchkit/tui/app.tcss +512 -0
  133. benchkit/tui/formatting.py +84 -0
  134. benchkit/tui/screens/__init__.py +1 -0
  135. benchkit/tui/screens/connect.py +191 -0
  136. benchkit/tui/screens/detail.py +288 -0
  137. benchkit/tui/screens/help.py +84 -0
  138. benchkit/tui/screens/modals.py +544 -0
  139. benchkit/tui/screens/results.py +361 -0
  140. benchkit/tui/screens/run.py +988 -0
  141. benchkit/tui/screens/setup.py +733 -0
  142. benchkit/tui/theme.py +135 -0
  143. benchkit/tui/widgets.py +120 -0
  144. benchkit_cli-0.1.0.dist-info/METADATA +294 -0
  145. benchkit_cli-0.1.0.dist-info/RECORD +149 -0
  146. benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
  147. benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
  148. benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  149. benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,291 @@
1
+ """Lightweight chat-template checks for llama.cpp-compatible servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+ from typing import Literal
8
+ from urllib.parse import quote
9
+
10
+ import httpx
11
+
12
+ from benchkit.client import InferenceClient, _without_v1
13
+
14
+ Level = Literal["pass", "warn", "fail"]
15
+
16
+ MARKERS = {
17
+ "system": "BK_SYSTEM_7F3A",
18
+ "user_1": "BK_USER_11B2",
19
+ "assistant": "BK_ASSISTANT_22C4",
20
+ "user_2": "BK_USER_33D6",
21
+ }
22
+ PROBE = [
23
+ {"role": "system", "content": MARKERS["system"]},
24
+ {"role": "user", "content": MARKERS["user_1"]},
25
+ {"role": "assistant", "content": MARKERS["assistant"]},
26
+ {"role": "user", "content": MARKERS["user_2"]},
27
+ ]
28
+
29
+ SPECIAL_TOKEN_RE = re.compile(
30
+ r"<\|[^|<>\s]{1,64}\|>|</?[a-z][a-z0-9_]{0,63}>|\[/?INST\]|\[/?SYSTEM_PROMPT\]",
31
+ re.IGNORECASE,
32
+ )
33
+ NONDETERMINISTIC_RE = re.compile(
34
+ r"strftime_now|\bnow\s*\(|datetime\.|random",
35
+ re.IGNORECASE,
36
+ )
37
+
38
+
39
+ class NativeEndpointUnavailable(RuntimeError):
40
+ """Raised when the server does not expose llama.cpp native endpoints."""
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class TemplateCheck:
45
+ name: str
46
+ level: Level
47
+ detail: str = ""
48
+
49
+
50
+ @dataclass
51
+ class TemplateReport:
52
+ model: str
53
+ checks: list[TemplateCheck] = field(default_factory=list)
54
+ unavailable: bool = False
55
+
56
+ def add(self, name: str, level: Level, detail: str = "") -> None:
57
+ self.checks.append(TemplateCheck(name, level, detail))
58
+
59
+ @property
60
+ def status(self) -> str:
61
+ if self.unavailable:
62
+ return "unavailable"
63
+ if any(check.level == "fail" for check in self.checks):
64
+ return "fail"
65
+ if any(check.level == "warn" for check in self.checks):
66
+ return "warn"
67
+ return "pass"
68
+
69
+ @property
70
+ def detail(self) -> str:
71
+ problems = [
72
+ check.detail or check.name
73
+ for check in self.checks
74
+ if check.level in {"warn", "fail"}
75
+ ]
76
+ return "; ".join(problems) or "All checks passed"
77
+
78
+
79
+ class _NativeAPI:
80
+ """Route native requests through llama-swap or a direct llama.cpp server."""
81
+
82
+ def __init__(self, client: InferenceClient, model: str) -> None:
83
+ self.client = client
84
+ self.model = model
85
+ self.base = _without_v1(client.host)
86
+ self.route: Literal["upstream", "direct"] | None = None
87
+
88
+ def _url(self, route: str, path: str) -> str:
89
+ if route == "upstream":
90
+ model = quote(self.model, safe="")
91
+ return f"{self.base}/upstream/{model}{path}"
92
+ return f"{self.base}{path}"
93
+
94
+ def request(self, method: str, path: str, body: dict | None = None) -> dict:
95
+ routes = [self.route] if self.route else ["upstream", "direct"]
96
+ last_response: httpx.Response | None = None
97
+ for route in routes:
98
+ payload = dict(body or {})
99
+ params = None
100
+ if route == "direct":
101
+ payload.setdefault("model", self.model)
102
+ if method == "GET":
103
+ params = {"model": self.model}
104
+ response = httpx.request(
105
+ method,
106
+ self._url(route, path),
107
+ headers=self.client._headers(),
108
+ params=params,
109
+ json=payload,
110
+ timeout=min(self.client.timeout, 120),
111
+ )
112
+ last_response = response
113
+ if response.status_code in {404, 405}:
114
+ if self.route is None:
115
+ continue
116
+ raise NativeEndpointUnavailable(f"{path} is not exposed by this server")
117
+ response.raise_for_status()
118
+ data = response.json()
119
+ if not isinstance(data, dict):
120
+ raise ValueError(f"{path} returned a non-object response")
121
+ self.route = route
122
+ return data
123
+
124
+ detail = "native llama.cpp endpoints are not exposed"
125
+ if last_response is not None and last_response.status_code not in {404, 405}:
126
+ detail = f"HTTP {last_response.status_code}"
127
+ raise NativeEndpointUnavailable(detail)
128
+
129
+ def props(self) -> dict:
130
+ return self.request("GET", "/props")
131
+
132
+ def render(self, messages: list[dict], *, generation_prompt: bool) -> str:
133
+ data = self.request(
134
+ "POST",
135
+ "/apply-template",
136
+ {
137
+ "messages": messages,
138
+ "add_generation_prompt": generation_prompt,
139
+ },
140
+ )
141
+ prompt = data.get("prompt")
142
+ if not isinstance(prompt, str):
143
+ raise ValueError("/apply-template response has no prompt")
144
+ return prompt
145
+
146
+ def tokenize(self, text: str, *, add_special: bool) -> list:
147
+ data = self.request(
148
+ "POST",
149
+ "/tokenize",
150
+ {
151
+ "content": text,
152
+ "add_special": add_special,
153
+ "parse_special": True,
154
+ "with_pieces": False,
155
+ },
156
+ )
157
+ tokens = data.get("tokens")
158
+ if not isinstance(tokens, list):
159
+ raise ValueError("/tokenize response has no token list")
160
+ return tokens
161
+
162
+
163
+ def _template_from_props(props: dict) -> str:
164
+ template = props.get("chat_template")
165
+ if isinstance(template, str):
166
+ return template
167
+ defaults = props.get("default_generation_settings")
168
+ if isinstance(defaults, dict):
169
+ template = defaults.get("chat_template")
170
+ if isinstance(template, str):
171
+ return template
172
+ return ""
173
+
174
+
175
+ def check_template(client: InferenceClient, model: str) -> TemplateReport:
176
+ """Run reference-free structural checks for one served model."""
177
+ report = TemplateReport(model)
178
+ if client.provider != "openai":
179
+ report.unavailable = True
180
+ report.add("native endpoints", "warn", "requires llama.cpp or llama-swap")
181
+ return report
182
+
183
+ api = _NativeAPI(client, model)
184
+ template = ""
185
+ try:
186
+ template = _template_from_props(api.props())
187
+ except NativeEndpointUnavailable:
188
+ # Some builds expose /apply-template without /props. Rendering remains
189
+ # useful, so only declare the feature unavailable if that also fails.
190
+ pass
191
+ except Exception as exc:
192
+ report.add("template metadata", "warn", str(exc))
193
+
194
+ report.add(
195
+ "template exposed",
196
+ "pass" if template else "warn",
197
+ f"{len(template)} characters" if template else "template source not reported",
198
+ )
199
+
200
+ try:
201
+ rendered = api.render(PROBE, generation_prompt=True)
202
+ except httpx.HTTPStatusError as exc:
203
+ detail = exc.response.text.strip().replace("\n", " ")[:160]
204
+ report.add(
205
+ "render probe",
206
+ "fail",
207
+ detail or f"HTTP {exc.response.status_code}",
208
+ )
209
+ return report
210
+ except NativeEndpointUnavailable as exc:
211
+ report.unavailable = True
212
+ report.add("render probe", "warn", str(exc))
213
+ return report
214
+ except Exception as exc:
215
+ report.add("render probe", "fail", f"{type(exc).__name__}: {exc}")
216
+ return report
217
+ report.add("render probe", "pass", f"{len(rendered)} characters")
218
+
219
+ try:
220
+ repeated = api.render(PROBE, generation_prompt=True)
221
+ if repeated != rendered:
222
+ report.add(
223
+ "deterministic render",
224
+ "fail",
225
+ "same messages rendered differently",
226
+ )
227
+ elif template and NONDETERMINISTIC_RE.search(template):
228
+ report.add(
229
+ "deterministic render",
230
+ "warn",
231
+ "template contains time or random logic",
232
+ )
233
+ else:
234
+ report.add("deterministic render", "pass")
235
+ except Exception as exc:
236
+ report.add("deterministic render", "warn", str(exc))
237
+
238
+ positions = {name: rendered.find(marker) for name, marker in MARKERS.items()}
239
+ missing = [name for name, position in positions.items() if position < 0]
240
+ if missing:
241
+ report.add("messages preserved", "fail", f"missing: {', '.join(missing)}")
242
+ elif list(positions.values()) != sorted(positions.values()):
243
+ report.add("messages preserved", "fail", "message order changed")
244
+ elif any(rendered.count(marker) != 1 for marker in MARKERS.values()):
245
+ report.add("messages preserved", "fail", "a message was duplicated")
246
+ else:
247
+ report.add("messages preserved", "pass")
248
+
249
+ tail_start = positions["user_2"] + len(MARKERS["user_2"])
250
+ if positions["user_2"] >= 0 and rendered[tail_start:].strip():
251
+ report.add("generation prompt", "pass")
252
+ else:
253
+ report.add(
254
+ "generation prompt",
255
+ "fail",
256
+ "nothing follows the final user message",
257
+ )
258
+
259
+ specials = sorted(set(SPECIAL_TOKEN_RE.findall(rendered)))
260
+ try:
261
+ split = [
262
+ token
263
+ for token in specials
264
+ if len(api.tokenize(token, add_special=False)) != 1
265
+ ]
266
+ if split:
267
+ report.add(
268
+ "special tokens",
269
+ "fail",
270
+ f"split by tokenizer: {', '.join(split[:3])}",
271
+ )
272
+ else:
273
+ report.add("special tokens", "pass", f"{len(specials)} checked")
274
+
275
+ empty = api.tokenize("", add_special=True)
276
+ prompt_tokens = api.tokenize(rendered, add_special=True)
277
+ double_bos = (
278
+ empty
279
+ and len(prompt_tokens) > 1
280
+ and prompt_tokens[:2] == [empty[0], empty[0]]
281
+ )
282
+ if double_bos:
283
+ report.add("single BOS", "fail", f"token {empty[0]} appears twice")
284
+ else:
285
+ report.add("single BOS", "pass")
286
+ except NativeEndpointUnavailable:
287
+ report.add("tokenization checks", "warn", "/tokenize is not exposed")
288
+ except Exception as exc:
289
+ report.add("tokenization checks", "warn", str(exc))
290
+
291
+ return report