brasskey 0.1.0a1__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 (89) hide show
  1. brasskey/__init__.py +11 -0
  2. brasskey/__main__.py +6 -0
  3. brasskey/bench/__init__.py +2 -0
  4. brasskey/bench/analysis.py +101 -0
  5. brasskey/bench/endpoint.py +133 -0
  6. brasskey/bench/guidellm.py +367 -0
  7. brasskey/bench/models.py +132 -0
  8. brasskey/bench/runner.py +73 -0
  9. brasskey/bench/stats.py +230 -0
  10. brasskey/bench/tokenizer.py +101 -0
  11. brasskey/cli/__init__.py +2 -0
  12. brasskey/cli/app.py +229 -0
  13. brasskey/cli/bench.py +591 -0
  14. brasskey/cli/doctor.py +174 -0
  15. brasskey/cli/options.py +49 -0
  16. brasskey/cli/run.py +766 -0
  17. brasskey/config/__init__.py +2 -0
  18. brasskey/config/intent.py +80 -0
  19. brasskey/config/io.py +89 -0
  20. brasskey/config/lock.py +108 -0
  21. brasskey/config/schema.py +34 -0
  22. brasskey/engines/__init__.py +6 -0
  23. brasskey/engines/base.py +204 -0
  24. brasskey/engines/capabilities.py +224 -0
  25. brasskey/engines/docker.py +216 -0
  26. brasskey/engines/gpu_failures.py +113 -0
  27. brasskey/engines/gpu_server.py +130 -0
  28. brasskey/engines/llamacpp.py +247 -0
  29. brasskey/engines/mlx.py +306 -0
  30. brasskey/engines/pyenv.py +74 -0
  31. brasskey/engines/registry.py +94 -0
  32. brasskey/engines/sglang.py +177 -0
  33. brasskey/engines/types.py +67 -0
  34. brasskey/engines/vllm.py +176 -0
  35. brasskey/errors.py +76 -0
  36. brasskey/hardware/__init__.py +31 -0
  37. brasskey/hardware/apple.py +165 -0
  38. brasskey/hardware/commands.py +81 -0
  39. brasskey/hardware/cpu.py +396 -0
  40. brasskey/hardware/nvidia.py +469 -0
  41. brasskey/hardware/osinfo.py +46 -0
  42. brasskey/hardware/probe.py +123 -0
  43. brasskey/hardware/types.py +175 -0
  44. brasskey/log.py +80 -0
  45. brasskey/models/__init__.py +2 -0
  46. brasskey/models/architecture.py +107 -0
  47. brasskey/models/catalog.py +46 -0
  48. brasskey/models/catalog.yaml +1123 -0
  49. brasskey/models/download.py +139 -0
  50. brasskey/models/fetch.py +120 -0
  51. brasskey/models/fit.py +297 -0
  52. brasskey/models/hub.py +445 -0
  53. brasskey/models/types.py +150 -0
  54. brasskey/output/__init__.py +6 -0
  55. brasskey/output/bench.py +167 -0
  56. brasskey/output/confirm.py +46 -0
  57. brasskey/output/envelope.py +48 -0
  58. brasskey/output/hardware.py +120 -0
  59. brasskey/output/next_step.py +27 -0
  60. brasskey/output/plan.py +166 -0
  61. brasskey/output/progress.py +59 -0
  62. brasskey/output/render.py +112 -0
  63. brasskey/output/result.py +25 -0
  64. brasskey/output/run.py +144 -0
  65. brasskey/output/security.py +46 -0
  66. brasskey/output/settings.py +70 -0
  67. brasskey/paths.py +57 -0
  68. brasskey/planner/__init__.py +12 -0
  69. brasskey/planner/replay.py +184 -0
  70. brasskey/planner/select.py +419 -0
  71. brasskey/py.typed +0 -0
  72. brasskey/runtime/__init__.py +2 -0
  73. brasskey/runtime/foreground.py +48 -0
  74. brasskey/runtime/ports.py +59 -0
  75. brasskey/runtime/proxy.py +264 -0
  76. brasskey/runtime/setup.py +65 -0
  77. brasskey/runtime/state.py +89 -0
  78. brasskey/runtime/supervisor.py +230 -0
  79. brasskey/security/__init__.py +2 -0
  80. brasskey/security/apikeys.py +110 -0
  81. brasskey/security/doctor.py +639 -0
  82. brasskey/security/netinfo.py +238 -0
  83. brasskey/security/policy.py +155 -0
  84. brasskey-0.1.0a1.dist-info/METADATA +150 -0
  85. brasskey-0.1.0a1.dist-info/RECORD +89 -0
  86. brasskey-0.1.0a1.dist-info/WHEEL +4 -0
  87. brasskey-0.1.0a1.dist-info/entry_points.txt +2 -0
  88. brasskey-0.1.0a1.dist-info/licenses/LICENSE +202 -0
  89. brasskey-0.1.0a1.dist-info/licenses/NOTICE +65 -0
