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 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 register_model(
 64        self,
 65        *,
 66        id: str,
 67        flavor: str,
 68        display_name: str = "",
 69        provider_model: str = "",
 70        base_url: str = "",
 71        notes: str = "",
 72    ) -> LlmModelState:
 73        """Self-register (or edit) one of the vendor's own models. Requires a
 74        ``submit``-scoped ``pv_`` token. The model lands ``pending`` review —
 75        an operator must approve it before it can run official campaigns."""
 76        resp = self._http.request(
 77            "POST",
 78            "/vendor/models",
 79            json={
 80                "id": id,
 81                "flavor": flavor,
 82                "display_name": display_name,
 83                "provider_model": provider_model,
 84                "base_url": base_url,
 85                "notes": notes,
 86            },
 87        )
 88        _raise_for_status(resp)
 89        return LlmModelState.model_validate(resp.json())
 90
 91    def upload_key(self, *, flavor: str, api_key: str, label: str = "") -> VendorKeyState:
 92        """Upload (or replace) the vendor's model API key for a flavor. Requires
 93        a ``submit``-scoped token. The key is encrypted at rest; the returned
 94        row carries only metadata (never the ciphertext)."""
 95        resp = self._http.request(
 96            "POST",
 97            "/vendor/keys",
 98            json={"flavor": flavor, "api_key": api_key, "label": label},
 99        )
100        _raise_for_status(resp)
101        return VendorKeyState.model_validate(resp.json())
102
103    def keys(self) -> builtins.list[VendorKeyState]:
104        """List the vendor's uploaded key metadata (ciphertext blanked)."""
105        resp = self._http.request("GET", "/vendor/keys")
106        _raise_for_status(resp)
107        return [VendorKeyState.model_validate(row) for row in resp.json()]
108
109    def delete_key(self, vendor_key_id: str) -> bool:
110        resp = self._http.request("DELETE", f"/vendor/keys/{vendor_key_id}")
111        _raise_for_status(resp)
112        return bool(resp.json().get("deleted"))
113
114    def campaigns(self, *, offset: int = 0, limit: int = 50) -> PagedList[EvalCampaignState]:
115        """The vendor's own campaigns (sandbox + self-reported), newest
116        first — one page, with the total (``limit`` at most 200)."""
117        resp = self._http.request(
118            "GET", "/vendor/campaigns", params={"offset": offset, "limit": limit}
119        )
120        _raise_for_status(resp)
121        return _extract_items(resp.json(), EvalCampaignState)
122
123    def submit_campaign(
124        self,
125        *,
126        model_id: str,
127        standard_set_id: str,
128        name: str = "",
129        num_samples: int = 1,
130        max_cost_usd: float = 0.0,
131        timeout_s: int = 0,
132        max_turns: int = 0,
133        publish: bool = False,
134    ) -> EvalCampaignState:
135        """Self-trigger a batch over an operator standard set on the vendor's
136        own key. ``publish=False`` → a private ``sandbox`` run; ``publish=True``
137        → a public but unverified ``self_reported`` run (requires an approved
138        model). Requires a ``submit``-scoped token + an uploaded key for the
139        model's flavor. Poll :meth:`campaigns` for progress."""
140        resp = self._http.request(
141            "POST",
142            "/vendor/campaigns",
143            json={
144                "model_id": model_id,
145                "standard_set_id": standard_set_id,
146                "name": name,
147                "num_samples": num_samples,
148                "max_cost_usd": max_cost_usd,
149                "timeout_s": timeout_s,
150                "max_turns": max_turns,
151                "publish": publish,
152            },
153        )
154        _raise_for_status(resp)
155        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 register_model(
 65        self,
 66        *,
 67        id: str,
 68        flavor: str,
 69        display_name: str = "",
 70        provider_model: str = "",
 71        base_url: str = "",
 72        notes: str = "",
 73    ) -> LlmModelState:
 74        """Self-register (or edit) one of the vendor's own models. Requires a
 75        ``submit``-scoped ``pv_`` token. The model lands ``pending`` review —
 76        an operator must approve it before it can run official campaigns."""
 77        resp = self._http.request(
 78            "POST",
 79            "/vendor/models",
 80            json={
 81                "id": id,
 82                "flavor": flavor,
 83                "display_name": display_name,
 84                "provider_model": provider_model,
 85                "base_url": base_url,
 86                "notes": notes,
 87            },
 88        )
 89        _raise_for_status(resp)
 90        return LlmModelState.model_validate(resp.json())
 91
 92    def upload_key(self, *, flavor: str, api_key: str, label: str = "") -> VendorKeyState:
 93        """Upload (or replace) the vendor's model API key for a flavor. Requires
 94        a ``submit``-scoped token. The key is encrypted at rest; the returned
 95        row carries only metadata (never the ciphertext)."""
 96        resp = self._http.request(
 97            "POST",
 98            "/vendor/keys",
 99            json={"flavor": flavor, "api_key": api_key, "label": label},
100        )
101        _raise_for_status(resp)
102        return VendorKeyState.model_validate(resp.json())
103
104    def keys(self) -> builtins.list[VendorKeyState]:
105        """List the vendor's uploaded key metadata (ciphertext blanked)."""
106        resp = self._http.request("GET", "/vendor/keys")
107        _raise_for_status(resp)
108        return [VendorKeyState.model_validate(row) for row in resp.json()]
109
110    def delete_key(self, vendor_key_id: str) -> bool:
111        resp = self._http.request("DELETE", f"/vendor/keys/{vendor_key_id}")
112        _raise_for_status(resp)
113        return bool(resp.json().get("deleted"))
114
115    def campaigns(self, *, offset: int = 0, limit: int = 50) -> PagedList[EvalCampaignState]:
116        """The vendor's own campaigns (sandbox + self-reported), newest
117        first — one page, with the total (``limit`` at most 200)."""
118        resp = self._http.request(
119            "GET", "/vendor/campaigns", params={"offset": offset, "limit": limit}
120        )
121        _raise_for_status(resp)
122        return _extract_items(resp.json(), EvalCampaignState)
123
124    def submit_campaign(
125        self,
126        *,
127        model_id: str,
128        standard_set_id: str,
129        name: str = "",
130        num_samples: int = 1,
131        max_cost_usd: float = 0.0,
132        timeout_s: int = 0,
133        max_turns: int = 0,
134        publish: bool = False,
135    ) -> EvalCampaignState:
136        """Self-trigger a batch over an operator standard set on the vendor's
137        own key. ``publish=False`` → a private ``sandbox`` run; ``publish=True``
138        → a public but unverified ``self_reported`` run (requires an approved
139        model). Requires a ``submit``-scoped token + an uploaded key for the
140        model's flavor. Poll :meth:`campaigns` for progress."""
141        resp = self._http.request(
142            "POST",
143            "/vendor/campaigns",
144            json={
145                "model_id": model_id,
146                "standard_set_id": standard_set_id,
147                "name": name,
148                "num_samples": num_samples,
149                "max_cost_usd": max_cost_usd,
150                "timeout_s": timeout_s,
151                "max_turns": max_turns,
152                "publish": publish,
153            },
154        )
155        _raise_for_status(resp)
156        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 register_model( self, *, id: str, flavor: str, display_name: str = '', provider_model: str = '', base_url: str = '', notes: str = '') -> ctfy.core.state.models.LlmModelState:
64    def register_model(
65        self,
66        *,
67        id: str,
68        flavor: str,
69        display_name: str = "",
70        provider_model: str = "",
71        base_url: str = "",
72        notes: str = "",
73    ) -> LlmModelState:
74        """Self-register (or edit) one of the vendor's own models. Requires a
75        ``submit``-scoped ``pv_`` token. The model lands ``pending`` review —
76        an operator must approve it before it can run official campaigns."""
77        resp = self._http.request(
78            "POST",
79            "/vendor/models",
80            json={
81                "id": id,
82                "flavor": flavor,
83                "display_name": display_name,
84                "provider_model": provider_model,
85                "base_url": base_url,
86                "notes": notes,
87            },
88        )
89        _raise_for_status(resp)
90        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:
 92    def upload_key(self, *, flavor: str, api_key: str, label: str = "") -> VendorKeyState:
 93        """Upload (or replace) the vendor's model API key for a flavor. Requires
 94        a ``submit``-scoped token. The key is encrypted at rest; the returned
 95        row carries only metadata (never the ciphertext)."""
 96        resp = self._http.request(
 97            "POST",
 98            "/vendor/keys",
 99            json={"flavor": flavor, "api_key": api_key, "label": label},
100        )
101        _raise_for_status(resp)
102        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]:
104    def keys(self) -> builtins.list[VendorKeyState]:
105        """List the vendor's uploaded key metadata (ciphertext blanked)."""
106        resp = self._http.request("GET", "/vendor/keys")
107        _raise_for_status(resp)
108        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:
110    def delete_key(self, vendor_key_id: str) -> bool:
111        resp = self._http.request("DELETE", f"/vendor/keys/{vendor_key_id}")
112        _raise_for_status(resp)
113        return bool(resp.json().get("deleted"))
def campaigns( self, *, offset: int = 0, limit: int = 50) -> ctfy.sdk._helpers.PagedList[ctfy.core.state.models.EvalCampaignState]:
115    def campaigns(self, *, offset: int = 0, limit: int = 50) -> PagedList[EvalCampaignState]:
116        """The vendor's own campaigns (sandbox + self-reported), newest
117        first — one page, with the total (``limit`` at most 200)."""
118        resp = self._http.request(
119            "GET", "/vendor/campaigns", params={"offset": offset, "limit": limit}
120        )
121        _raise_for_status(resp)
122        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:
124    def submit_campaign(
125        self,
126        *,
127        model_id: str,
128        standard_set_id: str,
129        name: str = "",
130        num_samples: int = 1,
131        max_cost_usd: float = 0.0,
132        timeout_s: int = 0,
133        max_turns: int = 0,
134        publish: bool = False,
135    ) -> EvalCampaignState:
136        """Self-trigger a batch over an operator standard set on the vendor's
137        own key. ``publish=False`` → a private ``sandbox`` run; ``publish=True``
138        → a public but unverified ``self_reported`` run (requires an approved
139        model). Requires a ``submit``-scoped token + an uploaded key for the
140        model's flavor. Poll :meth:`campaigns` for progress."""
141        resp = self._http.request(
142            "POST",
143            "/vendor/campaigns",
144            json={
145                "model_id": model_id,
146                "standard_set_id": standard_set_id,
147                "name": name,
148                "num_samples": num_samples,
149                "max_cost_usd": max_cost_usd,
150                "timeout_s": timeout_s,
151                "max_turns": max_turns,
152                "publish": publish,
153            },
154        )
155        _raise_for_status(resp)
156        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.