ctfy.sdk.admin_resources.eval
client.admin.llm_models + client.admin.eval_runs — model evaluation (admin).
1"""``client.admin.llm_models`` + ``client.admin.eval_runs`` — model evaluation (admin).""" 2 3from __future__ import annotations 4 5import builtins 6from typing import Any 7 8from ctfy.core.enums import RegistryReclaimPolicy 9from ctfy.core.state.models import ( 10 EvalCampaignState, 11 EvalRunState, 12 HarnessState, 13 LlmModelState, 14 VendorState, 15 VendorTokenState, 16) 17from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status 18from ctfy.sdk.base import BaseHttpClient 19from ctfy.server.models import ( 20 AdminHarnessRow, 21 AdminTaskInfo, 22 EvalAllowanceRow, 23 EvalArtifactReport, 24 EvalRangeFailureReport, 25 EvalRunCoverage, 26 EvalRunRangeFailure, 27 RegistryGcStatus, 28 RegistryReclaimPlan, 29 RegistryRepoList, 30 RegistryTagList, 31 RegistryUsageReport, 32) 33 34 35class AdminLlmModelsResource: 36 """The registry of models under evaluation: register / list / delete. 37 38 No API key is ever stored — a registered model is addressing only 39 (flavor + base_url + provider model id); credentials are supplied to the 40 run at dispatch time via env. 41 """ 42 43 def __init__(self, http: BaseHttpClient) -> None: 44 self._http = http 45 46 def list( 47 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 48 ) -> PagedList[LlmModelState]: 49 resp = self._http.request( 50 "GET", 51 "/admin/llm-models", 52 params={"active_only": active_only, "offset": offset, "limit": limit}, 53 ) 54 _raise_for_status(resp) 55 return _extract_items(resp.json(), LlmModelState) 56 57 def register( 58 self, 59 *, 60 id: str, 61 flavor: str, 62 display_name: str = "", 63 provider_model: str = "", 64 base_url: str = "", 65 vendor: str = "", 66 notes: str = "", 67 input_price_per_mtok: float = 0.0, 68 output_price_per_mtok: float = 0.0, 69 ) -> LlmModelState: 70 """Register (or overwrite) a model. ``flavor`` is ``anthropic`` / ``openai``. 71 72 ``*_price_per_mtok`` is USD per 1M tokens (0 = unpriced) — drives the 73 leaderboard's cost columns. 74 """ 75 resp = self._http.request( 76 "POST", 77 "/admin/llm-models", 78 json={ 79 "id": id, 80 "flavor": flavor, 81 "display_name": display_name, 82 "provider_model": provider_model, 83 "base_url": base_url, 84 "vendor": vendor, 85 "notes": notes, 86 "input_price_per_mtok": input_price_per_mtok, 87 "output_price_per_mtok": output_price_per_mtok, 88 }, 89 ) 90 _raise_for_status(resp) 91 return LlmModelState.model_validate(resp.json()) 92 93 def delete(self, model_id: str) -> bool: 94 resp = self._http.request("DELETE", f"/admin/llm-models/{model_id}") 95 _raise_for_status(resp) 96 return bool(resp.json().get("deleted")) 97 98 def mint_token( 99 self, 100 model_id: str, 101 *, 102 competition_id: str, 103 label: str = "", 104 expires_in_days: int | None = None, 105 ) -> dict[str, Any]: 106 """Mint a play credential bound to this model's synthetic team. 107 108 Returns ``{token, model_id, competition_id, team_id, 109 user_id}``; the plaintext ``pf_`` bearer is shown **once** — 110 store it now, only its hash is persisted. Scoped to 111 ``competition_id`` and carrying only what a run needs 112 (``challenges: read`` + ``instances: write`` + ``submissions: 113 write``). 114 115 This is what an *external* harness — one the platform does not 116 dispatch itself — uses to keep each model on its own team. 117 Sharing a team makes ``challenge_fully_solved`` inherit between 118 models and makes them share one wrong-answer budget, silently. 119 """ 120 resp = self._http.request( 121 "POST", 122 f"/admin/llm-models/{model_id}/tokens", 123 json={ 124 "competition_id": competition_id, 125 "label": label, 126 "expires_in_days": expires_in_days, 127 }, 128 ) 129 _raise_for_status(resp) 130 data: dict[str, Any] = resp.json() 131 return data 132 133 def review(self, model_id: str, *, status: str, note: str = "") -> LlmModelState: 134 """Approve / reject a model. ``status`` is ``approved`` / ``rejected`` / 135 ``pending``; only ``approved`` models may back an official campaign.""" 136 resp = self._http.request( 137 "POST", 138 f"/admin/llm-models/{model_id}/review", 139 json={"status": status, "note": note}, 140 ) 141 _raise_for_status(resp) 142 return LlmModelState.model_validate(resp.json()) 143 144 145class AdminEvalVendorsResource: 146 """The registry of model vendors / orgs: register / list / delete. 147 148 A vendor's ``id`` is a slug matching the ``vendor`` label on each of its 149 registered models, so the leaderboard's vendor scorecard resolves display 150 metadata from one curated row. 151 """ 152 153 def __init__(self, http: BaseHttpClient) -> None: 154 self._http = http 155 156 def list( 157 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 158 ) -> PagedList[VendorState]: 159 resp = self._http.request( 160 "GET", 161 "/admin/eval-vendors", 162 params={"active_only": active_only, "offset": offset, "limit": limit}, 163 ) 164 _raise_for_status(resp) 165 return _extract_items(resp.json(), VendorState) 166 167 def register( 168 self, 169 *, 170 id: str, 171 display_name: str = "", 172 homepage: str = "", 173 contact_email: str = "", 174 active: bool = True, 175 notes: str = "", 176 max_concurrent_campaigns: int = 1, 177 daily_run_quota: int = 200, 178 monthly_cost_budget_usd: float = 0.0, 179 ) -> VendorState: 180 """Register (or overwrite) a vendor by slug ``id``. 181 182 The quota trio caps the vendor's self-service campaign usage 183 (docs/model-eval-vendor-tenancy.md §6.2); ``0`` on any axis means "no 184 limit" there. A re-register overwrites the whole row, so pass the 185 current quota values to keep them.""" 186 resp = self._http.request( 187 "POST", 188 "/admin/eval-vendors", 189 json={ 190 "id": id, 191 "display_name": display_name, 192 "homepage": homepage, 193 "contact_email": contact_email, 194 "active": active, 195 "notes": notes, 196 "max_concurrent_campaigns": max_concurrent_campaigns, 197 "daily_run_quota": daily_run_quota, 198 "monthly_cost_budget_usd": monthly_cost_budget_usd, 199 }, 200 ) 201 _raise_for_status(resp) 202 return VendorState.model_validate(resp.json()) 203 204 def delete(self, vendor_id: str) -> bool: 205 resp = self._http.request("DELETE", f"/admin/eval-vendors/{vendor_id}") 206 _raise_for_status(resp) 207 return bool(resp.json().get("deleted")) 208 209 # -- vendor access tokens (tenancy) ------------------------------------- 210 211 def mint_token(self, vendor_id: str, *, label: str = "", scope: str = "read") -> dict[str, Any]: 212 """Mint a ``pv_`` vendor bearer. The plaintext ``token`` in the reply 213 is shown **once** — store it now; only its hash is persisted. ``scope`` 214 is ``read`` (default) or ``submit`` (self-service writes).""" 215 resp = self._http.request( 216 "POST", 217 f"/admin/eval-vendors/{vendor_id}/tokens", 218 json={"label": label, "scope": scope}, 219 ) 220 _raise_for_status(resp) 221 data: dict[str, Any] = resp.json() 222 return data 223 224 def list_tokens(self, vendor_id: str) -> builtins.list[VendorTokenState]: 225 """List a vendor's tokens (hashes blanked server-side).""" 226 resp = self._http.request("GET", f"/admin/eval-vendors/{vendor_id}/tokens") 227 _raise_for_status(resp) 228 return [VendorTokenState.model_validate(row) for row in resp.json()] 229 230 def revoke_token(self, vendor_id: str, token_id: str) -> bool: 231 resp = self._http.request("DELETE", f"/admin/eval-vendors/{vendor_id}/tokens/{token_id}") 232 _raise_for_status(resp) 233 return bool(resp.json().get("deleted")) 234 235 236class AdminEvalHarnessesResource: 237 """The registry of eval harnesses (agentic scaffolds): register / list / delete. 238 239 A harness's ``id`` is a slug (``ctfy`` / ``claude-code`` / …); ``image`` is 240 the container a node runs for it. Lets harness be a comparison variable 241 alongside the model. 242 """ 243 244 def __init__(self, http: BaseHttpClient) -> None: 245 self._http = http 246 247 def list( 248 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 249 ) -> PagedList[AdminHarnessRow]: 250 resp = self._http.request( 251 "GET", 252 "/admin/eval-harnesses", 253 params={"active_only": active_only, "offset": offset, "limit": limit}, 254 ) 255 _raise_for_status(resp) 256 return _extract_items(resp.json(), AdminHarnessRow) 257 258 def register( 259 self, 260 *, 261 id: str, 262 image: str = "", 263 display_name: str = "", 264 version: str = "", 265 notes: str = "", 266 active: bool = True, 267 ) -> HarnessState: 268 """Register (or overwrite) one of the *platform's* harnesses. 269 270 ⚠️ Refused with 409 on a row a user owns. The stamping here says 271 "the platform published this" — ``owner_user_id: ""`` is exactly 272 what the dispatch reads as our own image, run as it stands — so 273 aiming it at somebody's registration would strip the owner and 274 exempt the image from its admission and its isolation. 275 """ 276 resp = self._http.request( 277 "POST", 278 "/admin/eval-harnesses", 279 json={ 280 "id": id, 281 "image": image, 282 "display_name": display_name, 283 "version": version, 284 "notes": notes, 285 "active": active, 286 }, 287 ) 288 _raise_for_status(resp) 289 return HarnessState.model_validate(resp.json()) 290 291 def check(self, harness_id: str) -> AdminHarnessRow: 292 """Look at the image now and record what was found. 293 294 It records evidence the automatic admission reads (#2614): a user's 295 image runs once this check and the dry run pass on one digest. 296 297 ⚠️ **A registry that would not answer comes back 200 carrying 298 ``conformance_status == "error"``**, which is a recorded answer 299 about the registry rather than a claim about the image. Only a 300 row naming no image refuses (400 ``harness_has_no_image``): 301 "there is nothing to check" and "we could not look" send the 302 reader to different places. 303 """ 304 resp = self._http.request("POST", f"/admin/eval-harnesses/{harness_id}/conformance") 305 _raise_for_status(resp) 306 return AdminHarnessRow.model_validate(resp.json()) 307 308 def probe(self, harness_id: str) -> AdminHarnessRow: 309 """Put the image in the dry-run line (#2608); the row as it stands. 310 311 ⚠️ **Answers the row in line, never a verdict**: ``probe_status`` is 312 ``queued`` (with ``probe_reason`` and ``probe_position``) or 313 ``running``; the verdict lands on it later — poll :meth:`list`. 314 """ 315 resp = self._http.request("POST", f"/admin/eval-harnesses/{harness_id}/probe") 316 _raise_for_status(resp) 317 return AdminHarnessRow.model_validate(resp.json()) 318 319 def delete(self, harness_id: str) -> bool: 320 resp = self._http.request("DELETE", f"/admin/eval-harnesses/{harness_id}") 321 _raise_for_status(resp) 322 return bool(resp.json().get("deleted")) 323 324 325class AdminEvalAllowancesResource: 326 """Grants of the platform's own model key: list / grant / revoke. 327 328 ⚠️ This is the one custody mode that spends the deployment's money on 329 somebody else's behalf, so it is an explicit grant per 330 ``(account, competition)`` rather than a fall-through from "the user 331 has no key". Re-granting keeps what was already spent. 332 """ 333 334 def __init__(self, http: BaseHttpClient) -> None: 335 self._http = http 336 337 def list(self, *, competition_id: str = "") -> builtins.list[EvalAllowanceRow]: 338 """Every grant, or one competition's. ``""`` means every one.""" 339 resp = self._http.request( 340 "GET", "/admin/eval-allowances", params={"competition_id": competition_id} 341 ) 342 _raise_for_status(resp) 343 return [EvalAllowanceRow.model_validate(row) for row in resp.json()] 344 345 def grant( 346 self, user_id: str, competition_id: str, cap_usd: float, *, note: str = "" 347 ) -> EvalAllowanceRow: 348 """Let this account spend the platform's key, up to ``cap_usd``. 349 350 A repeat ``(account, competition)`` re-caps and **keeps the 351 spend** — raising a cap does not refund what went before. 352 """ 353 resp = self._http.request( 354 "POST", 355 "/admin/eval-allowances", 356 json={ 357 "user_id": user_id, 358 "competition_id": competition_id, 359 "cap_usd": cap_usd, 360 "note": note, 361 }, 362 ) 363 _raise_for_status(resp) 364 return EvalAllowanceRow.model_validate(resp.json()) 365 366 def revoke(self, user_id: str, competition_id: str) -> bool: 367 """Take it back. Runs already paid for stay paid for.""" 368 resp = self._http.request("DELETE", f"/admin/eval-allowances/{user_id}/{competition_id}") 369 _raise_for_status(resp) 370 return bool(resp.json().get("deleted", False)) 371 372 373class AdminEvalRunsResource: 374 """Submit + read model × challenge evaluation runs.""" 375 376 def __init__(self, http: BaseHttpClient) -> None: 377 self._http = http 378 379 def artifacts(self) -> EvalArtifactReport: 380 """How much disk the collected agent archives are using. 381 382 ⚠️ **Read ``by_status`` beside the headline.** Zero stored bytes 383 is four different facts — nobody has run an agent, this 384 deployment has no bucket, collection is failing, or a retention 385 window reclaimed them — and they are identical as a single 386 number. 387 """ 388 resp = self._http.request("GET", "/admin/eval-artifacts") 389 _raise_for_status(resp) 390 return EvalArtifactReport.model_validate(resp.json()) 391 392 def list( 393 self, 394 *, 395 model_id: str = "", 396 competition_id: str = "", 397 status: str = "", 398 campaign_id: str = "", 399 offset: int = 0, 400 limit: int = 50, 401 ) -> PagedList[EvalRunState]: 402 resp = self._http.request( 403 "GET", 404 "/admin/eval-runs", 405 params={ 406 "model_id": model_id, 407 "competition_id": competition_id, 408 "status": status, 409 "campaign_id": campaign_id, 410 "offset": offset, 411 "limit": limit, 412 }, 413 ) 414 _raise_for_status(resp) 415 return _extract_items(resp.json(), EvalRunState) 416 417 def get(self, eval_run_id: str) -> EvalRunState: 418 resp = self._http.request("GET", f"/admin/eval-runs/{eval_run_id}") 419 _raise_for_status(resp) 420 return EvalRunState.model_validate(resp.json()) 421 422 def coverage(self, eval_run_id: str) -> EvalRunCoverage: 423 """How much of the range this run reached, question by question. 424 425 The other number beside the Reporting grade: that one says 426 whether what was written is true, this one says how much there 427 was to write about. Composed from the challenge's own spec, so it 428 moves when the range does. 429 """ 430 resp = self._http.request("GET", f"/admin/eval-runs/{eval_run_id}/coverage") 431 _raise_for_status(resp) 432 return EvalRunCoverage.model_validate(resp.json()) 433 434 def range_failure(self, eval_run_id: str) -> EvalRunRangeFailure: 435 """The ranges this run's attempts started, each joined to its 436 instance record: node, stop reason and the node's own error (#3233).""" 437 resp = self._http.request("GET", f"/admin/eval-runs/{eval_run_id}/range-failure") 438 _raise_for_status(resp) 439 return EvalRunRangeFailure.model_validate(resp.json()) 440 441 def range_failures( 442 self, *, campaign_id: str = "", competition_id: str = "", since_ts: float = 0.0 443 ) -> EvalRangeFailureReport: 444 """``range_failed`` runs' attempts grouped by challenge × node (#3233).""" 445 resp = self._http.request( 446 "GET", 447 "/admin/eval-range-failures", 448 params={ 449 "campaign_id": campaign_id, 450 "competition_id": competition_id, 451 "since_ts": since_ts, 452 }, 453 ) 454 _raise_for_status(resp) 455 return EvalRangeFailureReport.model_validate(resp.json()) 456 457 def submit( 458 self, 459 *, 460 model_id: str, 461 challenge_id: str, 462 competition_id: str = "", 463 harness_id: str = "", 464 max_turns: int | None = None, 465 samples: int = 1, 466 ) -> AdminTaskInfo: 467 """Queue a ``model_eval_run`` task for one model × challenge. ``harness_id`` 468 selects the agentic scaffold (``""`` = the built-in ctfy harness); 469 ``samples`` > 1 queues k runs (pass@k). Returns the first pending task; 470 poll :meth:`list` for the resulting run records.""" 471 body: dict[str, Any] = { 472 "model_id": model_id, 473 "challenge_id": challenge_id, 474 "competition_id": competition_id, 475 "harness_id": harness_id, 476 "samples": samples, 477 } 478 if max_turns is not None: 479 body["max_turns"] = max_turns 480 resp = self._http.request("POST", "/admin/eval-runs", json=body) 481 _raise_for_status(resp) 482 return AdminTaskInfo.model_validate(resp.json()) 483 484 485class AdminEvalCampaignsResource: 486 """Submit + read operator batch evaluations (a model over a standard set).""" 487 488 def __init__(self, http: BaseHttpClient) -> None: 489 self._http = http 490 491 def list( 492 self, 493 *, 494 model_id: str = "", 495 vendor_id: str = "", 496 status: str = "", 497 tier: str = "", 498 offset: int = 0, 499 limit: int = 50, 500 ) -> PagedList[EvalCampaignState]: 501 resp = self._http.request( 502 "GET", 503 "/admin/eval-campaigns", 504 params={ 505 "model_id": model_id, 506 "vendor_id": vendor_id, 507 "status": status, 508 "tier": tier, 509 "offset": offset, 510 "limit": limit, 511 }, 512 ) 513 _raise_for_status(resp) 514 return _extract_items(resp.json(), EvalCampaignState) 515 516 def get(self, eval_campaign_id: str) -> EvalCampaignState: 517 resp = self._http.request("GET", f"/admin/eval-campaigns/{eval_campaign_id}") 518 _raise_for_status(resp) 519 return EvalCampaignState.model_validate(resp.json()) 520 521 def submit( 522 self, 523 *, 524 model_id: str, 525 standard_set_id: str, 526 name: str = "", 527 harness_id: str = "", 528 num_samples: int = 1, 529 max_cost_usd: float = 0.0, 530 timeout_s: int = 0, 531 max_turns: int = 0, 532 ) -> AdminTaskInfo: 533 """Queue an ``eval_campaign_run`` fan-out over a standard set. The model 534 must be ``approved``; the standard set (a competition id) must carry 535 challenges. Returns the driving task; poll :meth:`get` for the rollup.""" 536 resp = self._http.request( 537 "POST", 538 "/admin/eval-campaigns", 539 json={ 540 "model_id": model_id, 541 "standard_set_id": standard_set_id, 542 "name": name, 543 "harness_id": harness_id, 544 "num_samples": num_samples, 545 "max_cost_usd": max_cost_usd, 546 "timeout_s": timeout_s, 547 "max_turns": max_turns, 548 }, 549 ) 550 _raise_for_status(resp) 551 return AdminTaskInfo.model_validate(resp.json()) 552 553 def cancel(self, eval_campaign_id: str) -> EvalCampaignState: 554 """Cooperatively cancel a campaign via its driving task.""" 555 resp = self._http.request("POST", f"/admin/eval-campaigns/{eval_campaign_id}/cancel") 556 _raise_for_status(resp) 557 return EvalCampaignState.model_validate(resp.json()) 558 559 560class AdminRegistryResource: 561 """The registry this deployment runs: who holds what, and reclaiming it. 562 563 ⚠️ **There is deliberately no automatic GC**, so this is the whole 564 reclaim surface. ``approved_digest`` is what a dispatch *pulls*, so a 565 rule like "keep the newest K tags" would delete the image a campaign 566 that has not started yet will ask for — and the failure lands hours 567 later on somebody else's evaluation as an unexplained pull error. 568 """ 569 570 def __init__(self, http: BaseHttpClient) -> None: 571 self._http = http 572 573 def usage(self) -> RegistryUsageReport: 574 """What each account holds, against the ceiling it is judged by. 575 576 ⚠️ ``over_quota`` on each row is the **server's** verdict, from 577 the same predicate that refuses a push. Do not recompute it from 578 ``bytes``/``tags``: a second opinion is the one that eventually 579 disagrees with the one that decides. 580 """ 581 resp = self._http.request("GET", "/admin/registry/usage") 582 _raise_for_status(resp) 583 return RegistryUsageReport.model_validate(resp.json()) 584 585 def delete_manifest(self, repository: str, digest: str) -> str: 586 """Remove one manifest, and every tag pointing at it. 587 588 ⚠️ **Refused (409 ``registry_digest_approved``) when a harness is 589 approved at that digest**, naming the harnesses — a dispatch 590 pulls by digest, so removing it breaks runs that have not 591 started yet. Un-approve or re-approve those agents first. 592 """ 593 resp = self._http.request( 594 "DELETE", 595 "/admin/registry/manifests", 596 params={"repository": repository, "digest": digest}, 597 ) 598 _raise_for_status(resp) 599 return str(resp.json().get("digest", "")) 600 601 def repos(self) -> RegistryRepoList: 602 """Every repository, with its tag count and kind — the browse list.""" 603 resp = self._http.request("GET", "/admin/registry/repos") 604 _raise_for_status(resp) 605 return RegistryRepoList.model_validate(resp.json()) 606 607 def tags(self, repository: str, *, limit: int = 500) -> RegistryTagList: 608 """One repository's tags — size, push time, digest, references.""" 609 resp = self._http.request( 610 "GET", 611 "/admin/registry/tags", 612 params={"repository": repository, "limit": limit}, 613 ) 614 _raise_for_status(resp) 615 return RegistryTagList.model_validate(resp.json()) 616 617 def reclaim( 618 self, policy: RegistryReclaimPolicy | str, *, dry_run: bool = True 619 ) -> RegistryReclaimPlan: 620 """Preview (``dry_run=True``) or run a reference-checked reclaim. 621 622 ⚠️ Dry run is the default; only ``dry_run=False`` deletes, and it 623 never removes a manifest a live instance, published competition or 624 harness pin references. 625 """ 626 resp = self._http.request( 627 "POST", 628 "/admin/registry/reclaim", 629 json={"policy": str(policy), "dry_run": dry_run}, 630 ) 631 _raise_for_status(resp) 632 return RegistryReclaimPlan.model_validate(resp.json()) 633 634 def gc(self) -> RegistryGcStatus: 635 """Probe the registry and preview what garbage collection would free.""" 636 resp = self._http.request("GET", "/admin/registry/gc") 637 _raise_for_status(resp) 638 return RegistryGcStatus.model_validate(resp.json())
36class AdminLlmModelsResource: 37 """The registry of models under evaluation: register / list / delete. 38 39 No API key is ever stored — a registered model is addressing only 40 (flavor + base_url + provider model id); credentials are supplied to the 41 run at dispatch time via env. 42 """ 43 44 def __init__(self, http: BaseHttpClient) -> None: 45 self._http = http 46 47 def list( 48 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 49 ) -> PagedList[LlmModelState]: 50 resp = self._http.request( 51 "GET", 52 "/admin/llm-models", 53 params={"active_only": active_only, "offset": offset, "limit": limit}, 54 ) 55 _raise_for_status(resp) 56 return _extract_items(resp.json(), LlmModelState) 57 58 def register( 59 self, 60 *, 61 id: str, 62 flavor: str, 63 display_name: str = "", 64 provider_model: str = "", 65 base_url: str = "", 66 vendor: str = "", 67 notes: str = "", 68 input_price_per_mtok: float = 0.0, 69 output_price_per_mtok: float = 0.0, 70 ) -> LlmModelState: 71 """Register (or overwrite) a model. ``flavor`` is ``anthropic`` / ``openai``. 72 73 ``*_price_per_mtok`` is USD per 1M tokens (0 = unpriced) — drives the 74 leaderboard's cost columns. 75 """ 76 resp = self._http.request( 77 "POST", 78 "/admin/llm-models", 79 json={ 80 "id": id, 81 "flavor": flavor, 82 "display_name": display_name, 83 "provider_model": provider_model, 84 "base_url": base_url, 85 "vendor": vendor, 86 "notes": notes, 87 "input_price_per_mtok": input_price_per_mtok, 88 "output_price_per_mtok": output_price_per_mtok, 89 }, 90 ) 91 _raise_for_status(resp) 92 return LlmModelState.model_validate(resp.json()) 93 94 def delete(self, model_id: str) -> bool: 95 resp = self._http.request("DELETE", f"/admin/llm-models/{model_id}") 96 _raise_for_status(resp) 97 return bool(resp.json().get("deleted")) 98 99 def mint_token( 100 self, 101 model_id: str, 102 *, 103 competition_id: str, 104 label: str = "", 105 expires_in_days: int | None = None, 106 ) -> dict[str, Any]: 107 """Mint a play credential bound to this model's synthetic team. 108 109 Returns ``{token, model_id, competition_id, team_id, 110 user_id}``; the plaintext ``pf_`` bearer is shown **once** — 111 store it now, only its hash is persisted. Scoped to 112 ``competition_id`` and carrying only what a run needs 113 (``challenges: read`` + ``instances: write`` + ``submissions: 114 write``). 115 116 This is what an *external* harness — one the platform does not 117 dispatch itself — uses to keep each model on its own team. 118 Sharing a team makes ``challenge_fully_solved`` inherit between 119 models and makes them share one wrong-answer budget, silently. 120 """ 121 resp = self._http.request( 122 "POST", 123 f"/admin/llm-models/{model_id}/tokens", 124 json={ 125 "competition_id": competition_id, 126 "label": label, 127 "expires_in_days": expires_in_days, 128 }, 129 ) 130 _raise_for_status(resp) 131 data: dict[str, Any] = resp.json() 132 return data 133 134 def review(self, model_id: str, *, status: str, note: str = "") -> LlmModelState: 135 """Approve / reject a model. ``status`` is ``approved`` / ``rejected`` / 136 ``pending``; only ``approved`` models may back an official campaign.""" 137 resp = self._http.request( 138 "POST", 139 f"/admin/llm-models/{model_id}/review", 140 json={"status": status, "note": note}, 141 ) 142 _raise_for_status(resp) 143 return LlmModelState.model_validate(resp.json())
The registry of models under evaluation: register / list / delete.
No API key is ever stored — a registered model is addressing only (flavor + base_url + provider model id); credentials are supplied to the run at dispatch time via env.
47 def list( 48 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 49 ) -> PagedList[LlmModelState]: 50 resp = self._http.request( 51 "GET", 52 "/admin/llm-models", 53 params={"active_only": active_only, "offset": offset, "limit": limit}, 54 ) 55 _raise_for_status(resp) 56 return _extract_items(resp.json(), LlmModelState)
58 def register( 59 self, 60 *, 61 id: str, 62 flavor: str, 63 display_name: str = "", 64 provider_model: str = "", 65 base_url: str = "", 66 vendor: str = "", 67 notes: str = "", 68 input_price_per_mtok: float = 0.0, 69 output_price_per_mtok: float = 0.0, 70 ) -> LlmModelState: 71 """Register (or overwrite) a model. ``flavor`` is ``anthropic`` / ``openai``. 72 73 ``*_price_per_mtok`` is USD per 1M tokens (0 = unpriced) — drives the 74 leaderboard's cost columns. 75 """ 76 resp = self._http.request( 77 "POST", 78 "/admin/llm-models", 79 json={ 80 "id": id, 81 "flavor": flavor, 82 "display_name": display_name, 83 "provider_model": provider_model, 84 "base_url": base_url, 85 "vendor": vendor, 86 "notes": notes, 87 "input_price_per_mtok": input_price_per_mtok, 88 "output_price_per_mtok": output_price_per_mtok, 89 }, 90 ) 91 _raise_for_status(resp) 92 return LlmModelState.model_validate(resp.json())
Register (or overwrite) a model. flavor is anthropic / openai.
*_price_per_mtok is USD per 1M tokens (0 = unpriced) — drives the
leaderboard's cost columns.
99 def mint_token( 100 self, 101 model_id: str, 102 *, 103 competition_id: str, 104 label: str = "", 105 expires_in_days: int | None = None, 106 ) -> dict[str, Any]: 107 """Mint a play credential bound to this model's synthetic team. 108 109 Returns ``{token, model_id, competition_id, team_id, 110 user_id}``; the plaintext ``pf_`` bearer is shown **once** — 111 store it now, only its hash is persisted. Scoped to 112 ``competition_id`` and carrying only what a run needs 113 (``challenges: read`` + ``instances: write`` + ``submissions: 114 write``). 115 116 This is what an *external* harness — one the platform does not 117 dispatch itself — uses to keep each model on its own team. 118 Sharing a team makes ``challenge_fully_solved`` inherit between 119 models and makes them share one wrong-answer budget, silently. 120 """ 121 resp = self._http.request( 122 "POST", 123 f"/admin/llm-models/{model_id}/tokens", 124 json={ 125 "competition_id": competition_id, 126 "label": label, 127 "expires_in_days": expires_in_days, 128 }, 129 ) 130 _raise_for_status(resp) 131 data: dict[str, Any] = resp.json() 132 return data
Mint a play credential bound to this model's synthetic team.
Returns {token, model_id, competition_id, team_id,
user_id}; the plaintext pf_ bearer is shown once —
store it now, only its hash is persisted. Scoped to
competition_id and carrying only what a run needs
(challenges: read + instances: write + submissions:
write).
This is what an external harness — one the platform does not
dispatch itself — uses to keep each model on its own team.
Sharing a team makes challenge_fully_solved inherit between
models and makes them share one wrong-answer budget, silently.
134 def review(self, model_id: str, *, status: str, note: str = "") -> LlmModelState: 135 """Approve / reject a model. ``status`` is ``approved`` / ``rejected`` / 136 ``pending``; only ``approved`` models may back an official campaign.""" 137 resp = self._http.request( 138 "POST", 139 f"/admin/llm-models/{model_id}/review", 140 json={"status": status, "note": note}, 141 ) 142 _raise_for_status(resp) 143 return LlmModelState.model_validate(resp.json())
Approve / reject a model. status is approved / rejected /
pending; only approved models may back an official campaign.
146class AdminEvalVendorsResource: 147 """The registry of model vendors / orgs: register / list / delete. 148 149 A vendor's ``id`` is a slug matching the ``vendor`` label on each of its 150 registered models, so the leaderboard's vendor scorecard resolves display 151 metadata from one curated row. 152 """ 153 154 def __init__(self, http: BaseHttpClient) -> None: 155 self._http = http 156 157 def list( 158 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 159 ) -> PagedList[VendorState]: 160 resp = self._http.request( 161 "GET", 162 "/admin/eval-vendors", 163 params={"active_only": active_only, "offset": offset, "limit": limit}, 164 ) 165 _raise_for_status(resp) 166 return _extract_items(resp.json(), VendorState) 167 168 def register( 169 self, 170 *, 171 id: str, 172 display_name: str = "", 173 homepage: str = "", 174 contact_email: str = "", 175 active: bool = True, 176 notes: str = "", 177 max_concurrent_campaigns: int = 1, 178 daily_run_quota: int = 200, 179 monthly_cost_budget_usd: float = 0.0, 180 ) -> VendorState: 181 """Register (or overwrite) a vendor by slug ``id``. 182 183 The quota trio caps the vendor's self-service campaign usage 184 (docs/model-eval-vendor-tenancy.md §6.2); ``0`` on any axis means "no 185 limit" there. A re-register overwrites the whole row, so pass the 186 current quota values to keep them.""" 187 resp = self._http.request( 188 "POST", 189 "/admin/eval-vendors", 190 json={ 191 "id": id, 192 "display_name": display_name, 193 "homepage": homepage, 194 "contact_email": contact_email, 195 "active": active, 196 "notes": notes, 197 "max_concurrent_campaigns": max_concurrent_campaigns, 198 "daily_run_quota": daily_run_quota, 199 "monthly_cost_budget_usd": monthly_cost_budget_usd, 200 }, 201 ) 202 _raise_for_status(resp) 203 return VendorState.model_validate(resp.json()) 204 205 def delete(self, vendor_id: str) -> bool: 206 resp = self._http.request("DELETE", f"/admin/eval-vendors/{vendor_id}") 207 _raise_for_status(resp) 208 return bool(resp.json().get("deleted")) 209 210 # -- vendor access tokens (tenancy) ------------------------------------- 211 212 def mint_token(self, vendor_id: str, *, label: str = "", scope: str = "read") -> dict[str, Any]: 213 """Mint a ``pv_`` vendor bearer. The plaintext ``token`` in the reply 214 is shown **once** — store it now; only its hash is persisted. ``scope`` 215 is ``read`` (default) or ``submit`` (self-service writes).""" 216 resp = self._http.request( 217 "POST", 218 f"/admin/eval-vendors/{vendor_id}/tokens", 219 json={"label": label, "scope": scope}, 220 ) 221 _raise_for_status(resp) 222 data: dict[str, Any] = resp.json() 223 return data 224 225 def list_tokens(self, vendor_id: str) -> builtins.list[VendorTokenState]: 226 """List a vendor's tokens (hashes blanked server-side).""" 227 resp = self._http.request("GET", f"/admin/eval-vendors/{vendor_id}/tokens") 228 _raise_for_status(resp) 229 return [VendorTokenState.model_validate(row) for row in resp.json()] 230 231 def revoke_token(self, vendor_id: str, token_id: str) -> bool: 232 resp = self._http.request("DELETE", f"/admin/eval-vendors/{vendor_id}/tokens/{token_id}") 233 _raise_for_status(resp) 234 return bool(resp.json().get("deleted"))
The registry of model vendors / orgs: register / list / delete.
A vendor's id is a slug matching the vendor label on each of its
registered models, so the leaderboard's vendor scorecard resolves display
metadata from one curated row.
157 def list( 158 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 159 ) -> PagedList[VendorState]: 160 resp = self._http.request( 161 "GET", 162 "/admin/eval-vendors", 163 params={"active_only": active_only, "offset": offset, "limit": limit}, 164 ) 165 _raise_for_status(resp) 166 return _extract_items(resp.json(), VendorState)
168 def register( 169 self, 170 *, 171 id: str, 172 display_name: str = "", 173 homepage: str = "", 174 contact_email: str = "", 175 active: bool = True, 176 notes: str = "", 177 max_concurrent_campaigns: int = 1, 178 daily_run_quota: int = 200, 179 monthly_cost_budget_usd: float = 0.0, 180 ) -> VendorState: 181 """Register (or overwrite) a vendor by slug ``id``. 182 183 The quota trio caps the vendor's self-service campaign usage 184 (docs/model-eval-vendor-tenancy.md §6.2); ``0`` on any axis means "no 185 limit" there. A re-register overwrites the whole row, so pass the 186 current quota values to keep them.""" 187 resp = self._http.request( 188 "POST", 189 "/admin/eval-vendors", 190 json={ 191 "id": id, 192 "display_name": display_name, 193 "homepage": homepage, 194 "contact_email": contact_email, 195 "active": active, 196 "notes": notes, 197 "max_concurrent_campaigns": max_concurrent_campaigns, 198 "daily_run_quota": daily_run_quota, 199 "monthly_cost_budget_usd": monthly_cost_budget_usd, 200 }, 201 ) 202 _raise_for_status(resp) 203 return VendorState.model_validate(resp.json())
Register (or overwrite) a vendor by slug id.
The quota trio caps the vendor's self-service campaign usage
(docs/model-eval-vendor-tenancy.md §6.2); 0 on any axis means "no
limit" there. A re-register overwrites the whole row, so pass the
current quota values to keep them.
212 def mint_token(self, vendor_id: str, *, label: str = "", scope: str = "read") -> dict[str, Any]: 213 """Mint a ``pv_`` vendor bearer. The plaintext ``token`` in the reply 214 is shown **once** — store it now; only its hash is persisted. ``scope`` 215 is ``read`` (default) or ``submit`` (self-service writes).""" 216 resp = self._http.request( 217 "POST", 218 f"/admin/eval-vendors/{vendor_id}/tokens", 219 json={"label": label, "scope": scope}, 220 ) 221 _raise_for_status(resp) 222 data: dict[str, Any] = resp.json() 223 return data
Mint a pv_ vendor bearer. The plaintext token in the reply
is shown once — store it now; only its hash is persisted. scope
is read (default) or submit (self-service writes).
225 def list_tokens(self, vendor_id: str) -> builtins.list[VendorTokenState]: 226 """List a vendor's tokens (hashes blanked server-side).""" 227 resp = self._http.request("GET", f"/admin/eval-vendors/{vendor_id}/tokens") 228 _raise_for_status(resp) 229 return [VendorTokenState.model_validate(row) for row in resp.json()]
List a vendor's tokens (hashes blanked server-side).
237class AdminEvalHarnessesResource: 238 """The registry of eval harnesses (agentic scaffolds): register / list / delete. 239 240 A harness's ``id`` is a slug (``ctfy`` / ``claude-code`` / …); ``image`` is 241 the container a node runs for it. Lets harness be a comparison variable 242 alongside the model. 243 """ 244 245 def __init__(self, http: BaseHttpClient) -> None: 246 self._http = http 247 248 def list( 249 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 250 ) -> PagedList[AdminHarnessRow]: 251 resp = self._http.request( 252 "GET", 253 "/admin/eval-harnesses", 254 params={"active_only": active_only, "offset": offset, "limit": limit}, 255 ) 256 _raise_for_status(resp) 257 return _extract_items(resp.json(), AdminHarnessRow) 258 259 def register( 260 self, 261 *, 262 id: str, 263 image: str = "", 264 display_name: str = "", 265 version: str = "", 266 notes: str = "", 267 active: bool = True, 268 ) -> HarnessState: 269 """Register (or overwrite) one of the *platform's* harnesses. 270 271 ⚠️ Refused with 409 on a row a user owns. The stamping here says 272 "the platform published this" — ``owner_user_id: ""`` is exactly 273 what the dispatch reads as our own image, run as it stands — so 274 aiming it at somebody's registration would strip the owner and 275 exempt the image from its admission and its isolation. 276 """ 277 resp = self._http.request( 278 "POST", 279 "/admin/eval-harnesses", 280 json={ 281 "id": id, 282 "image": image, 283 "display_name": display_name, 284 "version": version, 285 "notes": notes, 286 "active": active, 287 }, 288 ) 289 _raise_for_status(resp) 290 return HarnessState.model_validate(resp.json()) 291 292 def check(self, harness_id: str) -> AdminHarnessRow: 293 """Look at the image now and record what was found. 294 295 It records evidence the automatic admission reads (#2614): a user's 296 image runs once this check and the dry run pass on one digest. 297 298 ⚠️ **A registry that would not answer comes back 200 carrying 299 ``conformance_status == "error"``**, which is a recorded answer 300 about the registry rather than a claim about the image. Only a 301 row naming no image refuses (400 ``harness_has_no_image``): 302 "there is nothing to check" and "we could not look" send the 303 reader to different places. 304 """ 305 resp = self._http.request("POST", f"/admin/eval-harnesses/{harness_id}/conformance") 306 _raise_for_status(resp) 307 return AdminHarnessRow.model_validate(resp.json()) 308 309 def probe(self, harness_id: str) -> AdminHarnessRow: 310 """Put the image in the dry-run line (#2608); the row as it stands. 311 312 ⚠️ **Answers the row in line, never a verdict**: ``probe_status`` is 313 ``queued`` (with ``probe_reason`` and ``probe_position``) or 314 ``running``; the verdict lands on it later — poll :meth:`list`. 315 """ 316 resp = self._http.request("POST", f"/admin/eval-harnesses/{harness_id}/probe") 317 _raise_for_status(resp) 318 return AdminHarnessRow.model_validate(resp.json()) 319 320 def delete(self, harness_id: str) -> bool: 321 resp = self._http.request("DELETE", f"/admin/eval-harnesses/{harness_id}") 322 _raise_for_status(resp) 323 return bool(resp.json().get("deleted"))
The registry of eval harnesses (agentic scaffolds): register / list / delete.
A harness's id is a slug (ctfy / claude-code / …); image is
the container a node runs for it. Lets harness be a comparison variable
alongside the model.
248 def list( 249 self, *, active_only: bool = False, offset: int = 0, limit: int = 100 250 ) -> PagedList[AdminHarnessRow]: 251 resp = self._http.request( 252 "GET", 253 "/admin/eval-harnesses", 254 params={"active_only": active_only, "offset": offset, "limit": limit}, 255 ) 256 _raise_for_status(resp) 257 return _extract_items(resp.json(), AdminHarnessRow)
259 def register( 260 self, 261 *, 262 id: str, 263 image: str = "", 264 display_name: str = "", 265 version: str = "", 266 notes: str = "", 267 active: bool = True, 268 ) -> HarnessState: 269 """Register (or overwrite) one of the *platform's* harnesses. 270 271 ⚠️ Refused with 409 on a row a user owns. The stamping here says 272 "the platform published this" — ``owner_user_id: ""`` is exactly 273 what the dispatch reads as our own image, run as it stands — so 274 aiming it at somebody's registration would strip the owner and 275 exempt the image from its admission and its isolation. 276 """ 277 resp = self._http.request( 278 "POST", 279 "/admin/eval-harnesses", 280 json={ 281 "id": id, 282 "image": image, 283 "display_name": display_name, 284 "version": version, 285 "notes": notes, 286 "active": active, 287 }, 288 ) 289 _raise_for_status(resp) 290 return HarnessState.model_validate(resp.json())
Register (or overwrite) one of the platform's harnesses.
⚠️ Refused with 409 on a row a user owns. The stamping here says
"the platform published this" — owner_user_id: "" is exactly
what the dispatch reads as our own image, run as it stands — so
aiming it at somebody's registration would strip the owner and
exempt the image from its admission and its isolation.
292 def check(self, harness_id: str) -> AdminHarnessRow: 293 """Look at the image now and record what was found. 294 295 It records evidence the automatic admission reads (#2614): a user's 296 image runs once this check and the dry run pass on one digest. 297 298 ⚠️ **A registry that would not answer comes back 200 carrying 299 ``conformance_status == "error"``**, which is a recorded answer 300 about the registry rather than a claim about the image. Only a 301 row naming no image refuses (400 ``harness_has_no_image``): 302 "there is nothing to check" and "we could not look" send the 303 reader to different places. 304 """ 305 resp = self._http.request("POST", f"/admin/eval-harnesses/{harness_id}/conformance") 306 _raise_for_status(resp) 307 return AdminHarnessRow.model_validate(resp.json())
Look at the image now and record what was found.
It records evidence the automatic admission reads (#2614): a user's image runs once this check and the dry run pass on one digest.
⚠️ A registry that would not answer comes back 200 carrying
conformance_status == "error", which is a recorded answer
about the registry rather than a claim about the image. Only a
row naming no image refuses (400 harness_has_no_image):
"there is nothing to check" and "we could not look" send the
reader to different places.
309 def probe(self, harness_id: str) -> AdminHarnessRow: 310 """Put the image in the dry-run line (#2608); the row as it stands. 311 312 ⚠️ **Answers the row in line, never a verdict**: ``probe_status`` is 313 ``queued`` (with ``probe_reason`` and ``probe_position``) or 314 ``running``; the verdict lands on it later — poll :meth:`list`. 315 """ 316 resp = self._http.request("POST", f"/admin/eval-harnesses/{harness_id}/probe") 317 _raise_for_status(resp) 318 return AdminHarnessRow.model_validate(resp.json())
Put the image in the dry-run line (#2608); the row as it stands.
⚠️ Answers the row in line, never a verdict: probe_status is
queued (with probe_reason and probe_position) or
running; the verdict lands on it later — poll list().
326class AdminEvalAllowancesResource: 327 """Grants of the platform's own model key: list / grant / revoke. 328 329 ⚠️ This is the one custody mode that spends the deployment's money on 330 somebody else's behalf, so it is an explicit grant per 331 ``(account, competition)`` rather than a fall-through from "the user 332 has no key". Re-granting keeps what was already spent. 333 """ 334 335 def __init__(self, http: BaseHttpClient) -> None: 336 self._http = http 337 338 def list(self, *, competition_id: str = "") -> builtins.list[EvalAllowanceRow]: 339 """Every grant, or one competition's. ``""`` means every one.""" 340 resp = self._http.request( 341 "GET", "/admin/eval-allowances", params={"competition_id": competition_id} 342 ) 343 _raise_for_status(resp) 344 return [EvalAllowanceRow.model_validate(row) for row in resp.json()] 345 346 def grant( 347 self, user_id: str, competition_id: str, cap_usd: float, *, note: str = "" 348 ) -> EvalAllowanceRow: 349 """Let this account spend the platform's key, up to ``cap_usd``. 350 351 A repeat ``(account, competition)`` re-caps and **keeps the 352 spend** — raising a cap does not refund what went before. 353 """ 354 resp = self._http.request( 355 "POST", 356 "/admin/eval-allowances", 357 json={ 358 "user_id": user_id, 359 "competition_id": competition_id, 360 "cap_usd": cap_usd, 361 "note": note, 362 }, 363 ) 364 _raise_for_status(resp) 365 return EvalAllowanceRow.model_validate(resp.json()) 366 367 def revoke(self, user_id: str, competition_id: str) -> bool: 368 """Take it back. Runs already paid for stay paid for.""" 369 resp = self._http.request("DELETE", f"/admin/eval-allowances/{user_id}/{competition_id}") 370 _raise_for_status(resp) 371 return bool(resp.json().get("deleted", False))
Grants of the platform's own model key: list / grant / revoke.
⚠️ This is the one custody mode that spends the deployment's money on
somebody else's behalf, so it is an explicit grant per
(account, competition) rather than a fall-through from "the user
has no key". Re-granting keeps what was already spent.
338 def list(self, *, competition_id: str = "") -> builtins.list[EvalAllowanceRow]: 339 """Every grant, or one competition's. ``""`` means every one.""" 340 resp = self._http.request( 341 "GET", "/admin/eval-allowances", params={"competition_id": competition_id} 342 ) 343 _raise_for_status(resp) 344 return [EvalAllowanceRow.model_validate(row) for row in resp.json()]
Every grant, or one competition's. "" means every one.
346 def grant( 347 self, user_id: str, competition_id: str, cap_usd: float, *, note: str = "" 348 ) -> EvalAllowanceRow: 349 """Let this account spend the platform's key, up to ``cap_usd``. 350 351 A repeat ``(account, competition)`` re-caps and **keeps the 352 spend** — raising a cap does not refund what went before. 353 """ 354 resp = self._http.request( 355 "POST", 356 "/admin/eval-allowances", 357 json={ 358 "user_id": user_id, 359 "competition_id": competition_id, 360 "cap_usd": cap_usd, 361 "note": note, 362 }, 363 ) 364 _raise_for_status(resp) 365 return EvalAllowanceRow.model_validate(resp.json())
Let this account spend the platform's key, up to cap_usd.
A repeat (account, competition) re-caps and keeps the
spend — raising a cap does not refund what went before.
367 def revoke(self, user_id: str, competition_id: str) -> bool: 368 """Take it back. Runs already paid for stay paid for.""" 369 resp = self._http.request("DELETE", f"/admin/eval-allowances/{user_id}/{competition_id}") 370 _raise_for_status(resp) 371 return bool(resp.json().get("deleted", False))
Take it back. Runs already paid for stay paid for.
374class AdminEvalRunsResource: 375 """Submit + read model × challenge evaluation runs.""" 376 377 def __init__(self, http: BaseHttpClient) -> None: 378 self._http = http 379 380 def artifacts(self) -> EvalArtifactReport: 381 """How much disk the collected agent archives are using. 382 383 ⚠️ **Read ``by_status`` beside the headline.** Zero stored bytes 384 is four different facts — nobody has run an agent, this 385 deployment has no bucket, collection is failing, or a retention 386 window reclaimed them — and they are identical as a single 387 number. 388 """ 389 resp = self._http.request("GET", "/admin/eval-artifacts") 390 _raise_for_status(resp) 391 return EvalArtifactReport.model_validate(resp.json()) 392 393 def list( 394 self, 395 *, 396 model_id: str = "", 397 competition_id: str = "", 398 status: str = "", 399 campaign_id: str = "", 400 offset: int = 0, 401 limit: int = 50, 402 ) -> PagedList[EvalRunState]: 403 resp = self._http.request( 404 "GET", 405 "/admin/eval-runs", 406 params={ 407 "model_id": model_id, 408 "competition_id": competition_id, 409 "status": status, 410 "campaign_id": campaign_id, 411 "offset": offset, 412 "limit": limit, 413 }, 414 ) 415 _raise_for_status(resp) 416 return _extract_items(resp.json(), EvalRunState) 417 418 def get(self, eval_run_id: str) -> EvalRunState: 419 resp = self._http.request("GET", f"/admin/eval-runs/{eval_run_id}") 420 _raise_for_status(resp) 421 return EvalRunState.model_validate(resp.json()) 422 423 def coverage(self, eval_run_id: str) -> EvalRunCoverage: 424 """How much of the range this run reached, question by question. 425 426 The other number beside the Reporting grade: that one says 427 whether what was written is true, this one says how much there 428 was to write about. Composed from the challenge's own spec, so it 429 moves when the range does. 430 """ 431 resp = self._http.request("GET", f"/admin/eval-runs/{eval_run_id}/coverage") 432 _raise_for_status(resp) 433 return EvalRunCoverage.model_validate(resp.json()) 434 435 def range_failure(self, eval_run_id: str) -> EvalRunRangeFailure: 436 """The ranges this run's attempts started, each joined to its 437 instance record: node, stop reason and the node's own error (#3233).""" 438 resp = self._http.request("GET", f"/admin/eval-runs/{eval_run_id}/range-failure") 439 _raise_for_status(resp) 440 return EvalRunRangeFailure.model_validate(resp.json()) 441 442 def range_failures( 443 self, *, campaign_id: str = "", competition_id: str = "", since_ts: float = 0.0 444 ) -> EvalRangeFailureReport: 445 """``range_failed`` runs' attempts grouped by challenge × node (#3233).""" 446 resp = self._http.request( 447 "GET", 448 "/admin/eval-range-failures", 449 params={ 450 "campaign_id": campaign_id, 451 "competition_id": competition_id, 452 "since_ts": since_ts, 453 }, 454 ) 455 _raise_for_status(resp) 456 return EvalRangeFailureReport.model_validate(resp.json()) 457 458 def submit( 459 self, 460 *, 461 model_id: str, 462 challenge_id: str, 463 competition_id: str = "", 464 harness_id: str = "", 465 max_turns: int | None = None, 466 samples: int = 1, 467 ) -> AdminTaskInfo: 468 """Queue a ``model_eval_run`` task for one model × challenge. ``harness_id`` 469 selects the agentic scaffold (``""`` = the built-in ctfy harness); 470 ``samples`` > 1 queues k runs (pass@k). Returns the first pending task; 471 poll :meth:`list` for the resulting run records.""" 472 body: dict[str, Any] = { 473 "model_id": model_id, 474 "challenge_id": challenge_id, 475 "competition_id": competition_id, 476 "harness_id": harness_id, 477 "samples": samples, 478 } 479 if max_turns is not None: 480 body["max_turns"] = max_turns 481 resp = self._http.request("POST", "/admin/eval-runs", json=body) 482 _raise_for_status(resp) 483 return AdminTaskInfo.model_validate(resp.json())
Submit + read model × challenge evaluation runs.
380 def artifacts(self) -> EvalArtifactReport: 381 """How much disk the collected agent archives are using. 382 383 ⚠️ **Read ``by_status`` beside the headline.** Zero stored bytes 384 is four different facts — nobody has run an agent, this 385 deployment has no bucket, collection is failing, or a retention 386 window reclaimed them — and they are identical as a single 387 number. 388 """ 389 resp = self._http.request("GET", "/admin/eval-artifacts") 390 _raise_for_status(resp) 391 return EvalArtifactReport.model_validate(resp.json())
How much disk the collected agent archives are using.
⚠️ Read by_status beside the headline. Zero stored bytes
is four different facts — nobody has run an agent, this
deployment has no bucket, collection is failing, or a retention
window reclaimed them — and they are identical as a single
number.
393 def list( 394 self, 395 *, 396 model_id: str = "", 397 competition_id: str = "", 398 status: str = "", 399 campaign_id: str = "", 400 offset: int = 0, 401 limit: int = 50, 402 ) -> PagedList[EvalRunState]: 403 resp = self._http.request( 404 "GET", 405 "/admin/eval-runs", 406 params={ 407 "model_id": model_id, 408 "competition_id": competition_id, 409 "status": status, 410 "campaign_id": campaign_id, 411 "offset": offset, 412 "limit": limit, 413 }, 414 ) 415 _raise_for_status(resp) 416 return _extract_items(resp.json(), EvalRunState)
423 def coverage(self, eval_run_id: str) -> EvalRunCoverage: 424 """How much of the range this run reached, question by question. 425 426 The other number beside the Reporting grade: that one says 427 whether what was written is true, this one says how much there 428 was to write about. Composed from the challenge's own spec, so it 429 moves when the range does. 430 """ 431 resp = self._http.request("GET", f"/admin/eval-runs/{eval_run_id}/coverage") 432 _raise_for_status(resp) 433 return EvalRunCoverage.model_validate(resp.json())
How much of the range this run reached, question by question.
The other number beside the Reporting grade: that one says whether what was written is true, this one says how much there was to write about. Composed from the challenge's own spec, so it moves when the range does.
435 def range_failure(self, eval_run_id: str) -> EvalRunRangeFailure: 436 """The ranges this run's attempts started, each joined to its 437 instance record: node, stop reason and the node's own error (#3233).""" 438 resp = self._http.request("GET", f"/admin/eval-runs/{eval_run_id}/range-failure") 439 _raise_for_status(resp) 440 return EvalRunRangeFailure.model_validate(resp.json())
The ranges this run's attempts started, each joined to its instance record: node, stop reason and the node's own error (#3233).
442 def range_failures( 443 self, *, campaign_id: str = "", competition_id: str = "", since_ts: float = 0.0 444 ) -> EvalRangeFailureReport: 445 """``range_failed`` runs' attempts grouped by challenge × node (#3233).""" 446 resp = self._http.request( 447 "GET", 448 "/admin/eval-range-failures", 449 params={ 450 "campaign_id": campaign_id, 451 "competition_id": competition_id, 452 "since_ts": since_ts, 453 }, 454 ) 455 _raise_for_status(resp) 456 return EvalRangeFailureReport.model_validate(resp.json())
range_failed runs' attempts grouped by challenge × node (#3233).
458 def submit( 459 self, 460 *, 461 model_id: str, 462 challenge_id: str, 463 competition_id: str = "", 464 harness_id: str = "", 465 max_turns: int | None = None, 466 samples: int = 1, 467 ) -> AdminTaskInfo: 468 """Queue a ``model_eval_run`` task for one model × challenge. ``harness_id`` 469 selects the agentic scaffold (``""`` = the built-in ctfy harness); 470 ``samples`` > 1 queues k runs (pass@k). Returns the first pending task; 471 poll :meth:`list` for the resulting run records.""" 472 body: dict[str, Any] = { 473 "model_id": model_id, 474 "challenge_id": challenge_id, 475 "competition_id": competition_id, 476 "harness_id": harness_id, 477 "samples": samples, 478 } 479 if max_turns is not None: 480 body["max_turns"] = max_turns 481 resp = self._http.request("POST", "/admin/eval-runs", json=body) 482 _raise_for_status(resp) 483 return AdminTaskInfo.model_validate(resp.json())
Queue a model_eval_run task for one model × challenge. harness_id
selects the agentic scaffold ("" = the built-in ctfy harness);
samples > 1 queues k runs (pass@k). Returns the first pending task;
poll list() for the resulting run records.
486class AdminEvalCampaignsResource: 487 """Submit + read operator batch evaluations (a model over a standard set).""" 488 489 def __init__(self, http: BaseHttpClient) -> None: 490 self._http = http 491 492 def list( 493 self, 494 *, 495 model_id: str = "", 496 vendor_id: str = "", 497 status: str = "", 498 tier: str = "", 499 offset: int = 0, 500 limit: int = 50, 501 ) -> PagedList[EvalCampaignState]: 502 resp = self._http.request( 503 "GET", 504 "/admin/eval-campaigns", 505 params={ 506 "model_id": model_id, 507 "vendor_id": vendor_id, 508 "status": status, 509 "tier": tier, 510 "offset": offset, 511 "limit": limit, 512 }, 513 ) 514 _raise_for_status(resp) 515 return _extract_items(resp.json(), EvalCampaignState) 516 517 def get(self, eval_campaign_id: str) -> EvalCampaignState: 518 resp = self._http.request("GET", f"/admin/eval-campaigns/{eval_campaign_id}") 519 _raise_for_status(resp) 520 return EvalCampaignState.model_validate(resp.json()) 521 522 def submit( 523 self, 524 *, 525 model_id: str, 526 standard_set_id: str, 527 name: str = "", 528 harness_id: str = "", 529 num_samples: int = 1, 530 max_cost_usd: float = 0.0, 531 timeout_s: int = 0, 532 max_turns: int = 0, 533 ) -> AdminTaskInfo: 534 """Queue an ``eval_campaign_run`` fan-out over a standard set. The model 535 must be ``approved``; the standard set (a competition id) must carry 536 challenges. Returns the driving task; poll :meth:`get` for the rollup.""" 537 resp = self._http.request( 538 "POST", 539 "/admin/eval-campaigns", 540 json={ 541 "model_id": model_id, 542 "standard_set_id": standard_set_id, 543 "name": name, 544 "harness_id": harness_id, 545 "num_samples": num_samples, 546 "max_cost_usd": max_cost_usd, 547 "timeout_s": timeout_s, 548 "max_turns": max_turns, 549 }, 550 ) 551 _raise_for_status(resp) 552 return AdminTaskInfo.model_validate(resp.json()) 553 554 def cancel(self, eval_campaign_id: str) -> EvalCampaignState: 555 """Cooperatively cancel a campaign via its driving task.""" 556 resp = self._http.request("POST", f"/admin/eval-campaigns/{eval_campaign_id}/cancel") 557 _raise_for_status(resp) 558 return EvalCampaignState.model_validate(resp.json())
Submit + read operator batch evaluations (a model over a standard set).
492 def list( 493 self, 494 *, 495 model_id: str = "", 496 vendor_id: str = "", 497 status: str = "", 498 tier: str = "", 499 offset: int = 0, 500 limit: int = 50, 501 ) -> PagedList[EvalCampaignState]: 502 resp = self._http.request( 503 "GET", 504 "/admin/eval-campaigns", 505 params={ 506 "model_id": model_id, 507 "vendor_id": vendor_id, 508 "status": status, 509 "tier": tier, 510 "offset": offset, 511 "limit": limit, 512 }, 513 ) 514 _raise_for_status(resp) 515 return _extract_items(resp.json(), EvalCampaignState)
522 def submit( 523 self, 524 *, 525 model_id: str, 526 standard_set_id: str, 527 name: str = "", 528 harness_id: str = "", 529 num_samples: int = 1, 530 max_cost_usd: float = 0.0, 531 timeout_s: int = 0, 532 max_turns: int = 0, 533 ) -> AdminTaskInfo: 534 """Queue an ``eval_campaign_run`` fan-out over a standard set. The model 535 must be ``approved``; the standard set (a competition id) must carry 536 challenges. Returns the driving task; poll :meth:`get` for the rollup.""" 537 resp = self._http.request( 538 "POST", 539 "/admin/eval-campaigns", 540 json={ 541 "model_id": model_id, 542 "standard_set_id": standard_set_id, 543 "name": name, 544 "harness_id": harness_id, 545 "num_samples": num_samples, 546 "max_cost_usd": max_cost_usd, 547 "timeout_s": timeout_s, 548 "max_turns": max_turns, 549 }, 550 ) 551 _raise_for_status(resp) 552 return AdminTaskInfo.model_validate(resp.json())
Queue an eval_campaign_run fan-out over a standard set. The model
must be approved; the standard set (a competition id) must carry
challenges. Returns the driving task; poll get() for the rollup.
554 def cancel(self, eval_campaign_id: str) -> EvalCampaignState: 555 """Cooperatively cancel a campaign via its driving task.""" 556 resp = self._http.request("POST", f"/admin/eval-campaigns/{eval_campaign_id}/cancel") 557 _raise_for_status(resp) 558 return EvalCampaignState.model_validate(resp.json())
Cooperatively cancel a campaign via its driving task.
561class AdminRegistryResource: 562 """The registry this deployment runs: who holds what, and reclaiming it. 563 564 ⚠️ **There is deliberately no automatic GC**, so this is the whole 565 reclaim surface. ``approved_digest`` is what a dispatch *pulls*, so a 566 rule like "keep the newest K tags" would delete the image a campaign 567 that has not started yet will ask for — and the failure lands hours 568 later on somebody else's evaluation as an unexplained pull error. 569 """ 570 571 def __init__(self, http: BaseHttpClient) -> None: 572 self._http = http 573 574 def usage(self) -> RegistryUsageReport: 575 """What each account holds, against the ceiling it is judged by. 576 577 ⚠️ ``over_quota`` on each row is the **server's** verdict, from 578 the same predicate that refuses a push. Do not recompute it from 579 ``bytes``/``tags``: a second opinion is the one that eventually 580 disagrees with the one that decides. 581 """ 582 resp = self._http.request("GET", "/admin/registry/usage") 583 _raise_for_status(resp) 584 return RegistryUsageReport.model_validate(resp.json()) 585 586 def delete_manifest(self, repository: str, digest: str) -> str: 587 """Remove one manifest, and every tag pointing at it. 588 589 ⚠️ **Refused (409 ``registry_digest_approved``) when a harness is 590 approved at that digest**, naming the harnesses — a dispatch 591 pulls by digest, so removing it breaks runs that have not 592 started yet. Un-approve or re-approve those agents first. 593 """ 594 resp = self._http.request( 595 "DELETE", 596 "/admin/registry/manifests", 597 params={"repository": repository, "digest": digest}, 598 ) 599 _raise_for_status(resp) 600 return str(resp.json().get("digest", "")) 601 602 def repos(self) -> RegistryRepoList: 603 """Every repository, with its tag count and kind — the browse list.""" 604 resp = self._http.request("GET", "/admin/registry/repos") 605 _raise_for_status(resp) 606 return RegistryRepoList.model_validate(resp.json()) 607 608 def tags(self, repository: str, *, limit: int = 500) -> RegistryTagList: 609 """One repository's tags — size, push time, digest, references.""" 610 resp = self._http.request( 611 "GET", 612 "/admin/registry/tags", 613 params={"repository": repository, "limit": limit}, 614 ) 615 _raise_for_status(resp) 616 return RegistryTagList.model_validate(resp.json()) 617 618 def reclaim( 619 self, policy: RegistryReclaimPolicy | str, *, dry_run: bool = True 620 ) -> RegistryReclaimPlan: 621 """Preview (``dry_run=True``) or run a reference-checked reclaim. 622 623 ⚠️ Dry run is the default; only ``dry_run=False`` deletes, and it 624 never removes a manifest a live instance, published competition or 625 harness pin references. 626 """ 627 resp = self._http.request( 628 "POST", 629 "/admin/registry/reclaim", 630 json={"policy": str(policy), "dry_run": dry_run}, 631 ) 632 _raise_for_status(resp) 633 return RegistryReclaimPlan.model_validate(resp.json()) 634 635 def gc(self) -> RegistryGcStatus: 636 """Probe the registry and preview what garbage collection would free.""" 637 resp = self._http.request("GET", "/admin/registry/gc") 638 _raise_for_status(resp) 639 return RegistryGcStatus.model_validate(resp.json())
The registry this deployment runs: who holds what, and reclaiming it.
⚠️ There is deliberately no automatic GC, so this is the whole
reclaim surface. approved_digest is what a dispatch pulls, so a
rule like "keep the newest K tags" would delete the image a campaign
that has not started yet will ask for — and the failure lands hours
later on somebody else's evaluation as an unexplained pull error.
574 def usage(self) -> RegistryUsageReport: 575 """What each account holds, against the ceiling it is judged by. 576 577 ⚠️ ``over_quota`` on each row is the **server's** verdict, from 578 the same predicate that refuses a push. Do not recompute it from 579 ``bytes``/``tags``: a second opinion is the one that eventually 580 disagrees with the one that decides. 581 """ 582 resp = self._http.request("GET", "/admin/registry/usage") 583 _raise_for_status(resp) 584 return RegistryUsageReport.model_validate(resp.json())
What each account holds, against the ceiling it is judged by.
⚠️ over_quota on each row is the server's verdict, from
the same predicate that refuses a push. Do not recompute it from
bytes/tags: a second opinion is the one that eventually
disagrees with the one that decides.
586 def delete_manifest(self, repository: str, digest: str) -> str: 587 """Remove one manifest, and every tag pointing at it. 588 589 ⚠️ **Refused (409 ``registry_digest_approved``) when a harness is 590 approved at that digest**, naming the harnesses — a dispatch 591 pulls by digest, so removing it breaks runs that have not 592 started yet. Un-approve or re-approve those agents first. 593 """ 594 resp = self._http.request( 595 "DELETE", 596 "/admin/registry/manifests", 597 params={"repository": repository, "digest": digest}, 598 ) 599 _raise_for_status(resp) 600 return str(resp.json().get("digest", ""))
Remove one manifest, and every tag pointing at it.
⚠️ Refused (409 registry_digest_approved) when a harness is
approved at that digest, naming the harnesses — a dispatch
pulls by digest, so removing it breaks runs that have not
started yet. Un-approve or re-approve those agents first.
602 def repos(self) -> RegistryRepoList: 603 """Every repository, with its tag count and kind — the browse list.""" 604 resp = self._http.request("GET", "/admin/registry/repos") 605 _raise_for_status(resp) 606 return RegistryRepoList.model_validate(resp.json())
Every repository, with its tag count and kind — the browse list.
618 def reclaim( 619 self, policy: RegistryReclaimPolicy | str, *, dry_run: bool = True 620 ) -> RegistryReclaimPlan: 621 """Preview (``dry_run=True``) or run a reference-checked reclaim. 622 623 ⚠️ Dry run is the default; only ``dry_run=False`` deletes, and it 624 never removes a manifest a live instance, published competition or 625 harness pin references. 626 """ 627 resp = self._http.request( 628 "POST", 629 "/admin/registry/reclaim", 630 json={"policy": str(policy), "dry_run": dry_run}, 631 ) 632 _raise_for_status(resp) 633 return RegistryReclaimPlan.model_validate(resp.json())
Preview (dry_run=True) or run a reference-checked reclaim.
⚠️ Dry run is the default; only dry_run=False deletes, and it
never removes a manifest a live instance, published competition or
harness pin references.
635 def gc(self) -> RegistryGcStatus: 636 """Probe the registry and preview what garbage collection would free.""" 637 resp = self._http.request("GET", "/admin/registry/gc") 638 _raise_for_status(resp) 639 return RegistryGcStatus.model_validate(resp.json())
Probe the registry and preview what garbage collection would free.