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())
class VendorResource:
 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.

VendorResource(http: ctfy.sdk.base.BaseHttpClient)
27    def __init__(self, http: BaseHttpClient) -> None:
28        self._http = http
def me(self) -> ctfy.core.state.models.VendorState:
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())
def models(self) -> list[ctfy.core.state.models.LlmModelState]:
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()]
def runs( self, *, offset: int = 0, limit: int = 100, fully_solved: bool | None = None) -> ctfy.sdk._helpers.PagedList[ctfy.core.state.models.EvalRunState]:
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.

def standard_sets(self) -> list[ctfy.server.models.VendorStandardSet]:
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).

def nodes(self) -> list[ctfy.server.models.NodeInfo]:
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.

def register_model( self, *, id: str, flavor: str, display_name: str = '', provider_model: str = '', base_url: str = '', notes: str = '') -> ctfy.core.state.models.LlmModelState:
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.

def upload_key( self, *, flavor: str, api_key: str, label: str = '') -> ctfy.core.state.models.VendorKeyState:
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).

def keys(self) -> list[ctfy.core.state.models.VendorKeyState]:
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).

def delete_key(self, vendor_key_id: str) -> bool:
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"))
def campaigns( self, *, offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.core.state.models.EvalCampaignState]:
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).

def submit_campaign( self, *, model_id: str, standard_set_id: str, name: str = '', num_samples: int = 1, max_cost_usd: float = 0.0, timeout_s: int = 0, max_turns: int = 0, publish: bool = False) -> ctfy.core.state.models.EvalCampaignState:
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.