diff --git a/CHANGELOG.md b/CHANGELOG.md index 0519ca0..f3e9eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,10 @@ - **The blocking path stops dropping the executed graph.** Against a bare runner, `start_and_wait` now lifts `pipe_output.graph_spec` onto `RunResults.graph_spec` instead of writing `None` — the runner has always returned the graph there — so the field carries the same document whichever path ran. - **A stored source nested too deeply to decode now fails as a `ValidationError`**: `parse_method_files` converts the JSON decoder's `RecursionError` into the `ValueError` its contract documents, so `MethodData`'s validator surfaces it as a `pydantic.ValidationError` like any other malformed response body instead of letting a bare `RecursionError` escape `get_method` past a caller's `except ValidationError`. +### Removed + +- **The Pipelex Gateway inference key is gone from the client (Breaking)**: `create_gateway_api_key` and `get_gateway_api_key`, with the `GatewayApiKey` and `GatewayApiKeyStatus` models, are removed — the `POST` and `GET /v1/gateway-api-key` routes behind them no longer exist on the hosted API. A caller brings its own provider keys, or runs against the hosted API with a Pipelex API key (`list_pipelex_api_keys` and friends, which are untouched). This has nothing to do with the hosted HTTP gateway's synchronous-execute ceiling, which is unchanged. + ## [v0.10.0] - 2026-09-13 ### Added diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 05248fd..de524c6 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -35,7 +35,7 @@ Source of truth for the Python lifecycle code being moved: `mthds-python/mthds/r - methods catalog CRUD `/v1/methods`, `getMe` `/v1/me` - organizations `/v1/organizations/*` - billing `/v1/billing/*` -- API keys `/v1/pipelex-api-keys/*`, `/v1/gateway-api-key` +- API keys `/v1/pipelex-api-keys/*` - onboarding `/v1/onboarding/submit` - storage `/v1/resolve-storage-url`, `/v1/upload` - run records `/v1/runs` (list, by method_id), `PUT /v1/runs/{id}` diff --git a/docs/architecture.md b/docs/architecture.md index d8ae152..6157bb6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -244,7 +244,6 @@ The wire models are snake_case Pydantic v2. Response models are extension-open ( - **Organizations** — `list_memberships()` → `MembershipsResponse` (memberships + active-org feature flags); `create_organization(name)` / `rename_organization(org_id, name)` → `Membership`. Organization *switch* is out of scope (a WorkOS session op, not a `/v1` route). - **Billing** — `get_subscription()`, `list_plans()`, `list_invoices()`, `create_checkout(plan)`. `change_plan(plan)` and `get_billing_portal()` surface a **409 `conflict`** (`ApiResponseError.code`) when there is no subscription yet — start one via `create_checkout` first. - **Pipelex API keys** — `list_pipelex_api_keys()`; `create_pipelex_api_key(label)` and `rotate_pipelex_api_key(id)` return the plaintext `api_key` **once**; `revoke_pipelex_api_key(id)`. Creation surfaces a **409 `pipelex_api_key_limit_reached`** when the per-account limit is hit. Rotation sends no body. -- **Gateway (LLM inference) key** — `create_gateway_api_key(promo_code)` **always sends a JSON body** (even with `promo_code=None` → `{"promo_code": null}`); the server 422s an empty body. `get_gateway_api_key()` → status (`gateway_api_key` is `None` until provisioned). - **Onboarding** — `submit_onboarding(OnboardingSubmission)` (`POST /v1/onboarding/submit`, empty 2xx body); absent optional fields are dropped. - **Storage** — `resolve_storage_url(uri)` → presigned URL for one reference; `resolve_storage_urls_bulk(uris)` → one verdict per reference through `POST /v1/resolve-storage-url/bulk`, the route the artifact stack is built on; `upload(UploadInput)` → the stored file handle. The higher-level `upload_file` / `prepare_inputs` preparation surface built on top of `upload` is now available — see [input-preparation.md](./input-preparation.md) — and its download twin is the artifact stack below. - **Run records** — `list_runs(method_id, …)` / `iterate_runs(method_id, …)` / `get_run_detail(run_id)` (the catalog-style reads, distinct from the lifecycle status/result routes); `update_run(run_id, UpdateRunInput)` (admin/manual status patch, empty 2xx body). diff --git a/pipelex_sdk/client.py b/pipelex_sdk/client.py index d053c6a..e27579b 100644 --- a/pipelex_sdk/client.py +++ b/pipelex_sdk/client.py @@ -69,8 +69,6 @@ BillingPortalResponse, ChangePlanResponse, CheckoutResponse, - GatewayApiKey, - GatewayApiKeyStatus, InvoiceView, Membership, MembershipsResponse, @@ -1123,17 +1121,6 @@ async def rotate_pipelex_api_key(self, key_id: str) -> PipelexApiKeyCreated: """ return PipelexApiKeyCreated.model_validate(await self._request_product("POST", f"pipelex-api-keys/{quote(key_id, safe='')}/rotate")) - async def create_gateway_api_key(self, promo_code: str | None) -> GatewayApiKey: - """Provision the gateway (LLM inference) API key — `POST /v1/gateway-api-key`. - - The JSON body is ALWAYS sent (even with `promo_code=None`) — the server 422s an empty body. - """ - return GatewayApiKey.model_validate(await self._request_product("POST", "gateway-api-key", body={"promo_code": promo_code})) - - async def get_gateway_api_key(self) -> GatewayApiKeyStatus: - """The gateway key status (`None` until provisioned) — `GET /v1/gateway-api-key`.""" - return GatewayApiKeyStatus.model_validate(await self._request_product("GET", "gateway-api-key")) - async def submit_onboarding(self, submission: OnboardingSubmission) -> None: """Submit the onboarding questionnaire — `POST /v1/onboarding/submit` (empty body).""" body = submission.model_dump(mode="json", exclude_none=True) diff --git a/pipelex_sdk/product_models.py b/pipelex_sdk/product_models.py index b53ffc6..eb0f5cc 100644 --- a/pipelex_sdk/product_models.py +++ b/pipelex_sdk/product_models.py @@ -2,7 +2,7 @@ These mirror `pipelex-sdk-js/src/product-models.ts`. They are the management surface the hosted product (`/v1/me`, `/v1/methods`, `/v1/organizations`, `/v1/billing/*`, -`/v1/pipelex-api-keys`, `/v1/gateway-api-key`, `/v1/onboarding/submit`, +`/v1/pipelex-api-keys`, `/v1/onboarding/submit`, `/v1/resolve-storage-url`, `/v1/upload`, `/v1/runs`) drives. The wire is snake_case. Each model holds only the fields the product actually @@ -494,27 +494,6 @@ class PipelexApiKeyList(BaseModel): keys: list[PipelexApiKey] -# ── Gateway API key (`/v1/gateway-api-key`, Portkey/LLM inference key) ──── - - -class GatewayApiKey(BaseModel): - """The provisioned gateway (LLM inference) API key — `POST /v1/gateway-api-key`.""" - - model_config = ConfigDict(extra="allow") - - gateway_api_key: str - budget_usd: float | None = None - - -class GatewayApiKeyStatus(BaseModel): - """The gateway key status — `GET /v1/gateway-api-key`.""" - - model_config = ConfigDict(extra="allow") - - #: None until a gateway key has been provisioned. - gateway_api_key: str | None - - # ── Onboarding (`/v1/onboarding/submit`) ───────────────────────────────── diff --git a/tests/unit/test_client_product.py b/tests/unit/test_client_product.py index 82b5cc9..9b4a19e 100644 --- a/tests/unit/test_client_product.py +++ b/tests/unit/test_client_product.py @@ -374,28 +374,6 @@ def test_rotate_pipelex_api_key_sends_no_body(self, mocker: MockerFixture) -> No assert sent.method == "POST" assert sent.body is None - # ── Gateway API key ────────────────────────────────────────────── - - def test_create_gateway_api_key_always_sends_body_even_when_promo_none(self, mocker: MockerFixture) -> None: - client = self._client() - send = self._mock_send(mocker, client, _response(200, json_body={"gateway_api_key": "gw"})) - - asyncio.run(client.create_gateway_api_key(None)) - - sent = self._sent(send) - assert sent.method == "POST" - assert sent.url == f"{_BASE_URL}/v1/gateway-api-key" - assert sent.body == {"promo_code": None} - - def test_get_gateway_api_key_null_until_provisioned(self, mocker: MockerFixture) -> None: - client = self._client() - send = self._mock_send(mocker, client, _response(200, json_body={"gateway_api_key": None})) - - result = asyncio.run(client.get_gateway_api_key()) - - assert self._sent(send).url == f"{_BASE_URL}/v1/gateway-api-key" - assert result.gateway_api_key is None - # ── Onboarding ─────────────────────────────────────────────────── def test_submit_onboarding_drops_absent_optionals(self, mocker: MockerFixture) -> None: