From ee5d5773d408ac8e61101fd0187ec071709bebab Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 19 Feb 2026 18:25:42 +0000
Subject: [PATCH 01/13] chore: format all `api.md` files
---
scripts/format | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/format b/scripts/format
index 3f910b784..c8e1f69d2 100755
--- a/scripts/format
+++ b/scripts/format
@@ -11,4 +11,4 @@ uv run ruff check --fix .
uv run ruff format
echo "==> Formatting docs"
-uv run python scripts/utils/ruffen-docs.py README.md api.md README-SDK.md
+uv run python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)
From 8b16ebe7aba9f96592f03ed2844211209b35d9e6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 19 Feb 2026 01:44:10 +0000
Subject: [PATCH 02/13] refactor: deprecate and make Nullable total_count and
remaining_count of ListViews (#7533)
---
.stats.yml | 4 +--
api.md | 2 +-
.../resources/devboxes/devboxes.py | 17 ++++++++--
src/runloop_api_client/types/__init__.py | 1 +
.../types/agent_list_view.py | 16 +++++++---
.../types/benchmark_job_list_view.py | 6 ++--
.../types/benchmark_run_list_view.py | 8 ++---
.../types/blueprint_list_view.py | 6 ++--
.../types/devbox_list_view.py | 6 ++--
.../types/devbox_shutdown_params.py | 12 +++++++
.../types/devbox_snapshot_list_view.py | 8 ++---
.../types/gateway_config_list_view.py | 9 ++++--
.../types/mcp_config_list_view.py | 9 ++++--
.../types/network_policy_list_view.py | 9 ++++--
.../types/object_list_view.py | 16 +++++++---
.../types/repository_connection_list_view.py | 8 ++---
.../types/scenario_definition_list_view.py | 8 ++---
.../types/scenario_run_list_view.py | 8 ++---
src/runloop_api_client/types/scenario_view.py | 3 ++
.../types/secret_list_view.py | 18 +++++++----
tests/api_resources/test_devboxes.py | 32 ++++++++++++++-----
21 files changed, 139 insertions(+), 67 deletions(-)
create mode 100644 src/runloop_api_client/types/devbox_shutdown_params.py
diff --git a/.stats.yml b/.stats.yml
index 3e985df2f..44040dae9 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-c8d61a0c8b88fa30666ba021d1239eb0d549902354513c4741e9216bcbfa8e6d.yml
-openapi_spec_hash: 433e6fb4ce076012b696f69ae7596c67
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-8704a652545c3831c8b7e246ec17f1a626b59911eb5701ddc922ca6cae1c8d57.yml
+openapi_spec_hash: e609bc5ed3fdce498bc7d16921bbba5e
config_hash: eb28692edd68a6ae95cf92af931c9976
diff --git a/api.md b/api.md
index 6cbe1e604..2c8438e72 100644
--- a/api.md
+++ b/api.md
@@ -160,7 +160,7 @@ Methods:
- client.devboxes.remove_tunnel(id, \*\*params) -> object
- client.devboxes.resume(id) -> DevboxView
- client.devboxes.retrieve_resource_usage(id) -> DevboxResourceUsageView
-- client.devboxes.shutdown(id) -> DevboxView
+- client.devboxes.shutdown(id, \*\*params) -> DevboxView
- client.devboxes.snapshot_disk(id, \*\*params) -> DevboxSnapshotView
- client.devboxes.snapshot_disk_async(id, \*\*params) -> DevboxSnapshotView
- client.devboxes.suspend(id) -> DevboxView
diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py
index 79ac29f11..84a286521 100644
--- a/src/runloop_api_client/resources/devboxes/devboxes.py
+++ b/src/runloop_api_client/resources/devboxes/devboxes.py
@@ -24,6 +24,7 @@
devbox_create_params,
devbox_update_params,
devbox_execute_params,
+ devbox_shutdown_params,
devbox_upload_file_params,
devbox_execute_sync_params,
devbox_create_tunnel_params,
@@ -1415,6 +1416,7 @@ def shutdown(
self,
id: str,
*,
+ force: 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,
@@ -1427,9 +1429,13 @@ def shutdown(
This will permanently stop the Devbox. If you want to
save the state of the Devbox, you should take a snapshot before shutting down or
- should suspend the Devbox instead of shutting down.
+ should suspend the Devbox instead of shutting down. If the Devbox has any
+ in-progress snapshots, the shutdown will be rejected with a 409 Conflict unless
+ force=true is specified.
Args:
+ force: If true, force shutdown even if snapshots are in progress. Defaults to false.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1450,6 +1456,7 @@ def shutdown(
extra_body=extra_body,
timeout=timeout,
idempotency_key=idempotency_key,
+ query=maybe_transform({"force": force}, devbox_shutdown_params.DevboxShutdownParams),
),
cast_to=DevboxView,
)
@@ -3077,6 +3084,7 @@ async def shutdown(
self,
id: str,
*,
+ force: 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,
@@ -3089,9 +3097,13 @@ async def shutdown(
This will permanently stop the Devbox. If you want to
save the state of the Devbox, you should take a snapshot before shutting down or
- should suspend the Devbox instead of shutting down.
+ should suspend the Devbox instead of shutting down. If the Devbox has any
+ in-progress snapshots, the shutdown will be rejected with a 409 Conflict unless
+ force=true is specified.
Args:
+ force: If true, force shutdown even if snapshots are in progress. Defaults to false.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -3112,6 +3124,7 @@ async def shutdown(
extra_body=extra_body,
timeout=timeout,
idempotency_key=idempotency_key,
+ query=await async_maybe_transform({"force": force}, devbox_shutdown_params.DevboxShutdownParams),
),
cast_to=DevboxView,
)
diff --git a/src/runloop_api_client/types/__init__.py b/src/runloop_api_client/types/__init__.py
index e6543484f..2cd10b43d 100644
--- a/src/runloop_api_client/types/__init__.py
+++ b/src/runloop_api_client/types/__init__.py
@@ -55,6 +55,7 @@
from .blueprint_list_params import BlueprintListParams as BlueprintListParams
from .devbox_execute_params import DevboxExecuteParams as DevboxExecuteParams
from .blueprint_preview_view import BlueprintPreviewView as BlueprintPreviewView
+from .devbox_shutdown_params import DevboxShutdownParams as DevboxShutdownParams
from .mcp_config_list_params import McpConfigListParams as McpConfigListParams
from .object_download_params import ObjectDownloadParams as ObjectDownloadParams
from .repository_list_params import RepositoryListParams as RepositoryListParams
diff --git a/src/runloop_api_client/types/agent_list_view.py b/src/runloop_api_client/types/agent_list_view.py
index bfb1560e1..9e57a9769 100644
--- a/src/runloop_api_client/types/agent_list_view.py
+++ b/src/runloop_api_client/types/agent_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .agent_view import AgentView
@@ -17,8 +17,14 @@ class AgentListView(BaseModel):
has_more: bool
"""Whether there are more Agents to fetch."""
- remaining_count: int
- """The count of remaining Agents."""
+ remaining_count: Optional[int] = None
+ """The count of remaining Agents.
- total_count: int
- """The total count of Agents."""
+ Deprecated: will be removed in a future breaking change.
+ """
+
+ total_count: Optional[int] = None
+ """The total count of Agents.
+
+ Deprecated: will be removed in a future breaking change.
+ """
diff --git a/src/runloop_api_client/types/benchmark_job_list_view.py b/src/runloop_api_client/types/benchmark_job_list_view.py
index 5090fe8e8..f0e1da7d0 100644
--- a/src/runloop_api_client/types/benchmark_job_list_view.py
+++ b/src/runloop_api_client/types/benchmark_job_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .benchmark_job_view import BenchmarkJobView
@@ -14,6 +14,6 @@ class BenchmarkJobListView(BaseModel):
jobs: List[BenchmarkJobView]
"""List of BenchmarkJobs matching filter."""
- remaining_count: int
+ remaining_count: Optional[int] = None
- total_count: int
+ total_count: Optional[int] = None
diff --git a/src/runloop_api_client/types/benchmark_run_list_view.py b/src/runloop_api_client/types/benchmark_run_list_view.py
index 0d0ca11e1..e85506bab 100644
--- a/src/runloop_api_client/types/benchmark_run_list_view.py
+++ b/src/runloop_api_client/types/benchmark_run_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .benchmark_run_view import BenchmarkRunView
@@ -11,9 +11,9 @@
class BenchmarkRunListView(BaseModel):
has_more: bool
- remaining_count: int
-
runs: List[BenchmarkRunView]
"""List of BenchmarkRuns matching filter."""
- total_count: int
+ remaining_count: Optional[int] = None
+
+ total_count: Optional[int] = None
diff --git a/src/runloop_api_client/types/blueprint_list_view.py b/src/runloop_api_client/types/blueprint_list_view.py
index ba7c6066f..7d97314f5 100644
--- a/src/runloop_api_client/types/blueprint_list_view.py
+++ b/src/runloop_api_client/types/blueprint_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .blueprint_view import BlueprintView
@@ -14,6 +14,6 @@ class BlueprintListView(BaseModel):
has_more: bool
- remaining_count: int
+ remaining_count: Optional[int] = None
- total_count: int
+ total_count: Optional[int] = None
diff --git a/src/runloop_api_client/types/devbox_list_view.py b/src/runloop_api_client/types/devbox_list_view.py
index f4f266240..6bd522ee5 100644
--- a/src/runloop_api_client/types/devbox_list_view.py
+++ b/src/runloop_api_client/types/devbox_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .devbox_view import DevboxView
@@ -14,6 +14,6 @@ class DevboxListView(BaseModel):
has_more: bool
- remaining_count: int
+ remaining_count: Optional[int] = None
- total_count: int
+ total_count: Optional[int] = None
diff --git a/src/runloop_api_client/types/devbox_shutdown_params.py b/src/runloop_api_client/types/devbox_shutdown_params.py
new file mode 100644
index 000000000..1a6af9336
--- /dev/null
+++ b/src/runloop_api_client/types/devbox_shutdown_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["DevboxShutdownParams"]
+
+
+class DevboxShutdownParams(TypedDict, total=False):
+ force: str
+ """If true, force shutdown even if snapshots are in progress. Defaults to false."""
diff --git a/src/runloop_api_client/types/devbox_snapshot_list_view.py b/src/runloop_api_client/types/devbox_snapshot_list_view.py
index 3e687f0cc..ca54f2eb1 100644
--- a/src/runloop_api_client/types/devbox_snapshot_list_view.py
+++ b/src/runloop_api_client/types/devbox_snapshot_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .devbox_snapshot_view import DevboxSnapshotView
@@ -11,9 +11,9 @@
class DevboxSnapshotListView(BaseModel):
has_more: bool
- remaining_count: int
-
snapshots: List[DevboxSnapshotView]
"""List of snapshots matching filter."""
- total_count: int
+ remaining_count: Optional[int] = None
+
+ total_count: Optional[int] = None
diff --git a/src/runloop_api_client/types/gateway_config_list_view.py b/src/runloop_api_client/types/gateway_config_list_view.py
index 77fce4455..05511971f 100644
--- a/src/runloop_api_client/types/gateway_config_list_view.py
+++ b/src/runloop_api_client/types/gateway_config_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .gateway_config_view import GatewayConfigView
@@ -17,5 +17,8 @@ class GatewayConfigListView(BaseModel):
has_more: bool
"""Whether there are more results available beyond this page."""
- total_count: int
- """Total count of GatewayConfigs that match the query."""
+ total_count: Optional[int] = None
+ """Total count of GatewayConfigs that match the query.
+
+ Deprecated: will be removed in a future breaking change.
+ """
diff --git a/src/runloop_api_client/types/mcp_config_list_view.py b/src/runloop_api_client/types/mcp_config_list_view.py
index 72075a255..4992698c5 100644
--- a/src/runloop_api_client/types/mcp_config_list_view.py
+++ b/src/runloop_api_client/types/mcp_config_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .mcp_config_view import McpConfigView
@@ -17,5 +17,8 @@ class McpConfigListView(BaseModel):
mcp_configs: List[McpConfigView]
"""The list of McpConfigs."""
- total_count: int
- """Total count of McpConfigs that match the query."""
+ total_count: Optional[int] = None
+ """Total count of McpConfigs that match the query.
+
+ Deprecated: will be removed in a future breaking change.
+ """
diff --git a/src/runloop_api_client/types/network_policy_list_view.py b/src/runloop_api_client/types/network_policy_list_view.py
index 17bc7868f..ba031d0a9 100644
--- a/src/runloop_api_client/types/network_policy_list_view.py
+++ b/src/runloop_api_client/types/network_policy_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .network_policy_view import NetworkPolicyView
@@ -17,5 +17,8 @@ class NetworkPolicyListView(BaseModel):
network_policies: List[NetworkPolicyView]
"""The list of NetworkPolicies."""
- total_count: int
- """Total count of items in this response."""
+ total_count: Optional[int] = None
+ """Total count of items in this response.
+
+ Deprecated: will be removed in a future breaking change.
+ """
diff --git a/src/runloop_api_client/types/object_list_view.py b/src/runloop_api_client/types/object_list_view.py
index cfd546c0c..689c0899b 100644
--- a/src/runloop_api_client/types/object_list_view.py
+++ b/src/runloop_api_client/types/object_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .object_view import ObjectView
@@ -17,8 +17,14 @@ class ObjectListView(BaseModel):
objects: List[ObjectView]
"""List of Object entities."""
- remaining_count: int
- """Number of Objects remaining after this page."""
+ remaining_count: Optional[int] = None
+ """Number of Objects remaining after this page.
- total_count: int
- """Total number of Objects across all pages."""
+ Deprecated: will be removed in a future breaking change.
+ """
+
+ total_count: Optional[int] = None
+ """Total number of Objects across all pages.
+
+ Deprecated: will be removed in a future breaking change.
+ """
diff --git a/src/runloop_api_client/types/repository_connection_list_view.py b/src/runloop_api_client/types/repository_connection_list_view.py
index 8085c4718..eea040bc4 100644
--- a/src/runloop_api_client/types/repository_connection_list_view.py
+++ b/src/runloop_api_client/types/repository_connection_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .repository_connection_view import RepositoryConnectionView
@@ -11,9 +11,9 @@
class RepositoryConnectionListView(BaseModel):
has_more: bool
- remaining_count: int
-
repositories: List[RepositoryConnectionView]
"""List of repositories matching filter."""
- total_count: int
+ remaining_count: Optional[int] = None
+
+ total_count: Optional[int] = None
diff --git a/src/runloop_api_client/types/scenario_definition_list_view.py b/src/runloop_api_client/types/scenario_definition_list_view.py
index 5aa4ac3e4..f39cf1ed9 100644
--- a/src/runloop_api_client/types/scenario_definition_list_view.py
+++ b/src/runloop_api_client/types/scenario_definition_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .scenario_view import ScenarioView
@@ -11,9 +11,9 @@
class ScenarioDefinitionListView(BaseModel):
has_more: bool
- remaining_count: int
-
scenarios: List[ScenarioView]
"""List of Scenarios matching filter."""
- total_count: int
+ remaining_count: Optional[int] = None
+
+ total_count: Optional[int] = None
diff --git a/src/runloop_api_client/types/scenario_run_list_view.py b/src/runloop_api_client/types/scenario_run_list_view.py
index db0d16d31..142292dda 100644
--- a/src/runloop_api_client/types/scenario_run_list_view.py
+++ b/src/runloop_api_client/types/scenario_run_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .scenario_run_view import ScenarioRunView
@@ -11,9 +11,9 @@
class ScenarioRunListView(BaseModel):
has_more: bool
- remaining_count: int
-
runs: List[ScenarioRunView]
"""List of ScenarioRuns matching filter."""
- total_count: int
+ remaining_count: Optional[int] = None
+
+ total_count: Optional[int] = None
diff --git a/src/runloop_api_client/types/scenario_view.py b/src/runloop_api_client/types/scenario_view.py
index 4604bd957..6685bbce2 100644
--- a/src/runloop_api_client/types/scenario_view.py
+++ b/src/runloop_api_client/types/scenario_view.py
@@ -31,6 +31,9 @@ class ScenarioView(BaseModel):
scoring_contract: ScoringContract
"""The scoring contract for the Scenario."""
+ status: str
+ """The state of the scenario."""
+
environment: Optional[ScenarioEnvironment] = None
"""The Environment in which the Scenario is run."""
diff --git a/src/runloop_api_client/types/secret_list_view.py b/src/runloop_api_client/types/secret_list_view.py
index 4d66fa2e4..2f10bae2c 100644
--- a/src/runloop_api_client/types/secret_list_view.py
+++ b/src/runloop_api_client/types/secret_list_view.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from .._models import BaseModel
from .secret_view import SecretView
@@ -14,11 +14,17 @@ class SecretListView(BaseModel):
has_more: bool
"""True if there are more results available beyond this page."""
- remaining_count: int
- """Number of Secrets remaining after this page."""
-
secrets: List[SecretView]
"""List of Secret objects. Values are omitted for security."""
- total_count: int
- """Total number of Secrets across all pages."""
+ remaining_count: Optional[int] = None
+ """Number of Secrets remaining after this page.
+
+ Deprecated: will be removed in a future breaking change.
+ """
+
+ total_count: Optional[int] = None
+ """Total number of Secrets across all pages.
+
+ Deprecated: will be removed in a future breaking change.
+ """
diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py
index eef022a71..12c9f5187 100644
--- a/tests/api_resources/test_devboxes.py
+++ b/tests/api_resources/test_devboxes.py
@@ -893,14 +893,22 @@ def test_path_params_retrieve_resource_usage(self, client: Runloop) -> None:
@parametrize
def test_method_shutdown(self, client: Runloop) -> None:
devbox = client.devboxes.shutdown(
- "id",
+ id="id",
+ )
+ assert_matches_type(DevboxView, devbox, path=["response"])
+
+ @parametrize
+ def test_method_shutdown_with_all_params(self, client: Runloop) -> None:
+ devbox = client.devboxes.shutdown(
+ id="id",
+ force="force",
)
assert_matches_type(DevboxView, devbox, path=["response"])
@parametrize
def test_raw_response_shutdown(self, client: Runloop) -> None:
response = client.devboxes.with_raw_response.shutdown(
- "id",
+ id="id",
)
assert response.is_closed is True
@@ -911,7 +919,7 @@ def test_raw_response_shutdown(self, client: Runloop) -> None:
@parametrize
def test_streaming_response_shutdown(self, client: Runloop) -> None:
with client.devboxes.with_streaming_response.shutdown(
- "id",
+ id="id",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -925,7 +933,7 @@ def test_streaming_response_shutdown(self, client: Runloop) -> None:
def test_path_params_shutdown(self, client: Runloop) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
client.devboxes.with_raw_response.shutdown(
- "",
+ id="",
)
@parametrize
@@ -2549,14 +2557,22 @@ async def test_path_params_retrieve_resource_usage(self, async_client: AsyncRunl
@parametrize
async def test_method_shutdown(self, async_client: AsyncRunloop) -> None:
devbox = await async_client.devboxes.shutdown(
- "id",
+ id="id",
+ )
+ assert_matches_type(DevboxView, devbox, path=["response"])
+
+ @parametrize
+ async def test_method_shutdown_with_all_params(self, async_client: AsyncRunloop) -> None:
+ devbox = await async_client.devboxes.shutdown(
+ id="id",
+ force="force",
)
assert_matches_type(DevboxView, devbox, path=["response"])
@parametrize
async def test_raw_response_shutdown(self, async_client: AsyncRunloop) -> None:
response = await async_client.devboxes.with_raw_response.shutdown(
- "id",
+ id="id",
)
assert response.is_closed is True
@@ -2567,7 +2583,7 @@ async def test_raw_response_shutdown(self, async_client: AsyncRunloop) -> None:
@parametrize
async def test_streaming_response_shutdown(self, async_client: AsyncRunloop) -> None:
async with async_client.devboxes.with_streaming_response.shutdown(
- "id",
+ id="id",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -2581,7 +2597,7 @@ async def test_streaming_response_shutdown(self, async_client: AsyncRunloop) ->
async def test_path_params_shutdown(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.shutdown(
- "",
+ id="",
)
@parametrize
From e6e5208c19360f24f10b3c0da84119f7eb356bb1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 20 Feb 2026 01:02:11 +0000
Subject: [PATCH 03/13] chore(scenarios): make scenario status enum instead of
string (#7552)
---
.stats.yml | 4 ++--
src/runloop_api_client/types/scenario_view.py | 7 +++++--
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 44040dae9..27e05a0be 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-8704a652545c3831c8b7e246ec17f1a626b59911eb5701ddc922ca6cae1c8d57.yml
-openapi_spec_hash: e609bc5ed3fdce498bc7d16921bbba5e
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-1ee005e7d8f97067abb96c6a38af53b69d362f2511ca775a9b78580a9643c253.yml
+openapi_spec_hash: 9c54d19abf7993fd55714759f3265f9c
config_hash: eb28692edd68a6ae95cf92af931c9976
diff --git a/src/runloop_api_client/types/scenario_view.py b/src/runloop_api_client/types/scenario_view.py
index 6685bbce2..3bb470708 100644
--- a/src/runloop_api_client/types/scenario_view.py
+++ b/src/runloop_api_client/types/scenario_view.py
@@ -31,8 +31,11 @@ class ScenarioView(BaseModel):
scoring_contract: ScoringContract
"""The scoring contract for the Scenario."""
- status: str
- """The state of the scenario."""
+ status: Literal["active", "archived"]
+ """Whether the scenario is active or archived.
+
+ Archived scenarios are excluded from listings and cannot be updated.
+ """
environment: Optional[ScenarioEnvironment] = None
"""The Environment in which the Scenario is run."""
From 273fb5ec761bcc5948571d9757780d86b6f0f50d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 20:32:18 +0000
Subject: [PATCH 04/13] chore: update mock server docs
---
CONTRIBUTING.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9201cf4cd..cdf385047 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -88,8 +88,7 @@ $ pip install ./path-to-wheel-file.whl
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
```sh
-# you will need npm installed
-npx prism mock path/to/your/openapi.yml
+$ ./scripts/mock
```
```sh
From 836f18d52c20b4b43f9135a52571f8c7e87a0477 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Feb 2026 00:39:49 +0000
Subject: [PATCH 05/13] chore: Add archive method to stainless (#7537)
---
.stats.yml | 8 +-
api.md | 1 +
.../resources/devboxes/devboxes.py | 22 ++++-
.../resources/scenarios/scenarios.py | 96 +++++++++++++++++++
.../types/devbox_create_params.py | 6 ++
.../types/devbox_enable_tunnel_params.py | 6 ++
src/runloop_api_client/types/tunnel_view.py | 6 ++
tests/api_resources/test_devboxes.py | 12 ++-
tests/api_resources/test_scenarios.py | 76 +++++++++++++++
9 files changed, 225 insertions(+), 8 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 27e05a0be..a869301e9 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-1ee005e7d8f97067abb96c6a38af53b69d362f2511ca775a9b78580a9643c253.yml
-openapi_spec_hash: 9c54d19abf7993fd55714759f3265f9c
-config_hash: eb28692edd68a6ae95cf92af931c9976
+configured_endpoints: 118
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-1a92de99959f20ab5850a7f89db21cb66269b2e0002ffc5e5afbe660d05b0768.yml
+openapi_spec_hash: 41e4660f26e27e6ab94d223da4ec1253
+config_hash: cbda3692cb48ab8582a0df1674b9e5c8
diff --git a/api.md b/api.md
index 2c8438e72..1c0166794 100644
--- a/api.md
+++ b/api.md
@@ -277,6 +277,7 @@ Methods:
- client.scenarios.retrieve(id) -> ScenarioView
- client.scenarios.update(id, \*\*params) -> ScenarioView
- client.scenarios.list(\*\*params) -> SyncScenariosCursorIDPage[ScenarioView]
+- client.scenarios.archive(id) -> ScenarioView
- client.scenarios.list_public(\*\*params) -> SyncScenariosCursorIDPage[ScenarioView]
- client.scenarios.start_run(\*\*params) -> ScenarioRunView
diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py
index 84a286521..79ac0022d 100644
--- a/src/runloop_api_client/resources/devboxes/devboxes.py
+++ b/src/runloop_api_client/resources/devboxes/devboxes.py
@@ -807,6 +807,7 @@ def enable_tunnel(
id: str,
*,
auth_mode: Optional[Literal["open", "authenticated"]] | Omit = omit,
+ http_keep_alive: Optional[bool] | 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,
@@ -826,6 +827,9 @@ def enable_tunnel(
Args:
auth_mode: Authentication mode for the tunnel. Defaults to 'public' if not specified.
+ http_keep_alive: When true, HTTP traffic through the tunnel counts as activity for idle lifecycle
+ policies, resetting the idle timer. Defaults to true if not specified.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -840,7 +844,13 @@ def enable_tunnel(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
f"/v1/devboxes/{id}/enable_tunnel",
- body=maybe_transform({"auth_mode": auth_mode}, devbox_enable_tunnel_params.DevboxEnableTunnelParams),
+ body=maybe_transform(
+ {
+ "auth_mode": auth_mode,
+ "http_keep_alive": http_keep_alive,
+ },
+ devbox_enable_tunnel_params.DevboxEnableTunnelParams,
+ ),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -2472,6 +2482,7 @@ async def enable_tunnel(
id: str,
*,
auth_mode: Optional[Literal["open", "authenticated"]] | Omit = omit,
+ http_keep_alive: Optional[bool] | 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,
@@ -2491,6 +2502,9 @@ async def enable_tunnel(
Args:
auth_mode: Authentication mode for the tunnel. Defaults to 'public' if not specified.
+ http_keep_alive: When true, HTTP traffic through the tunnel counts as activity for idle lifecycle
+ policies, resetting the idle timer. Defaults to true if not specified.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -2506,7 +2520,11 @@ async def enable_tunnel(
return await self._post(
f"/v1/devboxes/{id}/enable_tunnel",
body=await async_maybe_transform(
- {"auth_mode": auth_mode}, devbox_enable_tunnel_params.DevboxEnableTunnelParams
+ {
+ "auth_mode": auth_mode,
+ "http_keep_alive": http_keep_alive,
+ },
+ devbox_enable_tunnel_params.DevboxEnableTunnelParams,
),
options=make_request_options(
extra_headers=extra_headers,
diff --git a/src/runloop_api_client/resources/scenarios/scenarios.py b/src/runloop_api_client/resources/scenarios/scenarios.py
index b4a822cd3..5c31e4282 100644
--- a/src/runloop_api_client/resources/scenarios/scenarios.py
+++ b/src/runloop_api_client/resources/scenarios/scenarios.py
@@ -353,6 +353,48 @@ def list(
model=ScenarioView,
)
+ def archive(
+ 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,
+ ) -> ScenarioView:
+ """Archive a previously created Scenario.
+
+ The scenario will no longer appear in
+ list endpoints but can still be retrieved by ID.
+
+ 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/scenarios/{id}/archive",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=ScenarioView,
+ )
+
def list_public(
self,
*,
@@ -823,6 +865,48 @@ def list(
model=ScenarioView,
)
+ async def archive(
+ 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,
+ ) -> ScenarioView:
+ """Archive a previously created Scenario.
+
+ The scenario will no longer appear in
+ list endpoints but can still be retrieved by ID.
+
+ 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/scenarios/{id}/archive",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=ScenarioView,
+ )
+
def list_public(
self,
*,
@@ -1005,6 +1089,9 @@ def __init__(self, scenarios: ScenariosResource) -> None:
self.list = to_raw_response_wrapper(
scenarios.list,
)
+ self.archive = to_raw_response_wrapper(
+ scenarios.archive,
+ )
self.list_public = to_raw_response_wrapper(
scenarios.list_public,
)
@@ -1037,6 +1124,9 @@ def __init__(self, scenarios: AsyncScenariosResource) -> None:
self.list = async_to_raw_response_wrapper(
scenarios.list,
)
+ self.archive = async_to_raw_response_wrapper(
+ scenarios.archive,
+ )
self.list_public = async_to_raw_response_wrapper(
scenarios.list_public,
)
@@ -1069,6 +1159,9 @@ def __init__(self, scenarios: ScenariosResource) -> None:
self.list = to_streamed_response_wrapper(
scenarios.list,
)
+ self.archive = to_streamed_response_wrapper(
+ scenarios.archive,
+ )
self.list_public = to_streamed_response_wrapper(
scenarios.list_public,
)
@@ -1101,6 +1194,9 @@ def __init__(self, scenarios: AsyncScenariosResource) -> None:
self.list = async_to_streamed_response_wrapper(
scenarios.list,
)
+ self.archive = async_to_streamed_response_wrapper(
+ scenarios.archive,
+ )
self.list_public = async_to_streamed_response_wrapper(
scenarios.list_public,
)
diff --git a/src/runloop_api_client/types/devbox_create_params.py b/src/runloop_api_client/types/devbox_create_params.py
index ae2f0db59..f916d55f4 100644
--- a/src/runloop_api_client/types/devbox_create_params.py
+++ b/src/runloop_api_client/types/devbox_create_params.py
@@ -142,3 +142,9 @@ class Tunnel(TypedDict, total=False):
auth_mode: Optional[Literal["open", "authenticated"]]
"""Authentication mode for the tunnel. Defaults to 'public' if not specified."""
+
+ http_keep_alive: Optional[bool]
+ """
+ When true, HTTP traffic through the tunnel counts as activity for idle lifecycle
+ policies, resetting the idle timer. Defaults to true if not specified.
+ """
diff --git a/src/runloop_api_client/types/devbox_enable_tunnel_params.py b/src/runloop_api_client/types/devbox_enable_tunnel_params.py
index 9ccc53e75..4906c85ef 100644
--- a/src/runloop_api_client/types/devbox_enable_tunnel_params.py
+++ b/src/runloop_api_client/types/devbox_enable_tunnel_params.py
@@ -11,3 +11,9 @@
class DevboxEnableTunnelParams(TypedDict, total=False):
auth_mode: Optional[Literal["open", "authenticated"]]
"""Authentication mode for the tunnel. Defaults to 'public' if not specified."""
+
+ http_keep_alive: Optional[bool]
+ """
+ When true, HTTP traffic through the tunnel counts as activity for idle lifecycle
+ policies, resetting the idle timer. Defaults to true if not specified.
+ """
diff --git a/src/runloop_api_client/types/tunnel_view.py b/src/runloop_api_client/types/tunnel_view.py
index 540a44e7c..9287cd4d2 100644
--- a/src/runloop_api_client/types/tunnel_view.py
+++ b/src/runloop_api_client/types/tunnel_view.py
@@ -21,6 +21,12 @@ class TunnelView(BaseModel):
create_time_ms: int
"""Creation time of the tunnel (Unix timestamp milliseconds)."""
+ http_keep_alive: bool
+ """
+ When true, HTTP traffic through the tunnel counts as activity for idle lifecycle
+ policies, resetting the idle timer.
+ """
+
tunnel_key: str
"""The encrypted tunnel key used to construct the tunnel URL.
diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py
index 12c9f5187..c6f6625e3 100644
--- a/tests/api_resources/test_devboxes.py
+++ b/tests/api_resources/test_devboxes.py
@@ -111,7 +111,10 @@ 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"},
+ tunnel={
+ "auth_mode": "open",
+ "http_keep_alive": True,
+ },
)
assert_matches_type(DevboxView, devbox, path=["response"])
@@ -447,6 +450,7 @@ def test_method_enable_tunnel_with_all_params(self, client: Runloop) -> None:
devbox = client.devboxes.enable_tunnel(
id="id",
auth_mode="open",
+ http_keep_alive=True,
)
assert_matches_type(TunnelView, devbox, path=["response"])
@@ -1775,7 +1779,10 @@ 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"},
+ tunnel={
+ "auth_mode": "open",
+ "http_keep_alive": True,
+ },
)
assert_matches_type(DevboxView, devbox, path=["response"])
@@ -2111,6 +2118,7 @@ async def test_method_enable_tunnel_with_all_params(self, async_client: AsyncRun
devbox = await async_client.devboxes.enable_tunnel(
id="id",
auth_mode="open",
+ http_keep_alive=True,
)
assert_matches_type(TunnelView, devbox, path=["response"])
diff --git a/tests/api_resources/test_scenarios.py b/tests/api_resources/test_scenarios.py
index 5b79fd558..736a8395c 100644
--- a/tests/api_resources/test_scenarios.py
+++ b/tests/api_resources/test_scenarios.py
@@ -319,6 +319,44 @@ def test_streaming_response_list(self, client: Runloop) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_archive(self, client: Runloop) -> None:
+ scenario = client.scenarios.archive(
+ "id",
+ )
+ assert_matches_type(ScenarioView, scenario, path=["response"])
+
+ @parametrize
+ def test_raw_response_archive(self, client: Runloop) -> None:
+ response = client.scenarios.with_raw_response.archive(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ scenario = response.parse()
+ assert_matches_type(ScenarioView, scenario, path=["response"])
+
+ @parametrize
+ def test_streaming_response_archive(self, client: Runloop) -> None:
+ with client.scenarios.with_streaming_response.archive(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ scenario = response.parse()
+ assert_matches_type(ScenarioView, scenario, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_archive(self, client: Runloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.scenarios.with_raw_response.archive(
+ "",
+ )
+
@parametrize
def test_method_list_public(self, client: Runloop) -> None:
scenario = client.scenarios.list_public()
@@ -730,6 +768,44 @@ async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_method_archive(self, async_client: AsyncRunloop) -> None:
+ scenario = await async_client.scenarios.archive(
+ "id",
+ )
+ assert_matches_type(ScenarioView, scenario, path=["response"])
+
+ @parametrize
+ async def test_raw_response_archive(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.scenarios.with_raw_response.archive(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ scenario = await response.parse()
+ assert_matches_type(ScenarioView, scenario, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_archive(self, async_client: AsyncRunloop) -> None:
+ async with async_client.scenarios.with_streaming_response.archive(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ scenario = await response.parse()
+ assert_matches_type(ScenarioView, scenario, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_archive(self, async_client: AsyncRunloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.scenarios.with_raw_response.archive(
+ "",
+ )
+
@parametrize
async def test_method_list_public(self, async_client: AsyncRunloop) -> None:
scenario = await async_client.scenarios.list_public()
From e636aca4eedb81b35d80bd6f5229cc71153ec545 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Feb 2026 04:21:08 +0000
Subject: [PATCH 06/13] chore(internal): add request options to SSE classes
---
src/runloop_api_client/_response.py | 3 +++
src/runloop_api_client/_streaming.py | 9 +++++++--
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/runloop_api_client/_response.py b/src/runloop_api_client/_response.py
index 31591fad8..ddafda4fe 100644
--- a/src/runloop_api_client/_response.py
+++ b/src/runloop_api_client/_response.py
@@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
),
response=self.http_response,
client=cast(Any, self._client),
+ options=self._options,
),
)
@@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
cast_to=extract_stream_chunk_type(self._stream_cls),
response=self.http_response,
client=cast(Any, self._client),
+ options=self._options,
),
)
@@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
cast_to=cast_to,
response=self.http_response,
client=cast(Any, self._client),
+ options=self._options,
),
)
diff --git a/src/runloop_api_client/_streaming.py b/src/runloop_api_client/_streaming.py
index 509365648..ec516672b 100644
--- a/src/runloop_api_client/_streaming.py
+++ b/src/runloop_api_client/_streaming.py
@@ -32,6 +32,7 @@
if TYPE_CHECKING:
from ._client import Runloop, AsyncRunloop
+ from ._models import FinalRequestOptions
_T = TypeVar("_T")
@@ -41,7 +42,7 @@ class Stream(Generic[_T]):
"""Provides the core interface to iterate over a synchronous stream response."""
response: httpx.Response
-
+ _options: Optional[FinalRequestOptions] = None
_decoder: SSEBytesDecoder
def __init__(
@@ -50,10 +51,12 @@ def __init__(
cast_to: type[_T],
response: httpx.Response,
client: Runloop,
+ options: Optional[FinalRequestOptions] = None,
) -> None:
self.response = response
self._cast_to = cast_to
self._client = client
+ self._options = options
self._decoder = client._make_sse_decoder()
self._iterator = self.__stream__()
@@ -114,7 +117,7 @@ class AsyncStream(Generic[_T]):
"""Provides the core interface to iterate over an asynchronous stream response."""
response: httpx.Response
-
+ _options: Optional[FinalRequestOptions] = None
_decoder: SSEDecoder | SSEBytesDecoder
def __init__(
@@ -123,10 +126,12 @@ def __init__(
cast_to: type[_T],
response: httpx.Response,
client: AsyncRunloop,
+ options: Optional[FinalRequestOptions] = None,
) -> None:
self.response = response
self._cast_to = cast_to
self._client = client
+ self._options = options
self._decoder = client._make_sse_decoder()
self._iterator = self.__stream__()
From 564cd5730fbf94b7014390ee71d7595cb7d6e71a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Feb 2026 04:35:11 +0000
Subject: [PATCH 07/13] chore(internal): make
`test_proxy_environment_variables` more resilient
---
tests/test_client.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tests/test_client.py b/tests/test_client.py
index 3f5426415..f2b774db7 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1002,6 +1002,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
+ # Delete in case our environment has this set
+ monkeypatch.delenv("HTTP_PROXY", raising=False)
client = DefaultHttpxClient()
@@ -1953,6 +1955,8 @@ async def test_get_platform(self) -> None:
async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
+ # Delete in case our environment has this set
+ monkeypatch.delenv("HTTP_PROXY", raising=False)
client = DefaultAsyncHttpxClient()
From 21d4ce865094d9272841871eefde3cf15c183d19 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 04:17:46 +0000
Subject: [PATCH 08/13] chore(internal): make
`test_proxy_environment_variables` more resilient to env
---
tests/test_client.py | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index f2b774db7..725862258 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1002,8 +1002,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
- # Delete in case our environment has this set
+ # Delete in case our environment has any proxy env vars set
monkeypatch.delenv("HTTP_PROXY", raising=False)
+ monkeypatch.delenv("ALL_PROXY", raising=False)
+ monkeypatch.delenv("NO_PROXY", raising=False)
+ monkeypatch.delenv("http_proxy", raising=False)
+ monkeypatch.delenv("https_proxy", raising=False)
+ monkeypatch.delenv("all_proxy", raising=False)
+ monkeypatch.delenv("no_proxy", raising=False)
client = DefaultHttpxClient()
@@ -1955,8 +1961,14 @@ async def test_get_platform(self) -> None:
async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
- # Delete in case our environment has this set
+ # Delete in case our environment has any proxy env vars set
monkeypatch.delenv("HTTP_PROXY", raising=False)
+ monkeypatch.delenv("ALL_PROXY", raising=False)
+ monkeypatch.delenv("NO_PROXY", raising=False)
+ monkeypatch.delenv("http_proxy", raising=False)
+ monkeypatch.delenv("https_proxy", raising=False)
+ monkeypatch.delenv("all_proxy", raising=False)
+ monkeypatch.delenv("no_proxy", raising=False)
client = DefaultAsyncHttpxClient()
From 85f2146d2f25085d19d5bf34096066b0ba18cb5d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 19:05:09 +0000
Subject: [PATCH 09/13] feat: Add AI gateway and MCP gateway flags to network
policy create (#7638)
---
.stats.yml | 4 +--
.../resources/network_policies.py | 36 +++++++++++++++++++
.../types/network_policy_create_params.py | 12 +++++++
.../types/network_policy_update_params.py | 6 ++++
.../types/network_policy_view.py | 9 +++++
tests/api_resources/test_network_policies.py | 8 +++++
6 files changed, 73 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index a869301e9..e8c2e4563 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 118
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-1a92de99959f20ab5850a7f89db21cb66269b2e0002ffc5e5afbe660d05b0768.yml
-openapi_spec_hash: 41e4660f26e27e6ab94d223da4ec1253
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-dd0f43e15cb67179deaf86f83ae04c6fae4ff858ee33e82a3b89231566cdc5bb.yml
+openapi_spec_hash: 569176c1c4f48efd25a44fa526fad9d1
config_hash: cbda3692cb48ab8582a0df1674b9e5c8
diff --git a/src/runloop_api_client/resources/network_policies.py b/src/runloop_api_client/resources/network_policies.py
index 1cda5b42d..388126cee 100644
--- a/src/runloop_api_client/resources/network_policies.py
+++ b/src/runloop_api_client/resources/network_policies.py
@@ -48,8 +48,10 @@ def create(
self,
*,
name: str,
+ allow_ai_gateway: Optional[bool] | Omit = omit,
allow_all: Optional[bool] | Omit = omit,
allow_devbox_to_devbox: Optional[bool] | Omit = omit,
+ allow_mcp_gateway: Optional[bool] | Omit = omit,
allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit,
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.
@@ -69,6 +71,9 @@ def create(
name: The human-readable name for the NetworkPolicy. Must be unique within the
account.
+ allow_ai_gateway: (Optional) If true, allows devbox egress to the AI credential gateway for
+ credential proxying. Defaults to false.
+
allow_all: (Optional) If true, all egress traffic is allowed (ALLOW_ALL policy). Defaults
to false.
@@ -76,6 +81,9 @@ def create(
tunnels. Defaults to false. If allow_all is true, this is automatically set to
true.
+ allow_mcp_gateway: (Optional) If true, allows devbox egress to the MCP hub for MCP server access.
+ Defaults to false.
+
allowed_hostnames: (Optional) DNS-based allow list with wildcard support. Examples: ['github.com',
'*.npmjs.org'].
@@ -96,8 +104,10 @@ def create(
body=maybe_transform(
{
"name": name,
+ "allow_ai_gateway": allow_ai_gateway,
"allow_all": allow_all,
"allow_devbox_to_devbox": allow_devbox_to_devbox,
+ "allow_mcp_gateway": allow_mcp_gateway,
"allowed_hostnames": allowed_hostnames,
"description": description,
},
@@ -150,8 +160,10 @@ def update(
self,
id: str,
*,
+ allow_ai_gateway: Optional[bool] | Omit = omit,
allow_all: Optional[bool] | Omit = omit,
allow_devbox_to_devbox: Optional[bool] | Omit = omit,
+ allow_mcp_gateway: Optional[bool] | Omit = omit,
allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit,
description: Optional[str] | Omit = omit,
name: Optional[str] | Omit = omit,
@@ -168,10 +180,14 @@ def update(
All fields are optional.
Args:
+ allow_ai_gateway: If true, allows devbox egress to the AI credential gateway.
+
allow_all: If true, all egress traffic is allowed (ALLOW_ALL policy).
allow_devbox_to_devbox: If true, allows traffic between the account's own devboxes via tunnels.
+ allow_mcp_gateway: If true, allows devbox egress to the MCP hub.
+
allowed_hostnames: Updated DNS-based allow list with wildcard support. Examples: ['github.com',
'*.npmjs.org'].
@@ -195,8 +211,10 @@ def update(
f"/v1/network-policies/{id}",
body=maybe_transform(
{
+ "allow_ai_gateway": allow_ai_gateway,
"allow_all": allow_all,
"allow_devbox_to_devbox": allow_devbox_to_devbox,
+ "allow_mcp_gateway": allow_mcp_gateway,
"allowed_hostnames": allowed_hostnames,
"description": description,
"name": name,
@@ -334,8 +352,10 @@ async def create(
self,
*,
name: str,
+ allow_ai_gateway: Optional[bool] | Omit = omit,
allow_all: Optional[bool] | Omit = omit,
allow_devbox_to_devbox: Optional[bool] | Omit = omit,
+ allow_mcp_gateway: Optional[bool] | Omit = omit,
allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit,
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.
@@ -355,6 +375,9 @@ async def create(
name: The human-readable name for the NetworkPolicy. Must be unique within the
account.
+ allow_ai_gateway: (Optional) If true, allows devbox egress to the AI credential gateway for
+ credential proxying. Defaults to false.
+
allow_all: (Optional) If true, all egress traffic is allowed (ALLOW_ALL policy). Defaults
to false.
@@ -362,6 +385,9 @@ async def create(
tunnels. Defaults to false. If allow_all is true, this is automatically set to
true.
+ allow_mcp_gateway: (Optional) If true, allows devbox egress to the MCP hub for MCP server access.
+ Defaults to false.
+
allowed_hostnames: (Optional) DNS-based allow list with wildcard support. Examples: ['github.com',
'*.npmjs.org'].
@@ -382,8 +408,10 @@ async def create(
body=await async_maybe_transform(
{
"name": name,
+ "allow_ai_gateway": allow_ai_gateway,
"allow_all": allow_all,
"allow_devbox_to_devbox": allow_devbox_to_devbox,
+ "allow_mcp_gateway": allow_mcp_gateway,
"allowed_hostnames": allowed_hostnames,
"description": description,
},
@@ -436,8 +464,10 @@ async def update(
self,
id: str,
*,
+ allow_ai_gateway: Optional[bool] | Omit = omit,
allow_all: Optional[bool] | Omit = omit,
allow_devbox_to_devbox: Optional[bool] | Omit = omit,
+ allow_mcp_gateway: Optional[bool] | Omit = omit,
allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit,
description: Optional[str] | Omit = omit,
name: Optional[str] | Omit = omit,
@@ -454,10 +484,14 @@ async def update(
All fields are optional.
Args:
+ allow_ai_gateway: If true, allows devbox egress to the AI credential gateway.
+
allow_all: If true, all egress traffic is allowed (ALLOW_ALL policy).
allow_devbox_to_devbox: If true, allows traffic between the account's own devboxes via tunnels.
+ allow_mcp_gateway: If true, allows devbox egress to the MCP hub.
+
allowed_hostnames: Updated DNS-based allow list with wildcard support. Examples: ['github.com',
'*.npmjs.org'].
@@ -481,8 +515,10 @@ async def update(
f"/v1/network-policies/{id}",
body=await async_maybe_transform(
{
+ "allow_ai_gateway": allow_ai_gateway,
"allow_all": allow_all,
"allow_devbox_to_devbox": allow_devbox_to_devbox,
+ "allow_mcp_gateway": allow_mcp_gateway,
"allowed_hostnames": allowed_hostnames,
"description": description,
"name": name,
diff --git a/src/runloop_api_client/types/network_policy_create_params.py b/src/runloop_api_client/types/network_policy_create_params.py
index e1a718a48..91a023188 100644
--- a/src/runloop_api_client/types/network_policy_create_params.py
+++ b/src/runloop_api_client/types/network_policy_create_params.py
@@ -17,6 +17,12 @@ class NetworkPolicyCreateParams(TypedDict, total=False):
Must be unique within the account.
"""
+ allow_ai_gateway: Optional[bool]
+ """
+ (Optional) If true, allows devbox egress to the AI credential gateway for
+ credential proxying. Defaults to false.
+ """
+
allow_all: Optional[bool]
"""(Optional) If true, all egress traffic is allowed (ALLOW_ALL policy).
@@ -30,6 +36,12 @@ class NetworkPolicyCreateParams(TypedDict, total=False):
true.
"""
+ allow_mcp_gateway: Optional[bool]
+ """(Optional) If true, allows devbox egress to the MCP hub for MCP server access.
+
+ Defaults to false.
+ """
+
allowed_hostnames: Optional[SequenceNotStr[str]]
"""(Optional) DNS-based allow list with wildcard support.
diff --git a/src/runloop_api_client/types/network_policy_update_params.py b/src/runloop_api_client/types/network_policy_update_params.py
index 22e9550ff..ad7c62992 100644
--- a/src/runloop_api_client/types/network_policy_update_params.py
+++ b/src/runloop_api_client/types/network_policy_update_params.py
@@ -11,12 +11,18 @@
class NetworkPolicyUpdateParams(TypedDict, total=False):
+ allow_ai_gateway: Optional[bool]
+ """If true, allows devbox egress to the AI credential gateway."""
+
allow_all: Optional[bool]
"""If true, all egress traffic is allowed (ALLOW_ALL policy)."""
allow_devbox_to_devbox: Optional[bool]
"""If true, allows traffic between the account's own devboxes via tunnels."""
+ allow_mcp_gateway: Optional[bool]
+ """If true, allows devbox egress to the MCP hub."""
+
allowed_hostnames: Optional[SequenceNotStr[str]]
"""Updated DNS-based allow list with wildcard support.
diff --git a/src/runloop_api_client/types/network_policy_view.py b/src/runloop_api_client/types/network_policy_view.py
index 4d743cc8a..197902b02 100644
--- a/src/runloop_api_client/types/network_policy_view.py
+++ b/src/runloop_api_client/types/network_policy_view.py
@@ -10,6 +10,12 @@
class Egress(BaseModel):
"""The egress rules for this policy."""
+ allow_ai_gateway: bool
+ """
+ If true, allows devbox egress to the AI credential gateway for credential
+ proxying.
+ """
+
allow_all: bool
"""If true, all egress traffic is allowed and other fields are ignored.
@@ -19,6 +25,9 @@ class Egress(BaseModel):
allow_devbox_to_devbox: bool
"""If true, allows traffic between the account's own devboxes via tunnels."""
+ allow_mcp_gateway: bool
+ """If true, allows devbox egress to the MCP hub for MCP server access."""
+
allowed_hostnames: List[str]
"""DNS-based allow list with wildcard support.
diff --git a/tests/api_resources/test_network_policies.py b/tests/api_resources/test_network_policies.py
index a528b4256..22a868cc4 100644
--- a/tests/api_resources/test_network_policies.py
+++ b/tests/api_resources/test_network_policies.py
@@ -31,8 +31,10 @@ def test_method_create(self, client: Runloop) -> None:
def test_method_create_with_all_params(self, client: Runloop) -> None:
network_policy = client.network_policies.create(
name="name",
+ allow_ai_gateway=True,
allow_all=True,
allow_devbox_to_devbox=True,
+ allow_mcp_gateway=True,
allowed_hostnames=["string"],
description="description",
)
@@ -111,8 +113,10 @@ def test_method_update(self, client: Runloop) -> None:
def test_method_update_with_all_params(self, client: Runloop) -> None:
network_policy = client.network_policies.update(
id="id",
+ allow_ai_gateway=True,
allow_all=True,
allow_devbox_to_devbox=True,
+ allow_mcp_gateway=True,
allowed_hostnames=["string"],
description="description",
name="name",
@@ -240,8 +244,10 @@ async def test_method_create(self, async_client: AsyncRunloop) -> None:
async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None:
network_policy = await async_client.network_policies.create(
name="name",
+ allow_ai_gateway=True,
allow_all=True,
allow_devbox_to_devbox=True,
+ allow_mcp_gateway=True,
allowed_hostnames=["string"],
description="description",
)
@@ -320,8 +326,10 @@ async def test_method_update(self, async_client: AsyncRunloop) -> None:
async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None:
network_policy = await async_client.network_policies.update(
id="id",
+ allow_ai_gateway=True,
allow_all=True,
allow_devbox_to_devbox=True,
+ allow_mcp_gateway=True,
allowed_hostnames=["string"],
description="description",
name="name",
From ce6c53c3499499bacf278130e5f984ec43148d20 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 20:33:17 +0000
Subject: [PATCH 10/13] release: 1.9.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 25 +++++++++++++++++++++++++
pyproject.toml | 2 +-
src/runloop_api_client/_version.py | 2 +-
4 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index c523ce19f..c3c95522a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.8.0"
+ ".": "1.9.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78604da88..984b847cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
# Changelog
+## 1.9.0 (2026-02-25)
+
+Full Changelog: [v1.8.0...v1.9.0](https://github.com/runloopai/api-client-python/compare/v1.8.0...v1.9.0)
+
+### Features
+
+* Add AI gateway and MCP gateway flags to network policy create ([#7638](https://github.com/runloopai/api-client-python/issues/7638)) ([85f2146](https://github.com/runloopai/api-client-python/commit/85f2146d2f25085d19d5bf34096066b0ba18cb5d))
+
+
+### Chores
+
+* Add archive method to stainless ([#7537](https://github.com/runloopai/api-client-python/issues/7537)) ([836f18d](https://github.com/runloopai/api-client-python/commit/836f18d52c20b4b43f9135a52571f8c7e87a0477))
+* **benchmarks:** removed scorer ([#740](https://github.com/runloopai/api-client-python/issues/740)) ([1033264](https://github.com/runloopai/api-client-python/commit/1033264b0ed34cc47e2e16cac3ae5986f2a4e381))
+* format all `api.md` files ([ee5d577](https://github.com/runloopai/api-client-python/commit/ee5d5773d408ac8e61101fd0187ec071709bebab))
+* **internal:** add request options to SSE classes ([e636aca](https://github.com/runloopai/api-client-python/commit/e636aca4eedb81b35d80bd6f5229cc71153ec545))
+* **internal:** make `test_proxy_environment_variables` more resilient ([564cd57](https://github.com/runloopai/api-client-python/commit/564cd5730fbf94b7014390ee71d7595cb7d6e71a))
+* **internal:** make `test_proxy_environment_variables` more resilient to env ([21d4ce8](https://github.com/runloopai/api-client-python/commit/21d4ce865094d9272841871eefde3cf15c183d19))
+* **scenarios:** make scenario status enum instead of string ([#7552](https://github.com/runloopai/api-client-python/issues/7552)) ([e6e5208](https://github.com/runloopai/api-client-python/commit/e6e5208c19360f24f10b3c0da84119f7eb356bb1))
+* update mock server docs ([273fb5e](https://github.com/runloopai/api-client-python/commit/273fb5ec761bcc5948571d9757780d86b6f0f50d))
+
+
+### Refactors
+
+* deprecate and make Nullable total_count and remaining_count of ListViews ([#7533](https://github.com/runloopai/api-client-python/issues/7533)) ([8b16ebe](https://github.com/runloopai/api-client-python/commit/8b16ebe7aba9f96592f03ed2844211209b35d9e6))
+
## 1.8.0 (2026-02-12)
Full Changelog: [v1.7.0...v1.8.0](https://github.com/runloopai/api-client-python/compare/v1.7.0...v1.8.0)
diff --git a/pyproject.toml b/pyproject.toml
index c872715e3..8b0c80fa7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "runloop_api_client"
-version = "1.8.0"
+version = "1.9.0"
description = "The official Python library for the runloop API"
dynamic = ["readme"]
license = "MIT"
diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py
index da91dc1ce..f2dd2d557 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.8.0" # x-release-please-version
+__version__ = "1.9.0" # x-release-please-version
From 1a366d428305ce26bb9ee23e51c9689635494d73 Mon Sep 17 00:00:00 2001
From: Albert Li
Date: Wed, 25 Feb 2026 14:26:39 -0800
Subject: [PATCH 11/13] Fix tests
---
src/runloop_api_client/sdk/_types.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py
index 9e7f8780e..6b22c351e 100644
--- a/src/runloop_api_client/sdk/_types.py
+++ b/src/runloop_api_client/sdk/_types.py
@@ -1,4 +1,4 @@
-from typing import Union, Callable, Optional
+from typing import Literal, Union, Callable, Optional
from typing_extensions import TypedDict
from ..types import (
@@ -219,6 +219,9 @@ class ScenarioPreview(ScenarioView):
input_context: InputContextPreview # type: ignore[assignment]
"""The input context for the Scenario."""
+ status: Optional[Literal["active", "archived"]] = None # type: ignore[assignment]
+ """Status is not set for previews."""
+
class SDKBenchmarkCreateParams(BenchmarkCreateParams, LongRequestOptions):
pass
From f835bb736131213a712fb15f1604a7ee74942099 Mon Sep 17 00:00:00 2001
From: Albert Li
Date: Wed, 25 Feb 2026 14:30:27 -0800
Subject: [PATCH 12/13] lint
---
src/runloop_api_client/sdk/_types.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py
index 6b22c351e..1eea070a2 100644
--- a/src/runloop_api_client/sdk/_types.py
+++ b/src/runloop_api_client/sdk/_types.py
@@ -1,4 +1,4 @@
-from typing import Literal, Union, Callable, Optional
+from typing import Union, Literal, Callable, Optional
from typing_extensions import TypedDict
from ..types import (
From df0f1962e7a3eb875aa6d0b8161ccfe93c511eff Mon Sep 17 00:00:00 2001
From: Albert Li
Date: Wed, 25 Feb 2026 14:31:15 -0800
Subject: [PATCH 13/13] Update version
---
uv.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/uv.lock b/uv.lock
index dcf165cb8..26c8d993a 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2267,7 +2267,7 @@ wheels = [
[[package]]
name = "runloop-api-client"
-version = "1.8.0"
+version = "1.9.0"
source = { editable = "." }
dependencies = [
{ name = "anyio" },