diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index c658eefef..3e9af1b3a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.3.2"
+ ".": "1.4.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index f28b394ab..e2f90d495 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 106
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-63dab7833d6670810c4f4882df560ebbfe2de8e8e1a98d51422368607b5335ae.yml
-openapi_spec_hash: ebb5068064f7469f9239b18a51a6fe44
-config_hash: fd168de77f219e46a1427bbec2eecfb9
+configured_endpoints: 111
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-84e997ca5716b9378a58a1bdf3d6616cf3be80156a6aaed1bed469fe93ba2c95.yml
+openapi_spec_hash: b44a4ba1c2c3cb775c14545f2bab05a8
+config_hash: 22f65246be4646c23dde9f69f51252e7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97b063ea7..6adca673a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## 1.4.0 (2026-01-30)
+
+Full Changelog: [v1.3.2...v1.4.0](https://github.com/runloopai/api-client-python/compare/v1.3.2...v1.4.0)
+
+### Features
+
+* **client:** add custom JSON encoder for extended type support ([840addd](https://github.com/runloopai/api-client-python/commit/840addd8884fd93ede0c9bbc2a16cf399d6372d0))
+* **devbox:** add gateway routes ([#7212](https://github.com/runloopai/api-client-python/issues/7212)) ([2527bb7](https://github.com/runloopai/api-client-python/commit/2527bb7714f9ce9114964286bfbb35582ceaa976))
+* **devbox:** add new tunnel APIs and deprecate old tunnel API ([#7227](https://github.com/runloopai/api-client-python/issues/7227)) ([71620a1](https://github.com/runloopai/api-client-python/commit/71620a114080506898864b6d73378d4ff30b1070))
+
+
+### Chores
+
+* **devbox:** deprecate remove tunnel API ([#7230](https://github.com/runloopai/api-client-python/issues/7230)) ([d48832d](https://github.com/runloopai/api-client-python/commit/d48832d0aa076bd621decae3f5dadca7141c29d3))
+* **documentation:** made warning message language more accurate ([#7215](https://github.com/runloopai/api-client-python/issues/7215)) ([08e0586](https://github.com/runloopai/api-client-python/commit/08e058660cf02e67701899e3b46173c5e9011b7d))
+
## 1.3.2 (2026-01-30)
Full Changelog: [v1.3.1...v1.3.2](https://github.com/runloopai/api-client-python/compare/v1.3.1...v1.3.2)
diff --git a/api.md b/api.md
index 45d93e973..44ba928b2 100644
--- a/api.md
+++ b/api.md
@@ -397,3 +397,24 @@ Methods:
- client.network_policies.update(id, \*\*params) -> NetworkPolicyView
- client.network_policies.list(\*\*params) -> SyncNetworkPoliciesCursorIDPage[NetworkPolicyView]
- client.network_policies.delete(id) -> NetworkPolicyView
+
+# GatewayConfigs
+
+Types:
+
+```python
+from runloop_api_client.types import (
+ GatewayConfigCreateParameters,
+ GatewayConfigListView,
+ GatewayConfigUpdateParameters,
+ GatewayConfigView,
+)
+```
+
+Methods:
+
+- client.gateway_configs.create(\*\*params) -> GatewayConfigView
+- client.gateway_configs.retrieve(id) -> GatewayConfigView
+- client.gateway_configs.update(id, \*\*params) -> GatewayConfigView
+- client.gateway_configs.list(\*\*params) -> SyncGatewayConfigsCursorIDPage[GatewayConfigView]
+- client.gateway_configs.delete(id) -> GatewayConfigView
diff --git a/pyproject.toml b/pyproject.toml
index f62c63eb8..2879cccda 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "runloop_api_client"
-version = "1.3.2"
+version = "1.4.0"
description = "The official Python library for the runloop API"
dynamic = ["readme"]
license = "MIT"
diff --git a/src/runloop_api_client/_base_client.py b/src/runloop_api_client/_base_client.py
index bd2a51553..945a6d520 100644
--- a/src/runloop_api_client/_base_client.py
+++ b/src/runloop_api_client/_base_client.py
@@ -86,6 +86,7 @@
APIConnectionError,
APIResponseValidationError,
)
+from ._utils._json import openapi_dumps
log: logging.Logger = logging.getLogger(__name__)
@@ -554,8 +555,10 @@ def _build_request(
kwargs["content"] = options.content
elif isinstance(json_data, bytes):
kwargs["content"] = json_data
- else:
- kwargs["json"] = json_data if is_given(json_data) else None
+ elif not files:
+ # Don't set content when JSON is sent as multipart/form-data,
+ # since httpx's content param overrides other body arguments
+ kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
diff --git a/src/runloop_api_client/_client.py b/src/runloop_api_client/_client.py
index 3b469e7ec..32561bc0f 100644
--- a/src/runloop_api_client/_client.py
+++ b/src/runloop_api_client/_client.py
@@ -42,6 +42,7 @@
repositories,
benchmark_jobs,
benchmark_runs,
+ gateway_configs,
network_policies,
)
from .resources.agents import AgentsResource, AsyncAgentsResource
@@ -52,6 +53,7 @@
from .resources.repositories import RepositoriesResource, AsyncRepositoriesResource
from .resources.benchmark_jobs import BenchmarkJobsResource, AsyncBenchmarkJobsResource
from .resources.benchmark_runs import BenchmarkRunsResource, AsyncBenchmarkRunsResource
+ from .resources.gateway_configs import GatewayConfigsResource, AsyncGatewayConfigsResource
from .resources.network_policies import NetworkPoliciesResource, AsyncNetworkPoliciesResource
from .resources.devboxes.devboxes import DevboxesResource, AsyncDevboxesResource
from .resources.scenarios.scenarios import ScenariosResource, AsyncScenariosResource
@@ -182,6 +184,12 @@ def network_policies(self) -> NetworkPoliciesResource:
return NetworkPoliciesResource(self)
+ @cached_property
+ def gateway_configs(self) -> GatewayConfigsResource:
+ from .resources.gateway_configs import GatewayConfigsResource
+
+ return GatewayConfigsResource(self)
+
@cached_property
def with_raw_response(self) -> RunloopWithRawResponse:
return RunloopWithRawResponse(self)
@@ -418,6 +426,12 @@ def network_policies(self) -> AsyncNetworkPoliciesResource:
return AsyncNetworkPoliciesResource(self)
+ @cached_property
+ def gateway_configs(self) -> AsyncGatewayConfigsResource:
+ from .resources.gateway_configs import AsyncGatewayConfigsResource
+
+ return AsyncGatewayConfigsResource(self)
+
@cached_property
def with_raw_response(self) -> AsyncRunloopWithRawResponse:
return AsyncRunloopWithRawResponse(self)
@@ -603,6 +617,12 @@ def network_policies(self) -> network_policies.NetworkPoliciesResourceWithRawRes
return NetworkPoliciesResourceWithRawResponse(self._client.network_policies)
+ @cached_property
+ def gateway_configs(self) -> gateway_configs.GatewayConfigsResourceWithRawResponse:
+ from .resources.gateway_configs import GatewayConfigsResourceWithRawResponse
+
+ return GatewayConfigsResourceWithRawResponse(self._client.gateway_configs)
+
class AsyncRunloopWithRawResponse:
_client: AsyncRunloop
@@ -676,6 +696,12 @@ def network_policies(self) -> network_policies.AsyncNetworkPoliciesResourceWithR
return AsyncNetworkPoliciesResourceWithRawResponse(self._client.network_policies)
+ @cached_property
+ def gateway_configs(self) -> gateway_configs.AsyncGatewayConfigsResourceWithRawResponse:
+ from .resources.gateway_configs import AsyncGatewayConfigsResourceWithRawResponse
+
+ return AsyncGatewayConfigsResourceWithRawResponse(self._client.gateway_configs)
+
class RunloopWithStreamedResponse:
_client: Runloop
@@ -749,6 +775,12 @@ def network_policies(self) -> network_policies.NetworkPoliciesResourceWithStream
return NetworkPoliciesResourceWithStreamingResponse(self._client.network_policies)
+ @cached_property
+ def gateway_configs(self) -> gateway_configs.GatewayConfigsResourceWithStreamingResponse:
+ from .resources.gateway_configs import GatewayConfigsResourceWithStreamingResponse
+
+ return GatewayConfigsResourceWithStreamingResponse(self._client.gateway_configs)
+
class AsyncRunloopWithStreamedResponse:
_client: AsyncRunloop
@@ -822,6 +854,12 @@ def network_policies(self) -> network_policies.AsyncNetworkPoliciesResourceWithS
return AsyncNetworkPoliciesResourceWithStreamingResponse(self._client.network_policies)
+ @cached_property
+ def gateway_configs(self) -> gateway_configs.AsyncGatewayConfigsResourceWithStreamingResponse:
+ from .resources.gateway_configs import AsyncGatewayConfigsResourceWithStreamingResponse
+
+ return AsyncGatewayConfigsResourceWithStreamingResponse(self._client.gateway_configs)
+
Client = Runloop
diff --git a/src/runloop_api_client/_compat.py b/src/runloop_api_client/_compat.py
index bdef67f04..786ff42ad 100644
--- a/src/runloop_api_client/_compat.py
+++ b/src/runloop_api_client/_compat.py
@@ -139,6 +139,7 @@ def model_dump(
exclude_defaults: bool = False,
warnings: bool = True,
mode: Literal["json", "python"] = "python",
+ by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
return model.model_dump(
@@ -148,13 +149,12 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
+ by_alias=by_alias,
)
return cast(
"dict[str, Any]",
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
- exclude=exclude,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
+ exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
),
)
diff --git a/src/runloop_api_client/_utils/_json.py b/src/runloop_api_client/_utils/_json.py
new file mode 100644
index 000000000..60584214a
--- /dev/null
+++ b/src/runloop_api_client/_utils/_json.py
@@ -0,0 +1,35 @@
+import json
+from typing import Any
+from datetime import datetime
+from typing_extensions import override
+
+import pydantic
+
+from .._compat import model_dump
+
+
+def openapi_dumps(obj: Any) -> bytes:
+ """
+ Serialize an object to UTF-8 encoded JSON bytes.
+
+ Extends the standard json.dumps with support for additional types
+ commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
+ """
+ return json.dumps(
+ obj,
+ cls=_CustomEncoder,
+ # Uses the same defaults as httpx's JSON serialization
+ ensure_ascii=False,
+ separators=(",", ":"),
+ allow_nan=False,
+ ).encode()
+
+
+class _CustomEncoder(json.JSONEncoder):
+ @override
+ def default(self, o: Any) -> Any:
+ if isinstance(o, datetime):
+ return o.isoformat()
+ if isinstance(o, pydantic.BaseModel):
+ return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
+ return super().default(o)
diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py
index 521e1d656..0d6b7e275 100644
--- a/src/runloop_api_client/_version.py
+++ b/src/runloop_api_client/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "runloop_api_client"
-__version__ = "1.3.2" # x-release-please-version
+__version__ = "1.4.0" # x-release-please-version
diff --git a/src/runloop_api_client/pagination.py b/src/runloop_api_client/pagination.py
index c6e696214..e937259d7 100644
--- a/src/runloop_api_client/pagination.py
+++ b/src/runloop_api_client/pagination.py
@@ -30,6 +30,8 @@
"AsyncObjectsCursorIDPage",
"SyncNetworkPoliciesCursorIDPage",
"AsyncNetworkPoliciesCursorIDPage",
+ "SyncGatewayConfigsCursorIDPage",
+ "AsyncGatewayConfigsCursorIDPage",
]
_T = TypeVar("_T")
@@ -95,6 +97,11 @@ class NetworkPoliciesCursorIDPageItem(Protocol):
id: str
+@runtime_checkable
+class GatewayConfigsCursorIDPageItem(Protocol):
+ id: str
+
+
class SyncBlueprintsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
blueprints: List[_T]
has_more: Optional[bool] = None
@@ -909,3 +916,71 @@ def next_page_info(self) -> Optional[PageInfo]:
return None
return PageInfo(params={"starting_after": item.id})
+
+
+class SyncGatewayConfigsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+ gateway_configs: List[_T]
+ has_more: Optional[bool] = None
+ total_count: Optional[int] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ gateway_configs = self.gateway_configs
+ if not gateway_configs:
+ return []
+ return gateway_configs
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ gateway_configs = self.gateway_configs
+ if not gateway_configs:
+ return None
+
+ item = cast(Any, gateway_configs[-1])
+ if not isinstance(item, GatewayConfigsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison]
+ # TODO emit warning log
+ return None
+
+ return PageInfo(params={"starting_after": item.id})
+
+
+class AsyncGatewayConfigsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+ gateway_configs: List[_T]
+ has_more: Optional[bool] = None
+ total_count: Optional[int] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ gateway_configs = self.gateway_configs
+ if not gateway_configs:
+ return []
+ return gateway_configs
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ gateway_configs = self.gateway_configs
+ if not gateway_configs:
+ return None
+
+ item = cast(Any, gateway_configs[-1])
+ if not isinstance(item, GatewayConfigsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison]
+ # TODO emit warning log
+ return None
+
+ return PageInfo(params={"starting_after": item.id})
diff --git a/src/runloop_api_client/resources/__init__.py b/src/runloop_api_client/resources/__init__.py
index 325158535..877822444 100644
--- a/src/runloop_api_client/resources/__init__.py
+++ b/src/runloop_api_client/resources/__init__.py
@@ -80,6 +80,14 @@
BenchmarkRunsResourceWithStreamingResponse,
AsyncBenchmarkRunsResourceWithStreamingResponse,
)
+from .gateway_configs import (
+ GatewayConfigsResource,
+ AsyncGatewayConfigsResource,
+ GatewayConfigsResourceWithRawResponse,
+ AsyncGatewayConfigsResourceWithRawResponse,
+ GatewayConfigsResourceWithStreamingResponse,
+ AsyncGatewayConfigsResourceWithStreamingResponse,
+)
from .network_policies import (
NetworkPoliciesResource,
AsyncNetworkPoliciesResource,
@@ -156,4 +164,10 @@
"AsyncNetworkPoliciesResourceWithRawResponse",
"NetworkPoliciesResourceWithStreamingResponse",
"AsyncNetworkPoliciesResourceWithStreamingResponse",
+ "GatewayConfigsResource",
+ "AsyncGatewayConfigsResource",
+ "GatewayConfigsResourceWithRawResponse",
+ "AsyncGatewayConfigsResourceWithRawResponse",
+ "GatewayConfigsResourceWithStreamingResponse",
+ "AsyncGatewayConfigsResourceWithStreamingResponse",
]
diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py
index 429918891..ea563fa59 100644
--- a/src/runloop_api_client/resources/devboxes/devboxes.py
+++ b/src/runloop_api_client/resources/devboxes/devboxes.py
@@ -185,6 +185,7 @@ def create(
entrypoint: Optional[str] | Omit = omit,
environment_variables: Optional[Dict[str, str]] | Omit = omit,
file_mounts: Optional[Dict[str, str]] | Omit = omit,
+ gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit,
launch_parameters: Optional[LaunchParameters] | Omit = omit,
metadata: Optional[Dict[str, str]] | Omit = omit,
mounts: Optional[Iterable[Mount]] | Omit = omit,
@@ -192,6 +193,7 @@ def create(
repo_connection_id: Optional[str] | Omit = omit,
secrets: Optional[Dict[str, str]] | Omit = omit,
snapshot_id: Optional[str] | Omit = omit,
+ tunnel: Optional[devbox_create_params.Tunnel] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -227,6 +229,12 @@ def create(
file_mounts: Map of paths and file contents to write before setup. Use mounts instead.
+ gateways: [Beta] (Optional) Gateway specifications for credential proxying. Map key is the
+ environment variable prefix (e.g., 'GWS_ANTHROPIC'). The gateway will proxy
+ requests to external APIs using the specified credential without exposing the
+ real API key. Example: {'GWS_ANTHROPIC': {'gateway': 'anthropic', 'secret':
+ 'my_claude_key'}}
+
launch_parameters: Parameters to configure the resources and launch time behavior of the Devbox.
metadata: User defined metadata to attach to the devbox for organization.
@@ -245,6 +253,10 @@ def create(
snapshot_id: Snapshot ID to use for the Devbox. Only one of (Snapshot ID, Blueprint ID,
Blueprint name) should be specified.
+ tunnel: (Optional) Configuration for creating a V2 tunnel at Devbox launch time. When
+ specified, a tunnel will be automatically provisioned and the tunnel details
+ will be included in the Devbox response.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -265,6 +277,7 @@ def create(
"entrypoint": entrypoint,
"environment_variables": environment_variables,
"file_mounts": file_mounts,
+ "gateways": gateways,
"launch_parameters": launch_parameters,
"metadata": metadata,
"mounts": mounts,
@@ -272,6 +285,7 @@ def create(
"repo_connection_id": repo_connection_id,
"secrets": secrets,
"snapshot_id": snapshot_id,
+ "tunnel": tunnel,
},
devbox_create_params.DevboxCreateParams,
),
@@ -480,6 +494,7 @@ def create_and_await_running(
entrypoint: Optional[str] | Omit = omit,
environment_variables: Optional[Dict[str, str]] | Omit = omit,
file_mounts: Optional[Dict[str, str]] | Omit = omit,
+ gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit,
launch_parameters: Optional[LaunchParameters] | Omit = omit,
metadata: Optional[Dict[str, str]] | Omit = omit,
mounts: Optional[Iterable[Mount]] | Omit = omit,
@@ -488,6 +503,7 @@ def create_and_await_running(
repo_connection_id: Optional[str] | Omit = omit,
secrets: Optional[Dict[str, str]] | Omit = omit,
snapshot_id: Optional[str] | Omit = omit,
+ tunnel: Optional[devbox_create_params.Tunnel] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -519,6 +535,7 @@ def create_and_await_running(
entrypoint=entrypoint,
environment_variables=environment_variables,
file_mounts=file_mounts,
+ gateways=gateways,
launch_parameters=launch_parameters,
metadata=metadata,
mounts=mounts,
@@ -526,6 +543,7 @@ def create_and_await_running(
repo_connection_id=repo_connection_id,
secrets=secrets,
snapshot_id=snapshot_id,
+ tunnel=tunnel,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
@@ -632,6 +650,7 @@ def create_ssh_key(
cast_to=DevboxCreateSSHKeyResponse,
)
+ @typing_extensions.deprecated("deprecated")
def create_tunnel(
self,
id: str,
@@ -645,8 +664,11 @@ def create_tunnel(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
idempotency_key: str | None = None,
) -> DevboxTunnelView:
- """
- Create a live tunnel to an available port on the Devbox.
+ """[Deprecated] Use POST /v1/devboxes/{id}/enable_tunnel instead.
+
+ This endpoint
+ creates a legacy tunnel. The new enable_tunnel endpoint provides improved tunnel
+ functionality with authentication options.
Args:
port: Devbox port that tunnel will expose.
@@ -1197,6 +1219,7 @@ def read_file_contents(
cast_to=str,
)
+ @typing_extensions.deprecated("deprecated")
def remove_tunnel(
self,
id: str,
@@ -1210,8 +1233,10 @@ def remove_tunnel(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
idempotency_key: str | None = None,
) -> object:
- """
- Remove a previously opened tunnel on the Devbox.
+ """[Deprecated] Tunnels remain active until devbox is shutdown.
+
+ This endpoint
+ removes a legacy tunnel.
Args:
port: Devbox port that tunnel will expose.
@@ -1723,6 +1748,7 @@ async def create(
entrypoint: Optional[str] | Omit = omit,
environment_variables: Optional[Dict[str, str]] | Omit = omit,
file_mounts: Optional[Dict[str, str]] | Omit = omit,
+ gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit,
launch_parameters: Optional[LaunchParameters] | Omit = omit,
metadata: Optional[Dict[str, str]] | Omit = omit,
mounts: Optional[Iterable[Mount]] | Omit = omit,
@@ -1730,6 +1756,7 @@ async def create(
repo_connection_id: Optional[str] | Omit = omit,
secrets: Optional[Dict[str, str]] | Omit = omit,
snapshot_id: Optional[str] | Omit = omit,
+ tunnel: Optional[devbox_create_params.Tunnel] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -1765,6 +1792,12 @@ async def create(
file_mounts: Map of paths and file contents to write before setup. Use mounts instead.
+ gateways: [Beta] (Optional) Gateway specifications for credential proxying. Map key is the
+ environment variable prefix (e.g., 'GWS_ANTHROPIC'). The gateway will proxy
+ requests to external APIs using the specified credential without exposing the
+ real API key. Example: {'GWS_ANTHROPIC': {'gateway': 'anthropic', 'secret':
+ 'my_claude_key'}}
+
launch_parameters: Parameters to configure the resources and launch time behavior of the Devbox.
metadata: User defined metadata to attach to the devbox for organization.
@@ -1783,6 +1816,10 @@ async def create(
snapshot_id: Snapshot ID to use for the Devbox. Only one of (Snapshot ID, Blueprint ID,
Blueprint name) should be specified.
+ tunnel: (Optional) Configuration for creating a V2 tunnel at Devbox launch time. When
+ specified, a tunnel will be automatically provisioned and the tunnel details
+ will be included in the Devbox response.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1803,6 +1840,7 @@ async def create(
"entrypoint": entrypoint,
"environment_variables": environment_variables,
"file_mounts": file_mounts,
+ "gateways": gateways,
"launch_parameters": launch_parameters,
"metadata": metadata,
"mounts": mounts,
@@ -1810,6 +1848,7 @@ async def create(
"repo_connection_id": repo_connection_id,
"secrets": secrets,
"snapshot_id": snapshot_id,
+ "tunnel": tunnel,
},
devbox_create_params.DevboxCreateParams,
),
@@ -1865,6 +1904,7 @@ async def create_and_await_running(
entrypoint: Optional[str] | Omit = omit,
environment_variables: Optional[Dict[str, str]] | Omit = omit,
file_mounts: Optional[Dict[str, str]] | Omit = omit,
+ gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit,
launch_parameters: Optional[LaunchParameters] | Omit = omit,
metadata: Optional[Dict[str, str]] | Omit = omit,
mounts: Optional[Iterable[Mount]] | Omit = omit,
@@ -1873,6 +1913,7 @@ async def create_and_await_running(
repo_connection_id: Optional[str] | Omit = omit,
secrets: Optional[Dict[str, str]] | Omit = omit,
snapshot_id: Optional[str] | Omit = omit,
+ tunnel: Optional[devbox_create_params.Tunnel] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -1905,6 +1946,7 @@ async def create_and_await_running(
entrypoint=entrypoint,
environment_variables=environment_variables,
file_mounts=file_mounts,
+ gateways=gateways,
launch_parameters=launch_parameters,
metadata=metadata,
mounts=mounts,
@@ -1912,6 +1954,7 @@ async def create_and_await_running(
repo_connection_id=repo_connection_id,
secrets=secrets,
snapshot_id=snapshot_id,
+ tunnel=tunnel,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
@@ -2167,6 +2210,7 @@ async def create_ssh_key(
cast_to=DevboxCreateSSHKeyResponse,
)
+ @typing_extensions.deprecated("deprecated")
async def create_tunnel(
self,
id: str,
@@ -2180,8 +2224,11 @@ async def create_tunnel(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
idempotency_key: str | None = None,
) -> DevboxTunnelView:
- """
- Create a live tunnel to an available port on the Devbox.
+ """[Deprecated] Use POST /v1/devboxes/{id}/enable_tunnel instead.
+
+ This endpoint
+ creates a legacy tunnel. The new enable_tunnel endpoint provides improved tunnel
+ functionality with authentication options.
Args:
port: Devbox port that tunnel will expose.
@@ -2733,6 +2780,7 @@ async def read_file_contents(
cast_to=str,
)
+ @typing_extensions.deprecated("deprecated")
async def remove_tunnel(
self,
id: str,
@@ -2746,8 +2794,10 @@ async def remove_tunnel(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
idempotency_key: str | None = None,
) -> object:
- """
- Remove a previously opened tunnel on the Devbox.
+ """[Deprecated] Tunnels remain active until devbox is shutdown.
+
+ This endpoint
+ removes a legacy tunnel.
Args:
port: Devbox port that tunnel will expose.
@@ -3231,8 +3281,10 @@ def __init__(self, devboxes: DevboxesResource) -> None:
self.create_ssh_key = to_raw_response_wrapper(
devboxes.create_ssh_key,
)
- self.create_tunnel = to_raw_response_wrapper(
- devboxes.create_tunnel,
+ self.create_tunnel = ( # pyright: ignore[reportDeprecated]
+ to_raw_response_wrapper(
+ devboxes.create_tunnel, # pyright: ignore[reportDeprecated],
+ )
)
self.delete_disk_snapshot = to_raw_response_wrapper(
devboxes.delete_disk_snapshot,
@@ -3261,8 +3313,10 @@ def __init__(self, devboxes: DevboxesResource) -> None:
self.read_file_contents = to_raw_response_wrapper(
devboxes.read_file_contents,
)
- self.remove_tunnel = to_raw_response_wrapper(
- devboxes.remove_tunnel,
+ self.remove_tunnel = ( # pyright: ignore[reportDeprecated]
+ to_raw_response_wrapper(
+ devboxes.remove_tunnel, # pyright: ignore[reportDeprecated],
+ )
)
self.resume = to_raw_response_wrapper(
devboxes.resume,
@@ -3329,8 +3383,10 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None:
self.create_ssh_key = async_to_raw_response_wrapper(
devboxes.create_ssh_key,
)
- self.create_tunnel = async_to_raw_response_wrapper(
- devboxes.create_tunnel,
+ self.create_tunnel = ( # pyright: ignore[reportDeprecated]
+ async_to_raw_response_wrapper(
+ devboxes.create_tunnel, # pyright: ignore[reportDeprecated],
+ )
)
self.delete_disk_snapshot = async_to_raw_response_wrapper(
devboxes.delete_disk_snapshot,
@@ -3359,8 +3415,10 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None:
self.read_file_contents = async_to_raw_response_wrapper(
devboxes.read_file_contents,
)
- self.remove_tunnel = async_to_raw_response_wrapper(
- devboxes.remove_tunnel,
+ self.remove_tunnel = ( # pyright: ignore[reportDeprecated]
+ async_to_raw_response_wrapper(
+ devboxes.remove_tunnel, # pyright: ignore[reportDeprecated],
+ )
)
self.resume = async_to_raw_response_wrapper(
devboxes.resume,
@@ -3427,8 +3485,10 @@ def __init__(self, devboxes: DevboxesResource) -> None:
self.create_ssh_key = to_streamed_response_wrapper(
devboxes.create_ssh_key,
)
- self.create_tunnel = to_streamed_response_wrapper(
- devboxes.create_tunnel,
+ self.create_tunnel = ( # pyright: ignore[reportDeprecated]
+ to_streamed_response_wrapper(
+ devboxes.create_tunnel, # pyright: ignore[reportDeprecated],
+ )
)
self.delete_disk_snapshot = to_streamed_response_wrapper(
devboxes.delete_disk_snapshot,
@@ -3457,8 +3517,10 @@ def __init__(self, devboxes: DevboxesResource) -> None:
self.read_file_contents = to_streamed_response_wrapper(
devboxes.read_file_contents,
)
- self.remove_tunnel = to_streamed_response_wrapper(
- devboxes.remove_tunnel,
+ self.remove_tunnel = ( # pyright: ignore[reportDeprecated]
+ to_streamed_response_wrapper(
+ devboxes.remove_tunnel, # pyright: ignore[reportDeprecated],
+ )
)
self.resume = to_streamed_response_wrapper(
devboxes.resume,
@@ -3525,8 +3587,10 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None:
self.create_ssh_key = async_to_streamed_response_wrapper(
devboxes.create_ssh_key,
)
- self.create_tunnel = async_to_streamed_response_wrapper(
- devboxes.create_tunnel,
+ self.create_tunnel = ( # pyright: ignore[reportDeprecated]
+ async_to_streamed_response_wrapper(
+ devboxes.create_tunnel, # pyright: ignore[reportDeprecated],
+ )
)
self.delete_disk_snapshot = async_to_streamed_response_wrapper(
devboxes.delete_disk_snapshot,
@@ -3555,8 +3619,10 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None:
self.read_file_contents = async_to_streamed_response_wrapper(
devboxes.read_file_contents,
)
- self.remove_tunnel = async_to_streamed_response_wrapper(
- devboxes.remove_tunnel,
+ self.remove_tunnel = ( # pyright: ignore[reportDeprecated]
+ async_to_streamed_response_wrapper(
+ devboxes.remove_tunnel, # pyright: ignore[reportDeprecated],
+ )
)
self.resume = async_to_streamed_response_wrapper(
devboxes.resume,
diff --git a/src/runloop_api_client/resources/gateway_configs.py b/src/runloop_api_client/resources/gateway_configs.py
new file mode 100644
index 000000000..38c921fcd
--- /dev/null
+++ b/src/runloop_api_client/resources/gateway_configs.py
@@ -0,0 +1,658 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+
+import httpx
+
+from ..types import gateway_config_list_params, gateway_config_create_params, gateway_config_update_params
+from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from .._utils import maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncGatewayConfigsCursorIDPage, AsyncGatewayConfigsCursorIDPage
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.gateway_config_view import GatewayConfigView
+
+__all__ = ["GatewayConfigsResource", "AsyncGatewayConfigsResource"]
+
+
+class GatewayConfigsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> GatewayConfigsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers
+ """
+ return GatewayConfigsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> GatewayConfigsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response
+ """
+ return GatewayConfigsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ auth_mechanism: gateway_config_create_params.AuthMechanism,
+ endpoint: str,
+ name: str,
+ description: Optional[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> GatewayConfigView:
+ """
+ [Beta] Create a new GatewayConfig to proxy API requests through the credential
+ gateway. The config specifies the target endpoint and how credentials should be
+ applied.
+
+ Args:
+ auth_mechanism: How credentials should be applied to proxied requests. Specify the type
+ ('header', 'bearer') and optional key field.
+
+ endpoint: The target endpoint URL (e.g., 'https://api.anthropic.com').
+
+ name: The human-readable name for the GatewayConfig. Must be unique within your
+ account.
+
+ description: Optional description for this gateway configuration.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ return self._post(
+ "/v1/gateway-configs",
+ body=maybe_transform(
+ {
+ "auth_mechanism": auth_mechanism,
+ "endpoint": endpoint,
+ "name": name,
+ "description": description,
+ },
+ gateway_config_create_params.GatewayConfigCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=GatewayConfigView,
+ )
+
+ def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> GatewayConfigView:
+ """
+ [Beta] Get a specific GatewayConfig by its unique identifier.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ f"/v1/gateway-configs/{id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=GatewayConfigView,
+ )
+
+ def update(
+ self,
+ id: str,
+ *,
+ auth_mechanism: Optional[gateway_config_update_params.AuthMechanism] | Omit = omit,
+ description: Optional[str] | Omit = omit,
+ endpoint: Optional[str] | Omit = omit,
+ name: Optional[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> GatewayConfigView:
+ """[Beta] Update an existing GatewayConfig.
+
+ All fields are optional.
+
+ Args:
+ auth_mechanism: New authentication mechanism for applying credentials to proxied requests.
+
+ description: New description for this gateway configuration.
+
+ endpoint: New target endpoint URL (e.g., 'https://api.anthropic.com').
+
+ name: New name for the GatewayConfig. Must be unique within your account.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._post(
+ f"/v1/gateway-configs/{id}",
+ body=maybe_transform(
+ {
+ "auth_mechanism": auth_mechanism,
+ "description": description,
+ "endpoint": endpoint,
+ "name": name,
+ },
+ gateway_config_update_params.GatewayConfigUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=GatewayConfigView,
+ )
+
+ def list(
+ self,
+ *,
+ id: str | Omit = omit,
+ limit: int | Omit = omit,
+ name: str | Omit = omit,
+ starting_after: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncGatewayConfigsCursorIDPage[GatewayConfigView]:
+ """
+ [Beta] List all GatewayConfigs for the authenticated account, including
+ system-provided configs like 'anthropic' and 'openai'.
+
+ Args:
+ id: Filter by ID.
+
+ limit: The limit of items to return. Default is 20. Max is 5000.
+
+ name: Filter by name (partial match supported).
+
+ starting_after: Load the next page of data starting after the item with the given ID.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/gateway-configs",
+ page=SyncGatewayConfigsCursorIDPage[GatewayConfigView],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "id": id,
+ "limit": limit,
+ "name": name,
+ "starting_after": starting_after,
+ },
+ gateway_config_list_params.GatewayConfigListParams,
+ ),
+ ),
+ model=GatewayConfigView,
+ )
+
+ def delete(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> GatewayConfigView:
+ """[Beta] Delete an existing GatewayConfig.
+
+ This action is irreversible.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._post(
+ f"/v1/gateway-configs/{id}/delete",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=GatewayConfigView,
+ )
+
+
+class AsyncGatewayConfigsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncGatewayConfigsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncGatewayConfigsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncGatewayConfigsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response
+ """
+ return AsyncGatewayConfigsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ auth_mechanism: gateway_config_create_params.AuthMechanism,
+ endpoint: str,
+ name: str,
+ description: Optional[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> GatewayConfigView:
+ """
+ [Beta] Create a new GatewayConfig to proxy API requests through the credential
+ gateway. The config specifies the target endpoint and how credentials should be
+ applied.
+
+ Args:
+ auth_mechanism: How credentials should be applied to proxied requests. Specify the type
+ ('header', 'bearer') and optional key field.
+
+ endpoint: The target endpoint URL (e.g., 'https://api.anthropic.com').
+
+ name: The human-readable name for the GatewayConfig. Must be unique within your
+ account.
+
+ description: Optional description for this gateway configuration.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ return await self._post(
+ "/v1/gateway-configs",
+ body=await async_maybe_transform(
+ {
+ "auth_mechanism": auth_mechanism,
+ "endpoint": endpoint,
+ "name": name,
+ "description": description,
+ },
+ gateway_config_create_params.GatewayConfigCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=GatewayConfigView,
+ )
+
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> GatewayConfigView:
+ """
+ [Beta] Get a specific GatewayConfig by its unique identifier.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ f"/v1/gateway-configs/{id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=GatewayConfigView,
+ )
+
+ async def update(
+ self,
+ id: str,
+ *,
+ auth_mechanism: Optional[gateway_config_update_params.AuthMechanism] | Omit = omit,
+ description: Optional[str] | Omit = omit,
+ endpoint: Optional[str] | Omit = omit,
+ name: Optional[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> GatewayConfigView:
+ """[Beta] Update an existing GatewayConfig.
+
+ All fields are optional.
+
+ Args:
+ auth_mechanism: New authentication mechanism for applying credentials to proxied requests.
+
+ description: New description for this gateway configuration.
+
+ endpoint: New target endpoint URL (e.g., 'https://api.anthropic.com').
+
+ name: New name for the GatewayConfig. Must be unique within your account.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._post(
+ f"/v1/gateway-configs/{id}",
+ body=await async_maybe_transform(
+ {
+ "auth_mechanism": auth_mechanism,
+ "description": description,
+ "endpoint": endpoint,
+ "name": name,
+ },
+ gateway_config_update_params.GatewayConfigUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=GatewayConfigView,
+ )
+
+ def list(
+ self,
+ *,
+ id: str | Omit = omit,
+ limit: int | Omit = omit,
+ name: str | Omit = omit,
+ starting_after: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[GatewayConfigView, AsyncGatewayConfigsCursorIDPage[GatewayConfigView]]:
+ """
+ [Beta] List all GatewayConfigs for the authenticated account, including
+ system-provided configs like 'anthropic' and 'openai'.
+
+ Args:
+ id: Filter by ID.
+
+ limit: The limit of items to return. Default is 20. Max is 5000.
+
+ name: Filter by name (partial match supported).
+
+ starting_after: Load the next page of data starting after the item with the given ID.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/v1/gateway-configs",
+ page=AsyncGatewayConfigsCursorIDPage[GatewayConfigView],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "id": id,
+ "limit": limit,
+ "name": name,
+ "starting_after": starting_after,
+ },
+ gateway_config_list_params.GatewayConfigListParams,
+ ),
+ ),
+ model=GatewayConfigView,
+ )
+
+ async def delete(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> GatewayConfigView:
+ """[Beta] Delete an existing GatewayConfig.
+
+ This action is irreversible.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._post(
+ f"/v1/gateway-configs/{id}/delete",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=GatewayConfigView,
+ )
+
+
+class GatewayConfigsResourceWithRawResponse:
+ def __init__(self, gateway_configs: GatewayConfigsResource) -> None:
+ self._gateway_configs = gateway_configs
+
+ self.create = to_raw_response_wrapper(
+ gateway_configs.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ gateway_configs.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ gateway_configs.update,
+ )
+ self.list = to_raw_response_wrapper(
+ gateway_configs.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ gateway_configs.delete,
+ )
+
+
+class AsyncGatewayConfigsResourceWithRawResponse:
+ def __init__(self, gateway_configs: AsyncGatewayConfigsResource) -> None:
+ self._gateway_configs = gateway_configs
+
+ self.create = async_to_raw_response_wrapper(
+ gateway_configs.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ gateway_configs.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ gateway_configs.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ gateway_configs.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ gateway_configs.delete,
+ )
+
+
+class GatewayConfigsResourceWithStreamingResponse:
+ def __init__(self, gateway_configs: GatewayConfigsResource) -> None:
+ self._gateway_configs = gateway_configs
+
+ self.create = to_streamed_response_wrapper(
+ gateway_configs.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ gateway_configs.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ gateway_configs.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ gateway_configs.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ gateway_configs.delete,
+ )
+
+
+class AsyncGatewayConfigsResourceWithStreamingResponse:
+ def __init__(self, gateway_configs: AsyncGatewayConfigsResource) -> None:
+ self._gateway_configs = gateway_configs
+
+ self.create = async_to_streamed_response_wrapper(
+ gateway_configs.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ gateway_configs.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ gateway_configs.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ gateway_configs.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ gateway_configs.delete,
+ )
diff --git a/src/runloop_api_client/sdk/async_devbox.py b/src/runloop_api_client/sdk/async_devbox.py
index 5f5acc01d..7e327826f 100644
--- a/src/runloop_api_client/sdk/async_devbox.py
+++ b/src/runloop_api_client/sdk/async_devbox.py
@@ -4,6 +4,7 @@
import asyncio
import logging
+import warnings
from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Awaitable, cast
from typing_extensions import Unpack, override
@@ -740,10 +741,12 @@ async def create_tunnel(
>>> tunnel = await devbox.net.create_tunnel(port=8080)
>>> print(f"Public URL: {tunnel.url}")
"""
- return await self._devbox._client.devboxes.create_tunnel(
- self._devbox.id,
- **params,
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ return await self._devbox._client.devboxes.create_tunnel( # type: ignore[deprecated]
+ self._devbox.id,
+ **params,
+ )
async def remove_tunnel(
self,
@@ -758,7 +761,9 @@ async def remove_tunnel(
Example:
>>> await devbox.net.remove_tunnel(port=8080)
"""
- return await self._devbox._client.devboxes.remove_tunnel(
- self._devbox.id,
- **params,
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ return await self._devbox._client.devboxes.remove_tunnel( # type: ignore[deprecated]
+ self._devbox.id,
+ **params,
+ )
diff --git a/src/runloop_api_client/sdk/devbox.py b/src/runloop_api_client/sdk/devbox.py
index dee6261ab..0c11955a3 100644
--- a/src/runloop_api_client/sdk/devbox.py
+++ b/src/runloop_api_client/sdk/devbox.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
+import warnings
import threading
from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence
from typing_extensions import Unpack, override
@@ -743,10 +744,12 @@ def create_tunnel(
>>> tunnel = devbox.net.create_tunnel(port=8080)
>>> print(f"Public URL: {tunnel.url}")
"""
- return self._devbox._client.devboxes.create_tunnel(
- self._devbox.id,
- **params,
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ return self._devbox._client.devboxes.create_tunnel( # type: ignore[deprecated]
+ self._devbox.id,
+ **params,
+ )
def remove_tunnel(
self,
@@ -761,7 +764,9 @@ def remove_tunnel(
Example:
>>> devbox.net.remove_tunnel(port=8080)
"""
- return self._devbox._client.devboxes.remove_tunnel(
- self._devbox.id,
- **params,
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ return self._devbox._client.devboxes.remove_tunnel( # type: ignore[deprecated]
+ self._devbox.id,
+ **params,
+ )
diff --git a/src/runloop_api_client/types/__init__.py b/src/runloop_api_client/types/__init__.py
index 59130e662..be02c8312 100644
--- a/src/runloop_api_client/types/__init__.py
+++ b/src/runloop_api_client/types/__init__.py
@@ -37,6 +37,7 @@
from .agent_create_params import AgentCreateParams as AgentCreateParams
from .blueprint_build_log import BlueprintBuildLog as BlueprintBuildLog
from .blueprint_list_view import BlueprintListView as BlueprintListView
+from .gateway_config_view import GatewayConfigView as GatewayConfigView
from .input_context_param import InputContextParam as InputContextParam
from .network_policy_view import NetworkPolicyView as NetworkPolicyView
from .devbox_create_params import DevboxCreateParams as DevboxCreateParams
@@ -65,6 +66,7 @@
from .blueprint_create_params import BlueprintCreateParams as BlueprintCreateParams
from .inspection_source_param import InspectionSourceParam as InspectionSourceParam
from .blueprint_preview_params import BlueprintPreviewParams as BlueprintPreviewParams
+from .gateway_config_list_view import GatewayConfigListView as GatewayConfigListView
from .network_policy_list_view import NetworkPolicyListView as NetworkPolicyListView
from .object_download_url_view import ObjectDownloadURLView as ObjectDownloadURLView
from .repository_create_params import RepositoryCreateParams as RepositoryCreateParams
@@ -81,6 +83,7 @@
from .benchmark_start_run_params import BenchmarkStartRunParams as BenchmarkStartRunParams
from .blueprint_build_parameters import BlueprintBuildParameters as BlueprintBuildParameters
from .devbox_execute_sync_params import DevboxExecuteSyncParams as DevboxExecuteSyncParams
+from .gateway_config_list_params import GatewayConfigListParams as GatewayConfigListParams
from .input_context_update_param import InputContextUpdateParam as InputContextUpdateParam
from .network_policy_list_params import NetworkPolicyListParams as NetworkPolicyListParams
from .repository_connection_view import RepositoryConnectionView as RepositoryConnectionView
@@ -96,6 +99,8 @@
from .benchmark_list_public_params import BenchmarkListPublicParams as BenchmarkListPublicParams
from .blueprint_list_public_params import BlueprintListPublicParams as BlueprintListPublicParams
from .devbox_execution_detail_view import DevboxExecutionDetailView as DevboxExecutionDetailView
+from .gateway_config_create_params import GatewayConfigCreateParams as GatewayConfigCreateParams
+from .gateway_config_update_params import GatewayConfigUpdateParams as GatewayConfigUpdateParams
from .network_policy_create_params import NetworkPolicyCreateParams as NetworkPolicyCreateParams
from .network_policy_update_params import NetworkPolicyUpdateParams as NetworkPolicyUpdateParams
from .scoring_contract_result_view import ScoringContractResultView as ScoringContractResultView
diff --git a/src/runloop_api_client/types/devbox_create_params.py b/src/runloop_api_client/types/devbox_create_params.py
index 91651ad87..b31efdfe7 100644
--- a/src/runloop_api_client/types/devbox_create_params.py
+++ b/src/runloop_api_client/types/devbox_create_params.py
@@ -3,13 +3,13 @@
from __future__ import annotations
from typing import Dict, Iterable, Optional
-from typing_extensions import TypedDict
+from typing_extensions import Literal, Required, TypedDict
from .shared_params.mount import Mount
from .shared_params.launch_parameters import LaunchParameters
from .shared_params.code_mount_parameters import CodeMountParameters
-__all__ = ["DevboxCreateParams"]
+__all__ = ["DevboxCreateParams", "Gateways", "Tunnel"]
# We split up the original DevboxCreateParams into two nested types to enable us to
# omit blueprint_id, blueprint_name, and snapshot_id when we unpack the TypedDict
@@ -37,6 +37,15 @@ class DevboxBaseCreateParams(TypedDict, total=False):
file_mounts: Optional[Dict[str, str]]
"""Map of paths and file contents to write before setup. Use mounts instead."""
+ gateways: Optional[Dict[str, Gateways]]
+ """[Beta] (Optional) Gateway specifications for credential proxying.
+
+ Map key is the environment variable prefix (e.g., 'GWS_ANTHROPIC'). The gateway
+ will proxy requests to external APIs using the specified credential without
+ exposing the real API key. Example: {'GWS_ANTHROPIC': {'gateway': 'anthropic',
+ 'secret': 'my_claude_key'}}
+ """
+
launch_parameters: Optional[LaunchParameters]
"""Parameters to configure the resources and launch time behavior of the Devbox."""
@@ -84,3 +93,32 @@ class DevboxCreateParams(DevboxBaseCreateParams, total=False):
Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified.
"""
+
+ tunnel: Optional[Tunnel]
+ """(Optional) Configuration for creating a V2 tunnel at Devbox launch time.
+
+ When specified, a tunnel will be automatically provisioned and the tunnel
+ details will be included in the Devbox response.
+ """
+
+
+class Gateways(TypedDict, total=False):
+ """
+ [Beta] GatewaySpec links a gateway configuration to a secret for credential proxying in a devbox. The gateway will proxy requests to external APIs using the specified credential without exposing the real API key.
+ """
+
+ gateway: Required[str]
+ """The gateway config to use. Can be a gateway config ID (gwc_xxx) or name."""
+
+ secret: Required[str]
+ """The secret containing the credential. Can be a secret ID or name."""
+
+
+class Tunnel(TypedDict, total=False):
+ """(Optional) Configuration for creating a V2 tunnel at Devbox launch time.
+
+ When specified, a tunnel will be automatically provisioned and the tunnel details will be included in the Devbox response.
+ """
+
+ auth_mode: Optional[Literal["open", "authenticated"]]
+ """Authentication mode for the tunnel. Defaults to 'public' if not specified."""
diff --git a/src/runloop_api_client/types/devbox_view.py b/src/runloop_api_client/types/devbox_view.py
index cf9dc2383..50422db1f 100644
--- a/src/runloop_api_client/types/devbox_view.py
+++ b/src/runloop_api_client/types/devbox_view.py
@@ -6,7 +6,7 @@
from .._models import BaseModel
from .shared.launch_parameters import LaunchParameters
-__all__ = ["DevboxView", "StateTransition"]
+__all__ = ["DevboxView", "StateTransition", "GatewaySpecs", "Tunnel"]
class StateTransition(BaseModel):
@@ -30,6 +30,38 @@ class StateTransition(BaseModel):
"""The time the status change occurred"""
+class GatewaySpecs(BaseModel):
+ gateway_config_id: str
+ """The ID of the gateway config (e.g., gwc_123abc)."""
+
+ secret_id: str
+ """The ID of the secret containing the credential."""
+
+
+class Tunnel(BaseModel):
+ """
+ V2 tunnel information if a tunnel was created at launch time or via the createTunnel API.
+ """
+
+ auth_mode: Literal["public_", "authenticated"]
+ """The authentication mode for the tunnel."""
+
+ create_time_ms: int
+ """Creation time of the tunnel (Unix timestamp milliseconds)."""
+
+ tunnel_key: str
+ """The encrypted tunnel key used to construct the tunnel URL.
+
+ URL format: https://{port}-{tunnel_key}.tunnel.runloop.{domain}
+ """
+
+ auth_token: Optional[str] = None
+ """Bearer token for tunnel authentication.
+
+ Only present when auth_mode is 'authenticated'.
+ """
+
+
class DevboxView(BaseModel):
"""A Devbox represents a virtual development environment.
@@ -78,6 +110,12 @@ class DevboxView(BaseModel):
failure_reason: Optional[Literal["out_of_memory", "out_of_disk", "execution_failed"]] = None
"""The failure reason if the Devbox failed, if the Devbox has a 'failure' status."""
+ gateway_specs: Optional[Dict[str, GatewaySpecs]] = None
+ """[Beta] Gateway specifications configured for this devbox.
+
+ Map key is the environment variable prefix (e.g., 'GWS_ANTHROPIC').
+ """
+
initiator_id: Optional[str] = None
"""The ID of the initiator that created the Devbox."""
@@ -98,3 +136,9 @@ class DevboxView(BaseModel):
The Snapshot ID used in creation of the Devbox, if the devbox was created from a
Snapshot.
"""
+
+ tunnel: Optional[Tunnel] = None
+ """
+ V2 tunnel information if a tunnel was created at launch time or via the
+ createTunnel API.
+ """
diff --git a/src/runloop_api_client/types/gateway_config_create_params.py b/src/runloop_api_client/types/gateway_config_create_params.py
new file mode 100644
index 000000000..6a39055e4
--- /dev/null
+++ b/src/runloop_api_client/types/gateway_config_create_params.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import Required, TypedDict
+
+__all__ = ["GatewayConfigCreateParams", "AuthMechanism"]
+
+
+class GatewayConfigCreateParams(TypedDict, total=False):
+ auth_mechanism: Required[AuthMechanism]
+ """How credentials should be applied to proxied requests.
+
+ Specify the type ('header', 'bearer') and optional key field.
+ """
+
+ endpoint: Required[str]
+ """The target endpoint URL (e.g., 'https://api.anthropic.com')."""
+
+ name: Required[str]
+ """The human-readable name for the GatewayConfig.
+
+ Must be unique within your account.
+ """
+
+ description: Optional[str]
+ """Optional description for this gateway configuration."""
+
+
+class AuthMechanism(TypedDict, total=False):
+ """How credentials should be applied to proxied requests.
+
+ Specify the type ('header', 'bearer') and optional key field.
+ """
+
+ type: Required[str]
+ """The type of authentication mechanism: 'header', 'bearer'."""
+
+ key: Optional[str]
+ """For 'header' type: the header name (e.g., 'x-api-key')."""
diff --git a/src/runloop_api_client/types/gateway_config_list_params.py b/src/runloop_api_client/types/gateway_config_list_params.py
new file mode 100644
index 000000000..cc8706b95
--- /dev/null
+++ b/src/runloop_api_client/types/gateway_config_list_params.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["GatewayConfigListParams"]
+
+
+class GatewayConfigListParams(TypedDict, total=False):
+ id: str
+ """Filter by ID."""
+
+ limit: int
+ """The limit of items to return. Default is 20. Max is 5000."""
+
+ name: str
+ """Filter by name (partial match supported)."""
+
+ starting_after: str
+ """Load the next page of data starting after the item with the given ID."""
diff --git a/src/runloop_api_client/types/gateway_config_list_view.py b/src/runloop_api_client/types/gateway_config_list_view.py
new file mode 100644
index 000000000..77fce4455
--- /dev/null
+++ b/src/runloop_api_client/types/gateway_config_list_view.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from .._models import BaseModel
+from .gateway_config_view import GatewayConfigView
+
+__all__ = ["GatewayConfigListView"]
+
+
+class GatewayConfigListView(BaseModel):
+ """A paginated list of GatewayConfigs."""
+
+ gateway_configs: List[GatewayConfigView]
+ """The list of GatewayConfigs."""
+
+ has_more: bool
+ """Whether there are more results available beyond this page."""
+
+ total_count: int
+ """Total count of GatewayConfigs that match the query."""
diff --git a/src/runloop_api_client/types/gateway_config_update_params.py b/src/runloop_api_client/types/gateway_config_update_params.py
new file mode 100644
index 000000000..cdf385702
--- /dev/null
+++ b/src/runloop_api_client/types/gateway_config_update_params.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import Required, TypedDict
+
+__all__ = ["GatewayConfigUpdateParams", "AuthMechanism"]
+
+
+class GatewayConfigUpdateParams(TypedDict, total=False):
+ auth_mechanism: Optional[AuthMechanism]
+ """New authentication mechanism for applying credentials to proxied requests."""
+
+ description: Optional[str]
+ """New description for this gateway configuration."""
+
+ endpoint: Optional[str]
+ """New target endpoint URL (e.g., 'https://api.anthropic.com')."""
+
+ name: Optional[str]
+ """New name for the GatewayConfig. Must be unique within your account."""
+
+
+class AuthMechanism(TypedDict, total=False):
+ """New authentication mechanism for applying credentials to proxied requests."""
+
+ type: Required[str]
+ """The type of authentication mechanism: 'header', 'bearer'."""
+
+ key: Optional[str]
+ """For 'header' type: the header name (e.g., 'x-api-key')."""
diff --git a/src/runloop_api_client/types/gateway_config_view.py b/src/runloop_api_client/types/gateway_config_view.py
new file mode 100644
index 000000000..a6e3b3ac4
--- /dev/null
+++ b/src/runloop_api_client/types/gateway_config_view.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+
+__all__ = ["GatewayConfigView", "AuthMechanism"]
+
+
+class AuthMechanism(BaseModel):
+ """How credentials should be applied to proxied requests."""
+
+ type: str
+ """The type of authentication mechanism: 'header', 'bearer'."""
+
+ key: Optional[str] = None
+ """For 'header' type: the header name (e.g., 'x-api-key')."""
+
+
+class GatewayConfigView(BaseModel):
+ """
+ A GatewayConfig defines a configuration for proxying API requests through the credential gateway. It specifies the target endpoint and how credentials should be applied.
+ """
+
+ id: str
+ """The unique identifier of the GatewayConfig."""
+
+ auth_mechanism: AuthMechanism
+ """How credentials should be applied to proxied requests."""
+
+ create_time_ms: int
+ """Creation time of the GatewayConfig (Unix timestamp in milliseconds)."""
+
+ endpoint: str
+ """The target endpoint URL (e.g., 'https://api.anthropic.com')."""
+
+ name: str
+ """The human-readable name of the GatewayConfig.
+
+ Unique per account (or globally for system configs).
+ """
+
+ account_id: Optional[str] = None
+ """The account ID that owns this config."""
+
+ description: Optional[str] = None
+ """Optional description for this gateway configuration."""
diff --git a/src/runloop_api_client/types/shared/launch_parameters.py b/src/runloop_api_client/types/shared/launch_parameters.py
index 04d7f27a0..0264fa5c8 100644
--- a/src/runloop_api_client/types/shared/launch_parameters.py
+++ b/src/runloop_api_client/types/shared/launch_parameters.py
@@ -44,22 +44,13 @@ class LaunchParameters(BaseModel):
"""
custom_cpu_cores: Optional[int] = None
- """custom resource size, number of cpu cores, must be multiple of 2.
-
- Min is 1, max is 16.
- """
+ """Custom CPU cores. Must be 0.5, 1, or a multiple of 2. Max is 16."""
custom_disk_size: Optional[int] = None
- """custom disk size, number in GiB, must be a multiple of 2.
-
- Min is 2GiB, max is 64GiB.
- """
+ """Custom disk size in GiB. Must be a multiple of 2. Min is 2GiB, max is 64GiB."""
custom_gb_memory: Optional[int] = None
- """custom memory size, number in GiB, must be a multiple of 2.
-
- Min is 2GiB, max is 64GiB.
- """
+ """Custom memory size in GiB. Must be 1 or a multiple of 2. Max is 64GiB."""
keep_alive_time_seconds: Optional[int] = None
"""Time in seconds after which Devbox will automatically shutdown.
diff --git a/src/runloop_api_client/types/shared_params/launch_parameters.py b/src/runloop_api_client/types/shared_params/launch_parameters.py
index e3f00d1d3..5c785b9f9 100644
--- a/src/runloop_api_client/types/shared_params/launch_parameters.py
+++ b/src/runloop_api_client/types/shared_params/launch_parameters.py
@@ -46,22 +46,13 @@ class LaunchParameters(TypedDict, total=False):
"""
custom_cpu_cores: Optional[int]
- """custom resource size, number of cpu cores, must be multiple of 2.
-
- Min is 1, max is 16.
- """
+ """Custom CPU cores. Must be 0.5, 1, or a multiple of 2. Max is 16."""
custom_disk_size: Optional[int]
- """custom disk size, number in GiB, must be a multiple of 2.
-
- Min is 2GiB, max is 64GiB.
- """
+ """Custom disk size in GiB. Must be a multiple of 2. Min is 2GiB, max is 64GiB."""
custom_gb_memory: Optional[int]
- """custom memory size, number in GiB, must be a multiple of 2.
-
- Min is 2GiB, max is 64GiB.
- """
+ """Custom memory size in GiB. Must be 1 or a multiple of 2. Max is 64GiB."""
keep_alive_time_seconds: Optional[int]
"""Time in seconds after which Devbox will automatically shutdown.
diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py
index d9b75d1ac..8af777181 100644
--- a/tests/api_resources/test_devboxes.py
+++ b/tests/api_resources/test_devboxes.py
@@ -65,6 +65,12 @@ def test_method_create_with_all_params(self, client: Runloop) -> None:
entrypoint="entrypoint",
environment_variables={"foo": "string"},
file_mounts={"foo": "string"},
+ gateways={
+ "foo": {
+ "gateway": "gateway",
+ "secret": "secret",
+ }
+ },
launch_parameters={
"after_idle": {
"idle_time_seconds": 0,
@@ -97,6 +103,7 @@ def test_method_create_with_all_params(self, client: Runloop) -> None:
repo_connection_id="repo_connection_id",
secrets={"foo": "string"},
snapshot_id="snapshot_id",
+ tunnel={"auth_mode": "open"},
)
assert_matches_type(DevboxView, devbox, path=["response"])
@@ -279,18 +286,21 @@ def test_path_params_create_ssh_key(self, client: Runloop) -> None:
@parametrize
def test_method_create_tunnel(self, client: Runloop) -> None:
- devbox = client.devboxes.create_tunnel(
- id="id",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ devbox = client.devboxes.create_tunnel(
+ id="id",
+ port=0,
+ )
+
assert_matches_type(DevboxTunnelView, devbox, path=["response"])
@parametrize
def test_raw_response_create_tunnel(self, client: Runloop) -> None:
- response = client.devboxes.with_raw_response.create_tunnel(
- id="id",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ response = client.devboxes.with_raw_response.create_tunnel(
+ id="id",
+ port=0,
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -299,25 +309,27 @@ def test_raw_response_create_tunnel(self, client: Runloop) -> None:
@parametrize
def test_streaming_response_create_tunnel(self, client: Runloop) -> None:
- with client.devboxes.with_streaming_response.create_tunnel(
- id="id",
- port=0,
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ with pytest.warns(DeprecationWarning):
+ with client.devboxes.with_streaming_response.create_tunnel(
+ id="id",
+ port=0,
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- devbox = response.parse()
- assert_matches_type(DevboxTunnelView, devbox, path=["response"])
+ devbox = response.parse()
+ assert_matches_type(DevboxTunnelView, devbox, path=["response"])
assert cast(Any, response.is_closed) is True
@parametrize
def test_path_params_create_tunnel(self, client: Runloop) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- client.devboxes.with_raw_response.create_tunnel(
- id="",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.devboxes.with_raw_response.create_tunnel(
+ id="",
+ port=0,
+ )
@parametrize
def test_method_delete_disk_snapshot(self, client: Runloop) -> None:
@@ -703,18 +715,21 @@ def test_path_params_read_file_contents(self, client: Runloop) -> None:
@parametrize
def test_method_remove_tunnel(self, client: Runloop) -> None:
- devbox = client.devboxes.remove_tunnel(
- id="id",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ devbox = client.devboxes.remove_tunnel(
+ id="id",
+ port=0,
+ )
+
assert_matches_type(object, devbox, path=["response"])
@parametrize
def test_raw_response_remove_tunnel(self, client: Runloop) -> None:
- response = client.devboxes.with_raw_response.remove_tunnel(
- id="id",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ response = client.devboxes.with_raw_response.remove_tunnel(
+ id="id",
+ port=0,
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -723,25 +738,27 @@ def test_raw_response_remove_tunnel(self, client: Runloop) -> None:
@parametrize
def test_streaming_response_remove_tunnel(self, client: Runloop) -> None:
- with client.devboxes.with_streaming_response.remove_tunnel(
- id="id",
- port=0,
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ with pytest.warns(DeprecationWarning):
+ with client.devboxes.with_streaming_response.remove_tunnel(
+ id="id",
+ port=0,
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- devbox = response.parse()
- assert_matches_type(object, devbox, path=["response"])
+ devbox = response.parse()
+ assert_matches_type(object, devbox, path=["response"])
assert cast(Any, response.is_closed) is True
@parametrize
def test_path_params_remove_tunnel(self, client: Runloop) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- client.devboxes.with_raw_response.remove_tunnel(
- id="",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.devboxes.with_raw_response.remove_tunnel(
+ id="",
+ port=0,
+ )
@parametrize
def test_method_resume(self, client: Runloop) -> None:
@@ -1614,6 +1631,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -
entrypoint="entrypoint",
environment_variables={"foo": "string"},
file_mounts={"foo": "string"},
+ gateways={
+ "foo": {
+ "gateway": "gateway",
+ "secret": "secret",
+ }
+ },
launch_parameters={
"after_idle": {
"idle_time_seconds": 0,
@@ -1646,6 +1669,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -
repo_connection_id="repo_connection_id",
secrets={"foo": "string"},
snapshot_id="snapshot_id",
+ tunnel={"auth_mode": "open"},
)
assert_matches_type(DevboxView, devbox, path=["response"])
@@ -1828,18 +1852,21 @@ async def test_path_params_create_ssh_key(self, async_client: AsyncRunloop) -> N
@parametrize
async def test_method_create_tunnel(self, async_client: AsyncRunloop) -> None:
- devbox = await async_client.devboxes.create_tunnel(
- id="id",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ devbox = await async_client.devboxes.create_tunnel(
+ id="id",
+ port=0,
+ )
+
assert_matches_type(DevboxTunnelView, devbox, path=["response"])
@parametrize
async def test_raw_response_create_tunnel(self, async_client: AsyncRunloop) -> None:
- response = await async_client.devboxes.with_raw_response.create_tunnel(
- id="id",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ response = await async_client.devboxes.with_raw_response.create_tunnel(
+ id="id",
+ port=0,
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -1848,25 +1875,27 @@ async def test_raw_response_create_tunnel(self, async_client: AsyncRunloop) -> N
@parametrize
async def test_streaming_response_create_tunnel(self, async_client: AsyncRunloop) -> None:
- async with async_client.devboxes.with_streaming_response.create_tunnel(
- id="id",
- port=0,
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ with pytest.warns(DeprecationWarning):
+ async with async_client.devboxes.with_streaming_response.create_tunnel(
+ id="id",
+ port=0,
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- devbox = await response.parse()
- assert_matches_type(DevboxTunnelView, devbox, path=["response"])
+ devbox = await response.parse()
+ assert_matches_type(DevboxTunnelView, devbox, path=["response"])
assert cast(Any, response.is_closed) is True
@parametrize
async def test_path_params_create_tunnel(self, async_client: AsyncRunloop) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- await async_client.devboxes.with_raw_response.create_tunnel(
- id="",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.devboxes.with_raw_response.create_tunnel(
+ id="",
+ port=0,
+ )
@parametrize
async def test_method_delete_disk_snapshot(self, async_client: AsyncRunloop) -> None:
@@ -2252,18 +2281,21 @@ async def test_path_params_read_file_contents(self, async_client: AsyncRunloop)
@parametrize
async def test_method_remove_tunnel(self, async_client: AsyncRunloop) -> None:
- devbox = await async_client.devboxes.remove_tunnel(
- id="id",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ devbox = await async_client.devboxes.remove_tunnel(
+ id="id",
+ port=0,
+ )
+
assert_matches_type(object, devbox, path=["response"])
@parametrize
async def test_raw_response_remove_tunnel(self, async_client: AsyncRunloop) -> None:
- response = await async_client.devboxes.with_raw_response.remove_tunnel(
- id="id",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ response = await async_client.devboxes.with_raw_response.remove_tunnel(
+ id="id",
+ port=0,
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -2272,25 +2304,27 @@ async def test_raw_response_remove_tunnel(self, async_client: AsyncRunloop) -> N
@parametrize
async def test_streaming_response_remove_tunnel(self, async_client: AsyncRunloop) -> None:
- async with async_client.devboxes.with_streaming_response.remove_tunnel(
- id="id",
- port=0,
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ with pytest.warns(DeprecationWarning):
+ async with async_client.devboxes.with_streaming_response.remove_tunnel(
+ id="id",
+ port=0,
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- devbox = await response.parse()
- assert_matches_type(object, devbox, path=["response"])
+ devbox = await response.parse()
+ assert_matches_type(object, devbox, path=["response"])
assert cast(Any, response.is_closed) is True
@parametrize
async def test_path_params_remove_tunnel(self, async_client: AsyncRunloop) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- await async_client.devboxes.with_raw_response.remove_tunnel(
- id="",
- port=0,
- )
+ with pytest.warns(DeprecationWarning):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.devboxes.with_raw_response.remove_tunnel(
+ id="",
+ port=0,
+ )
@parametrize
async def test_method_resume(self, async_client: AsyncRunloop) -> None:
diff --git a/tests/api_resources/test_gateway_configs.py b/tests/api_resources/test_gateway_configs.py
new file mode 100644
index 000000000..6265b9875
--- /dev/null
+++ b/tests/api_resources/test_gateway_configs.py
@@ -0,0 +1,453 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from runloop_api_client import Runloop, AsyncRunloop
+from runloop_api_client.types import (
+ GatewayConfigView,
+)
+from runloop_api_client.pagination import SyncGatewayConfigsCursorIDPage, AsyncGatewayConfigsCursorIDPage
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestGatewayConfigs:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_create(self, client: Runloop) -> None:
+ gateway_config = client.gateway_configs.create(
+ auth_mechanism={"type": "type"},
+ endpoint="endpoint",
+ name="name",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_method_create_with_all_params(self, client: Runloop) -> None:
+ gateway_config = client.gateway_configs.create(
+ auth_mechanism={
+ "type": "type",
+ "key": "key",
+ },
+ endpoint="endpoint",
+ name="name",
+ description="description",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: Runloop) -> None:
+ response = client.gateway_configs.with_raw_response.create(
+ auth_mechanism={"type": "type"},
+ endpoint="endpoint",
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: Runloop) -> None:
+ with client.gateway_configs.with_streaming_response.create(
+ auth_mechanism={"type": "type"},
+ endpoint="endpoint",
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_retrieve(self, client: Runloop) -> None:
+ gateway_config = client.gateway_configs.retrieve(
+ "id",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Runloop) -> None:
+ response = client.gateway_configs.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Runloop) -> None:
+ with client.gateway_configs.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: Runloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.gateway_configs.with_raw_response.retrieve(
+ "",
+ )
+
+ @parametrize
+ def test_method_update(self, client: Runloop) -> None:
+ gateway_config = client.gateway_configs.update(
+ id="id",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_method_update_with_all_params(self, client: Runloop) -> None:
+ gateway_config = client.gateway_configs.update(
+ id="id",
+ auth_mechanism={
+ "type": "type",
+ "key": "key",
+ },
+ description="description",
+ endpoint="endpoint",
+ name="name",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_raw_response_update(self, client: Runloop) -> None:
+ response = client.gateway_configs.with_raw_response.update(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_streaming_response_update(self, client: Runloop) -> None:
+ with client.gateway_configs.with_streaming_response.update(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_update(self, client: Runloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.gateway_configs.with_raw_response.update(
+ id="",
+ )
+
+ @parametrize
+ def test_method_list(self, client: Runloop) -> None:
+ gateway_config = client.gateway_configs.list()
+ assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"])
+
+ @parametrize
+ def test_method_list_with_all_params(self, client: Runloop) -> None:
+ gateway_config = client.gateway_configs.list(
+ id="id",
+ limit=0,
+ name="name",
+ starting_after="starting_after",
+ )
+ assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: Runloop) -> None:
+ response = client.gateway_configs.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = response.parse()
+ assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: Runloop) -> None:
+ with client.gateway_configs.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = response.parse()
+ assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_delete(self, client: Runloop) -> None:
+ gateway_config = client.gateway_configs.delete(
+ "id",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_raw_response_delete(self, client: Runloop) -> None:
+ response = client.gateway_configs.with_raw_response.delete(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ def test_streaming_response_delete(self, client: Runloop) -> None:
+ with client.gateway_configs.with_streaming_response.delete(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_delete(self, client: Runloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.gateway_configs.with_raw_response.delete(
+ "",
+ )
+
+
+class TestAsyncGatewayConfigs:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_create(self, async_client: AsyncRunloop) -> None:
+ gateway_config = await async_client.gateway_configs.create(
+ auth_mechanism={"type": "type"},
+ endpoint="endpoint",
+ name="name",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None:
+ gateway_config = await async_client.gateway_configs.create(
+ auth_mechanism={
+ "type": "type",
+ "key": "key",
+ },
+ endpoint="endpoint",
+ name="name",
+ description="description",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.gateway_configs.with_raw_response.create(
+ auth_mechanism={"type": "type"},
+ endpoint="endpoint",
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = await response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None:
+ async with async_client.gateway_configs.with_streaming_response.create(
+ auth_mechanism={"type": "type"},
+ endpoint="endpoint",
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = await response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncRunloop) -> None:
+ gateway_config = await async_client.gateway_configs.retrieve(
+ "id",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.gateway_configs.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = await response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None:
+ async with async_client.gateway_configs.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = await response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.gateway_configs.with_raw_response.retrieve(
+ "",
+ )
+
+ @parametrize
+ async def test_method_update(self, async_client: AsyncRunloop) -> None:
+ gateway_config = await async_client.gateway_configs.update(
+ id="id",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None:
+ gateway_config = await async_client.gateway_configs.update(
+ id="id",
+ auth_mechanism={
+ "type": "type",
+ "key": "key",
+ },
+ description="description",
+ endpoint="endpoint",
+ name="name",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_raw_response_update(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.gateway_configs.with_raw_response.update(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = await response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None:
+ async with async_client.gateway_configs.with_streaming_response.update(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = await response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_update(self, async_client: AsyncRunloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.gateway_configs.with_raw_response.update(
+ id="",
+ )
+
+ @parametrize
+ async def test_method_list(self, async_client: AsyncRunloop) -> None:
+ gateway_config = await async_client.gateway_configs.list()
+ assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"])
+
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None:
+ gateway_config = await async_client.gateway_configs.list(
+ id="id",
+ limit=0,
+ name="name",
+ starting_after="starting_after",
+ )
+ assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.gateway_configs.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = await response.parse()
+ assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None:
+ async with async_client.gateway_configs.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = await response.parse()
+ assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncRunloop) -> None:
+ gateway_config = await async_client.gateway_configs.delete(
+ "id",
+ )
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.gateway_configs.with_raw_response.delete(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ gateway_config = await response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None:
+ async with async_client.gateway_configs.with_streaming_response.delete(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ gateway_config = await response.parse()
+ assert_matches_type(GatewayConfigView, gateway_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncRunloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.gateway_configs.with_raw_response.delete(
+ "",
+ )
diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py
new file mode 100644
index 000000000..7e9390783
--- /dev/null
+++ b/tests/test_utils/test_json.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import datetime
+from typing import Union
+
+import pydantic
+
+from runloop_api_client import _compat
+from runloop_api_client._utils._json import openapi_dumps
+
+
+class TestOpenapiDumps:
+ def test_basic(self) -> None:
+ data = {"key": "value", "number": 42}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"key":"value","number":42}'
+
+ def test_datetime_serialization(self) -> None:
+ dt = datetime.datetime(2023, 1, 1, 12, 0, 0)
+ data = {"datetime": dt}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}'
+
+ def test_pydantic_model_serialization(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str
+ last_name: str
+ age: int
+
+ model_instance = User(first_name="John", last_name="Kramer", age=83)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}'
+
+ def test_pydantic_model_with_default_values(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+ score: int = 0
+
+ model_instance = User(name="Alice")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Alice"}}'
+
+ def test_pydantic_model_with_default_values_overridden(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+
+ model_instance = User(name="Bob", role="admin", active=False)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}'
+
+ def test_pydantic_model_with_alias(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str = pydantic.Field(alias="firstName")
+ last_name: str = pydantic.Field(alias="lastName")
+
+ model_instance = User(firstName="John", lastName="Doe")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}'
+
+ def test_pydantic_model_with_alias_and_default(self) -> None:
+ class User(pydantic.BaseModel):
+ user_name: str = pydantic.Field(alias="userName")
+ user_role: str = pydantic.Field(default="member", alias="userRole")
+ is_active: bool = pydantic.Field(default=True, alias="isActive")
+
+ model_instance = User(userName="charlie")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"charlie"}}'
+
+ model_with_overrides = User(userName="diana", userRole="admin", isActive=False)
+ data = {"model": model_with_overrides}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}'
+
+ def test_pydantic_model_with_nested_models_and_defaults(self) -> None:
+ class Address(pydantic.BaseModel):
+ street: str
+ city: str = "Unknown"
+
+ class User(pydantic.BaseModel):
+ name: str
+ address: Address
+ verified: bool = False
+
+ if _compat.PYDANTIC_V1:
+ # to handle forward references in Pydantic v1
+ User.update_forward_refs(**locals()) # type: ignore[reportDeprecated]
+
+ address = Address(street="123 Main St")
+ user = User(name="Diana", address=address)
+ data = {"user": user}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}'
+
+ address_with_city = Address(street="456 Oak Ave", city="Boston")
+ user_verified = User(name="Eve", address=address_with_city, verified=True)
+ data = {"user": user_verified}
+ json_bytes = openapi_dumps(data)
+ assert (
+ json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}'
+ )
+
+ def test_pydantic_model_with_optional_fields(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ email: Union[str, None]
+ phone: Union[str, None]
+
+ model_with_none = User(name="Eve", email=None, phone=None)
+ data = {"model": model_with_none}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}'
+
+ model_with_values = User(name="Frank", email="frank@example.com", phone=None)
+ data = {"model": model_with_values}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}'
diff --git a/uv.lock b/uv.lock
index 4aa6876fd..bbb1cac9e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2237,7 +2237,7 @@ wheels = [
[[package]]
name = "runloop-api-client"
-version = "1.3.0a0"
+version = "1.4.0"
source = { editable = "." }
dependencies = [
{ name = "anyio" },