brasskey/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Brasskey: the key to every engine.
3
+
4
+ Deploy, tune, verify and serve open models on any hardware, measured on your own traffic.
5
+ The core logic lives in importable modules; ``brasskey.cli`` is a thin command-line layer on top.
6
+ """
7
+
8
+ __all__ = ["__version__"]
9
+
10
+ # Single source of truth for the version (read by hatchling at build time).
11
+ __version__ = "0.1.0a1"
brasskey/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Allow ``python -m brasskey``."""
3
+
4
+ from brasskey.cli.app import main
5
+
6
+ raise SystemExit(main())
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Benchmarks: GuideLLM runs, statistics and comparison verdicts (`brasskey bench`)."""
@@ -0,0 +1,101 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Turn repeated runs into metric summaries, and two endpoints into a verdict (pure functions)."""
3
+
4
+ import logging
5
+ from collections.abc import Sequence
6
+
7
+ from brasskey.bench.models import (
8
+ METRICS,
9
+ ComparisonResult,
10
+ EndpointResult,
11
+ MetricComparison,
12
+ MetricInfo,
13
+ MetricSummary,
14
+ RunMetrics,
15
+ )
16
+ from brasskey.bench.stats import NOISY_CV, Goal, LogComparison, Verdict, compare_runs, mean_interval
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def _values(runs: Sequence[RunMetrics], info: MetricInfo) -> tuple[float, ...]:
22
+ return tuple(float(getattr(run, info.key)) for run in runs)
23
+
24
+
25
+ def summarize(runs: Sequence[RunMetrics]) -> tuple[MetricSummary, ...]:
26
+ summaries: list[MetricSummary] = []
27
+ for info in METRICS:
28
+ values = _values(runs, info)
29
+ interval = mean_interval(values)
30
+ summaries.append(
31
+ MetricSummary(
32
+ key=info.key,
33
+ label=info.label,
34
+ unit=info.unit,
35
+ better=info.better,
36
+ values=values,
37
+ mean=interval.mean,
38
+ ci_low=interval.low,
39
+ ci_high=interval.high,
40
+ cv=interval.cv,
41
+ noisy=interval.cv is not None and interval.cv > NOISY_CV,
42
+ )
43
+ )
44
+ return tuple(summaries)
45
+
46
+
47
+ def compare(
48
+ baseline: EndpointResult,
49
+ candidate: EndpointResult,
50
+ *,
51
+ goal: Goal,
52
+ margin: float,
53
+ primary: str,
54
+ baseline_source: str,
55
+ ) -> ComparisonResult:
56
+ """Every metric compared; the primary metric decides the overall verdict."""
57
+ compared: list[MetricComparison] = []
58
+ for info in METRICS:
59
+ result = compare_runs(
60
+ baseline.metric(info.key).values,
61
+ candidate.metric(info.key).values,
62
+ better=info.better,
63
+ goal=goal,
64
+ margin=margin,
65
+ )
66
+ if isinstance(result, LogComparison):
67
+ compared.append(
68
+ MetricComparison(
69
+ key=info.key,
70
+ label=info.label,
71
+ better=info.better,
72
+ change=result.change,
73
+ change_low=result.change_low,
74
+ change_high=result.change_high,
75
+ verdict=result.verdict,
76
+ reason=result.reason,
77
+ )
78
+ )
79
+ else: # not enough data for an interval
80
+ compared.append(
81
+ MetricComparison(
82
+ key=info.key,
83
+ label=info.label,
84
+ better=info.better,
85
+ verdict=Verdict.INCONCLUSIVE,
86
+ reason=result,
87
+ )
88
+ )
89
+ deciding = next(item for item in compared if item.key == primary)
90
+ logger.debug(
91
+ "Comparison on %s: %s (change %s)", primary, deciding.verdict.value, deciding.change
92
+ )
93
+ return ComparisonResult(
94
+ baseline_source="file" if baseline_source == "file" else "endpoint",
95
+ goal=goal,
96
+ margin=margin,
97
+ primary_metric=primary,
98
+ metrics=tuple(compared),
99
+ verdict=deciding.verdict,
100
+ reason=f"{deciding.label}: {deciding.reason}",
101
+ )
@@ -0,0 +1,133 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The endpoints a benchmark talks to: URL checks, the served model, and Brasskey servers."""
3
+
4
+ import logging
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+ from typing import cast
8
+ from urllib.parse import urlsplit
9
+
10
+ import httpx2
11
+
12
+ from brasskey.bench.guidellm import BenchError
13
+ from brasskey.output.next_step import NextStep
14
+ from brasskey.runtime.state import ServerRecord
15
+ from brasskey.security.apikeys import ApiKey
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ PROBE_TIMEOUT_S = 10.0
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Endpoint:
24
+ api_base: str # e.g. http://127.0.0.1:8000/v1
25
+ root: str # e.g. http://127.0.0.1:8000 (what GuideLLM calls the target)
26
+
27
+ @property
28
+ def is_https(self) -> bool:
29
+ return self.api_base.startswith("https://")
30
+
31
+
32
+ def parse_endpoint(url: str) -> Endpoint:
33
+ """Accept ``http://host:port`` or ``http://host:port/v1`` (an OpenAI-compatible base URL)."""
34
+ parts = urlsplit(url.strip())
35
+ if parts.scheme not in ("http", "https") or not parts.hostname:
36
+ raise BenchError(
37
+ f"{url!r} isn't an http(s) URL.",
38
+ fix="Pass the server's base URL, e.g. http://127.0.0.1:8000/v1",
39
+ code="BK-E-USAGE",
40
+ )
41
+ if parts.username or parts.password:
42
+ raise BenchError(
43
+ "The URL contains credentials.",
44
+ fix="Remove them from the URL and put the API key in an environment variable "
45
+ "(BRASSKEY_API_KEY, or name one with --api-key-env).",
46
+ code="BK-E-USAGE",
47
+ )
48
+ path = parts.path.rstrip("/")
49
+ if path.endswith("/models"):
50
+ path = path.removesuffix("/models")
51
+ if not path.endswith("/v1"):
52
+ path = f"{path}/v1"
53
+ root = f"{parts.scheme}://{parts.netloc}{path.removesuffix('/v1')}"
54
+ return Endpoint(api_base=f"{root}/v1", root=root)
55
+
56
+
57
+ def served_models(client: httpx2.Client, endpoint: Endpoint, key: ApiKey | None) -> list[str]:
58
+ """Model ids the endpoint lists; plain errors for the usual problems."""
59
+ headers = {"Authorization": f"Bearer {key.value}"} if key is not None else {}
60
+ try:
61
+ response = client.get(f"{endpoint.api_base}/models", headers=headers)
62
+ except httpx2.HTTPError as error:
63
+ raise BenchError(
64
+ f"Nothing answered at {endpoint.api_base}.",
65
+ fix="Start a server first (`brasskey run`), or check the URL and port.",
66
+ code="BK-E-BENCH-ENDPOINT",
67
+ next_step=NextStep(command="brasskey doctor", hint="see the running servers"),
68
+ ) from error
69
+ if response.status_code in (401, 403):
70
+ raise BenchError(
71
+ f"{endpoint.api_base} needs an API key"
72
+ + (" and refused this one." if key is not None else "."),
73
+ fix="Set BRASSKEY_API_KEY (or name the variable with --api-key-env), then run again.",
74
+ code="BK-E-BENCH-AUTH",
75
+ )
76
+ if response.status_code != 200:
77
+ raise BenchError(
78
+ f"{endpoint.api_base}/models answered HTTP {response.status_code}.",
79
+ fix="Check that this is an OpenAI-compatible server.",
80
+ code="BK-E-BENCH-ENDPOINT",
81
+ )
82
+ try:
83
+ body: object = response.json()
84
+ except ValueError as error:
85
+ raise BenchError(f"{endpoint.api_base}/models didn't return JSON.") from error
86
+ entries = cast(dict[str, object], body).get("data") if isinstance(body, dict) else None
87
+ ids = [
88
+ str(cast(dict[str, object], entry).get("id"))
89
+ for entry in (cast(list[object], entries) if isinstance(entries, list) else [])
90
+ if isinstance(entry, dict) and cast(dict[str, object], entry).get("id")
91
+ ]
92
+ logger.debug("%s serves %d model(s)", endpoint.api_base, len(ids))
93
+ return ids
94
+
95
+
96
+ def choose_model(endpoint: Endpoint, served: Sequence[str], requested: str | None) -> str:
97
+ if requested is not None:
98
+ if served and requested not in served:
99
+ raise BenchError(
100
+ f"{endpoint.api_base} doesn't serve {requested!r} (it serves {', '.join(served)}).",
101
+ fix="Pass one of those with --model.",
102
+ code="BK-E-USAGE",
103
+ )
104
+ return requested
105
+ if len(served) == 1:
106
+ return served[0]
107
+ if not served:
108
+ raise BenchError(
109
+ f"{endpoint.api_base} lists no models.",
110
+ fix="Pass the model name with --model.",
111
+ code="BK-E-USAGE",
112
+ )
113
+ raise BenchError(
114
+ f"{endpoint.api_base} serves several models: {', '.join(served)}.",
115
+ fix="Pick one with --model.",
116
+ code="BK-E-USAGE",
117
+ )
118
+
119
+
120
+ def brasskey_server(endpoint: Endpoint, records: Sequence[ServerRecord]) -> ServerRecord | None:
121
+ """The running Brasskey server behind this URL, if any (to label results with its engine)."""
122
+ for record in records:
123
+ if record.base_url.rstrip("/") == endpoint.api_base:
124
+ return record
125
+ loopback = {"127.0.0.1", "localhost", "[::1]"}
126
+ mine, theirs = urlsplit(endpoint.api_base), urlsplit(record.base_url)
127
+ if (
128
+ mine.port == theirs.port
129
+ and mine.hostname in loopback | {"::1"}
130
+ and theirs.hostname in loopback | {"::1"}
131
+ ):
132
+ return record
133
+ return None
@@ -0,0 +1,367 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """GuideLLM (Apache-2.0, vLLM project) as Brasskey's load generator, run as a separate process.
3
+
4
+ Where it comes from (ADR 0002, ADR 0011):
5
+ - ``guidellm`` on PATH: used as is;
6
+ - otherwise a pinned, isolated environment run by ``uv tool run``: GuideLLM 0.8.1 with its
7
+ dependencies resolved as of a fixed date and CPU-only PyTorch (GuideLLM only tokenizes; CUDA
8
+ wheels would add gigabytes). Set up once after the user confirms; later runs start offline.
9
+
10
+ Each run (checked against GuideLLM 0.8.1):
11
+ - synthetic prompts of a fixed token shape, a fixed number of concurrent streams, a fixed
12
+ duration and a seed;
13
+ - ``--metrics sample_size=0``: GuideLLM keeps no prompt or response text, only numbers;
14
+ - the API key reaches GuideLLM through ``GUIDELLM__SPEC__BACKEND__API_KEY`` (its settings
15
+ variable), never argv; GuideLLM masks it in its report;
16
+ - it runs in a private folder (so no stray ``.env`` there can change it) and writes its JSON
17
+ report there; Brasskey reads the report and deletes it.
18
+
19
+ The report parser checks GuideLLM's report format version and refuses unknown ones, so a
20
+ GuideLLM upgrade can't silently change what the numbers mean.
21
+ """
22
+
23
+ import json
24
+ import logging
25
+ import os
26
+ import shutil
27
+ import subprocess
28
+ from collections.abc import Callable, Mapping, Sequence
29
+ from dataclasses import dataclass, field
30
+ from pathlib import Path
31
+ from typing import Literal, cast
32
+
33
+ from brasskey.bench.models import RunMetrics, Workload
34
+ from brasskey.engines.base import ENGINE_BASE_ENV
35
+ from brasskey.errors import BrasskeyError
36
+ from brasskey.hardware.commands import CommandRunner, run_command
37
+ from brasskey.paths import open_private_file
38
+ from brasskey.security.apikeys import ApiKey
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ GUIDELLM_VERSION = "0.8.1"
43
+ RESOLVE_CUTOFF = "2026-11-01T00:00:00Z" # dependencies resolved as of this date: reproducible
44
+ UV_PYTHON = "3.12"
45
+ SETUP_NOTE = "about 300 MB to download, 1 GB on disk, one time"
46
+ REPORT_FORMAT_VERSION = 2 # GuideLLM 0.8 reports: metadata.version
47
+ KEY_ENV = "GUIDELLM__SPEC__BACKEND__API_KEY"
48
+ EXECUTABLE = "guidellm"
49
+ FALLBACK_DIRS = (Path.home() / ".local" / "bin", Path("/opt/homebrew/bin"), Path("/usr/local/bin"))
50
+ DOCS_URL = "https://github.com/vllm-project/guidellm"
51
+ RUN_GRACE_S = 300.0 # start-up, tokenizer loading and finishing in-flight requests
52
+
53
+
54
+ class BenchError(BrasskeyError):
55
+ default_code = "BK-E-BENCH"
56
+
57
+
58
+ class ReportFormatError(BenchError):
59
+ default_code = "BK-E-BENCH-REPORT"
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class GuidellmInstall:
64
+ version: str
65
+ runtime: Literal["process", "uvx"]
66
+ executable: tuple[str, ...]
67
+ needs_setup: bool = False
68
+ setup_argv: tuple[str, ...] = ()
69
+ setup_note: str | None = None
70
+
71
+ @property
72
+ def label(self) -> str:
73
+ return f"GuideLLM {self.version}"
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class RunTarget:
78
+ """One endpoint as GuideLLM sees it."""
79
+
80
+ target: str # server root, e.g. http://127.0.0.1:8000 (GuideLLM adds /v1/...)
81
+ model: str
82
+ tokenizer: str # local folder or Hugging Face id
83
+ tokenizer_local: bool
84
+ api_key: ApiKey | None = field(default=None, repr=False)
85
+
86
+
87
+ def uv_base_command(uv: str, *, offline: bool = False) -> tuple[str, ...]:
88
+ """``uv tool run`` with the pinned GuideLLM environment (its arguments come after)."""
89
+ return (
90
+ uv, "tool", "run", *(("--offline",) if offline else ()),
91
+ "--managed-python", "--python", UV_PYTHON,
92
+ "--exclude-newer", RESOLVE_CUTOFF,
93
+ "--torch-backend", "cpu",
94
+ "--from", f"guidellm=={GUIDELLM_VERSION}",
95
+ EXECUTABLE,
96
+ ) # fmt: skip
97
+
98
+
99
+ def parse_version(text: str) -> str | None:
100
+ """``guidellm version: 0.8.1`` -> ``0.8.1``."""
101
+ for line in text.splitlines():
102
+ if "version" in line.lower() and ":" in line:
103
+ value = line.split(":", 1)[1].strip()
104
+ if value and value[0].isdigit():
105
+ return value
106
+ return None
107
+
108
+
109
+ def _find(name: str, which: Callable[[str], str | None], dirs: Sequence[Path]) -> str | None:
110
+ found = which(name)
111
+ if found:
112
+ return found
113
+ for directory in dirs:
114
+ candidate = directory / name
115
+ if candidate.is_file():
116
+ return str(candidate)
117
+ return None
118
+
119
+
120
+ def detect_guidellm(
121
+ *,
122
+ run: CommandRunner = run_command,
123
+ which: Callable[[str], str | None] = shutil.which,
124
+ fallback_dirs: Sequence[Path] = FALLBACK_DIRS,
125
+ ) -> GuidellmInstall | None:
126
+ """GuideLLM on PATH, else the pinned uv environment (maybe not set up yet), else None."""
127
+ on_path = _find(EXECUTABLE, which, fallback_dirs)
128
+ if on_path is not None:
129
+ out = run([on_path, "--version"], timeout_s=60)
130
+ version = parse_version(out.stdout + out.stderr) if out.ok else None
131
+ if version is not None:
132
+ logger.debug("GuideLLM on PATH: %s %s", on_path, version)
133
+ return GuidellmInstall(version=version, runtime="process", executable=(on_path,))
134
+ logger.debug("%s --version failed (exit %d)", on_path, out.returncode)
135
+ uv = _find("uv", which, fallback_dirs)
136
+ if uv is None:
137
+ return None
138
+ ready = run([*uv_base_command(uv, offline=True), "--version"], timeout_s=60)
139
+ if ready.ok and parse_version(ready.stdout + ready.stderr):
140
+ logger.debug("Pinned GuideLLM environment ready")
141
+ return GuidellmInstall(
142
+ version=GUIDELLM_VERSION, runtime="uvx", executable=uv_base_command(uv)
143
+ )
144
+ logger.debug("Pinned GuideLLM environment not set up yet (uv exit %d)", ready.returncode)
145
+ return GuidellmInstall(
146
+ version=GUIDELLM_VERSION,
147
+ runtime="uvx",
148
+ executable=uv_base_command(uv),
149
+ needs_setup=True,
150
+ setup_argv=(*uv_base_command(uv), "--version"),
151
+ setup_note=SETUP_NOTE,
152
+ )
153
+
154
+
155
+ def install_hint() -> tuple[str, tuple[str, ...]]:
156
+ return (
157
+ "Install uv so Brasskey can set GuideLLM up automatically (or install GuideLLM).",
158
+ ("brew install uv", f"or: uv tool install guidellm=={GUIDELLM_VERSION}"),
159
+ )
160
+
161
+
162
+ def _json(value: Mapping[str, object]) -> str:
163
+ return json.dumps(value, separators=(",", ":"), sort_keys=True)
164
+
165
+
166
+ def build_argv(
167
+ install: GuidellmInstall,
168
+ target: RunTarget,
169
+ workload: Workload,
170
+ *,
171
+ seed: int,
172
+ duration_s: int,
173
+ report_path: Path,
174
+ ) -> list[str]:
175
+ """The ``guidellm run`` command for one run (JSON options: paths may contain commas)."""
176
+ return [
177
+ *install.executable,
178
+ "run",
179
+ "--backend", _json({"kind": "openai_http", "target": target.target, "model": target.model}),
180
+ "--tokenizer", _json({"kind": "huggingface_auto", "model": target.tokenizer}),
181
+ "--data", _json({"kind": "synthetic_text", "prompt_tokens": workload.prompt_tokens,
182
+ "output_tokens": workload.output_tokens}),
183
+ "--profile", _json({"kind": "concurrent", "streams": workload.concurrency}),
184
+ "--constraint", _json({"kind": "max_duration", "seconds": duration_s}),
185
+ "--seed", _json({"kind": "static", "value": seed}),
186
+ "--metrics", _json({"kind": "generative", "sample_size": 0}),
187
+ "--output", _json({"kind": "json", "path": str(report_path)}),
188
+ "--disable-console",
189
+ ] # fmt: skip
190
+
191
+
192
+ def run_environment(target: RunTarget, base: Mapping[str, str]) -> dict[str, str]:
193
+ """GuideLLM's environment: no Brasskey keys, no telemetry, and the target's key by name."""
194
+ env = {
195
+ name: value
196
+ for name, value in base.items()
197
+ if not name.startswith("GUIDELLM__") and "API_KEY" not in name
198
+ }
199
+ env.update(ENGINE_BASE_ENV)
200
+ env["HF_HUB_OFFLINE"] = "1" if target.tokenizer_local else "0"
201
+ if target.api_key is not None:
202
+ env[KEY_ENV] = target.api_key.value
203
+ return env
204
+
205
+
206
+ # --- the report --------------------------------------------------------------------------------
207
+
208
+
209
+ def _path(data: object, *keys: str) -> object:
210
+ current = data
211
+ for key in keys:
212
+ if not isinstance(current, dict):
213
+ raise ReportFormatError(
214
+ f"GuideLLM's report has no {'.'.join(keys)}.",
215
+ fix=f"Use GuideLLM {GUIDELLM_VERSION} (let Brasskey run its pinned copy by "
216
+ "removing other guidellm installs from PATH).",
217
+ )
218
+ current = cast(dict[str, object], current).get(key)
219
+ return current
220
+
221
+
222
+ def _number(data: object, *keys: str) -> float:
223
+ value = _path(data, *keys)
224
+ if isinstance(value, bool) or not isinstance(value, int | float):
225
+ raise ReportFormatError(
226
+ f"GuideLLM's report has no number at {'.'.join(keys)}.",
227
+ fix=f"Use GuideLLM {GUIDELLM_VERSION}.",
228
+ )
229
+ return float(value)
230
+
231
+
232
+ def parse_report(text: str, *, seed: int) -> RunMetrics:
233
+ """The numbers Brasskey uses from one GuideLLM JSON report (format version 2)."""
234
+ try:
235
+ data: object = json.loads(text)
236
+ except ValueError as error:
237
+ raise ReportFormatError("GuideLLM's report isn't valid JSON.") from error
238
+ version = _path(data, "metadata", "version")
239
+ if version != REPORT_FORMAT_VERSION:
240
+ tool = _path(data, "metadata", "guidellm_version")
241
+ raise ReportFormatError(
242
+ f"GuideLLM {tool} writes report format {version}; this Brasskey reads format "
243
+ f"{REPORT_FORMAT_VERSION} (GuideLLM 0.8).",
244
+ fix=f"Use GuideLLM {GUIDELLM_VERSION}: remove other guidellm installs from PATH so "
245
+ "Brasskey runs its pinned copy, or update Brasskey.",
246
+ )
247
+ benchmarks = _path(data, "benchmarks")
248
+ if not isinstance(benchmarks, list) or len(cast(list[object], benchmarks)) != 1:
249
+ raise ReportFormatError("GuideLLM's report should hold exactly one benchmark.")
250
+ bench: object = cast(list[object], benchmarks)[0]
251
+ metrics = _path(bench, "metrics")
252
+ ok = ("successful",)
253
+ return RunMetrics(
254
+ seed=seed,
255
+ duration_s=_number(bench, "duration"),
256
+ successful=int(_number(metrics, "request_totals", "successful")),
257
+ errored=int(_number(metrics, "request_totals", "errored")),
258
+ incomplete=int(_number(metrics, "request_totals", "incomplete")),
259
+ output_tokens_per_s=_number(metrics, "output_tokens_per_second", *ok, "mean"),
260
+ requests_per_s=_number(metrics, "requests_per_second", *ok, "mean"),
261
+ ttft_ms_p50=_number(metrics, "time_to_first_token_ms", *ok, "median"),
262
+ ttft_ms_p95=_number(metrics, "time_to_first_token_ms", *ok, "percentiles", "p95"),
263
+ itl_ms_p50=_number(metrics, "inter_token_latency_ms", *ok, "median"),
264
+ latency_s_p50=_number(metrics, "request_latency", *ok, "median"),
265
+ latency_s_p95=_number(metrics, "request_latency", *ok, "percentiles", "p95"),
266
+ )
267
+
268
+
269
+ # --- one run ---------------------------------------------------------------------------------
270
+
271
+
272
+ _FAILURES: tuple[tuple[tuple[str, ...], str, str], ...] = (
273
+ (
274
+ ("401", "unauthorized", "invalid_api_key"),
275
+ "The endpoint refused GuideLLM's requests (API key).",
276
+ "Set the key's variable (BRASSKEY_API_KEY or --api-key-env), then run again.",
277
+ ),
278
+ (
279
+ ("connecterror", "connection refused", "all connection attempts failed"),
280
+ "GuideLLM couldn't reach the endpoint.",
281
+ "Check that the server is still running (`brasskey doctor` lists Brasskey servers).",
282
+ ),
283
+ (
284
+ ("can't load tokenizer", "tokenizer", "not a valid model identifier"),
285
+ "GuideLLM couldn't load the tokenizer.",
286
+ "Pass --tokenizer with the model's Hugging Face id or a local folder with tokenizer.json.",
287
+ ),
288
+ (
289
+ ("no such option", "invalid value for", "unrecognized arguments"),
290
+ "This GuideLLM version doesn't accept Brasskey's options.",
291
+ f"Use GuideLLM {GUIDELLM_VERSION}: remove other guidellm installs from PATH so Brasskey "
292
+ "runs its pinned copy.",
293
+ ),
294
+ )
295
+
296
+
297
+ def explain_failure(log_tail: Sequence[str]) -> tuple[str, str] | None:
298
+ text = "\n".join(log_tail).lower()
299
+ for fragments, message, fix in _FAILURES:
300
+ if any(fragment in text for fragment in fragments):
301
+ return message, fix
302
+ return None
303
+
304
+
305
+ def run_once(
306
+ install: GuidellmInstall,
307
+ target: RunTarget,
308
+ workload: Workload,
309
+ *,
310
+ seed: int,
311
+ duration_s: int,
312
+ work_dir: Path,
313
+ log_path: Path,
314
+ env: Mapping[str, str] | None = None,
315
+ ) -> RunMetrics:
316
+ """One GuideLLM run; its raw report (which may hold request details) is deleted at once."""
317
+ report = work_dir / f"report-{seed}.json"
318
+ argv = build_argv(
319
+ install, target, workload, seed=seed, duration_s=duration_s, report_path=report
320
+ )
321
+ environment = run_environment(target, os.environ if env is None else env)
322
+ fd = open_private_file(log_path, append=True) # one log for the whole bench
323
+ try:
324
+ logger.debug(
325
+ "GuideLLM run: seed=%d duration=%ds target=%s", seed, duration_s, target.target
326
+ )
327
+ # argv is built here from fixed options and values Brasskey checked; never a shell.
328
+ completed = subprocess.run( # noqa: S603
329
+ argv,
330
+ stdin=subprocess.DEVNULL,
331
+ stdout=fd,
332
+ stderr=subprocess.STDOUT,
333
+ cwd=work_dir,
334
+ env=environment,
335
+ timeout=duration_s + RUN_GRACE_S,
336
+ check=False,
337
+ )
338
+ except subprocess.TimeoutExpired as error:
339
+ raise BenchError(
340
+ f"GuideLLM didn't finish within {duration_s + RUN_GRACE_S:.0f}s.",
341
+ fix=f"Check that the endpoint still answers. GuideLLM's log: {log_path}",
342
+ ) from error
343
+ except FileNotFoundError as error:
344
+ raise BenchError(
345
+ f"GuideLLM couldn't start ({argv[0]} not found).",
346
+ fix="Install uv (`brew install uv`) and run again.",
347
+ ) from error
348
+ finally:
349
+ os.close(fd)
350
+ try:
351
+ if completed.returncode != 0 or not report.is_file():
352
+ tail = log_path.read_text(encoding="utf-8", errors="replace").splitlines()[-40:]
353
+ explained = explain_failure(tail)
354
+ message, fix = explained or (
355
+ f"GuideLLM failed (exit code {completed.returncode}).",
356
+ "See its log for details.",
357
+ )
358
+ raise BenchError(message, fix=f"{fix} GuideLLM's log: {log_path}")
359
+ metrics = parse_report(report.read_text(encoding="utf-8"), seed=seed)
360
+ finally:
361
+ report.unlink(missing_ok=True)
362
+ if metrics.successful == 0:
363
+ raise BenchError(
364
+ f"No request succeeded ({metrics.errored} failed).",
365
+ fix=f"Check the endpoint with `curl`, then run again. GuideLLM's log: {log_path}",
366
+ )
367
+ return metrics