Download evals/cli.py from TorpedoSoftware/Roblox-LLM-Leaderboard: direct link, hf CLI and curl.
- Browser
- Download file 24.6 kB
-
https://huggingface.co/spaces/TorpedoSoftware/Roblox-LLM-Leaderboard/resolve/main/evals/cli.py
- Command line
-
hf download hf://spaces/TorpedoSoftware/Roblox-LLM-Leaderboard/evals/cli.py
-
curl -L -o cli.py https://huggingface.co/spaces/TorpedoSoftware/Roblox-LLM-Leaderboard/resolve/main/evals/cli.py
24.6 kB
| """ | |
| evaluate — run the three leaderboard evals for one model and publish the results. | |
| uv run evaluate "GPT 5.5" --model gpt-5.5 --provider openai --effort medium \\ | |
| --provider-name OpenAI --url https://openai.com --release-date 2026-08-01 \\ | |
| --price-input 1.25 --price-output 10 | |
| uv run evaluate "GPT 5.5" # resume or rerun with its recorded config | |
| uv run evaluate "GPT 5.5" --status # peek at run/batch progress | |
| uv run evaluate --verify # offline rescore of every published run | |
| The first argument is the model's display name: its entry in data/models.json. | |
| For a model already on the board, the run identity (model id, provider, | |
| effort, ...) comes from its published result files, so a bare `evaluate | |
| "Name"` resumes or reproduces exactly that run; any identity flag you pass | |
| overrides it (and so starts a different run). For a new model, pass the | |
| identity flags plus the metadata the harness cannot know (provider name, url, | |
| release date, price). | |
| Each eval (freeform, multiple choice, leetcode) runs as its own process, one | |
| after another or all at once with --parallel. When all three results exist, | |
| the entry's resultFiles (and kilowattHour, with --energy) are written to | |
| models.json and `uv run build --write` rebuilds the site data — all | |
| under one lock, so parallel `evaluate` processes never clobber each other. | |
| """ | |
| import argparse | |
| import os | |
| import subprocess | |
| import sys | |
| import threading | |
| from pathlib import Path | |
| from evals import luau_leetcode as lc | |
| from evals import registry | |
| from evals import roblox_qa as qa | |
| # Each tag selects a model key, eval module, and optional mode. | |
| EVALS = { | |
| "ff": ("freeform", qa, "--freeform"), | |
| "mc": ("multipleChoice", qa, "--multiple-choice"), | |
| "lc": ("leetcode", lc, None), | |
| } | |
| KEY_TO_TAG = {key: tag for tag, (key, _, _) in EVALS.items()} | |
| # These keys name the run folder and results file. The eval sets the mode. | |
| IDENTITY = tuple(k for k in qa.RUN_KEYS if k != "mode") | |
| # Electricity-priced entries need these kilowattHour keys. | |
| KWH_KEYS = ("freeform", "multipleChoice", "easy", "medium", "hard") | |
| # Use --allow-errors above this failure limit. Dead endpoints and empty keys | |
| # usually exceed it. | |
| MAX_ERROR_RATE = 0.05 | |
| # --- arguments --------------------------------------------------------------------- | |
| def parse_args(argv: list[str] | None = None) -> argparse.Namespace: | |
| ap = argparse.ArgumentParser( | |
| prog="evaluate", | |
| description="Run the RobloxQA (freeform + multiple choice) and Luau Leetcode " | |
| "evals for one model, then update data/models.json and rebuild the " | |
| "leaderboard.", | |
| ) | |
| ap.add_argument("name", nargs="?", help="display name (the models.json entry)") | |
| ident = ap.add_argument_group( | |
| "run identity", | |
| "Defaults come from the model's published result files. Passing one " | |
| "changes the run (new run folder and results file).", | |
| ) | |
| ident.add_argument("--model", help="model id, or checkpoint dir for --provider local") | |
| ident.add_argument("--provider", choices=["live", "openai", "anthropic", "local"]) | |
| ident.add_argument("--base-url", help="live only: OpenAI-compatible endpoint") | |
| reasoning = ident.add_mutually_exclusive_group() | |
| reasoning.add_argument("--effort", help="reasoning effort (e.g. low/medium/high)") | |
| reasoning.add_argument( | |
| "--thinking", | |
| action="store_true", | |
| default=None, | |
| help="anthropic yes/no extended thinking, or local enable_thinking", | |
| ) | |
| ident.add_argument("--route", help="live only: pin OpenRouter provider routing") | |
| ident.add_argument("--max-tokens", type=int, help="candidate completion cap") | |
| ident.add_argument("--system-prompt-prefix", dest="system_prefix") | |
| ident.add_argument("--adapter", help="local only: LoRA adapter dir") | |
| ident.add_argument("--limit", type=int, help="smoke test: first N questions/problems") | |
| ident.add_argument( | |
| "--split", | |
| choices=["test", "train"], | |
| help="RobloxQA split; 'train' is for generation runs, never scores", | |
| ) | |
| launch = ap.add_argument_group("how to run (not part of the run identity)") | |
| launch.add_argument( | |
| "--api-key-env", | |
| help="live only: env var holding the endpoint's key (OPENROUTER_API_KEY is " | |
| "picked automatically for openrouter.ai)", | |
| ) | |
| launch.add_argument("--responses-api", action="store_true") | |
| launch.add_argument("--workers", type=int, help="live only: parallel requests per eval (default 2)") | |
| launch.add_argument( | |
| "--allow-errors", | |
| action="store_true", | |
| help=f"publish even when more than {MAX_ERROR_RATE:.0%} of a run's answers " | |
| "are API errors or unparseable verdicts", | |
| ) | |
| launch.add_argument("--jest-url", help="JestLuaTestServer URL (default JEST_SERVER_URL)") | |
| launch.add_argument( | |
| "--evals", | |
| default="ff,mc,lc", | |
| help="comma list of evals to run: ff (freeform), mc (multiple choice), lc (leetcode). Default: all three", | |
| ) | |
| launch.add_argument( | |
| "--parallel", | |
| action="store_true", | |
| help="run the evals at the same time instead of one after another", | |
| ) | |
| launch.add_argument( | |
| "--force", | |
| action="store_true", | |
| help="discard existing run state and start over (re-spends money)", | |
| ) | |
| launch.add_argument( | |
| "--energy", | |
| action="store_true", | |
| help="log request spans and write measured kilowattHour from the HWiNFO log " | |
| "(checks first that HWiNFO is logging)", | |
| ) | |
| launch.add_argument("--energy-log", type=Path, help="HWiNFO CSV (default C:\\hwinfo\\eval.csv)") | |
| launch.add_argument( | |
| "--no-build", | |
| action="store_true", | |
| help="run the evals but leave models.json and the site data alone", | |
| ) | |
| launch.add_argument("--dry-run", action="store_true", help="print the plan and exit") | |
| launch.add_argument("--status", action="store_true", help="print run status and exit") | |
| launch.add_argument( | |
| "--verify", | |
| action="store_true", | |
| help="rescore published runs offline and compare with their result files (all models, or just NAME)", | |
| ) | |
| meta = ap.add_argument_group("models.json metadata (required for a new model, optional updates otherwise)") | |
| meta.add_argument("--provider-name", help='display provider, e.g. "OpenAI"') | |
| meta.add_argument("--url", help="model page") | |
| meta.add_argument("--release-date", help="YYYY-MM-DD") | |
| meta.add_argument( | |
| "--size", | |
| nargs="+", | |
| metavar="BYTES_OR_LINK", | |
| help="weight size: a byte count, or Hugging Face links to the weights " | |
| "(a file, part 1 of a split GGUF, a folder, or a repo's safetensors). " | |
| "Several values are added up, e.g. the GGUF and its mmproj file", | |
| ) | |
| meta.add_argument("--price-input", type=float, help="USD per million input tokens") | |
| meta.add_argument("--price-output", type=float, help="USD per million output tokens") | |
| meta.add_argument("--price-electricity", type=float, help="USD per kWh (local models)") | |
| args = ap.parse_args(argv) | |
| if not args.verify and not args.name: | |
| ap.error("a model name is required (or pass --verify)") | |
| tags = [t.strip() for t in args.evals.split(",") if t.strip()] | |
| unknown = [t for t in tags if t not in EVALS] | |
| if unknown or not tags: | |
| ap.error(f"--evals takes a comma list of ff, mc, lc (got {args.evals!r})") | |
| args.tags = tags | |
| for v in args.size or []: | |
| if not (v.isdigit() or v.startswith(("http://", "https://"))): | |
| ap.error(f"--size takes byte counts or Hugging Face links (got {v!r})") | |
| if args.price_electricity is not None and (args.price_input is not None or args.price_output is not None): | |
| ap.error("--price-electricity excludes --price-input/--price-output") | |
| return args | |
| # --- run planning -------------------------------------------------------------------- | |
| def published_configs(entry: dict | None) -> dict[str, dict]: | |
| """tag -> config of each published result file of this entry.""" | |
| out = {} | |
| for key, fname in ((entry or {}).get("resultFiles") or {}).items(): | |
| tag = KEY_TO_TAG.get(key) | |
| if tag is None or not fname: | |
| continue | |
| path = EVALS[tag][1].RESULTS_DIR / fname | |
| if path.exists(): | |
| out[tag] = qa.normalize_config(qa.read_results(path)["config"]) | |
| return out | |
| def identity_for(tag: str, published: dict[str, dict], args) -> dict: | |
| """The run identity for one eval: its published config (or a sibling's), | |
| with any identity flag from the command line on top.""" | |
| base = published.get(tag) or next(iter(published.values()), {}) | |
| ident = {k: base.get(k) for k in IDENTITY if k in base} | |
| for k in IDENTITY: | |
| v = getattr(args, k) | |
| if v is not None: | |
| ident[k] = v | |
| return ident | |
| def module_argv(tag: str, ident: dict, args) -> list[str]: | |
| """Command-line flags for one eval module.""" | |
| _, _, mode_flag = EVALS[tag] | |
| argv = [mode_flag] if mode_flag else [] | |
| provider = ident.get("provider") or "live" | |
| argv += ["--provider", provider] | |
| if ident.get("model"): | |
| argv += ["--model", ident["model"]] | |
| if provider == "live" and ident.get("base_url"): | |
| argv += ["--base-url", ident["base_url"]] | |
| for key, flag in ( | |
| ("effort", "--effort"), | |
| ("route", "--route"), | |
| ("max_tokens", "--max-tokens"), | |
| ("adapter", "--adapter"), | |
| ("limit", "--limit"), | |
| ("system_prefix", "--system-prompt-prefix"), | |
| ): | |
| if ident.get(key) is not None: | |
| argv += [flag, str(ident[key])] | |
| if ident.get("thinking"): | |
| argv.append("--thinking") | |
| if tag != "lc" and ident.get("split") not in (None, "test"): | |
| argv += ["--split", ident["split"]] | |
| if args.api_key_env and provider == "live": | |
| argv += ["--api-key-env", args.api_key_env] | |
| if args.responses_api: | |
| argv.append("--responses-api") | |
| if args.workers is not None: | |
| argv += ["--workers", str(args.workers)] | |
| if tag == "lc" and args.jest_url: | |
| argv += ["--jest-url", args.jest_url] | |
| if args.energy: | |
| argv.append("--energy") | |
| if args.force: | |
| argv.append("--force") | |
| return argv | |
| def run_for(tag: str, argv: list[str]): | |
| """The module's Run for these flags (its config, run folder, results path).""" | |
| module = EVALS[tag][1] | |
| run = module.Run.from_args(module.parse_args(argv)) | |
| return run, module.results_path(run.config) | |
| # --- running ----------------------------------------------------------------------- | |
| def _child_env() -> dict: | |
| env = dict(os.environ) | |
| env["PYTHONIOENCODING"] = "utf-8" | |
| env["PYTHONUNBUFFERED"] = "1" | |
| return env | |
| def run_serial(jobs: list[tuple[str, list[str]]]) -> dict[str, int]: | |
| codes = {} | |
| for tag, cmd in jobs: | |
| print(f"\n===== {tag}: {' '.join(cmd[2:])}", flush=True) | |
| codes[tag] = subprocess.run(cmd, env=_child_env(), check=False).returncode | |
| return codes | |
| def run_parallel(jobs: list[tuple[str, list[str]]]) -> dict[str, int]: | |
| """All evals at once; each output line is prefixed with its eval's tag.""" | |
| print_lock = threading.Lock() | |
| def pump(tag: str, proc: subprocess.Popen) -> None: | |
| for line in proc.stdout: | |
| with print_lock: | |
| print(f"[{tag}] {line}", end="", flush=True) | |
| procs = {} | |
| threads = [] | |
| for tag, cmd in jobs: | |
| print(f"[{tag}] {' '.join(cmd[2:])}", flush=True) | |
| proc = subprocess.Popen( | |
| cmd, | |
| env=_child_env(), | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| encoding="utf-8", | |
| errors="replace", | |
| ) | |
| procs[tag] = proc | |
| t = threading.Thread(target=pump, args=(tag, proc), daemon=True) | |
| t.start() | |
| threads.append(t) | |
| codes = {tag: proc.wait() for tag, proc in procs.items()} | |
| for t in threads: | |
| t.join() | |
| return codes | |
| def error_heavy(finished: dict[str, Path]) -> list[str]: | |
| """One line per result whose API errors and unparseable verdicts exceed | |
| MAX_ERROR_RATE of its answers.""" | |
| lines = [] | |
| for tag, path in finished.items(): | |
| summary = qa.read_results(path)["summary"] | |
| summary = summary.get("overall", summary) # leetcode nests its totals | |
| failed = (summary.get("apiErrors") or 0) + (summary.get("judgeUnparseable") or 0) | |
| if failed > MAX_ERROR_RATE * summary["n"]: | |
| lines.append(f"{tag}: {failed} of {summary['n']} answers failed ({path.name})") | |
| return lines | |
| # --- models.json ----------------------------------------------------------------------- | |
| def resolve_size(values: list[str]) -> int: | |
| """--size values (byte counts and Hugging Face links) added up.""" | |
| from evals import metadata | |
| total = 0 | |
| for v in values: | |
| if v.isdigit(): | |
| total += int(v) | |
| continue | |
| try: | |
| size = metadata.weights_size(v) | |
| except Exception as e: # noqa: BLE001 — bad link, gated repo, offline | |
| raise SystemExit(f"error: --size {v}: {e}") from None | |
| print(f" --size {v}: {size:,} bytes") | |
| total += size | |
| return total | |
| def fill_metadata(args, config: dict) -> None: | |
| """Fill the metadata flags a new model left out, from OpenRouter and | |
| Hugging Face (evals/metadata.py), and print what was filled.""" | |
| from evals import metadata | |
| found = metadata.lookup(config, args.url) | |
| filled = [] | |
| for flag, attr, key in ( | |
| ("--provider-name", "provider_name", "provider"), | |
| ("--url", "url", "url"), | |
| ("--release-date", "release_date", "releaseDate"), | |
| ("--size", "size", "size"), | |
| ): | |
| if getattr(args, attr) is None and key in found: | |
| setattr(args, attr, found[key]) | |
| filled.append(f"{flag} {found[key]}") | |
| no_price = args.price_electricity is None and args.price_input is None and args.price_output is None | |
| if no_price and "price" in found: | |
| args.price_input, args.price_output = ( | |
| found["price"]["input"], | |
| found["price"]["output"], | |
| ) | |
| filled.append(f"--price-input {args.price_input} --price-output {args.price_output}") | |
| for line in filled: | |
| print(f" looked up {line}") | |
| def new_entry_fields(args, entry: dict | None) -> dict: | |
| """Metadata from the flags, merged over the existing entry.""" | |
| e = dict(entry or {"name": args.name}) | |
| if args.provider_name is not None: | |
| e["provider"] = args.provider_name | |
| if args.url is not None: | |
| e["url"] = args.url | |
| if args.release_date is not None: | |
| e["releaseDate"] = args.release_date | |
| if args.size is not None: | |
| e["size"] = args.size | |
| e.setdefault("size", None) | |
| if args.price_electricity is not None: | |
| e["price"] = {"electricity": args.price_electricity} | |
| elif args.price_input is not None or args.price_output is not None: | |
| price = dict(e.get("price") or {}) | |
| price.pop("electricity", None) | |
| if args.price_input is not None: | |
| price["input"] = args.price_input | |
| if args.price_output is not None: | |
| price["output"] = args.price_output | |
| e["price"] = price | |
| return e | |
| ENTRY_ORDER = ( | |
| "name", | |
| "provider", | |
| "url", | |
| "releaseDate", | |
| "size", | |
| "price", | |
| "kilowattHour", | |
| "resultFiles", | |
| ) | |
| def order_keys(e: dict) -> dict: | |
| """The hand-edited key order, so a new entry reads like the others.""" | |
| return {k: e[k] for k in ENTRY_ORDER if k in e} | {k: v for k, v in e.items() if k not in ENTRY_ORDER} | |
| def check_new_entry(args, entry: dict | None) -> None: | |
| if entry is not None: | |
| return | |
| missing = [ | |
| flag | |
| for flag, value in ( | |
| ("--provider-name", args.provider_name), | |
| ("--url", args.url), | |
| ("--release-date", args.release_date), | |
| ) | |
| if value is None | |
| ] | |
| if args.price_electricity is None and (args.price_input is None or args.price_output is None): | |
| missing.append("--price-input/--price-output or --price-electricity") | |
| if missing: | |
| raise SystemExit( | |
| f'error: "{args.name}" is not in data/models.json yet; a new model also needs {", ".join(missing)}' | |
| ) | |
| def measure_energy(runs: dict[str, object], log: Path | None) -> dict[str, float]: | |
| """kilowattHour keys for the evals that ran, from their logged spans.""" | |
| from evals import energy | |
| all_dirs = [f.parent for bench in (qa.CACHE_DIR, lc.CACHE_DIR) for f in bench.glob(f"*/{energy.INTERVALS_FILE}")] | |
| targets = [run.dir for run in runs.values()] | |
| shares = energy.attribute_runs(targets, all_dirs, log or energy.DEFAULT_LOG) | |
| kwh: dict[str, float] = {} | |
| for tag, run in runs.items(): | |
| by_label = shares.get(run.dir, {}) | |
| if tag == "lc": # split by difficulty band | |
| for band in ("easy", "medium", "hard"): | |
| kwh[band] = round(by_label.get(band, 0.0), 3) | |
| else: | |
| kwh[EVALS[tag][0]] = round(by_label.get("all", 0.0), 3) | |
| # Keep the earlier value when this run measures zero energy. | |
| for k, v in kwh.items(): | |
| if v == 0: | |
| print(f" warning: no energy measured for {k} (no logged spans, or no HWiNFO samples then)") | |
| return {k: v for k, v in kwh.items() if v} | |
| def publish(args, results: dict[str, Path], kwh: dict[str, float]) -> None: | |
| """Record finished results in models.json and rebuild the site data. | |
| The build accepts a model with all three resultFiles or none, and an | |
| electricity-priced model only with all five kilowattHour values. Until | |
| that holds, the entry keeps its previous (valid) state and only the | |
| metadata is updated.""" | |
| def mutate(entry: dict | None) -> dict: | |
| e = new_entry_fields(args, entry) | |
| if not all(tag in results for tag in EVALS): | |
| have = ", ".join(t for t in EVALS if t in results) or "none" | |
| print(f" finished so far: {have}; resultFiles is set once all three exist") | |
| return order_keys(e) | |
| electricity = "electricity" in (e.get("price") or {}) | |
| kwh_all = dict(e.get("kilowattHour") or {}) | kwh | |
| if electricity and not all(isinstance(kwh_all.get(k), (int, float)) for k in KWH_KEYS): | |
| print( | |
| " resultFiles not set: this model is priced by electricity and " | |
| "needs all five kilowattHour values (run with --energy, or add " | |
| "them to data/models.json by hand)" | |
| ) | |
| return order_keys(e) | |
| if electricity: | |
| e["kilowattHour"] = {k: kwh_all[k] for k in KWH_KEYS} | |
| e["resultFiles"] = {key: results[tag].name for tag, (key, _, _) in EVALS.items()} | |
| return order_keys(e) | |
| registry.update_entry(args.name, mutate) | |
| # --- verify -------------------------------------------------------------------------- | |
| def verify(names: list[str]) -> int: | |
| """Offline rescore of published runs; nonzero exit on any mismatch.""" | |
| models = registry.load_models() | |
| if names: | |
| chosen = [m for m in models if m["name"] in names] | |
| unknown = set(names) - {m["name"] for m in chosen} | |
| if unknown: | |
| raise SystemExit(f"error: not in data/models.json: {sorted(unknown)}") | |
| else: | |
| chosen = [m for m in models if m.get("resultFiles")] | |
| bad = 0 | |
| for m in chosen: | |
| for key, fname in (m.get("resultFiles") or {}).items(): | |
| tag = KEY_TO_TAG[key] | |
| module = EVALS[tag][1] | |
| path = module.RESULTS_DIR / fname | |
| try: | |
| old = qa.read_results(path) | |
| summary, results = module.rescore(old["config"]) | |
| except Exception as e: # noqa: BLE001 — report and keep going | |
| print(f"FAIL {m['name']} [{tag}] {fname}: {e}", flush=True) | |
| bad += 1 | |
| continue | |
| diffs = [] | |
| if summary != old["summary"]: | |
| diffs.append( | |
| "summary: " | |
| + ", ".join( | |
| f"{k} {old['summary'].get(k)!r} -> {summary.get(k)!r}" | |
| for k in sorted(set(summary) | set(old["summary"])) | |
| if summary.get(k) != old["summary"].get(k) | |
| ) | |
| ) | |
| rows = sum(a != b for a, b in zip(results, old["results"])) | |
| rows += abs(len(results) - len(old["results"])) | |
| if rows: | |
| diffs.append(f"{rows} result rows differ") | |
| if diffs: | |
| bad += 1 | |
| print(f"DIFF {m['name']} [{tag}] {fname}: {'; '.join(diffs)}", flush=True) | |
| else: | |
| print(f"ok {m['name']} [{tag}] {fname}", flush=True) | |
| print(f"\n{'all runs reproduce' if not bad else f'{bad} run(s) do not reproduce'}") | |
| return 1 if bad else 0 | |
| # --- main -------------------------------------------------------------------------- | |
| def is_smoke_test(runs: dict) -> bool: | |
| """--limit and train-split runs never go on the board.""" | |
| return any( | |
| run.config.get("limit") is not None or run.config.get("split", "test") != "test" for run in runs.values() | |
| ) | |
| def main(argv: list[str] | None = None) -> None: | |
| sys.stdout.reconfigure(line_buffering=True, encoding="utf-8", errors="replace") | |
| args = parse_args(argv) | |
| if args.verify: | |
| raise SystemExit(verify([args.name] if args.name else [])) | |
| if args.size is not None: | |
| args.size = resolve_size(args.size) | |
| entry = registry.find_entry(registry.load_models(), args.name) | |
| if entry is None and args.model is None and args.provider is None: | |
| raise SystemExit("error: a new model needs --model and --provider") | |
| published = published_configs(entry) | |
| # Plan every eval. Run selected ones and use existing results for the rest. | |
| runs, results_paths, cmds = {}, {}, {} | |
| for tag in EVALS: | |
| argv_t = module_argv(tag, identity_for(tag, published, args), args) | |
| runs[tag], results_paths[tag] = run_for(tag, argv_t) | |
| cmds[tag] = [sys.executable, "-m", EVALS[tag][1].__name__, *argv_t] | |
| jobs = [(tag, cmds[tag]) for tag in args.tags] | |
| smoke = is_smoke_test(runs) | |
| if entry is None and not (args.status or smoke): | |
| fill_metadata(args, runs["ff"].config) | |
| if not (args.status or args.dry_run or args.no_build or smoke): | |
| check_new_entry(args, entry) # fail before spending anything | |
| if args.energy and not (args.status or args.dry_run): | |
| from evals import energy | |
| energy.check_logging(args.energy_log or energy.DEFAULT_LOG) | |
| print(f"{args.name}:") | |
| for tag, cmd in jobs: | |
| print(f" {tag}: {subprocess.list2cmdline(cmd[2:])}") | |
| print(f" run folder {runs[tag].dir}") | |
| print(f" results {results_paths[tag]}") | |
| if args.dry_run: | |
| return | |
| if args.status: | |
| for tag in args.tags: | |
| qa.print_status(runs[tag]) | |
| return | |
| codes = run_parallel(jobs) if args.parallel else run_serial(jobs) | |
| failed = [tag for tag, code in codes.items() if code != 0] | |
| finished = {tag: path for tag, path in results_paths.items() if tag not in failed and path.exists()} | |
| kwh: dict[str, float] = {} | |
| if args.energy and finished: | |
| try: | |
| kwh = measure_energy({t: runs[t] for t in finished}, args.energy_log) | |
| print(f" measured kWh: {kwh}") | |
| except Exception as e: # noqa: BLE001 — the scores still stand | |
| print(f" energy attribution failed: {e}") | |
| heavy = error_heavy(finished) | |
| if smoke: | |
| print(" smoke test (--limit/--split): models.json left alone") | |
| elif args.no_build: | |
| print(" --no-build: models.json left alone") | |
| elif heavy and not args.allow_errors: | |
| print(" not published: too many failed answers") | |
| for line in heavy: | |
| print(f" {line}") | |
| print( | |
| " Rerun to retry the failed live answers. If the failures are final " | |
| "(batch errors), pass --allow-errors to publish anyway." | |
| ) | |
| elif finished: | |
| publish(args, finished, kwh) | |
| if failed: | |
| raise SystemExit(f"error: {', '.join(failed)} failed (see the log above); rerun to resume") | |
| if heavy and not (smoke or args.no_build or args.allow_errors): | |
| raise SystemExit("error: not published: too many failed answers (see above)") | |
| if __name__ == "__main__": | |
| main() | |