Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand Down
1 change: 0 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
13 changes: 0 additions & 13 deletions pipelex_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@
BillingPortalResponse,
ChangePlanResponse,
CheckoutResponse,
GatewayApiKey,
GatewayApiKeyStatus,
InvoiceView,
Membership,
MembershipsResponse,
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 1 addition & 22 deletions pipelex_sdk/product_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`) ─────────────────────────────────


Expand Down
22 changes: 0 additions & 22 deletions tests/unit/test_client_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading