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