ctfy.sdk.resources.vendor
client.vendor — read-only vendor tenancy (authenticate with a pv_ token).
1"""``client.vendor`` — read-only vendor tenancy (authenticate with a ``pv_`` token).""" 2 3from __future__ import annotations 4 5import builtins 6 7from ctfy.core.state.models import ( 8 EvalCampaignState, 9 EvalRunState, 10 LlmModelState, 11 VendorKeyState, 12 VendorState, 13) 14from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status 15from ctfy.sdk.base import BaseHttpClient 16from ctfy.server.models import NodeInfo, VendorStandardSet 17 18 19class VendorResource: 20 """The calling vendor's own profile / models / runs. 21 22 Requires a ``pv_`` bearer (minted by an admin); the token resolves to a 23 single vendor and exposes only that vendor's data — never the platform's. 24 """ 25 26 def __init__(self, http: BaseHttpClient) -> None: 27 self._http = http 28 29 def me(self) -> VendorState: 30 resp = self._http.request("GET", "/vendor/me") 31 _raise_for_status(resp) 32 return VendorState.model_validate(resp.json()) 33 34 def models(self) -> builtins.list[LlmModelState]: 35 resp = self._http.request("GET", "/vendor/models") 36 _raise_for_status(resp) 37 return [LlmModelState.model_validate(row) for row in resp.json()] 38 39 def runs( 40 self, *, offset: int = 0, limit: int = 100, fully_solved: bool | None = None 41 ) -> PagedList[EvalRunState]: 42 """This vendor's runs, newest first — one page, with the total. 43 44 ``fully_solved`` narrows to runs that did (``True``) or did not 45 (``False``) solve their whole challenge; ``.total`` then counts 46 the narrowed set, so ``runs(fully_solved=True, limit=1).total`` 47 is how many runs solved, without reading them. 48 """ 49 params: dict[str, int | bool] = {"offset": offset, "limit": limit} 50 if fully_solved is not None: 51 params["fully_solved"] = fully_solved 52 resp = self._http.request("GET", "/vendor/runs", params=params) 53 _raise_for_status(resp) 54 return _extract_items(resp.json(), EvalRunState) 55 56 def standard_sets(self) -> builtins.list[VendorStandardSet]: 57 """The operator eval standard sets a vendor may launch a campaign over 58 (id / title / challenge count / corpus pin — the launch-picker source).""" 59 resp = self._http.request("GET", "/vendor/standard-sets") 60 _raise_for_status(resp) 61 return [VendorStandardSet.model_validate(row) for row in resp.json()] 62 63 def nodes(self) -> builtins.list[NodeInfo]: 64 """The vendor's own worker nodes (bring-your-own-node), with live 65 health — so the vendor can confirm a node is online before launching a 66 BYO-node campaign that would run on it.""" 67 resp = self._http.request("GET", "/vendor/nodes") 68 _raise_for_status(resp) 69 return [NodeInfo.model_validate(row) for row in resp.json()] 70 71 def register_model( 72 self, 73 *, 74 id: str, 75 flavor: str, 76 display_name: str = "", 77 provider_model: str = "", 78 base_url: str = "", 79 notes: str = "", 80 ) -> LlmModelState: 81 """Self-register (or edit) one of the vendor's own models. Requires a 82 ``submit``-scoped ``pv_`` token. The model lands ``pending`` review — 83 an operator must approve it before it can run official campaigns.""" 84 resp = self._http.request( 85 "POST", 86 "/vendor/models", 87 json={ 88 "id": id, 89 "flavor": flavor, 90 "display_name": display_name, 91 "provider_model": provider_model, 92 "base_url": base_url, 93 "notes": notes, 94 }, 95 ) 96 _raise_for_status(resp) 97 return LlmModelState.model_validate(resp.json()) 98 99 def upload_key(self, *, flavor: str, api_key: str, label: str = "") -> VendorKeyState: 100 """Upload (or replace) the vendor's model API key for a flavor. Requires 101 a ``submit``-scoped token. The key is encrypted at rest; the returned 102 row carries only metadata (never the ciphertext).""" 103 resp = self._http.request( 104 "POST", 105 "/vendor/keys", 106 json={"flavor": flavor, "api_key": api_key, "label": label}, 107 ) 108 _raise_for_status(resp) 109 return VendorKeyState.model_validate(resp.json()) 110 111 def keys(self) -> builtins.list[VendorKeyState]: 112 """List the vendor's uploaded key metadata (ciphertext blanked).""" 113 resp = self._http.request("GET", "/vendor/keys") 114 _raise_for_status(resp) 115 return [VendorKeyState.model_validate(row) for row in resp.json()] 116 117 def delete_key(self, vendor_key_id: str) -> bool: 118 resp = self._http.request("DELETE", f"/vendor/keys/{vendor_key_id}") 119 _raise_for_status(resp) 120 return bool(resp.json().get("deleted")) 121 122 def campaigns(self, *, offset: int = 0, limit: int = 50) -> PagedList[EvalCampaignState]: 123 """The vendor's own campaigns (sandbox + self-reported), newest 124 first — one page, with the total (``limit`` at most 200).""" 125 resp = self._http.request( 126 "GET", "/vendor/campaigns", params={"offset": offset, "limit": limit} 127 ) 128 _raise_for_status(resp) 129 return _extract_items(resp.json(), EvalCampaignState) 130 131 def submit_campaign( 132 self, 133 *, 134 model_id: str, 135 standard_set_id: str, 136 name: str = "", 137 num_samples: int = 1, 138 max_cost_usd: float = 0.0, 139 timeout_s: int = 0, 140 max_turns: int = 0, 141 publish: bool = False, 142 ) -> EvalCampaignState: 143 """Self-trigger a batch over an operator standard set on the vendor's 144 own key. ``publish=False`` → a private ``sandbox`` run; ``publish=True`` 145 → a public but unverified ``self_reported`` run (requires an approved 146 model). Requires a ``submit``-scoped token + an uploaded key for the 147 model's flavor. Poll :meth:`campaigns` for progress.""" 148 resp = self._http.request( 149 "POST", 150 "/vendor/campaigns", 151 json={ 152 "model_id": model_id, 153 "standard_set_id": standard_set_id, 154 "name": name, 155 "num_samples": num_samples, 156 "max_cost_usd": max_cost_usd, 157 "timeout_s": timeout_s, 158 "max_turns": max_turns, 159 "publish": publish, 160 }, 161 ) 162 _raise_for_status(resp) 163 return EvalCampaignState.model_validate(resp.json())
20class VendorResource: 21 """The calling vendor's own profile / models / runs. 22 23 Requires a ``pv_`` bearer (minted by an admin); the token resolves to a 24 single vendor and exposes only that vendor's data — never the platform's. 25 """ 26 27 def __init__(self, http: BaseHttpClient) -> None: 28 self._http = http 29 30 def me(self) -> VendorState: 31 resp = self._http.request("GET", "/vendor/me") 32 _raise_for_status(resp) 33 return VendorState.model_validate(resp.json()) 34 35 def models(self) -> builtins.list[LlmModelState]: 36 resp = self._http.request("GET", "/vendor/models") 37 _raise_for_status(resp) 38 return [LlmModelState.model_validate(row) for row in resp.json()] 39 40 def runs( 41 self, *, offset: int = 0, limit: int = 100, fully_solved: bool | None = None 42 ) -> PagedList[EvalRunState]: 43 """This vendor's runs, newest first — one page, with the total. 44 45 ``fully_solved`` narrows to runs that did (``True``) or did not 46 (``False``) solve their whole challenge; ``.total`` then counts 47 the narrowed set, so ``runs(fully_solved=True, limit=1).total`` 48 is how many runs solved, without reading them. 49 """ 50 params: dict[str, int | bool] = {"offset": offset, "limit": limit} 51 if fully_solved is not None: 52 params["fully_solved"] = fully_solved 53 resp = self._http.request("GET", "/vendor/runs", params=params) 54 _raise_for_status(resp) 55 return _extract_items(resp.json(), EvalRunState) 56 57 def standard_sets(self) -> builtins.list[VendorStandardSet]: 58 """The operator eval standard sets a vendor may launch a campaign over 59 (id / title / challenge count / corpus pin — the launch-picker source).""" 60 resp = self._http.request("GET", "/vendor/standard-sets") 61 _raise_for_status(resp) 62 return [VendorStandardSet.model_validate(row) for row in resp.json()] 63 64 def nodes(self) -> builtins.list[NodeInfo]: 65 """The vendor's own worker nodes (bring-your-own-node), with live 66 health — so the vendor can confirm a node is online before launching a 67 BYO-node campaign that would run on it.""" 68 resp = self._http.request("GET", "/vendor/nodes") 69 _raise_for_status(resp) 70 return [NodeInfo.model_validate(row) for row in resp.json()] 71 72 def register_model( 73 self, 74 *, 75 id: str, 76 flavor: str, 77 display_name: str = "", 78 provider_model: str = "", 79 base_url: str = "", 80 notes: str = "", 81 ) -> LlmModelState: 82 """Self-register (or edit) one of the vendor's own models. Requires a 83 ``submit``-scoped ``pv_`` token. The model lands ``pending`` review — 84 an operator must approve it before it can run official campaigns.""" 85 resp = self._http.request( 86 "POST", 87 "/vendor/models", 88 json={ 89 "id": id, 90 "flavor": flavor, 91 "display_name": display_name, 92 "provider_model": provider_model, 93 "base_url": base_url, 94 "notes": notes, 95 }, 96 ) 97 _raise_for_status(resp) 98 return LlmModelState.model_validate(resp.json()) 99 100 def upload_key(self, *, flavor: str, api_key: str, label: str = "") -> VendorKeyState: 101 """Upload (or replace) the vendor's model API key for a flavor. Requires 102 a ``submit``-scoped token. The key is encrypted at rest; the returned 103 row carries only metadata (never the ciphertext).""" 104 resp = self._http.request( 105 "POST", 106 "/vendor/keys", 107 json={"flavor": flavor, "api_key": api_key, "label": label}, 108 ) 109 _raise_for_status(resp) 110 return VendorKeyState.model_validate(resp.json()) 111 112 def keys(self) -> builtins.list[VendorKeyState]: 113 """List the vendor's uploaded key metadata (ciphertext blanked).""" 114 resp = self._http.request("GET", "/vendor/keys") 115 _raise_for_status(resp) 116 return [VendorKeyState.model_validate(row) for row in resp.json()] 117 118 def delete_key(self, vendor_key_id: str) -> bool: 119 resp = self._http.request("DELETE", f"/vendor/keys/{vendor_key_id}") 120 _raise_for_status(resp) 121 return bool(resp.json().get("deleted")) 122 123 def campaigns(self, *, offset: int = 0, limit: int = 50) -> PagedList[EvalCampaignState]: 124 """The vendor's own campaigns (sandbox + self-reported), newest 125 first — one page, with the total (``limit`` at most 200).""" 126 resp = self._http.request( 127 "GET", "/vendor/campaigns", params={"offset": offset, "limit": limit} 128 ) 129 _raise_for_status(resp) 130 return _extract_items(resp.json(), EvalCampaignState) 131 132 def submit_campaign( 133 self, 134 *, 135 model_id: str, 136 standard_set_id: str, 137 name: str = "", 138 num_samples: int = 1, 139 max_cost_usd: float = 0.0, 140 timeout_s: int = 0, 141 max_turns: int = 0, 142 publish: bool = False, 143 ) -> EvalCampaignState: 144 """Self-trigger a batch over an operator standard set on the vendor's 145 own key. ``publish=False`` → a private ``sandbox`` run; ``publish=True`` 146 → a public but unverified ``self_reported`` run (requires an approved 147 model). Requires a ``submit``-scoped token + an uploaded key for the 148 model's flavor. Poll :meth:`campaigns` for progress.""" 149 resp = self._http.request( 150 "POST", 151 "/vendor/campaigns", 152 json={ 153 "model_id": model_id, 154 "standard_set_id": standard_set_id, 155 "name": name, 156 "num_samples": num_samples, 157 "max_cost_usd": max_cost_usd, 158 "timeout_s": timeout_s, 159 "max_turns": max_turns, 160 "publish": publish, 161 }, 162 ) 163 _raise_for_status(resp) 164 return EvalCampaignState.model_validate(resp.json())
The calling vendor's own profile / models / runs.
Requires a pv_ bearer (minted by an admin); the token resolves to a
single vendor and exposes only that vendor's data — never the platform's.
40 def runs( 41 self, *, offset: int = 0, limit: int = 100, fully_solved: bool | None = None 42 ) -> PagedList[EvalRunState]: 43 """This vendor's runs, newest first — one page, with the total. 44 45 ``fully_solved`` narrows to runs that did (``True``) or did not 46 (``False``) solve their whole challenge; ``.total`` then counts 47 the narrowed set, so ``runs(fully_solved=True, limit=1).total`` 48 is how many runs solved, without reading them. 49 """ 50 params: dict[str, int | bool] = {"offset": offset, "limit": limit} 51 if fully_solved is not None: 52 params["fully_solved"] = fully_solved 53 resp = self._http.request("GET", "/vendor/runs", params=params) 54 _raise_for_status(resp) 55 return _extract_items(resp.json(), EvalRunState)
This vendor's runs, newest first — one page, with the total.
fully_solved narrows to runs that did (True) or did not
(False) solve their whole challenge; .total then counts
the narrowed set, so runs(fully_solved=True, limit=1).total
is how many runs solved, without reading them.
57 def standard_sets(self) -> builtins.list[VendorStandardSet]: 58 """The operator eval standard sets a vendor may launch a campaign over 59 (id / title / challenge count / corpus pin — the launch-picker source).""" 60 resp = self._http.request("GET", "/vendor/standard-sets") 61 _raise_for_status(resp) 62 return [VendorStandardSet.model_validate(row) for row in resp.json()]
The operator eval standard sets a vendor may launch a campaign over (id / title / challenge count / corpus pin — the launch-picker source).
64 def nodes(self) -> builtins.list[NodeInfo]: 65 """The vendor's own worker nodes (bring-your-own-node), with live 66 health — so the vendor can confirm a node is online before launching a 67 BYO-node campaign that would run on it.""" 68 resp = self._http.request("GET", "/vendor/nodes") 69 _raise_for_status(resp) 70 return [NodeInfo.model_validate(row) for row in resp.json()]
The vendor's own worker nodes (bring-your-own-node), with live health — so the vendor can confirm a node is online before launching a BYO-node campaign that would run on it.
72 def register_model( 73 self, 74 *, 75 id: str, 76 flavor: str, 77 display_name: str = "", 78 provider_model: str = "", 79 base_url: str = "", 80 notes: str = "", 81 ) -> LlmModelState: 82 """Self-register (or edit) one of the vendor's own models. Requires a 83 ``submit``-scoped ``pv_`` token. The model lands ``pending`` review — 84 an operator must approve it before it can run official campaigns.""" 85 resp = self._http.request( 86 "POST", 87 "/vendor/models", 88 json={ 89 "id": id, 90 "flavor": flavor, 91 "display_name": display_name, 92 "provider_model": provider_model, 93 "base_url": base_url, 94 "notes": notes, 95 }, 96 ) 97 _raise_for_status(resp) 98 return LlmModelState.model_validate(resp.json())
Self-register (or edit) one of the vendor's own models. Requires a
submit-scoped pv_ token. The model lands pending review —
an operator must approve it before it can run official campaigns.
100 def upload_key(self, *, flavor: str, api_key: str, label: str = "") -> VendorKeyState: 101 """Upload (or replace) the vendor's model API key for a flavor. Requires 102 a ``submit``-scoped token. The key is encrypted at rest; the returned 103 row carries only metadata (never the ciphertext).""" 104 resp = self._http.request( 105 "POST", 106 "/vendor/keys", 107 json={"flavor": flavor, "api_key": api_key, "label": label}, 108 ) 109 _raise_for_status(resp) 110 return VendorKeyState.model_validate(resp.json())
Upload (or replace) the vendor's model API key for a flavor. Requires
a submit-scoped token. The key is encrypted at rest; the returned
row carries only metadata (never the ciphertext).
112 def keys(self) -> builtins.list[VendorKeyState]: 113 """List the vendor's uploaded key metadata (ciphertext blanked).""" 114 resp = self._http.request("GET", "/vendor/keys") 115 _raise_for_status(resp) 116 return [VendorKeyState.model_validate(row) for row in resp.json()]
List the vendor's uploaded key metadata (ciphertext blanked).
123 def campaigns(self, *, offset: int = 0, limit: int = 50) -> PagedList[EvalCampaignState]: 124 """The vendor's own campaigns (sandbox + self-reported), newest 125 first — one page, with the total (``limit`` at most 200).""" 126 resp = self._http.request( 127 "GET", "/vendor/campaigns", params={"offset": offset, "limit": limit} 128 ) 129 _raise_for_status(resp) 130 return _extract_items(resp.json(), EvalCampaignState)
The vendor's own campaigns (sandbox + self-reported), newest
first — one page, with the total (limit at most 200).
132 def submit_campaign( 133 self, 134 *, 135 model_id: str, 136 standard_set_id: str, 137 name: str = "", 138 num_samples: int = 1, 139 max_cost_usd: float = 0.0, 140 timeout_s: int = 0, 141 max_turns: int = 0, 142 publish: bool = False, 143 ) -> EvalCampaignState: 144 """Self-trigger a batch over an operator standard set on the vendor's 145 own key. ``publish=False`` → a private ``sandbox`` run; ``publish=True`` 146 → a public but unverified ``self_reported`` run (requires an approved 147 model). Requires a ``submit``-scoped token + an uploaded key for the 148 model's flavor. Poll :meth:`campaigns` for progress.""" 149 resp = self._http.request( 150 "POST", 151 "/vendor/campaigns", 152 json={ 153 "model_id": model_id, 154 "standard_set_id": standard_set_id, 155 "name": name, 156 "num_samples": num_samples, 157 "max_cost_usd": max_cost_usd, 158 "timeout_s": timeout_s, 159 "max_turns": max_turns, 160 "publish": publish, 161 }, 162 ) 163 _raise_for_status(resp) 164 return EvalCampaignState.model_validate(resp.json())
Self-trigger a batch over an operator standard set on the vendor's
own key. publish=False → a private sandbox run; publish=True
→ a public but unverified self_reported run (requires an approved
model). Requires a submit-scoped token + an uploaded key for the
model's flavor. Poll campaigns() for progress.