From db66a278483f9608df47d3e7016a578aa7115bf6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 19 Nov 2024 08:49:13 -0800
Subject: [PATCH 01/10] release: 1.0.1 (#52)

* feat(api): api update (#48)

* feat(api): api update (#51)

* chore: rebuild project due to codegen change (#53)

* chore: rebuild project due to codegen change (#54)

* chore: rebuild project due to codegen change (#55)

* chore: rebuild project due to codegen change (#57)

* chore: rebuild project due to codegen change (#58)

* release: 1.0.1

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Stainless Bot <dev+git@stainlessapi.com>
---
 .release-please-manifest.json                 |  2 +-
 .stats.yml                                    |  2 +-
 CHANGELOG.md                                  | 18 ++++++++++++++++
 README.md                                     |  4 ++--
 pyproject.toml                                |  7 +++----
 src/browserbase/_compat.py                    |  6 ++++--
 src/browserbase/_models.py                    |  9 +++++---
 src/browserbase/_utils/__init__.py            |  1 +
 src/browserbase/_utils/_transform.py          | 14 +++++++++++--
 src/browserbase/_utils/_utils.py              | 17 +++++++++++++++
 src/browserbase/_version.py                   |  2 +-
 src/browserbase/resources/projects.py         |  4 ++--
 .../resources/sessions/sessions.py            |  4 ++--
 .../types/sessions/session_recording.py       |  2 --
 tests/api_resources/test_sessions.py          | 16 +++++++-------
 tests/test_client.py                          |  4 ++--
 tests/test_models.py                          | 21 +++++++------------
 tests/test_transform.py                       | 15 +++++++++++++
 18 files changed, 102 insertions(+), 46 deletions(-)

diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index fea3454..a8f7122 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.0.0"
+  ".": "1.0.1"
 }
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 1a6b2b5..d42b050 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,2 @@
 configured_endpoints: 18
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-b341dd9d5bb77c4f217b94b186763e730fd798fbb773a5e90bb4e2a8d4a2c822.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-7f88912695bab2b98cb73137e6f36125d02fdfaf8eed4532ee1c82385609a259.yml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a17bbc5..0df64a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
 # Changelog
 
+## 1.0.1 (2024-11-18)
+
+Full Changelog: [v1.0.0...v1.0.1](https://github.com/browserbase/sdk-python/compare/v1.0.0...v1.0.1)
+
+### Features
+
+* **api:** api update ([#48](https://github.com/browserbase/sdk-python/issues/48)) ([b17a3b8](https://github.com/browserbase/sdk-python/commit/b17a3b8e6984447421a7581ca56c0521cb3b55dd))
+* **api:** api update ([#51](https://github.com/browserbase/sdk-python/issues/51)) ([dc2da25](https://github.com/browserbase/sdk-python/commit/dc2da25d2e33d55e5655cbb8000fd4afdd6bbf62))
+
+
+### Chores
+
+* rebuild project due to codegen change ([#53](https://github.com/browserbase/sdk-python/issues/53)) ([b1684fa](https://github.com/browserbase/sdk-python/commit/b1684fa889aecf2fe7965a37ebd9c73977136ef6))
+* rebuild project due to codegen change ([#54](https://github.com/browserbase/sdk-python/issues/54)) ([e6a41da](https://github.com/browserbase/sdk-python/commit/e6a41dab6f0de6894a97067611166b1bc61893a2))
+* rebuild project due to codegen change ([#55](https://github.com/browserbase/sdk-python/issues/55)) ([ff17087](https://github.com/browserbase/sdk-python/commit/ff1708757bdeaa4e6b8d1959d1830105bd7f4b92))
+* rebuild project due to codegen change ([#57](https://github.com/browserbase/sdk-python/issues/57)) ([dfd0e19](https://github.com/browserbase/sdk-python/commit/dfd0e199c2447d4bd1b6704745d22f959a6b6bb1))
+* rebuild project due to codegen change ([#58](https://github.com/browserbase/sdk-python/issues/58)) ([f3be0be](https://github.com/browserbase/sdk-python/commit/f3be0bec13d95c65ab4cc81565b456cb566a62e2))
+
 ## 1.0.0 (2024-10-29)
 
 Full Changelog: [v1.0.0-alpha.0...v1.0.0](https://github.com/browserbase/sdk-python/compare/v1.0.0-alpha.0...v1.0.0)
diff --git a/README.md b/README.md
index c790a0f..2ddc05b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 [![PyPI version](https://img.shields.io/pypi/v/browserbase.svg)](https://pypi.org/project/browserbase/)
 
-The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.7+
+The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.8+
 application. The library includes type definitions for all request params and response fields,
 and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
 
@@ -340,7 +340,7 @@ print(browserbase.__version__)
 
 ## Requirements
 
-Python 3.7 or higher.
+Python 3.8 or higher.
 
 ## Contributing
 
diff --git a/pyproject.toml b/pyproject.toml
index 9cdf351..2e4cc0e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.0.0"
+version = "1.0.1"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
@@ -16,11 +16,10 @@ dependencies = [
     "sniffio",
     "cached-property; python_version < '3.8'",
 ]
-requires-python = ">= 3.7"
+requires-python = ">= 3.8"
 classifiers = [
   "Typing :: Typed",
   "Intended Audience :: Developers",
-  "Programming Language :: Python :: 3.7",
   "Programming Language :: Python :: 3.8",
   "Programming Language :: Python :: 3.9",
   "Programming Language :: Python :: 3.10",
@@ -148,7 +147,7 @@ filterwarnings = [
 # there are a couple of flags that are still disabled by
 # default in strict mode as they are experimental and niche.
 typeCheckingMode = "strict"
-pythonVersion = "3.7"
+pythonVersion = "3.8"
 
 exclude = [
     "_dev",
diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py
index d89920d..4794129 100644
--- a/src/browserbase/_compat.py
+++ b/src/browserbase/_compat.py
@@ -2,7 +2,7 @@
 
 from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
 from datetime import date, datetime
-from typing_extensions import Self
+from typing_extensions import Self, Literal
 
 import pydantic
 from pydantic.fields import FieldInfo
@@ -137,9 +137,11 @@ def model_dump(
     exclude_unset: bool = False,
     exclude_defaults: bool = False,
     warnings: bool = True,
+    mode: Literal["json", "python"] = "python",
 ) -> dict[str, Any]:
-    if PYDANTIC_V2:
+    if PYDANTIC_V2 or hasattr(model, "model_dump"):
         return model.model_dump(
+            mode=mode,
             exclude=exclude,
             exclude_unset=exclude_unset,
             exclude_defaults=exclude_defaults,
diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py
index 42551b7..6cb469e 100644
--- a/src/browserbase/_models.py
+++ b/src/browserbase/_models.py
@@ -37,6 +37,7 @@
     PropertyInfo,
     is_list,
     is_given,
+    json_safe,
     lru_cache,
     is_mapping,
     parse_date,
@@ -279,8 +280,8 @@ def model_dump(
             Returns:
                 A dictionary representation of the model.
             """
-            if mode != "python":
-                raise ValueError("mode is only supported in Pydantic v2")
+            if mode not in {"json", "python"}:
+                raise ValueError("mode must be either 'json' or 'python'")
             if round_trip != False:
                 raise ValueError("round_trip is only supported in Pydantic v2")
             if warnings != True:
@@ -289,7 +290,7 @@ def model_dump(
                 raise ValueError("context is only supported in Pydantic v2")
             if serialize_as_any != False:
                 raise ValueError("serialize_as_any is only supported in Pydantic v2")
-            return super().dict(  # pyright: ignore[reportDeprecated]
+            dumped = super().dict(  # pyright: ignore[reportDeprecated]
                 include=include,
                 exclude=exclude,
                 by_alias=by_alias,
@@ -298,6 +299,8 @@ def model_dump(
                 exclude_none=exclude_none,
             )
 
+            return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped
+
         @override
         def model_dump_json(
             self,
diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py
index 3efe66c..a7cff3c 100644
--- a/src/browserbase/_utils/__init__.py
+++ b/src/browserbase/_utils/__init__.py
@@ -6,6 +6,7 @@
     is_list as is_list,
     is_given as is_given,
     is_tuple as is_tuple,
+    json_safe as json_safe,
     lru_cache as lru_cache,
     is_mapping as is_mapping,
     is_tuple_t as is_tuple_t,
diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py
index 47e262a..a6b62ca 100644
--- a/src/browserbase/_utils/_transform.py
+++ b/src/browserbase/_utils/_transform.py
@@ -173,6 +173,11 @@ def _transform_recursive(
         # Iterable[T]
         or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
     ):
+        # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
+        # intended as an iterable, so we don't transform it.
+        if isinstance(data, dict):
+            return cast(object, data)
+
         inner_type = extract_type_arg(stripped_type, 0)
         return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
 
@@ -186,7 +191,7 @@ def _transform_recursive(
         return data
 
     if isinstance(data, pydantic.BaseModel):
-        return model_dump(data, exclude_unset=True)
+        return model_dump(data, exclude_unset=True, mode="json")
 
     annotated_type = _get_annotated_type(annotation)
     if annotated_type is None:
@@ -311,6 +316,11 @@ async def _async_transform_recursive(
         # Iterable[T]
         or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
     ):
+        # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
+        # intended as an iterable, so we don't transform it.
+        if isinstance(data, dict):
+            return cast(object, data)
+
         inner_type = extract_type_arg(stripped_type, 0)
         return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
 
@@ -324,7 +334,7 @@ async def _async_transform_recursive(
         return data
 
     if isinstance(data, pydantic.BaseModel):
-        return model_dump(data, exclude_unset=True)
+        return model_dump(data, exclude_unset=True, mode="json")
 
     annotated_type = _get_annotated_type(annotation)
     if annotated_type is None:
diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py
index 0bba17c..e5811bb 100644
--- a/src/browserbase/_utils/_utils.py
+++ b/src/browserbase/_utils/_utils.py
@@ -16,6 +16,7 @@
     overload,
 )
 from pathlib import Path
+from datetime import date, datetime
 from typing_extensions import TypeGuard
 
 import sniffio
@@ -395,3 +396,19 @@ def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]:
         maxsize=maxsize,
     )
     return cast(Any, wrapper)  # type: ignore[no-any-return]
+
+
+def json_safe(data: object) -> object:
+    """Translates a mapping / sequence recursively in the same fashion
+    as `pydantic` v2's `model_dump(mode="json")`.
+    """
+    if is_mapping(data):
+        return {json_safe(key): json_safe(value) for key, value in data.items()}
+
+    if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)):
+        return [json_safe(item) for item in data]
+
+    if isinstance(data, (datetime, date)):
+        return data.isoformat()
+
+    return data
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index 1f27c64..0942842 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.0.0"  # x-release-please-version
+__version__ = "1.0.1"  # x-release-please-version
diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py
index f8b1936..bf4a5df 100644
--- a/src/browserbase/resources/projects.py
+++ b/src/browserbase/resources/projects.py
@@ -84,7 +84,7 @@ def list(
         extra_body: Body | None = None,
         timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
     ) -> ProjectListResponse:
-        """List all projects"""
+        """List projects"""
         return self._get(
             "/v1/projects",
             options=make_request_options(
@@ -190,7 +190,7 @@ async def list(
         extra_body: Body | None = None,
         timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
     ) -> ProjectListResponse:
-        """List all projects"""
+        """List projects"""
         return await self._get(
             "/v1/projects",
             options=make_request_options(
diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py
index fc4cac3..45ef082 100644
--- a/src/browserbase/resources/sessions/sessions.py
+++ b/src/browserbase/resources/sessions/sessions.py
@@ -155,7 +155,7 @@ def create(
                     "keep_alive": keep_alive,
                     "proxies": proxies,
                     "region": region,
-                    "timeout": api_timeout,
+                    "api_timeout": api_timeout,
                 },
                 session_create_params.SessionCreateParams,
             ),
@@ -409,7 +409,7 @@ async def create(
                     "keep_alive": keep_alive,
                     "proxies": proxies,
                     "region": region,
-                    "timeout": api_timeout,
+                    "api_timeout": api_timeout,
                 },
                 session_create_params.SessionCreateParams,
             ),
diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py
index d3e0325..c847137 100644
--- a/src/browserbase/types/sessions/session_recording.py
+++ b/src/browserbase/types/sessions/session_recording.py
@@ -10,8 +10,6 @@
 
 
 class SessionRecording(BaseModel):
-    id: str
-
     data: Dict[str, object]
     """
     See
diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py
index 8ebd5da..7b9fbce 100644
--- a/tests/api_resources/test_sessions.py
+++ b/tests/api_resources/test_sessions.py
@@ -41,11 +41,11 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None:
                 },
                 "extension_id": "extensionId",
                 "fingerprint": {
-                    "browsers": ["chrome", "edge", "firefox"],
-                    "devices": ["desktop", "mobile"],
+                    "browsers": ["chrome"],
+                    "devices": ["desktop"],
                     "http_version": 1,
-                    "locales": ["string", "string", "string"],
-                    "operating_systems": ["android", "ios", "linux"],
+                    "locales": ["string"],
+                    "operating_systems": ["android"],
                     "screen": {
                         "max_height": 0,
                         "max_width": 0,
@@ -270,11 +270,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas
                 },
                 "extension_id": "extensionId",
                 "fingerprint": {
-                    "browsers": ["chrome", "edge", "firefox"],
-                    "devices": ["desktop", "mobile"],
+                    "browsers": ["chrome"],
+                    "devices": ["desktop"],
                     "http_version": 1,
-                    "locales": ["string", "string", "string"],
-                    "operating_systems": ["android", "ios", "linux"],
+                    "locales": ["string"],
+                    "operating_systems": ["android"],
                     "screen": {
                         "max_height": 0,
                         "max_width": 0,
diff --git a/tests/test_client.py b/tests/test_client.py
index c70ef50..9cf62b3 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -703,7 +703,7 @@ class Model(BaseModel):
             [3, "", 0.5],
             [2, "", 0.5 * 2.0],
             [1, "", 0.5 * 4.0],
-            [-1100, "", 7.8],  # test large number potentially overflowing
+            [-1100, "", 8],  # test large number potentially overflowing
         ],
     )
     @mock.patch("time.time", mock.MagicMock(return_value=1696004797))
@@ -1482,7 +1482,7 @@ class Model(BaseModel):
             [3, "", 0.5],
             [2, "", 0.5 * 2.0],
             [1, "", 0.5 * 4.0],
-            [-1100, "", 7.8],  # test large number potentially overflowing
+            [-1100, "", 8],  # test large number potentially overflowing
         ],
     )
     @mock.patch("time.time", mock.MagicMock(return_value=1696004797))
diff --git a/tests/test_models.py b/tests/test_models.py
index 5b8044f..c199e94 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -520,19 +520,15 @@ class Model(BaseModel):
     assert m3.to_dict(exclude_none=True) == {}
     assert m3.to_dict(exclude_defaults=True) == {}
 
-    if PYDANTIC_V2:
-
-        class Model2(BaseModel):
-            created_at: datetime
+    class Model2(BaseModel):
+        created_at: datetime
 
-        time_str = "2024-03-21T11:39:01.275859"
-        m4 = Model2.construct(created_at=time_str)
-        assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)}
-        assert m4.to_dict(mode="json") == {"created_at": time_str}
-    else:
-        with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"):
-            m.to_dict(mode="json")
+    time_str = "2024-03-21T11:39:01.275859"
+    m4 = Model2.construct(created_at=time_str)
+    assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)}
+    assert m4.to_dict(mode="json") == {"created_at": time_str}
 
+    if not PYDANTIC_V2:
         with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"):
             m.to_dict(warnings=False)
 
@@ -558,9 +554,6 @@ class Model(BaseModel):
     assert m3.model_dump(exclude_none=True) == {}
 
     if not PYDANTIC_V2:
-        with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"):
-            m.model_dump(mode="json")
-
         with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"):
             m.model_dump(round_trip=True)
 
diff --git a/tests/test_transform.py b/tests/test_transform.py
index 436b818..03c2ecd 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -177,17 +177,32 @@ class DateDict(TypedDict, total=False):
     foo: Annotated[date, PropertyInfo(format="iso8601")]
 
 
+class DatetimeModel(BaseModel):
+    foo: datetime
+
+
+class DateModel(BaseModel):
+    foo: Optional[date]
+
+
 @parametrize
 @pytest.mark.asyncio
 async def test_iso8601_format(use_async: bool) -> None:
     dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")
+    tz = "Z" if PYDANTIC_V2 else "+00:00"
     assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"}  # type: ignore[comparison-overlap]
+    assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz}  # type: ignore[comparison-overlap]
 
     dt = dt.replace(tzinfo=None)
     assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"}  # type: ignore[comparison-overlap]
+    assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"}  # type: ignore[comparison-overlap]
 
     assert await transform({"foo": None}, DateDict, use_async) == {"foo": None}  # type: ignore[comparison-overlap]
+    assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None}  # type: ignore
     assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"}  # type: ignore[comparison-overlap]
+    assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == {
+        "foo": "2023-02-23"
+    }  # type: ignore[comparison-overlap]
 
 
 @parametrize

From 41d1427645e5173b72cfd12ee7e8c41ff80e665a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 21 Nov 2024 09:33:48 -0800
Subject: [PATCH 02/10] release: 1.0.2 (#63)

* chore: rebuild project due to codegen change (#59)

* release: 1.0.2

---------

Co-authored-by: Stainless Bot <dev+git@stainlessapi.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
---
 .release-please-manifest.json   |  2 +-
 CHANGELOG.md                    |  8 +++
 pyproject.toml                  |  3 +-
 requirements-dev.lock           |  1 +
 src/browserbase/_utils/_sync.py | 90 +++++++++++++++------------------
 src/browserbase/_version.py     |  2 +-
 tests/test_client.py            | 38 ++++++++++++++
 7 files changed, 91 insertions(+), 53 deletions(-)

diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index a8f7122..06d6df2 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.0.1"
+  ".": "1.0.2"
 }
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0df64a3..e3e35f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## 1.0.2 (2024-11-19)
+
+Full Changelog: [v1.0.1...v1.0.2](https://github.com/browserbase/sdk-python/compare/v1.0.1...v1.0.2)
+
+### Chores
+
+* rebuild project due to codegen change ([#59](https://github.com/browserbase/sdk-python/issues/59)) ([bd52098](https://github.com/browserbase/sdk-python/commit/bd520989c50f8353c7184930d0da661bdc8625fa))
+
 ## 1.0.1 (2024-11-18)
 
 Full Changelog: [v1.0.0...v1.0.1](https://github.com/browserbase/sdk-python/compare/v1.0.0...v1.0.1)
diff --git a/pyproject.toml b/pyproject.toml
index 2e4cc0e..b7a9090 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.0.1"
+version = "1.0.2"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
@@ -56,6 +56,7 @@ dev-dependencies = [
     "dirty-equals>=0.6.0",
     "importlib-metadata>=6.7.0",
     "rich>=13.7.1",
+    "nest_asyncio==1.6.0",
     "python-dotenv",
     "playwright",
     "selenium",
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 83ce020..86a29c6 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -66,6 +66,7 @@ mdurl==0.1.2
 mypy==1.13.0
 mypy-extensions==1.0.0
     # via mypy
+nest-asyncio==1.6.0
 nodeenv==1.9.1
     # via pyright
 nox==2024.10.9
diff --git a/src/browserbase/_utils/_sync.py b/src/browserbase/_utils/_sync.py
index d0d8103..8b3aaf2 100644
--- a/src/browserbase/_utils/_sync.py
+++ b/src/browserbase/_utils/_sync.py
@@ -1,56 +1,62 @@
 from __future__ import annotations
 
+import sys
+import asyncio
 import functools
-from typing import TypeVar, Callable, Awaitable
+import contextvars
+from typing import Any, TypeVar, Callable, Awaitable
 from typing_extensions import ParamSpec
 
-import anyio
-import anyio.to_thread
-
-from ._reflection import function_has_argument
-
 T_Retval = TypeVar("T_Retval")
 T_ParamSpec = ParamSpec("T_ParamSpec")
 
 
-# copied from `asyncer`, https://github.com/tiangolo/asyncer
-def asyncify(
-    function: Callable[T_ParamSpec, T_Retval],
-    *,
-    cancellable: bool = False,
-    limiter: anyio.CapacityLimiter | None = None,
-) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
+if sys.version_info >= (3, 9):
+    to_thread = asyncio.to_thread
+else:
+    # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
+    # for Python 3.8 support
+    async def to_thread(
+        func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
+    ) -> Any:
+        """Asynchronously run function *func* in a separate thread.
+
+        Any *args and **kwargs supplied for this function are directly passed
+        to *func*. Also, the current :class:`contextvars.Context` is propagated,
+        allowing context variables from the main thread to be accessed in the
+        separate thread.
+
+        Returns a coroutine that can be awaited to get the eventual result of *func*.
+        """
+        loop = asyncio.events.get_running_loop()
+        ctx = contextvars.copy_context()
+        func_call = functools.partial(ctx.run, func, *args, **kwargs)
+        return await loop.run_in_executor(None, func_call)
+
+
+# inspired by `asyncer`, https://github.com/tiangolo/asyncer
+def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
     """
     Take a blocking function and create an async one that receives the same
-    positional and keyword arguments, and that when called, calls the original function
-    in a worker thread using `anyio.to_thread.run_sync()`. Internally,
-    `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports
-    keyword arguments additional to positional arguments and it adds better support for
-    autocompletion and inline errors for the arguments of the function called and the
-    return value.
-
-    If the `cancellable` option is enabled and the task waiting for its completion is
-    cancelled, the thread will still run its course but its return value (or any raised
-    exception) will be ignored.
+    positional and keyword arguments. For python version 3.9 and above, it uses
+    asyncio.to_thread to run the function in a separate thread. For python version
+    3.8, it uses locally defined copy of the asyncio.to_thread function which was
+    introduced in python 3.9.
 
-    Use it like this:
+    Usage:
 
-    ```Python
-    def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
-        # Do work
-        return "Some result"
+    ```python
+    def blocking_func(arg1, arg2, kwarg1=None):
+        # blocking code
+        return result
 
 
-    result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b")
-    print(result)
+    result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1)
     ```
 
     ## Arguments
 
     `function`: a blocking regular callable (e.g. a function)
-    `cancellable`: `True` to allow cancellation of the operation
-    `limiter`: capacity limiter to use to limit the total amount of threads running
-        (if omitted, the default limiter is used)
 
     ## Return
 
@@ -60,22 +66,6 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
     """
 
     async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
-        partial_f = functools.partial(function, *args, **kwargs)
-
-        # In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old
-        # `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid
-        # surfacing deprecation warnings.
-        if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"):
-            return await anyio.to_thread.run_sync(
-                partial_f,
-                abandon_on_cancel=cancellable,
-                limiter=limiter,
-            )
-
-        return await anyio.to_thread.run_sync(
-            partial_f,
-            cancellable=cancellable,
-            limiter=limiter,
-        )
+        return await to_thread(function, *args, **kwargs)
 
     return wrapper
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index 0942842..bf5d1e1 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.0.1"  # x-release-please-version
+__version__ = "1.0.2"  # x-release-please-version
diff --git a/tests/test_client.py b/tests/test_client.py
index 9cf62b3..aa0043e 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -4,11 +4,14 @@
 
 import gc
 import os
+import sys
 import json
 import asyncio
 import inspect
+import subprocess
 import tracemalloc
 from typing import Any, Union, cast
+from textwrap import dedent
 from unittest import mock
 from typing_extensions import Literal
 
@@ -1608,3 +1611,38 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
         )
 
         assert response.http_request.headers.get("x-stainless-retry-count") == "42"
+
+    def test_get_platform(self) -> None:
+        # A previous implementation of asyncify could leave threads unterminated when
+        # used with nest_asyncio.
+        #
+        # Since nest_asyncio.apply() is global and cannot be un-applied, this
+        # test is run in a separate process to avoid affecting other tests.
+        test_code = dedent("""
+        import asyncio
+        import nest_asyncio
+        import threading
+
+        from browserbase._utils import asyncify
+        from browserbase._base_client import get_platform 
+
+        async def test_main() -> None:
+            result = await asyncify(get_platform)()
+            print(result)
+            for thread in threading.enumerate():
+                print(thread.name)
+
+        nest_asyncio.apply()
+        asyncio.run(test_main())
+        """)
+        with subprocess.Popen(
+            [sys.executable, "-c", test_code],
+            text=True,
+        ) as process:
+            try:
+                process.wait(2)
+                if process.returncode:
+                    raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
+            except subprocess.TimeoutExpired as e:
+                process.kill()
+                raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e

From d515d0406044223a4c402ebbb18f0d70d458dc13 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 22 Nov 2024 10:55:08 -0800
Subject: [PATCH 03/10] release: 1.0.3 (#66)

* chore(internal): fix compat model_dump method when warnings are passed (#65)

* release: 1.0.3

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
---
 .release-please-manifest.json | 2 +-
 CHANGELOG.md                  | 8 ++++++++
 pyproject.toml                | 2 +-
 src/browserbase/_compat.py    | 3 ++-
 src/browserbase/_version.py   | 2 +-
 tests/test_models.py          | 8 ++++++++
 6 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 06d6df2..b7634f9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.0.2"
+  ".": "1.0.3"
 }
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3e35f4..9b355e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## 1.0.3 (2024-11-22)
+
+Full Changelog: [v1.0.2...v1.0.3](https://github.com/browserbase/sdk-python/compare/v1.0.2...v1.0.3)
+
+### Chores
+
+* **internal:** fix compat model_dump method when warnings are passed ([#65](https://github.com/browserbase/sdk-python/issues/65)) ([4e999de](https://github.com/browserbase/sdk-python/commit/4e999de99372f6b348e74aa37663dd809c5d0da7))
+
 ## 1.0.2 (2024-11-19)
 
 Full Changelog: [v1.0.1...v1.0.2](https://github.com/browserbase/sdk-python/compare/v1.0.1...v1.0.2)
diff --git a/pyproject.toml b/pyproject.toml
index b7a9090..2580344 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.0.2"
+version = "1.0.3"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py
index 4794129..df173f8 100644
--- a/src/browserbase/_compat.py
+++ b/src/browserbase/_compat.py
@@ -145,7 +145,8 @@ def model_dump(
             exclude=exclude,
             exclude_unset=exclude_unset,
             exclude_defaults=exclude_defaults,
-            warnings=warnings,
+            # warnings are not supported in Pydantic v1
+            warnings=warnings if PYDANTIC_V2 else True,
         )
     return cast(
         "dict[str, Any]",
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index bf5d1e1..f1870ed 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.0.2"  # x-release-please-version
+__version__ = "1.0.3"  # x-release-please-version
diff --git a/tests/test_models.py b/tests/test_models.py
index c199e94..37671b0 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -561,6 +561,14 @@ class Model(BaseModel):
             m.model_dump(warnings=False)
 
 
+def test_compat_method_no_error_for_warnings() -> None:
+    class Model(BaseModel):
+        foo: Optional[str]
+
+    m = Model(foo="hello")
+    assert isinstance(model_dump(m, warnings=False), dict)
+
+
 def test_to_json() -> None:
     class Model(BaseModel):
         foo: Optional[str] = Field(alias="FOO", default=None)

From a4b244fed5d4a4b25200d605ac9f572f4693e2b0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 29 Nov 2024 09:17:35 -0800
Subject: [PATCH 04/10] release: 1.0.4 (#69)

* chore(internal): codegen related update (#68)

* chore(internal): exclude mypy from running on tests (#70)

* fix(client): compat with new httpx 0.28.0 release (#71)

* release: 1.0.4

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
---
 .release-please-manifest.json   |  2 +-
 CHANGELOG.md                    | 14 ++++++++++++++
 README.md                       |  6 ++++--
 mypy.ini                        |  5 ++++-
 pyproject.toml                  |  3 +--
 src/browserbase/_base_client.py | 12 ++++++++----
 src/browserbase/_compat.py      |  5 +----
 src/browserbase/_version.py     |  2 +-
 8 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b7634f9..80d368a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.0.3"
+  ".": "1.0.4"
 }
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b355e7..f1f81d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
 # Changelog
 
+## 1.0.4 (2024-11-29)
+
+Full Changelog: [v1.0.3...v1.0.4](https://github.com/browserbase/sdk-python/compare/v1.0.3...v1.0.4)
+
+### Bug Fixes
+
+* **client:** compat with new httpx 0.28.0 release ([#71](https://github.com/browserbase/sdk-python/issues/71)) ([7b87947](https://github.com/browserbase/sdk-python/commit/7b87947d0cdf555c73a1527b3e396cd40175d0b4))
+
+
+### Chores
+
+* **internal:** codegen related update ([#68](https://github.com/browserbase/sdk-python/issues/68)) ([3e4372e](https://github.com/browserbase/sdk-python/commit/3e4372ed8790e32850e1196c402e0023cd8a0f9d))
+* **internal:** exclude mypy from running on tests ([#70](https://github.com/browserbase/sdk-python/issues/70)) ([edd3628](https://github.com/browserbase/sdk-python/commit/edd3628710ed8f863bce5df336385dd6d380041e))
+
 ## 1.0.3 (2024-11-22)
 
 Full Changelog: [v1.0.2...v1.0.3](https://github.com/browserbase/sdk-python/compare/v1.0.2...v1.0.3)
diff --git a/README.md b/README.md
index 2ddc05b..63a3836 100644
--- a/README.md
+++ b/README.md
@@ -192,12 +192,14 @@ Note that requests that time out are [retried twice by default](#retries).
 
 We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module.
 
-You can enable logging by setting the environment variable `BROWSERBASE_LOG` to `debug`.
+You can enable logging by setting the environment variable `BROWSERBASE_LOG` to `info`.
 
 ```shell
-$ export BROWSERBASE_LOG=debug
+$ export BROWSERBASE_LOG=info
 ```
 
+Or to `debug` for more verbose logging.
+
 ### How to tell whether `None` means `null` or missing
 
 In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`:
diff --git a/mypy.ini b/mypy.ini
index 94f60e5..9e79a7c 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -5,7 +5,10 @@ show_error_codes = True
 # Exclude _files.py because mypy isn't smart enough to apply
 # the correct type narrowing and as this is an internal module
 # it's fine to just use Pyright.
-exclude = ^(src/browserbase/_files\.py|_dev/.*\.py)$
+#
+# We also exclude our `tests` as mypy doesn't always infer
+# types correctly and Pyright will still catch any type errors.
+exclude = ^(src/browserbase/_files\.py|_dev/.*\.py|tests/.*)$
 
 strict_equality = True
 implicit_reexport = True
diff --git a/pyproject.toml b/pyproject.toml
index 2580344..2bfcc94 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.0.3"
+version = "1.0.4"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
@@ -14,7 +14,6 @@ dependencies = [
     "anyio>=3.5.0, <5",
     "distro>=1.7.0, <2",
     "sniffio",
-    "cached-property; python_version < '3.8'",
 ]
 requires-python = ">= 3.8"
 classifiers = [
diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py
index f17e8d2..d8b28d9 100644
--- a/src/browserbase/_base_client.py
+++ b/src/browserbase/_base_client.py
@@ -792,6 +792,7 @@ def __init__(
         custom_query: Mapping[str, object] | None = None,
         _strict_response_validation: bool,
     ) -> None:
+        kwargs: dict[str, Any] = {}
         if limits is not None:
             warnings.warn(
                 "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
@@ -804,6 +805,7 @@ def __init__(
             limits = DEFAULT_CONNECTION_LIMITS
 
         if transport is not None:
+            kwargs["transport"] = transport
             warnings.warn(
                 "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
                 category=DeprecationWarning,
@@ -813,6 +815,7 @@ def __init__(
                 raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
 
         if proxies is not None:
+            kwargs["proxies"] = proxies
             warnings.warn(
                 "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
                 category=DeprecationWarning,
@@ -856,10 +859,9 @@ def __init__(
             base_url=base_url,
             # cast to a valid type because mypy doesn't understand our type narrowing
             timeout=cast(Timeout, timeout),
-            proxies=proxies,
-            transport=transport,
             limits=limits,
             follow_redirects=True,
+            **kwargs,  # type: ignore
         )
 
     def is_closed(self) -> bool:
@@ -1358,6 +1360,7 @@ def __init__(
         custom_headers: Mapping[str, str] | None = None,
         custom_query: Mapping[str, object] | None = None,
     ) -> None:
+        kwargs: dict[str, Any] = {}
         if limits is not None:
             warnings.warn(
                 "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
@@ -1370,6 +1373,7 @@ def __init__(
             limits = DEFAULT_CONNECTION_LIMITS
 
         if transport is not None:
+            kwargs["transport"] = transport
             warnings.warn(
                 "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
                 category=DeprecationWarning,
@@ -1379,6 +1383,7 @@ def __init__(
                 raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
 
         if proxies is not None:
+            kwargs["proxies"] = proxies
             warnings.warn(
                 "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
                 category=DeprecationWarning,
@@ -1422,10 +1427,9 @@ def __init__(
             base_url=base_url,
             # cast to a valid type because mypy doesn't understand our type narrowing
             timeout=cast(Timeout, timeout),
-            proxies=proxies,
-            transport=transport,
             limits=limits,
             follow_redirects=True,
+            **kwargs,  # type: ignore
         )
 
     def is_closed(self) -> bool:
diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py
index df173f8..92d9ee6 100644
--- a/src/browserbase/_compat.py
+++ b/src/browserbase/_compat.py
@@ -214,9 +214,6 @@ def __set_name__(self, owner: type[Any], name: str) -> None: ...
         # __set__ is not defined at runtime, but @cached_property is designed to be settable
         def __set__(self, instance: object, value: _T) -> None: ...
 else:
-    try:
-        from functools import cached_property as cached_property
-    except ImportError:
-        from cached_property import cached_property as cached_property
+    from functools import cached_property as cached_property
 
     typed_cached_property = cached_property
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index f1870ed..6e25ae8 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.0.3"  # x-release-please-version
+__version__ = "1.0.4"  # x-release-please-version

From 0558b2bc9f9068a248b1493c7d1801c1fe118361 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Dec 2024 09:01:44 -0800
Subject: [PATCH 05/10] release: 1.0.5 (#75)

* chore(internal): bump pyright (#73)

* release: 1.0.5

---------

Co-authored-by: Stainless Bot <dev+git@stainlessapi.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
---
 .release-please-manifest.json | 2 +-
 CHANGELOG.md                  | 8 ++++++++
 pyproject.toml                | 2 +-
 requirements-dev.lock         | 4 ++--
 src/browserbase/_version.py   | 2 +-
 5 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 80d368a..1214610 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.0.4"
+  ".": "1.0.5"
 }
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1f81d4..6bb2966 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## 1.0.5 (2024-12-03)
+
+Full Changelog: [v1.0.4...v1.0.5](https://github.com/browserbase/sdk-python/compare/v1.0.4...v1.0.5)
+
+### Chores
+
+* **internal:** bump pyright ([#73](https://github.com/browserbase/sdk-python/issues/73)) ([d5f9711](https://github.com/browserbase/sdk-python/commit/d5f97119b2ec2334f47029541173e78ca846abae))
+
 ## 1.0.4 (2024-11-29)
 
 Full Changelog: [v1.0.3...v1.0.4](https://github.com/browserbase/sdk-python/compare/v1.0.3...v1.0.4)
diff --git a/pyproject.toml b/pyproject.toml
index 2bfcc94..82209c4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.0.4"
+version = "1.0.5"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 86a29c6..48e714f 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -75,7 +75,7 @@ outcome==1.3.0.post0
 packaging==24.1
     # via nox
     # via pytest
-platformdirs==4.3.6
+platformdirs==3.11.0
     # via virtualenv
 playwright==1.48.0
     # via pytest-playwright
@@ -145,7 +145,7 @@ typing-extensions==4.12.2
 urllib3==2.2.3
     # via requests
     # via selenium
-virtualenv==20.27.1
+virtualenv==20.24.5
     # via nox
 websocket-client==1.8.0
     # via selenium
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index 6e25ae8..c871d01 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.0.4"  # x-release-please-version
+__version__ = "1.0.5"  # x-release-please-version

From 9b267008906d151aa5dfdabc3cb91126eb30e027 Mon Sep 17 00:00:00 2001
From: Anirudh Kamath <github@kamath.io>
Date: Wed, 4 Dec 2024 09:21:59 -0800
Subject: [PATCH 06/10] Rename bb to client in README (#80)

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 63a3836..1f1c0f9 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ from browserbase import Browserbase
 BROWSERBASE_API_KEY = os.environ.get("BROWSERBASE_API_KEY")
 BROWSERBASE_PROJECT_ID = os.environ.get("BROWSERBASE_PROJECT_ID")
 
-bb = Browserbase(
+client = Browserbase(
     # This is the default and can be omitted
     api_key=BROWSERBASE_API_KEY,
 )

From 0a3c371efad36b42f7d02faf241bce7a2253330a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 28 Jan 2025 10:34:51 -0800
Subject: [PATCH 07/10] release: 1.1.0 (#79)

* chore: make the `Omit` type public (#78)

* chore(internal): bump pydantic dependency (#81)

* docs(readme): fix http client proxies example (#82)

* chore(internal): bump pyright (#83)

* chore(internal): add support for TypeAliasType (#85)

* chore(internal): codegen related update (#86)

* chore(internal): codegen related update (#87)

* chore(internal): codegen related update (#88)

* chore(internal): remove some duplicated imports (#89)

* chore(internal): updated imports (#90)

* docs(readme): example snippet for client context manager (#91)

* chore(internal): fix some typos (#92)

* chore(internal): codegen related update (#93)

* chore: add missing isclass check (#94)

* chore(internal): bump httpx dependency (#95)

* fix(client): only call .close() when needed (#97)

* docs: fix typos (#98)

* chore(internal): codegen related update (#99)

* fix: correctly handle deserialising `cls` fields (#100)

* feat(api): api update (#101)

* chore(internal): codegen related update (#102)

* feat(api): api update (#104)

* feat(api): api update (#105)

* chore(internal): codegen related update (#106)

* chore(internal): codegen related update (#107)

* feat(api): api update (#109)

* chore(internal): minor formatting changes (#110)

* feat(api): api update (#111)

* release: 1.1.0

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
---
 .github/workflows/ci.yml                      |  3 +-
 .release-please-manifest.json                 |  2 +-
 .stats.yml                                    |  2 +-
 CHANGELOG.md                                  | 47 +++++++++++++
 LICENSE                                       |  2 +-
 README.md                                     | 29 +++++---
 examples/e2e/test_playwright.py               |  1 +
 examples/playwright_basic.py                  |  4 +-
 examples/playwright_captcha.py                |  4 +-
 examples/playwright_contexts.py               | 16 ++---
 examples/playwright_extensions.py             | 20 ++----
 examples/playwright_proxy.py                  |  4 +-
 examples/playwright_upload.py                 |  8 +--
 examples/selenium_basic.py                    |  3 +-
 mypy.ini                                      |  2 +-
 pyproject.toml                                |  5 +-
 requirements-dev.lock                         | 15 ++--
 requirements.lock                             |  7 +-
 scripts/bootstrap                             |  2 +-
 scripts/lint                                  |  1 -
 src/browserbase/__init__.py                   |  3 +-
 src/browserbase/_base_client.py               |  6 ++
 src/browserbase/_client.py                    | 69 ++++++++++---------
 src/browserbase/_models.py                    | 17 +++--
 src/browserbase/_response.py                  | 32 +++++----
 src/browserbase/_types.py                     |  6 +-
 src/browserbase/_utils/__init__.py            |  1 +
 src/browserbase/_utils/_typing.py             | 31 ++++++++-
 src/browserbase/_version.py                   |  2 +-
 src/browserbase/resources/contexts.py         |  4 +-
 src/browserbase/resources/extensions.py       |  4 +-
 src/browserbase/resources/projects.py         |  4 +-
 .../resources/sessions/downloads.py           |  4 +-
 src/browserbase/resources/sessions/logs.py    |  4 +-
 .../resources/sessions/recording.py           |  4 +-
 .../resources/sessions/sessions.py            | 54 ++++++++++++---
 src/browserbase/resources/sessions/uploads.py |  4 +-
 src/browserbase/types/session.py              |  9 ++-
 .../types/session_create_params.py            | 12 +++-
 .../types/session_create_response.py          |  9 ++-
 src/browserbase/types/session_list_params.py  |  8 +++
 src/browserbase/types/sessions/session_log.py | 12 ++--
 tests/api_resources/test_sessions.py          |  6 ++
 tests/test_client.py                          | 33 ++++++---
 tests/test_models.py                          | 28 +++++++-
 tests/utils.py                                |  4 ++
 46 files changed, 367 insertions(+), 180 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4029396..c8a8a4f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,6 @@ jobs:
   lint:
     name: lint
     runs-on: ubuntu-latest
-    
 
     steps:
       - uses: actions/checkout@v4
@@ -30,6 +29,7 @@ jobs:
 
       - name: Run lints
         run: ./scripts/lint
+
   test:
     name: test
     runs-on: ubuntu-latest
@@ -50,4 +50,3 @@ jobs:
 
       - name: Run tests
         run: ./scripts/test
-
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 1214610..2601677 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.0.5"
+  ".": "1.1.0"
 }
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index d42b050..be07776 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,2 @@
 configured_endpoints: 18
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-7f88912695bab2b98cb73137e6f36125d02fdfaf8eed4532ee1c82385609a259.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-396a2b9092f645c5a9e46a1f3be8c2e45ca9ae079e1d39761eb0a73f56e24b15.yml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bb2966..e9586e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,52 @@
 # Changelog
 
+## 1.1.0 (2025-01-28)
+
+Full Changelog: [v1.0.5...v1.1.0](https://github.com/browserbase/sdk-python/compare/v1.0.5...v1.1.0)
+
+### Features
+
+* **api:** api update ([#101](https://github.com/browserbase/sdk-python/issues/101)) ([5be14e9](https://github.com/browserbase/sdk-python/commit/5be14e9b49b95daa2bc043ed8c33b2d4527a7361))
+* **api:** api update ([#104](https://github.com/browserbase/sdk-python/issues/104)) ([c13b2f9](https://github.com/browserbase/sdk-python/commit/c13b2f95924c940deece1f6e3b1e4ca2dfbd9fe7))
+* **api:** api update ([#105](https://github.com/browserbase/sdk-python/issues/105)) ([fc3b82f](https://github.com/browserbase/sdk-python/commit/fc3b82f224e92e273d484f8b0f52eb433210e38b))
+* **api:** api update ([#109](https://github.com/browserbase/sdk-python/issues/109)) ([faca7e9](https://github.com/browserbase/sdk-python/commit/faca7e94c6086d461b81f2806868af2e1506e035))
+* **api:** api update ([#111](https://github.com/browserbase/sdk-python/issues/111)) ([42ae774](https://github.com/browserbase/sdk-python/commit/42ae77474c2fbe9eefd9929e15d8d51cbf40bc00))
+
+
+### Bug Fixes
+
+* **client:** only call .close() when needed ([#97](https://github.com/browserbase/sdk-python/issues/97)) ([01d5bd5](https://github.com/browserbase/sdk-python/commit/01d5bd5eb7675fc069fe01e7651d769df182270a))
+* correctly handle deserialising `cls` fields ([#100](https://github.com/browserbase/sdk-python/issues/100)) ([b617b85](https://github.com/browserbase/sdk-python/commit/b617b85ef3cce3c16e38125bec483c72bc3d43c0))
+
+
+### Chores
+
+* add missing isclass check ([#94](https://github.com/browserbase/sdk-python/issues/94)) ([de5856d](https://github.com/browserbase/sdk-python/commit/de5856dac77567813f681615bef7d147e505a6a0))
+* **internal:** add support for TypeAliasType ([#85](https://github.com/browserbase/sdk-python/issues/85)) ([64448c6](https://github.com/browserbase/sdk-python/commit/64448c6e020aaeb4b39b7ec8f1b28a6b8f0c746a))
+* **internal:** bump httpx dependency ([#95](https://github.com/browserbase/sdk-python/issues/95)) ([d592266](https://github.com/browserbase/sdk-python/commit/d592266e85c40d14e4929089f8ae4db814d04ce7))
+* **internal:** bump pydantic dependency ([#81](https://github.com/browserbase/sdk-python/issues/81)) ([e35a0d8](https://github.com/browserbase/sdk-python/commit/e35a0d85ef0e45aed1a5f58757427bf7c16a76f5))
+* **internal:** bump pyright ([#83](https://github.com/browserbase/sdk-python/issues/83)) ([894b4c4](https://github.com/browserbase/sdk-python/commit/894b4c45b0c36963822923535391aa34dbfec766))
+* **internal:** codegen related update ([#102](https://github.com/browserbase/sdk-python/issues/102)) ([f648bbb](https://github.com/browserbase/sdk-python/commit/f648bbbae4520a1003ecaf5cbd299da9aabfb90f))
+* **internal:** codegen related update ([#106](https://github.com/browserbase/sdk-python/issues/106)) ([3fc9cde](https://github.com/browserbase/sdk-python/commit/3fc9cde212c1ea7f1010c9e688bd75841d828ace))
+* **internal:** codegen related update ([#107](https://github.com/browserbase/sdk-python/issues/107)) ([c97e138](https://github.com/browserbase/sdk-python/commit/c97e1383ac673d05861653c0818c1d1c5b0fa5c8))
+* **internal:** codegen related update ([#86](https://github.com/browserbase/sdk-python/issues/86)) ([ab76578](https://github.com/browserbase/sdk-python/commit/ab76578bdce5eba2410b09f497758fbf0e0d8cf0))
+* **internal:** codegen related update ([#87](https://github.com/browserbase/sdk-python/issues/87)) ([f7f189e](https://github.com/browserbase/sdk-python/commit/f7f189ec317394f2fc532b8f95c3d15304298027))
+* **internal:** codegen related update ([#88](https://github.com/browserbase/sdk-python/issues/88)) ([85f1492](https://github.com/browserbase/sdk-python/commit/85f1492efc58d86ebc34511ca1269a0db2a4d223))
+* **internal:** codegen related update ([#93](https://github.com/browserbase/sdk-python/issues/93)) ([57f0977](https://github.com/browserbase/sdk-python/commit/57f0977c8e050b85b2c2de91202f6775299f80bf))
+* **internal:** codegen related update ([#99](https://github.com/browserbase/sdk-python/issues/99)) ([f817bcb](https://github.com/browserbase/sdk-python/commit/f817bcb67c2080a954c476c15dc048c2c628243a))
+* **internal:** fix some typos ([#92](https://github.com/browserbase/sdk-python/issues/92)) ([51d9f42](https://github.com/browserbase/sdk-python/commit/51d9f42a32d17d2d2277eb8a7b8f35a980c7c485))
+* **internal:** minor formatting changes ([#110](https://github.com/browserbase/sdk-python/issues/110)) ([195c595](https://github.com/browserbase/sdk-python/commit/195c595bfbe2ed97ae4b551658618f4a99a255f0))
+* **internal:** remove some duplicated imports ([#89](https://github.com/browserbase/sdk-python/issues/89)) ([a82ae7d](https://github.com/browserbase/sdk-python/commit/a82ae7d418b1daf68c85e70dea61e628eb785b79))
+* **internal:** updated imports ([#90](https://github.com/browserbase/sdk-python/issues/90)) ([dc6e187](https://github.com/browserbase/sdk-python/commit/dc6e187bfe9585692b2de1b67fc83f027a52c43c))
+* make the `Omit` type public ([#78](https://github.com/browserbase/sdk-python/issues/78)) ([a7bdc57](https://github.com/browserbase/sdk-python/commit/a7bdc57ab7f327da61121986ba7b006238d0e5b5))
+
+
+### Documentation
+
+* fix typos ([#98](https://github.com/browserbase/sdk-python/issues/98)) ([d4f4bae](https://github.com/browserbase/sdk-python/commit/d4f4bae46341e91ac537e121bba38e511c7026bc))
+* **readme:** example snippet for client context manager ([#91](https://github.com/browserbase/sdk-python/issues/91)) ([950c8af](https://github.com/browserbase/sdk-python/commit/950c8af19db4581fabd5b965ca4f0af3cc5cd6dc))
+* **readme:** fix http client proxies example ([#82](https://github.com/browserbase/sdk-python/issues/82)) ([cc67c77](https://github.com/browserbase/sdk-python/commit/cc67c773b11b42b406b677f466c7c0ef090b254e))
+
 ## 1.0.5 (2024-12-03)
 
 Full Changelog: [v1.0.4...v1.0.5](https://github.com/browserbase/sdk-python/compare/v1.0.4...v1.0.5)
diff --git a/LICENSE b/LICENSE
index 915e6f8..2cec9d4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright 2024 Browserbase
+   Copyright 2025 Browserbase
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 1f1c0f9..1a508e6 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ session = client.sessions.create(
 )
 print(session.id)
 
+
 def run(playwright: Playwright) -> None:
     # Connect to the remote session
     chromium = playwright.chromium
@@ -51,9 +52,7 @@ def run(playwright: Playwright) -> None:
     # Execute Playwright actions on the remote browser tab
     page.goto("https://news.ycombinator.com/")
     page_title = page.title()
-    assert (
-        page_title == "Hacker News"
-    ), f"Page title is not 'Hacker News', it is '{page_title}'"
+    assert page_title == "Hacker News", f"Page title is not 'Hacker News', it is '{page_title}'"
     page.screenshot(path="screenshot.png")
 
     page.close()
@@ -121,7 +120,7 @@ except browserbase.APIStatusError as e:
     print(e.response)
 ```
 
-Error codes are as followed:
+Error codes are as follows:
 
 | Status Code | Error Type                 |
 | ----------- | -------------------------- |
@@ -260,8 +259,7 @@ If you need to access undocumented endpoints, params, or response properties, th
 #### Undocumented endpoints
 
 To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other
-http verbs. Options on the client will be respected (such as retries) will be respected when making this
-request.
+http verbs. Options on the client will be respected (such as retries) when making this request.
 
 ```py
 import httpx
@@ -290,18 +288,19 @@ can also get all the extra fields on the Pydantic model as a dict with
 
 You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including:
 
-- Support for proxies
-- Custom transports
+- Support for [proxies](https://www.python-httpx.org/advanced/proxies/)
+- Custom [transports](https://www.python-httpx.org/advanced/transports/)
 - Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality
 
 ```python
+import httpx
 from browserbase import Browserbase, DefaultHttpxClient
 
 client = Browserbase(
     # Or use the `BROWSERBASE_BASE_URL` env var
     base_url="http://my.test.server.example.com:8083",
     http_client=DefaultHttpxClient(
-        proxies="http://my.test.proxy.example.com",
+        proxy="http://my.test.proxy.example.com",
         transport=httpx.HTTPTransport(local_address="0.0.0.0"),
     ),
 )
@@ -317,12 +316,22 @@ client.with_options(http_client=DefaultHttpxClient(...))
 
 By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.
 
+```py
+from browserbase import Browserbase
+
+with Browserbase() as client:
+  # make requests here
+  ...
+
+# HTTP client is now closed
+```
+
 ## Versioning
 
 This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
 
 1. Changes that only affect static types, without breaking runtime behavior.
-2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_.
+2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
 3. Changes that we do not expect to impact the vast majority of users in practice.
 
 We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
diff --git a/examples/e2e/test_playwright.py b/examples/e2e/test_playwright.py
index 2e58c70..afd94f1 100644
--- a/examples/e2e/test_playwright.py
+++ b/examples/e2e/test_playwright.py
@@ -29,6 +29,7 @@ def playwright() -> Generator[Playwright, None, None]:
     with sync_playwright() as p:
         yield p
 
+
 def test_playwright_basic(playwright: Playwright) -> None:
     playwright_basic.run(playwright)
 
diff --git a/examples/playwright_basic.py b/examples/playwright_basic.py
index 06fc93c..33bf88a 100644
--- a/examples/playwright_basic.py
+++ b/examples/playwright_basic.py
@@ -19,9 +19,7 @@ def run(playwright: Playwright) -> None:
     # Execute Playwright actions on the remote browser tab
     page.goto("https://news.ycombinator.com/")
     page_title = page.title()
-    assert (
-        page_title == "Hacker News"
-    ), f"Page title is not 'Hacker News', it is '{page_title}'"
+    assert page_title == "Hacker News", f"Page title is not 'Hacker News', it is '{page_title}'"
     page.screenshot(path="screenshot.png")
 
     page.close()
diff --git a/examples/playwright_captcha.py b/examples/playwright_captcha.py
index 7bc2ff4..980504f 100644
--- a/examples/playwright_captcha.py
+++ b/examples/playwright_captcha.py
@@ -34,9 +34,7 @@ def handle_console(msg: ConsoleMessage) -> None:
     page.on("console", handle_console)
 
     page.goto(DEFAULT_CAPTCHA_URL, wait_until="networkidle")
-    page.wait_for_function(
-        "() => window.captchaSolvingFinished === true", timeout=OVERRIDE_TIMEOUT
-    )
+    page.wait_for_function("() => window.captchaSolvingFinished === true", timeout=OVERRIDE_TIMEOUT)
 
     assert captcha_solving_started, "Captcha solving did not start"
     assert captcha_solving_finished, "Captcha solving did not finish"
diff --git a/examples/playwright_contexts.py b/examples/playwright_contexts.py
index 610636f..acf4633 100644
--- a/examples/playwright_contexts.py
+++ b/examples/playwright_contexts.py
@@ -41,15 +41,11 @@ def run(playwright: Playwright) -> None:
     # Step 2: Creates a session with the context
     session = bb.sessions.create(
         project_id=BROWSERBASE_PROJECT_ID,
-        browser_settings=TypeAdapter(BrowserSettings).validate_python(
-            {"context": {"id": context_id, "persist": True}}
-        ),
+        browser_settings=TypeAdapter(BrowserSettings).validate_python({"context": {"id": context_id, "persist": True}}),
     )
     print(session)
 
-    assert (
-        session.context_id == context_id
-    ), f"Session context_id is {session.context_id}, expected {context_id}"
+    assert session.context_id == context_id, f"Session context_id is {session.context_id}, expected {context_id}"
     session_id = session.id
 
     # Step 3: Populates and persists the context
@@ -90,13 +86,9 @@ def run(playwright: Playwright) -> None:
     # Step 4: Creates another session with the same context
     session = bb.sessions.create(
         project_id=BROWSERBASE_PROJECT_ID,
-        browser_settings=BrowserSettings(
-            context=BrowserSettingsContext(id=context_id, persist=True)
-        ),
+        browser_settings=BrowserSettings(context=BrowserSettingsContext(id=context_id, persist=True)),
     )
-    assert (
-        session.context_id == context_id
-    ), f"Session context_id is {session.context_id}, expected {context_id}"
+    assert session.context_id == context_id, f"Session context_id is {session.context_id}, expected {context_id}"
     session_id = session.id
 
     # Step 5: Uses context to find previous state
diff --git a/examples/playwright_extensions.py b/examples/playwright_extensions.py
index 7bdb942..6ef1f98 100644
--- a/examples/playwright_extensions.py
+++ b/examples/playwright_extensions.py
@@ -12,9 +12,7 @@
 )
 from browserbase.types import Extension, SessionCreateResponse
 
-PATH_TO_EXTENSION = (
-    Path.cwd() / "examples" / "packages" / "extensions" / "browserbase-test"
-)
+PATH_TO_EXTENSION = Path.cwd() / "examples" / "packages" / "extensions" / "browserbase-test"
 
 
 def zip_extension(path: Path = PATH_TO_EXTENSION, save_local: bool = False) -> BytesIO:
@@ -23,9 +21,7 @@ def zip_extension(path: Path = PATH_TO_EXTENSION, save_local: bool = False) -> B
     Mark save_local=True to save the zip file to a local file.
     """
     # Ensure we're looking at an extension
-    assert "manifest.json" in os.listdir(
-        path
-    ), "No manifest.json found in the extension folder."
+    assert "manifest.json" in os.listdir(path), "No manifest.json found in the extension folder."
 
     # Create a BytesIO object to hold the zip file in memory
     memory_zip = BytesIO()
@@ -51,9 +47,7 @@ def zip_extension(path: Path = PATH_TO_EXTENSION, save_local: bool = False) -> B
 
 def create_extension() -> str:
     zip_data = zip_extension(save_local=True)
-    extension: Extension = bb.extensions.create(
-        file=("extension.zip", zip_data.getvalue())
-    )
+    extension: Extension = bb.extensions.create(file=("extension.zip", zip_data.getvalue()))
     return extension.id
 
 
@@ -75,9 +69,7 @@ def check_for_message(page: Page, message: str) -> None:
     while time.time() - start < 10:
         if message in console_messages:
             break
-    assert (
-        message in console_messages
-    ), f"Expected message not found in console logs. Messages: {console_messages}"
+    assert message in console_messages, f"Expected message not found in console logs. Messages: {console_messages}"
 
 
 def run(playwright: Playwright) -> None:
@@ -141,9 +133,7 @@ def run(playwright: Playwright) -> None:
             project_id=BROWSERBASE_PROJECT_ID,
             extension_id=extension_id,
         )
-        raise AssertionError(
-            "Expected to fail when creating session with deleted extension"
-        )
+        raise AssertionError("Expected to fail when creating session with deleted extension")
     except Exception as e:
         print(f"Failed to create session with deleted extension as expected: {str(e)}")
 
diff --git a/examples/playwright_proxy.py b/examples/playwright_proxy.py
index 8378e29..47c706b 100644
--- a/examples/playwright_proxy.py
+++ b/examples/playwright_proxy.py
@@ -11,9 +11,7 @@
 
 
 def check_proxy_bytes(session_id: str) -> None:
-    bb.sessions.update(
-        id=session_id, project_id=BROWSERBASE_PROJECT_ID, status="REQUEST_RELEASE"
-    )
+    bb.sessions.update(id=session_id, project_id=BROWSERBASE_PROJECT_ID, status="REQUEST_RELEASE")
     time.sleep(GRACEFUL_SHUTDOWN_TIMEOUT / 1000)
     updated_session = bb.sessions.retrieve(id=session_id)
     assert (
diff --git a/examples/playwright_upload.py b/examples/playwright_upload.py
index da1e32c..6bba6e0 100644
--- a/examples/playwright_upload.py
+++ b/examples/playwright_upload.py
@@ -33,12 +33,8 @@ def run(playwright: Playwright) -> None:
         file_size = int(file_size_span.inner_text())
 
         # Assert the file name and size
-        assert (
-            file_name == "logo.png"
-        ), f"Expected file name to be 'logo.png', but got '{file_name}'"
-        assert (
-            file_size > 0
-        ), f"Expected file size to be greater than 0, but got {file_size}"
+        assert file_name == "logo.png", f"Expected file name to be 'logo.png', but got '{file_name}'"
+        assert file_size > 0, f"Expected file size to be greater than 0, but got {file_size}"
 
         print("File upload test passed successfully!")
 
diff --git a/examples/selenium_basic.py b/examples/selenium_basic.py
index 83b7307..91c7c32 100644
--- a/examples/selenium_basic.py
+++ b/examples/selenium_basic.py
@@ -34,7 +34,8 @@ def run() -> None:
     session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID)
     connection = BrowserbaseConnection(session.id, session.selenium_remote_url)
     driver = webdriver.Remote(
-        command_executor=connection, options=webdriver.ChromeOptions()  # type: ignore
+        command_executor=connection,
+        options=webdriver.ChromeOptions(),  # type: ignore
     )
 
     # Print a bit of info about the browser we've connected to
diff --git a/mypy.ini b/mypy.ini
index 9e79a7c..811af71 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -41,7 +41,7 @@ cache_fine_grained = True
 # ```
 # Changing this codegen to make mypy happy would increase complexity
 # and would not be worth it.
-disable_error_code = func-returns-value
+disable_error_code = func-returns-value,overload-cannot-match
 
 # https://github.com/python/mypy/issues/12162
 [mypy.overrides]
diff --git a/pyproject.toml b/pyproject.toml
index 82209c4..7e1b0e9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.0.5"
+version = "1.1.0"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
@@ -10,7 +10,7 @@ authors = [
 dependencies = [
     "httpx>=0.23.0, <1",
     "pydantic>=1.9.0, <3",
-    "typing-extensions>=4.7, <5",
+    "typing-extensions>=4.10, <5",
     "anyio>=3.5.0, <5",
     "distro>=1.7.0, <2",
     "sniffio",
@@ -138,6 +138,7 @@ testpaths = ["tests"]
 addopts = "--tb=short"
 xfail_strict = true
 asyncio_mode = "auto"
+asyncio_default_fixture_loop_scope = "session"
 filterwarnings = [
   "error"
 ]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 48e714f..30dd44d 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -4,7 +4,7 @@
 # last locked with the following flags:
 #   pre: false
 #   features: []
-#   all-features: false
+#   all-features: true
 #   with-sources: false
 #   generate-hashes: false
 #   universal: false
@@ -48,7 +48,7 @@ h11==0.14.0
     # via wsproto
 httpcore==1.0.6
     # via httpx
-httpx==0.27.2
+httpx==0.28.1
     # via browserbase
     # via respx
 idna==3.10
@@ -63,7 +63,7 @@ markdown-it-py==3.0.0
     # via rich
 mdurl==0.1.2
     # via markdown-it-py
-mypy==1.13.0
+mypy==1.14.1
 mypy-extensions==1.0.0
     # via mypy
 nest-asyncio==1.6.0
@@ -81,15 +81,15 @@ playwright==1.48.0
     # via pytest-playwright
 pluggy==1.5.0
     # via pytest
-pydantic==2.9.2
+pydantic==2.10.3
     # via browserbase
-pydantic-core==2.23.4
+pydantic-core==2.27.1
     # via pydantic
 pyee==12.0.0
     # via playwright
 pygments==2.18.0
     # via rich
-pyright==1.1.386
+pyright==1.1.392.post0
 pysocks==1.7.1
     # via urllib3
 pytest==8.3.3
@@ -107,7 +107,7 @@ python-slugify==8.0.4
     # via pytest-playwright
 requests==2.32.3
     # via pytest-base-url
-respx==0.21.1
+respx==0.22.0
 rich==13.9.3
 ruff==0.7.1
 selenium==4.25.0
@@ -116,7 +116,6 @@ six==1.16.0
 sniffio==1.3.1
     # via anyio
     # via browserbase
-    # via httpx
     # via trio
 sortedcontainers==2.4.0
     # via trio
diff --git a/requirements.lock b/requirements.lock
index 4bff3dd..9efa54d 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -26,19 +26,18 @@ h11==0.14.0
     # via httpcore
 httpcore==1.0.2
     # via httpx
-httpx==0.25.2
+httpx==0.28.1
     # via browserbase
 idna==3.4
     # via anyio
     # via httpx
-pydantic==2.9.2
+pydantic==2.10.3
     # via browserbase
-pydantic-core==2.23.4
+pydantic-core==2.27.1
     # via pydantic
 sniffio==1.3.0
     # via anyio
     # via browserbase
-    # via httpx
 typing-extensions==4.12.2
     # via anyio
     # via browserbase
diff --git a/scripts/bootstrap b/scripts/bootstrap
index 8c5c60e..e84fe62 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,7 +4,7 @@ set -e
 
 cd "$(dirname "$0")/.."
 
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then
+if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then
   brew bundle check >/dev/null 2>&1 || {
     echo "==> Installing Homebrew dependencies…"
     brew bundle
diff --git a/scripts/lint b/scripts/lint
index a74a198..feccbdd 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -9,4 +9,3 @@ rye run lint
 
 echo "==> Making sure it imports"
 rye run python -c 'import browserbase'
-
diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py
index 4b1d280..ce50c4e 100644
--- a/src/browserbase/__init__.py
+++ b/src/browserbase/__init__.py
@@ -1,7 +1,7 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 from . import types
-from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes
+from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes
 from ._utils import file_from_path
 from ._client import (
     Client,
@@ -46,6 +46,7 @@
     "ProxiesTypes",
     "NotGiven",
     "NOT_GIVEN",
+    "Omit",
     "BrowserbaseError",
     "APIError",
     "APIStatusError",
diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py
index d8b28d9..b21f479 100644
--- a/src/browserbase/_base_client.py
+++ b/src/browserbase/_base_client.py
@@ -767,6 +767,9 @@ def __init__(self, **kwargs: Any) -> None:
 
 class SyncHttpxClientWrapper(DefaultHttpxClient):
     def __del__(self) -> None:
+        if self.is_closed:
+            return
+
         try:
             self.close()
         except Exception:
@@ -1334,6 +1337,9 @@ def __init__(self, **kwargs: Any) -> None:
 
 class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
     def __del__(self) -> None:
+        if self.is_closed:
+            return
+
         try:
             # TODO(someday): support non asyncio runtimes here
             asyncio.get_running_loop().create_task(self.aclose())
diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py
index b3ed32f..b845ea8 100644
--- a/src/browserbase/_client.py
+++ b/src/browserbase/_client.py
@@ -8,7 +8,7 @@
 
 import httpx
 
-from . import resources, _exceptions
+from . import _exceptions
 from ._qs import Querystring
 from ._types import (
     NOT_GIVEN,
@@ -24,6 +24,7 @@
     get_async_library,
 )
 from ._version import __version__
+from .resources import contexts, projects, extensions
 from ._streaming import Stream as Stream, AsyncStream as AsyncStream
 from ._exceptions import APIStatusError, BrowserbaseError
 from ._base_client import (
@@ -31,13 +32,13 @@
     SyncAPIClient,
     AsyncAPIClient,
 )
+from .resources.sessions import sessions
 
 __all__ = [
     "Timeout",
     "Transport",
     "ProxiesTypes",
     "RequestOptions",
-    "resources",
     "Browserbase",
     "AsyncBrowserbase",
     "Client",
@@ -46,10 +47,10 @@
 
 
 class Browserbase(SyncAPIClient):
-    contexts: resources.ContextsResource
-    extensions: resources.ExtensionsResource
-    projects: resources.ProjectsResource
-    sessions: resources.SessionsResource
+    contexts: contexts.ContextsResource
+    extensions: extensions.ExtensionsResource
+    projects: projects.ProjectsResource
+    sessions: sessions.SessionsResource
     with_raw_response: BrowserbaseWithRawResponse
     with_streaming_response: BrowserbaseWithStreamedResponse
 
@@ -107,10 +108,10 @@ def __init__(
             _strict_response_validation=_strict_response_validation,
         )
 
-        self.contexts = resources.ContextsResource(self)
-        self.extensions = resources.ExtensionsResource(self)
-        self.projects = resources.ProjectsResource(self)
-        self.sessions = resources.SessionsResource(self)
+        self.contexts = contexts.ContextsResource(self)
+        self.extensions = extensions.ExtensionsResource(self)
+        self.projects = projects.ProjectsResource(self)
+        self.sessions = sessions.SessionsResource(self)
         self.with_raw_response = BrowserbaseWithRawResponse(self)
         self.with_streaming_response = BrowserbaseWithStreamedResponse(self)
 
@@ -220,10 +221,10 @@ def _make_status_error(
 
 
 class AsyncBrowserbase(AsyncAPIClient):
-    contexts: resources.AsyncContextsResource
-    extensions: resources.AsyncExtensionsResource
-    projects: resources.AsyncProjectsResource
-    sessions: resources.AsyncSessionsResource
+    contexts: contexts.AsyncContextsResource
+    extensions: extensions.AsyncExtensionsResource
+    projects: projects.AsyncProjectsResource
+    sessions: sessions.AsyncSessionsResource
     with_raw_response: AsyncBrowserbaseWithRawResponse
     with_streaming_response: AsyncBrowserbaseWithStreamedResponse
 
@@ -281,10 +282,10 @@ def __init__(
             _strict_response_validation=_strict_response_validation,
         )
 
-        self.contexts = resources.AsyncContextsResource(self)
-        self.extensions = resources.AsyncExtensionsResource(self)
-        self.projects = resources.AsyncProjectsResource(self)
-        self.sessions = resources.AsyncSessionsResource(self)
+        self.contexts = contexts.AsyncContextsResource(self)
+        self.extensions = extensions.AsyncExtensionsResource(self)
+        self.projects = projects.AsyncProjectsResource(self)
+        self.sessions = sessions.AsyncSessionsResource(self)
         self.with_raw_response = AsyncBrowserbaseWithRawResponse(self)
         self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self)
 
@@ -395,34 +396,34 @@ def _make_status_error(
 
 class BrowserbaseWithRawResponse:
     def __init__(self, client: Browserbase) -> None:
-        self.contexts = resources.ContextsResourceWithRawResponse(client.contexts)
-        self.extensions = resources.ExtensionsResourceWithRawResponse(client.extensions)
-        self.projects = resources.ProjectsResourceWithRawResponse(client.projects)
-        self.sessions = resources.SessionsResourceWithRawResponse(client.sessions)
+        self.contexts = contexts.ContextsResourceWithRawResponse(client.contexts)
+        self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions)
+        self.projects = projects.ProjectsResourceWithRawResponse(client.projects)
+        self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions)
 
 
 class AsyncBrowserbaseWithRawResponse:
     def __init__(self, client: AsyncBrowserbase) -> None:
-        self.contexts = resources.AsyncContextsResourceWithRawResponse(client.contexts)
-        self.extensions = resources.AsyncExtensionsResourceWithRawResponse(client.extensions)
-        self.projects = resources.AsyncProjectsResourceWithRawResponse(client.projects)
-        self.sessions = resources.AsyncSessionsResourceWithRawResponse(client.sessions)
+        self.contexts = contexts.AsyncContextsResourceWithRawResponse(client.contexts)
+        self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions)
+        self.projects = projects.AsyncProjectsResourceWithRawResponse(client.projects)
+        self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions)
 
 
 class BrowserbaseWithStreamedResponse:
     def __init__(self, client: Browserbase) -> None:
-        self.contexts = resources.ContextsResourceWithStreamingResponse(client.contexts)
-        self.extensions = resources.ExtensionsResourceWithStreamingResponse(client.extensions)
-        self.projects = resources.ProjectsResourceWithStreamingResponse(client.projects)
-        self.sessions = resources.SessionsResourceWithStreamingResponse(client.sessions)
+        self.contexts = contexts.ContextsResourceWithStreamingResponse(client.contexts)
+        self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions)
+        self.projects = projects.ProjectsResourceWithStreamingResponse(client.projects)
+        self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions)
 
 
 class AsyncBrowserbaseWithStreamedResponse:
     def __init__(self, client: AsyncBrowserbase) -> None:
-        self.contexts = resources.AsyncContextsResourceWithStreamingResponse(client.contexts)
-        self.extensions = resources.AsyncExtensionsResourceWithStreamingResponse(client.extensions)
-        self.projects = resources.AsyncProjectsResourceWithStreamingResponse(client.projects)
-        self.sessions = resources.AsyncSessionsResourceWithStreamingResponse(client.sessions)
+        self.contexts = contexts.AsyncContextsResourceWithStreamingResponse(client.contexts)
+        self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions)
+        self.projects = projects.AsyncProjectsResourceWithStreamingResponse(client.projects)
+        self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions)
 
 
 Client = Browserbase
diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py
index 6cb469e..9a918aa 100644
--- a/src/browserbase/_models.py
+++ b/src/browserbase/_models.py
@@ -46,6 +46,7 @@
     strip_not_given,
     extract_type_arg,
     is_annotated_type,
+    is_type_alias_type,
     strip_annotated_type,
 )
 from ._compat import (
@@ -178,14 +179,14 @@ def __str__(self) -> str:
     @classmethod
     @override
     def construct(  # pyright: ignore[reportIncompatibleMethodOverride]
-        cls: Type[ModelT],
+        __cls: Type[ModelT],
         _fields_set: set[str] | None = None,
         **values: object,
     ) -> ModelT:
-        m = cls.__new__(cls)
+        m = __cls.__new__(__cls)
         fields_values: dict[str, object] = {}
 
-        config = get_model_config(cls)
+        config = get_model_config(__cls)
         populate_by_name = (
             config.allow_population_by_field_name
             if isinstance(config, _ConfigProtocol)
@@ -195,7 +196,7 @@ def construct(  # pyright: ignore[reportIncompatibleMethodOverride]
         if _fields_set is None:
             _fields_set = set()
 
-        model_fields = get_model_fields(cls)
+        model_fields = get_model_fields(__cls)
         for name, field in model_fields.items():
             key = field.alias
             if key is None or (key not in values and populate_by_name):
@@ -428,6 +429,8 @@ def construct_type(*, value: object, type_: object) -> object:
     # we allow `object` as the input type because otherwise, passing things like
     # `Literal['value']` will be reported as a type error by type checkers
     type_ = cast("type[object]", type_)
+    if is_type_alias_type(type_):
+        type_ = type_.__value__  # type: ignore[unreachable]
 
     # unwrap `Annotated[T, ...]` -> `T`
     if is_annotated_type(type_):
@@ -485,7 +488,11 @@ def construct_type(*, value: object, type_: object) -> object:
         _, items_type = get_args(type_)  # Dict[_, items_type]
         return {key: construct_type(value=item, type_=items_type) for key, item in value.items()}
 
-    if not is_literal_type(type_) and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)):
+    if (
+        not is_literal_type(type_)
+        and inspect.isclass(origin)
+        and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel))
+    ):
         if is_list(value):
             return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value]
 
diff --git a/src/browserbase/_response.py b/src/browserbase/_response.py
index 81ae082..e79cb15 100644
--- a/src/browserbase/_response.py
+++ b/src/browserbase/_response.py
@@ -25,7 +25,7 @@
 import pydantic
 
 from ._types import NoneType
-from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base
+from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base
 from ._models import BaseModel, is_basemodel
 from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
 from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type
@@ -126,9 +126,17 @@ def __repr__(self) -> str:
         )
 
     def _parse(self, *, to: type[_T] | None = None) -> R | _T:
+        cast_to = to if to is not None else self._cast_to
+
+        # unwrap `TypeAlias('Name', T)` -> `T`
+        if is_type_alias_type(cast_to):
+            cast_to = cast_to.__value__  # type: ignore[unreachable]
+
         # unwrap `Annotated[T, ...]` -> `T`
-        if to and is_annotated_type(to):
-            to = extract_type_arg(to, 0)
+        if cast_to and is_annotated_type(cast_to):
+            cast_to = extract_type_arg(cast_to, 0)
+
+        origin = get_origin(cast_to) or cast_to
 
         if self._is_sse_stream:
             if to:
@@ -164,18 +172,12 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
             return cast(
                 R,
                 stream_cls(
-                    cast_to=self._cast_to,
+                    cast_to=cast_to,
                     response=self.http_response,
                     client=cast(Any, self._client),
                 ),
             )
 
-        cast_to = to if to is not None else self._cast_to
-
-        # unwrap `Annotated[T, ...]` -> `T`
-        if is_annotated_type(cast_to):
-            cast_to = extract_type_arg(cast_to, 0)
-
         if cast_to is NoneType:
             return cast(R, None)
 
@@ -195,8 +197,6 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
         if cast_to == bool:
             return cast(R, response.text.lower() == "true")
 
-        origin = get_origin(cast_to) or cast_to
-
         if origin == APIResponse:
             raise RuntimeError("Unexpected state - cast_to is `APIResponse`")
 
@@ -210,7 +210,13 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
                 raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`")
             return cast(R, response)
 
-        if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel):
+        if (
+            inspect.isclass(
+                origin  # pyright: ignore[reportUnknownArgumentType]
+            )
+            and not issubclass(origin, BaseModel)
+            and issubclass(origin, pydantic.BaseModel)
+        ):
             raise TypeError(
                 "Pydantic models must subclass our base model type, e.g. `from browserbase import BaseModel`"
             )
diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py
index 1691090..a8833dc 100644
--- a/src/browserbase/_types.py
+++ b/src/browserbase/_types.py
@@ -192,10 +192,8 @@ def get(self, __key: str) -> str | None: ...
 StrBytesIntFloat = Union[str, bytes, int, float]
 
 # Note: copied from Pydantic
-# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49
-IncEx: TypeAlias = Union[
-    Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]]
-]
+# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79
+IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]]
 
 PostParser = Callable[[Any], Any]
 
diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py
index a7cff3c..d4fda26 100644
--- a/src/browserbase/_utils/__init__.py
+++ b/src/browserbase/_utils/__init__.py
@@ -39,6 +39,7 @@
     is_iterable_type as is_iterable_type,
     is_required_type as is_required_type,
     is_annotated_type as is_annotated_type,
+    is_type_alias_type as is_type_alias_type,
     strip_annotated_type as strip_annotated_type,
     extract_type_var_from_base as extract_type_var_from_base,
 )
diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py
index c036991..278749b 100644
--- a/src/browserbase/_utils/_typing.py
+++ b/src/browserbase/_utils/_typing.py
@@ -1,8 +1,17 @@
 from __future__ import annotations
 
+import sys
+import typing
+import typing_extensions
 from typing import Any, TypeVar, Iterable, cast
 from collections import abc as _c_abc
-from typing_extensions import Required, Annotated, get_args, get_origin
+from typing_extensions import (
+    TypeIs,
+    Required,
+    Annotated,
+    get_args,
+    get_origin,
+)
 
 from .._types import InheritsGeneric
 from .._compat import is_union as _is_union
@@ -36,6 +45,26 @@ def is_typevar(typ: type) -> bool:
     return type(typ) == TypeVar  # type: ignore
 
 
+_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,)
+if sys.version_info >= (3, 12):
+    _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType)
+
+
+def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]:
+    """Return whether the provided argument is an instance of `TypeAliasType`.
+
+    ```python
+    type Int = int
+    is_type_alias_type(Int)
+    # > True
+    Str = TypeAliasType("Str", str)
+    is_type_alias_type(Str)
+    # > True
+    ```
+    """
+    return isinstance(tp, _TYPE_ALIAS_TYPES)
+
+
 # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
 def strip_annotated_type(typ: type) -> type:
     if is_required_type(typ) or is_annotated_type(typ):
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index c871d01..9621169 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.0.5"  # x-release-please-version
+__version__ = "1.1.0"  # x-release-please-version
diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py
index 806cb01..486cd5f 100644
--- a/src/browserbase/resources/contexts.py
+++ b/src/browserbase/resources/contexts.py
@@ -30,7 +30,7 @@ class ContextsResource(SyncAPIResource):
     @cached_property
     def with_raw_response(self) -> ContextsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -153,7 +153,7 @@ class AsyncContextsResource(AsyncAPIResource):
     @cached_property
     def with_raw_response(self) -> AsyncContextsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py
index dc6c0ac..a98685a 100644
--- a/src/browserbase/resources/extensions.py
+++ b/src/browserbase/resources/extensions.py
@@ -32,7 +32,7 @@ class ExtensionsResource(SyncAPIResource):
     @cached_property
     def with_raw_response(self) -> ExtensionsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -159,7 +159,7 @@ class AsyncExtensionsResource(AsyncAPIResource):
     @cached_property
     def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py
index bf4a5df..fb337a0 100644
--- a/src/browserbase/resources/projects.py
+++ b/src/browserbase/resources/projects.py
@@ -25,7 +25,7 @@ class ProjectsResource(SyncAPIResource):
     @cached_property
     def with_raw_response(self) -> ProjectsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -131,7 +131,7 @@ class AsyncProjectsResource(AsyncAPIResource):
     @cached_property
     def with_raw_response(self) -> AsyncProjectsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py
index 461163b..9ee4975 100644
--- a/src/browserbase/resources/sessions/downloads.py
+++ b/src/browserbase/resources/sessions/downloads.py
@@ -26,7 +26,7 @@ class DownloadsResource(SyncAPIResource):
     @cached_property
     def with_raw_response(self) -> DownloadsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -81,7 +81,7 @@ class AsyncDownloadsResource(AsyncAPIResource):
     @cached_property
     def with_raw_response(self) -> AsyncDownloadsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
diff --git a/src/browserbase/resources/sessions/logs.py b/src/browserbase/resources/sessions/logs.py
index 07fb581..2a42c9d 100644
--- a/src/browserbase/resources/sessions/logs.py
+++ b/src/browserbase/resources/sessions/logs.py
@@ -23,7 +23,7 @@ class LogsResource(SyncAPIResource):
     @cached_property
     def with_raw_response(self) -> LogsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -77,7 +77,7 @@ class AsyncLogsResource(AsyncAPIResource):
     @cached_property
     def with_raw_response(self) -> AsyncLogsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
diff --git a/src/browserbase/resources/sessions/recording.py b/src/browserbase/resources/sessions/recording.py
index b216fd9..856b292 100644
--- a/src/browserbase/resources/sessions/recording.py
+++ b/src/browserbase/resources/sessions/recording.py
@@ -23,7 +23,7 @@ class RecordingResource(SyncAPIResource):
     @cached_property
     def with_raw_response(self) -> RecordingResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -77,7 +77,7 @@ class AsyncRecordingResource(AsyncAPIResource):
     @cached_property
     def with_raw_response(self) -> AsyncRecordingResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py
index 45ef082..0572d91 100644
--- a/src/browserbase/resources/sessions/sessions.py
+++ b/src/browserbase/resources/sessions/sessions.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import Union, Iterable
+from typing import Dict, Union, Iterable
 from typing_extensions import Literal
 
 import httpx
@@ -82,7 +82,7 @@ def uploads(self) -> UploadsResource:
     @cached_property
     def with_raw_response(self) -> SessionsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -108,6 +108,7 @@ def create(
         proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN,
         region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN,
         api_timeout: int | NotGiven = NOT_GIVEN,
+        user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN,
         # 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,
@@ -137,6 +138,9 @@ def create(
           api_timeout: Duration in seconds after which the session will automatically end. Defaults to
               the Project's `defaultTimeout`.
 
+          user_metadata: Arbitrary user metadata to attach to the session. To learn more about user
+              metadata, see [User Metadata](/features/sessions#user-metadata).
+
           extra_headers: Send extra headers
 
           extra_query: Add additional query parameters to the request
@@ -156,6 +160,7 @@ def create(
                     "proxies": proxies,
                     "region": region,
                     "api_timeout": api_timeout,
+                    "user_metadata": user_metadata,
                 },
                 session_create_params.SessionCreateParams,
             ),
@@ -250,6 +255,7 @@ def update(
     def list(
         self,
         *,
+        q: str | NotGiven = NOT_GIVEN,
         status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN,
         # 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.
@@ -258,10 +264,15 @@ def list(
         extra_body: Body | None = None,
         timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
     ) -> SessionListResponse:
-        """
-        List Sessions
+        """List Sessions
 
         Args:
+          q: Query sessions by user metadata.
+
+        See
+              [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata)
+              for the schema of this query.
+
           extra_headers: Send extra headers
 
           extra_query: Add additional query parameters to the request
@@ -277,7 +288,13 @@ def list(
                 extra_query=extra_query,
                 extra_body=extra_body,
                 timeout=timeout,
-                query=maybe_transform({"status": status}, session_list_params.SessionListParams),
+                query=maybe_transform(
+                    {
+                        "q": q,
+                        "status": status,
+                    },
+                    session_list_params.SessionListParams,
+                ),
             ),
             cast_to=SessionListResponse,
         )
@@ -336,7 +353,7 @@ def uploads(self) -> AsyncUploadsResource:
     @cached_property
     def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -362,6 +379,7 @@ async def create(
         proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN,
         region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN,
         api_timeout: int | NotGiven = NOT_GIVEN,
+        user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN,
         # 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,
@@ -391,6 +409,9 @@ async def create(
           api_timeout: Duration in seconds after which the session will automatically end. Defaults to
               the Project's `defaultTimeout`.
 
+          user_metadata: Arbitrary user metadata to attach to the session. To learn more about user
+              metadata, see [User Metadata](/features/sessions#user-metadata).
+
           extra_headers: Send extra headers
 
           extra_query: Add additional query parameters to the request
@@ -410,6 +431,7 @@ async def create(
                     "proxies": proxies,
                     "region": region,
                     "api_timeout": api_timeout,
+                    "user_metadata": user_metadata,
                 },
                 session_create_params.SessionCreateParams,
             ),
@@ -504,6 +526,7 @@ async def update(
     async def list(
         self,
         *,
+        q: str | NotGiven = NOT_GIVEN,
         status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN,
         # 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.
@@ -512,10 +535,15 @@ async def list(
         extra_body: Body | None = None,
         timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
     ) -> SessionListResponse:
-        """
-        List Sessions
+        """List Sessions
 
         Args:
+          q: Query sessions by user metadata.
+
+        See
+              [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata)
+              for the schema of this query.
+
           extra_headers: Send extra headers
 
           extra_query: Add additional query parameters to the request
@@ -531,7 +559,13 @@ async def list(
                 extra_query=extra_query,
                 extra_body=extra_body,
                 timeout=timeout,
-                query=await async_maybe_transform({"status": status}, session_list_params.SessionListParams),
+                query=await async_maybe_transform(
+                    {
+                        "q": q,
+                        "status": status,
+                    },
+                    session_list_params.SessionListParams,
+                ),
             ),
             cast_to=SessionListResponse,
         )
@@ -715,4 +749,4 @@ def recording(self) -> AsyncRecordingResourceWithStreamingResponse:
 
     @cached_property
     def uploads(self) -> AsyncUploadsResourceWithStreamingResponse:
-        return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads)
\ No newline at end of file
+        return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads)
diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py
index e985e4d..eed9349 100644
--- a/src/browserbase/resources/sessions/uploads.py
+++ b/src/browserbase/resources/sessions/uploads.py
@@ -32,7 +32,7 @@ class UploadsResource(SyncAPIResource):
     @cached_property
     def with_raw_response(self) -> UploadsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
@@ -95,7 +95,7 @@ class AsyncUploadsResource(AsyncAPIResource):
     @cached_property
     def with_raw_response(self) -> AsyncUploadsResourceWithRawResponse:
         """
-        This property can be used as a prefix for any HTTP method call to return the
+        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/browserbase/sdk-python#accessing-raw-response-data-eg-headers
diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py
index 8bd47f9..16450e2 100644
--- a/src/browserbase/types/session.py
+++ b/src/browserbase/types/session.py
@@ -1,6 +1,6 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
-from typing import Optional
+from typing import Dict, Optional
 from datetime import datetime
 from typing_extensions import Literal
 
@@ -46,3 +46,10 @@ class Session(BaseModel):
 
     memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None)
     """Memory used by the Session"""
+
+    user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None)
+    """Arbitrary user metadata to attach to the session.
+
+    To learn more about user metadata, see
+    [User Metadata](/features/sessions#user-metadata).
+    """
diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py
index bd643b3..5e76037 100644
--- a/src/browserbase/types/session_create_params.py
+++ b/src/browserbase/types/session_create_params.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import List, Union, Iterable
+from typing import Dict, List, Union, Iterable
 from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict
 
 from .._utils import PropertyInfo
@@ -57,6 +57,13 @@ class SessionCreateParams(TypedDict, total=False):
     Defaults to the Project's `defaultTimeout`.
     """
 
+    user_metadata: Annotated[Dict[str, object], PropertyInfo(alias="userMetadata")]
+    """Arbitrary user metadata to attach to the session.
+
+    To learn more about user metadata, see
+    [User Metadata](/features/sessions#user-metadata).
+    """
+
 
 class BrowserSettingsContext(TypedDict, total=False):
     id: Required[str]
@@ -107,6 +114,9 @@ class BrowserSettingsViewport(TypedDict, total=False):
 
 
 class BrowserSettings(TypedDict, total=False):
+    advanced_stealth: Annotated[bool, PropertyInfo(alias="advancedStealth")]
+    """Advanced Browser Stealth Mode"""
+
     block_ads: Annotated[bool, PropertyInfo(alias="blockAds")]
     """Enable or disable ad blocking in the browser. Defaults to `false`."""
 
diff --git a/src/browserbase/types/session_create_response.py b/src/browserbase/types/session_create_response.py
index 8c9ae09..b548d50 100644
--- a/src/browserbase/types/session_create_response.py
+++ b/src/browserbase/types/session_create_response.py
@@ -1,6 +1,6 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
-from typing import Optional
+from typing import Dict, Optional
 from datetime import datetime
 from typing_extensions import Literal
 
@@ -55,3 +55,10 @@ class SessionCreateResponse(BaseModel):
 
     memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None)
     """Memory used by the Session"""
+
+    user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None)
+    """Arbitrary user metadata to attach to the session.
+
+    To learn more about user metadata, see
+    [User Metadata](/features/sessions#user-metadata).
+    """
diff --git a/src/browserbase/types/session_list_params.py b/src/browserbase/types/session_list_params.py
index 7ba4798..54b0a05 100644
--- a/src/browserbase/types/session_list_params.py
+++ b/src/browserbase/types/session_list_params.py
@@ -8,4 +8,12 @@
 
 
 class SessionListParams(TypedDict, total=False):
+    q: str
+    """Query sessions by user metadata.
+
+    See
+    [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata)
+    for the schema of this query.
+    """
+
     status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"]
diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py
index d15eb83..428f518 100644
--- a/src/browserbase/types/sessions/session_log.py
+++ b/src/browserbase/types/sessions/session_log.py
@@ -14,7 +14,7 @@ class Request(BaseModel):
 
     raw_body: str = FieldInfo(alias="rawBody")
 
-    timestamp: int
+    timestamp: Optional[int] = None
     """milliseconds that have elapsed since the UNIX epoch"""
 
 
@@ -23,22 +23,17 @@ class Response(BaseModel):
 
     result: Dict[str, object]
 
-    timestamp: int
+    timestamp: Optional[int] = None
     """milliseconds that have elapsed since the UNIX epoch"""
 
 
 class SessionLog(BaseModel):
-    event_id: str = FieldInfo(alias="eventId")
-
     method: str
 
     page_id: int = FieldInfo(alias="pageId")
 
     session_id: str = FieldInfo(alias="sessionId")
 
-    timestamp: int
-    """milliseconds that have elapsed since the UNIX epoch"""
-
     frame_id: Optional[str] = FieldInfo(alias="frameId", default=None)
 
     loader_id: Optional[str] = FieldInfo(alias="loaderId", default=None)
@@ -46,3 +41,6 @@ class SessionLog(BaseModel):
     request: Optional[Request] = None
 
     response: Optional[Response] = None
+
+    timestamp: Optional[int] = None
+    """milliseconds that have elapsed since the UNIX epoch"""
diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py
index 7b9fbce..482624f 100644
--- a/tests/api_resources/test_sessions.py
+++ b/tests/api_resources/test_sessions.py
@@ -34,6 +34,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None:
         session = client.sessions.create(
             project_id="projectId",
             browser_settings={
+                "advanced_stealth": True,
                 "block_ads": True,
                 "context": {
                     "id": "id",
@@ -66,6 +67,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None:
             proxies=True,
             region="us-west-2",
             api_timeout=60,
+            user_metadata={"foo": "bar"},
         )
         assert_matches_type(SessionCreateResponse, session, path=["response"])
 
@@ -185,6 +187,7 @@ def test_method_list(self, client: Browserbase) -> None:
     @parametrize
     def test_method_list_with_all_params(self, client: Browserbase) -> None:
         session = client.sessions.list(
+            q="q",
             status="RUNNING",
         )
         assert_matches_type(SessionListResponse, session, path=["response"])
@@ -263,6 +266,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas
         session = await async_client.sessions.create(
             project_id="projectId",
             browser_settings={
+                "advanced_stealth": True,
                 "block_ads": True,
                 "context": {
                     "id": "id",
@@ -295,6 +299,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas
             proxies=True,
             region="us-west-2",
             api_timeout=60,
+            user_metadata={"foo": "bar"},
         )
         assert_matches_type(SessionCreateResponse, session, path=["response"])
 
@@ -414,6 +419,7 @@ async def test_method_list(self, async_client: AsyncBrowserbase) -> None:
     @parametrize
     async def test_method_list_with_all_params(self, async_client: AsyncBrowserbase) -> None:
         session = await async_client.sessions.list(
+            q="q",
             status="RUNNING",
         )
         assert_matches_type(SessionListResponse, session, path=["response"])
diff --git a/tests/test_client.py b/tests/test_client.py
index aa0043e..20ac94b 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -6,6 +6,7 @@
 import os
 import sys
 import json
+import time
 import asyncio
 import inspect
 import subprocess
@@ -355,11 +356,11 @@ def test_default_query_option(self) -> None:
             FinalRequestOptions(
                 method="get",
                 url="/foo",
-                params={"foo": "baz", "query_param": "overriden"},
+                params={"foo": "baz", "query_param": "overridden"},
             )
         )
         url = httpx.URL(request.url)
-        assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
+        assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
 
     def test_request_extra_json(self) -> None:
         request = self.client._build_request(
@@ -1131,11 +1132,11 @@ def test_default_query_option(self) -> None:
             FinalRequestOptions(
                 method="get",
                 url="/foo",
-                params={"foo": "baz", "query_param": "overriden"},
+                params={"foo": "baz", "query_param": "overridden"},
             )
         )
         url = httpx.URL(request.url)
-        assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
+        assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
 
     def test_request_extra_json(self) -> None:
         request = self.client._build_request(
@@ -1639,10 +1640,20 @@ async def test_main() -> None:
             [sys.executable, "-c", test_code],
             text=True,
         ) as process:
-            try:
-                process.wait(2)
-                if process.returncode:
-                    raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
-            except subprocess.TimeoutExpired as e:
-                process.kill()
-                raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e
+            timeout = 10  # seconds
+
+            start_time = time.monotonic()
+            while True:
+                return_code = process.poll()
+                if return_code is not None:
+                    if return_code != 0:
+                        raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
+
+                    # success
+                    break
+
+                if time.monotonic() - start_time > timeout:
+                    process.kill()
+                    raise AssertionError("calling get_platform using asyncify resulted in a hung process")
+
+                time.sleep(0.1)
diff --git a/tests/test_models.py b/tests/test_models.py
index 37671b0..669d219 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,7 +1,7 @@
 import json
 from typing import Any, Dict, List, Union, Optional, cast
 from datetime import datetime, timezone
-from typing_extensions import Literal, Annotated
+from typing_extensions import Literal, Annotated, TypeAliasType
 
 import pytest
 import pydantic
@@ -828,3 +828,29 @@ class B(BaseModel):
     # if the discriminator details object stays the same between invocations then
     # we hit the cache
     assert UnionType.__discriminator__ is discriminator
+
+
+@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1")
+def test_type_alias_type() -> None:
+    Alias = TypeAliasType("Alias", str)
+
+    class Model(BaseModel):
+        alias: Alias
+        union: Union[int, Alias]
+
+    m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model)
+    assert isinstance(m, Model)
+    assert isinstance(m.alias, str)
+    assert m.alias == "foo"
+    assert isinstance(m.union, str)
+    assert m.union == "bar"
+
+
+@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1")
+def test_field_named_cls() -> None:
+    class Model(BaseModel):
+        cls: str
+
+    m = construct_type(value={"cls": "foo"}, type_=Model)
+    assert isinstance(m, Model)
+    assert isinstance(m.cls, str)
diff --git a/tests/utils.py b/tests/utils.py
index ad9be37..ac183a7 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -16,6 +16,7 @@
     is_union_type,
     extract_type_arg,
     is_annotated_type,
+    is_type_alias_type,
 )
 from browserbase._compat import PYDANTIC_V2, field_outer_type, get_model_fields
 from browserbase._models import BaseModel
@@ -51,6 +52,9 @@ def assert_matches_type(
     path: list[str],
     allow_none: bool = False,
 ) -> None:
+    if is_type_alias_type(type_):
+        type_ = type_.__value__
+
     # unwrap `Annotated[T, ...]` -> `T`
     if is_annotated_type(type_):
         type_ = extract_type_arg(type_, 0)

From 797daee99c588564939c0292da3075f65baf23ee Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 11 Feb 2025 14:56:35 -0800
Subject: [PATCH 08/10] release: 1.2.0 (#114)

* chore(internal): change default timeout to an int (#113)

* chore(internal): bummp ruff dependency (#115)

* feat(client): send `X-Stainless-Read-Timeout` header (#117)

* chore(internal): fix type traversing dictionary params (#118)

* chore(internal): minor type handling changes (#119)

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
---
 .release-please-manifest.json        |  2 +-
 CHANGELOG.md                         | 16 ++++++++++++++++
 pyproject.toml                       |  4 ++--
 scripts/utils/ruffen-docs.py         |  4 ++--
 src/browserbase/_base_client.py      | 11 +++++++++--
 src/browserbase/_constants.py        |  2 +-
 src/browserbase/_models.py           | 10 ++++++++--
 src/browserbase/_utils/_transform.py | 12 +++++++++++-
 src/browserbase/_version.py          |  2 +-
 tests/test_transform.py              | 11 ++++++++++-
 10 files changed, 61 insertions(+), 13 deletions(-)

diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2601677..d0ab664 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.1.0"
+  ".": "1.2.0"
 }
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9586e9..4091999 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
 # Changelog
 
+## 1.2.0 (2025-02-11)
+
+Full Changelog: [v1.1.0...v1.2.0](https://github.com/browserbase/sdk-python/compare/v1.1.0...v1.2.0)
+
+### Features
+
+* **client:** send `X-Stainless-Read-Timeout` header ([#117](https://github.com/browserbase/sdk-python/issues/117)) ([e53c47a](https://github.com/browserbase/sdk-python/commit/e53c47ae14f4dca507cc146b37b81d5e59845806))
+
+
+### Chores
+
+* **internal:** bummp ruff dependency ([#115](https://github.com/browserbase/sdk-python/issues/115)) ([f687590](https://github.com/browserbase/sdk-python/commit/f68759062445e8336ca0f6c9b0bde3b0d2ca1e62))
+* **internal:** change default timeout to an int ([#113](https://github.com/browserbase/sdk-python/issues/113)) ([081bb21](https://github.com/browserbase/sdk-python/commit/081bb216f4b9a4df0dfdd51bcbcacef0154fe636))
+* **internal:** fix type traversing dictionary params ([#118](https://github.com/browserbase/sdk-python/issues/118)) ([cc59fe8](https://github.com/browserbase/sdk-python/commit/cc59fe8950fa4e66ee5efd598b69da9c0c8f08a0))
+* **internal:** minor type handling changes ([#119](https://github.com/browserbase/sdk-python/issues/119)) ([7be3940](https://github.com/browserbase/sdk-python/commit/7be3940cfb0bb947a6774ec225b5eb450a951e88))
+
 ## 1.1.0 (2025-01-28)
 
 Full Changelog: [v1.0.5...v1.1.0](https://github.com/browserbase/sdk-python/compare/v1.0.5...v1.1.0)
diff --git a/pyproject.toml b/pyproject.toml
index 7e1b0e9..f55d5c3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.1.0"
+version = "1.2.0"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
@@ -186,7 +186,7 @@ select = [
   "T201",
   "T203",
   # misuse of typing.TYPE_CHECKING
-  "TCH004",
+  "TC004",
   # import rules
   "TID251",
 ]
diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py
index 37b3d94..0cf2bd2 100644
--- a/scripts/utils/ruffen-docs.py
+++ b/scripts/utils/ruffen-docs.py
@@ -47,7 +47,7 @@ def _md_match(match: Match[str]) -> str:
         with _collect_error(match):
             code = format_code_block(code)
         code = textwrap.indent(code, match["indent"])
-        return f'{match["before"]}{code}{match["after"]}'
+        return f"{match['before']}{code}{match['after']}"
 
     def _pycon_match(match: Match[str]) -> str:
         code = ""
@@ -97,7 +97,7 @@ def finish_fragment() -> None:
     def _md_pycon_match(match: Match[str]) -> str:
         code = _pycon_match(match)
         code = textwrap.indent(code, match["indent"])
-        return f'{match["before"]}{code}{match["after"]}'
+        return f"{match['before']}{code}{match['after']}"
 
     src = MD_RE.sub(_md_match, src)
     src = MD_PYCON_RE.sub(_md_pycon_match, src)
diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py
index b21f479..0e3d4ae 100644
--- a/src/browserbase/_base_client.py
+++ b/src/browserbase/_base_client.py
@@ -418,10 +418,17 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0
         if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
             headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
 
-        # Don't set the retry count header if it was already set or removed by the caller. We check
+        # Don't set these headers if they were already set or removed by the caller. We check
         # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
-        if "x-stainless-retry-count" not in (header.lower() for header in custom_headers):
+        lower_custom_headers = [header.lower() for header in custom_headers]
+        if "x-stainless-retry-count" not in lower_custom_headers:
             headers["x-stainless-retry-count"] = str(retries_taken)
+        if "x-stainless-read-timeout" not in lower_custom_headers:
+            timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout
+            if isinstance(timeout, Timeout):
+                timeout = timeout.read
+            if timeout is not None:
+                headers["x-stainless-read-timeout"] = str(timeout)
 
         return headers
 
diff --git a/src/browserbase/_constants.py b/src/browserbase/_constants.py
index a2ac3b6..6ddf2c7 100644
--- a/src/browserbase/_constants.py
+++ b/src/browserbase/_constants.py
@@ -6,7 +6,7 @@
 OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to"
 
 # default timeout is 1 minute
-DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0)
+DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0)
 DEFAULT_MAX_RETRIES = 2
 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20)
 
diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py
index 9a918aa..c4401ff 100644
--- a/src/browserbase/_models.py
+++ b/src/browserbase/_models.py
@@ -172,7 +172,7 @@ def to_json(
     @override
     def __str__(self) -> str:
         # mypy complains about an invalid self arg
-        return f'{self.__repr_name__()}({self.__repr_str__(", ")})'  # type: ignore[misc]
+        return f"{self.__repr_name__()}({self.__repr_str__(', ')})"  # type: ignore[misc]
 
     # Override the 'construct' method in a way that supports recursive parsing without validation.
     # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
@@ -426,10 +426,16 @@ def construct_type(*, value: object, type_: object) -> object:
 
     If the given value does not match the expected type then it is returned as-is.
     """
+
+    # store a reference to the original type we were given before we extract any inner
+    # types so that we can properly resolve forward references in `TypeAliasType` annotations
+    original_type = None
+
     # we allow `object` as the input type because otherwise, passing things like
     # `Literal['value']` will be reported as a type error by type checkers
     type_ = cast("type[object]", type_)
     if is_type_alias_type(type_):
+        original_type = type_  # type: ignore[unreachable]
         type_ = type_.__value__  # type: ignore[unreachable]
 
     # unwrap `Annotated[T, ...]` -> `T`
@@ -446,7 +452,7 @@ def construct_type(*, value: object, type_: object) -> object:
 
     if is_union(origin):
         try:
-            return validate_type(type_=cast("type[object]", type_), value=value)
+            return validate_type(type_=cast("type[object]", original_type or type_), value=value)
         except Exception:
             pass
 
diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py
index a6b62ca..18afd9d 100644
--- a/src/browserbase/_utils/_transform.py
+++ b/src/browserbase/_utils/_transform.py
@@ -25,7 +25,7 @@
     is_annotated_type,
     strip_annotated_type,
 )
-from .._compat import model_dump, is_typeddict
+from .._compat import get_origin, model_dump, is_typeddict
 
 _T = TypeVar("_T")
 
@@ -164,9 +164,14 @@ def _transform_recursive(
         inner_type = annotation
 
     stripped_type = strip_annotated_type(inner_type)
+    origin = get_origin(stripped_type) or stripped_type
     if is_typeddict(stripped_type) and is_mapping(data):
         return _transform_typeddict(data, stripped_type)
 
+    if origin == dict and is_mapping(data):
+        items_type = get_args(stripped_type)[1]
+        return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
+
     if (
         # List[T]
         (is_list_type(stripped_type) and is_list(data))
@@ -307,9 +312,14 @@ async def _async_transform_recursive(
         inner_type = annotation
 
     stripped_type = strip_annotated_type(inner_type)
+    origin = get_origin(stripped_type) or stripped_type
     if is_typeddict(stripped_type) and is_mapping(data):
         return await _async_transform_typeddict(data, stripped_type)
 
+    if origin == dict and is_mapping(data):
+        items_type = get_args(stripped_type)[1]
+        return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
+
     if (
         # List[T]
         (is_list_type(stripped_type) and is_list(data))
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index 9621169..4207df3 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.1.0"  # x-release-please-version
+__version__ = "1.2.0"  # x-release-please-version
diff --git a/tests/test_transform.py b/tests/test_transform.py
index 03c2ecd..32c44ae 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -2,7 +2,7 @@
 
 import io
 import pathlib
-from typing import Any, List, Union, TypeVar, Iterable, Optional, cast
+from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast
 from datetime import date, datetime
 from typing_extensions import Required, Annotated, TypedDict
 
@@ -388,6 +388,15 @@ def my_iter() -> Iterable[Baz8]:
     }
 
 
+@parametrize
+@pytest.mark.asyncio
+async def test_dictionary_items(use_async: bool) -> None:
+    class DictItems(TypedDict):
+        foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]
+
+    assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}}
+
+
 class TypedDictIterableUnionStr(TypedDict):
     foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")]
 

From 7d044017aa92498fa381020485b4e91c67cc2201 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 8 May 2025 11:23:07 -0700
Subject: [PATCH 09/10] release: 1.3.0 (#122)

* chore(internal): update client tests (#121)

* fix: asyncify on non-asyncio runtimes (#123)

* chore(internal): codegen related update (#124)

* feat(client): allow passing `NotGiven` for body (#125)

fix(client): mark some request bodies as optional

* chore(internal): fix devcontainers setup (#126)

* chore(internal): properly set __pydantic_private__ (#127)

* docs: update URLs from stainlessapi.com to stainless.com (#128)

More details at https://www.stainless.com/changelog/stainless-com

* chore(docs): update client docstring (#129)

* chore(internal): remove unused http client options forwarding (#130)

* feat(api): api update (#131)

* chore(internal): codegen related update (#132)

* feat(api): api update (#133)

* chore(internal): remove extra empty newlines (#134)

* chore(internal): bump rye to 0.44.0 (#136)

* fix(types): handle more discriminated union shapes (#137)

* fix(ci): ensure pip is always available (#138)

* fix(ci): remove publishing patch (#139)

* codegen metadata

* feat(api): api update (#140)

* feat(api): api update (#141)

* chore: fix typos (#142)

* codegen metadata

* feat(api): api update (#143)

* chore(internal): remove trailing character (#145)

* chore(internal): slight transform perf improvement (#147)

* chore: slight wording improvement in README (#148)

* chore(internal): expand CI branch coverage

* chore(internal): reduce CI branch coverage

* fix(perf): skip traversing types for NotGiven values

* fix(perf): optimize some hot paths

* chore(internal): update pyright settings

* chore(client): minor internal fixes

* release: 1.3.0

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
---
 .devcontainer/Dockerfile                      |   4 +-
 .devcontainer/devcontainer.json               |   3 +
 .github/workflows/ci.yml                      |   6 +-
 .github/workflows/publish-pypi.yml            |   2 +-
 .release-please-manifest.json                 |   2 +-
 .stats.yml                                    |   4 +-
 CHANGELOG.md                                  |  50 ++++++++
 README.md                                     |  63 +++++++++-
 SECURITY.md                                   |   4 +-
 api.md                                        |  10 +-
 bin/publish-pypi                              |   3 -
 pyproject.toml                                |   7 +-
 scripts/test                                  |   2 +
 src/browserbase/_base_client.py               | 118 +++---------------
 src/browserbase/_client.py                    |   2 +-
 src/browserbase/_files.py                     |   2 +-
 src/browserbase/_models.py                    |   9 +-
 src/browserbase/_utils/_sync.py               |  19 ++-
 src/browserbase/_utils/_transform.py          |  49 +++++++-
 src/browserbase/_utils/_typing.py             |   2 +
 src/browserbase/_version.py                   |   2 +-
 .../resources/sessions/sessions.py            |  17 +--
 src/browserbase/types/__init__.py             |   1 +
 src/browserbase/types/project.py              |   3 +
 .../types/session_create_params.py            |   4 +-
 .../types/session_retrieve_response.py        |  64 ++++++++++
 tests/api_resources/test_sessions.py          |  17 +--
 tests/test_client.py                          |  12 +-
 tests/test_models.py                          |  32 +++++
 tests/test_transform.py                       |  21 +++-
 30 files changed, 380 insertions(+), 154 deletions(-)
 create mode 100644 src/browserbase/types/session_retrieve_response.py

diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index ac9a2e7..ff261ba 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
 
 USER vscode
 
-RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash
+RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash
 ENV PATH=/home/vscode/.rye/shims:$PATH
 
-RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc
+RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index bbeb30b..c17fdc1 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -24,6 +24,9 @@
         }
       }
     }
+  },
+  "features": {
+    "ghcr.io/devcontainers/features/node:1": {}
   }
 
   // Features to add to the dev container. More info: https://containers.dev/features.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c8a8a4f..81f6dc2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,6 @@ jobs:
   lint:
     name: lint
     runs-on: ubuntu-latest
-
     steps:
       - uses: actions/checkout@v4
 
@@ -21,7 +20,7 @@ jobs:
           curl -sSf https://rye.astral.sh/get | bash
           echo "$HOME/.rye/shims" >> $GITHUB_PATH
         env:
-          RYE_VERSION: '0.35.0'
+          RYE_VERSION: '0.44.0'
           RYE_INSTALL_OPTION: '--yes'
 
       - name: Install dependencies
@@ -33,7 +32,6 @@ jobs:
   test:
     name: test
     runs-on: ubuntu-latest
-
     steps:
       - uses: actions/checkout@v4
 
@@ -42,7 +40,7 @@ jobs:
           curl -sSf https://rye.astral.sh/get | bash
           echo "$HOME/.rye/shims" >> $GITHUB_PATH
         env:
-          RYE_VERSION: '0.35.0'
+          RYE_VERSION: '0.44.0'
           RYE_INSTALL_OPTION: '--yes'
 
       - name: Bootstrap
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index db8cf94..b3c832c 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -21,7 +21,7 @@ jobs:
           curl -sSf https://rye.astral.sh/get | bash
           echo "$HOME/.rye/shims" >> $GITHUB_PATH
         env:
-          RYE_VERSION: '0.35.0'
+          RYE_VERSION: '0.44.0'
           RYE_INSTALL_OPTION: '--yes'
 
       - name: Publish to PyPI
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d0ab664..2a8f4ff 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.2.0"
+  ".": "1.3.0"
 }
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index be07776..92128df 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,4 @@
 configured_endpoints: 18
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-396a2b9092f645c5a9e46a1f3be8c2e45ca9ae079e1d39761eb0a73f56e24b15.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-45328621800082e652c9b2f34b176b89ebba3af423ea9f4fed91a359cf4e0ae4.yml
+openapi_spec_hash: c20658b49312b14d158ce5c88f34ee34
+config_hash: 74882e23a455dece33e43a27e67f0fbb
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4091999..f7ab3da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,55 @@
 # Changelog
 
+## 1.3.0 (2025-04-15)
+
+Full Changelog: [v1.2.0...v1.3.0](https://github.com/browserbase/sdk-python/compare/v1.2.0...v1.3.0)
+
+### Features
+
+* **api:** api update ([#131](https://github.com/browserbase/sdk-python/issues/131)) ([1be828d](https://github.com/browserbase/sdk-python/commit/1be828d5c83e48af8740886303f73620bc71b1ba))
+* **api:** api update ([#133](https://github.com/browserbase/sdk-python/issues/133)) ([2a08d98](https://github.com/browserbase/sdk-python/commit/2a08d98914d26cdc36d080bccebd66786b5247ff))
+* **api:** api update ([#140](https://github.com/browserbase/sdk-python/issues/140)) ([134049e](https://github.com/browserbase/sdk-python/commit/134049e29ba480a2238a08c327070bda96b05109))
+* **api:** api update ([#141](https://github.com/browserbase/sdk-python/issues/141)) ([145e5cb](https://github.com/browserbase/sdk-python/commit/145e5cbfc76ac2731b1d6eb3c069cba59a9fbcd9))
+* **api:** api update ([#143](https://github.com/browserbase/sdk-python/issues/143)) ([d55e411](https://github.com/browserbase/sdk-python/commit/d55e4118972d7badbe09a2dd46257d2e66822b85))
+* **client:** allow passing `NotGiven` for body ([#125](https://github.com/browserbase/sdk-python/issues/125)) ([6cdee1b](https://github.com/browserbase/sdk-python/commit/6cdee1ba5775d3c72e0cbd9fe757a1b7452780bd))
+
+
+### Bug Fixes
+
+* asyncify on non-asyncio runtimes ([#123](https://github.com/browserbase/sdk-python/issues/123)) ([c8b2cd7](https://github.com/browserbase/sdk-python/commit/c8b2cd77f4cb07d06a00a09cac3eaa55cf6c6925))
+* **ci:** ensure pip is always available ([#138](https://github.com/browserbase/sdk-python/issues/138)) ([173fdde](https://github.com/browserbase/sdk-python/commit/173fddeea8867f93428bddc5ab1d9e1fcd5a925e))
+* **ci:** remove publishing patch ([#139](https://github.com/browserbase/sdk-python/issues/139)) ([bd66d56](https://github.com/browserbase/sdk-python/commit/bd66d56eec53a7778ca624a7ccd00fcf8a9f69af))
+* **client:** mark some request bodies as optional ([6cdee1b](https://github.com/browserbase/sdk-python/commit/6cdee1ba5775d3c72e0cbd9fe757a1b7452780bd))
+* **perf:** optimize some hot paths ([042f048](https://github.com/browserbase/sdk-python/commit/042f048847634ed606d475a0aaeedc5fd129ddbd))
+* **perf:** skip traversing types for NotGiven values ([5cc6c58](https://github.com/browserbase/sdk-python/commit/5cc6c58561556e2b50fccbeed5e123adf3aba72d))
+* **types:** handle more discriminated union shapes ([#137](https://github.com/browserbase/sdk-python/issues/137)) ([d9e09e3](https://github.com/browserbase/sdk-python/commit/d9e09e3d2428a92c29a4411533564637ce5b3121))
+
+
+### Chores
+
+* **client:** minor internal fixes ([47df6f5](https://github.com/browserbase/sdk-python/commit/47df6f5956507649f684df46bf2b5bb18aa7bc93))
+* **docs:** update client docstring ([#129](https://github.com/browserbase/sdk-python/issues/129)) ([b2201f1](https://github.com/browserbase/sdk-python/commit/b2201f1d9f99f67a3b8fa21ba19560e72a245611))
+* fix typos ([#142](https://github.com/browserbase/sdk-python/issues/142)) ([0157632](https://github.com/browserbase/sdk-python/commit/015763281689247799dd97e46884ba3be520c2f5))
+* **internal:** bump rye to 0.44.0 ([#136](https://github.com/browserbase/sdk-python/issues/136)) ([9aeac01](https://github.com/browserbase/sdk-python/commit/9aeac01a20df8303f806e22b274bdd10adaeea49))
+* **internal:** codegen related update ([#124](https://github.com/browserbase/sdk-python/issues/124)) ([0678102](https://github.com/browserbase/sdk-python/commit/0678102eee40182b0fc2c2a2b2e3f965a2885a50))
+* **internal:** codegen related update ([#132](https://github.com/browserbase/sdk-python/issues/132)) ([3248d7e](https://github.com/browserbase/sdk-python/commit/3248d7e6242808bcb74427cb1b78ac52dee0948c))
+* **internal:** expand CI branch coverage ([4494839](https://github.com/browserbase/sdk-python/commit/449483977d4af8b56b916d555bea966f25304ac7))
+* **internal:** fix devcontainers setup ([#126](https://github.com/browserbase/sdk-python/issues/126)) ([eaf577b](https://github.com/browserbase/sdk-python/commit/eaf577b05bd72e2bb40105131a65e7c13172c3bb))
+* **internal:** properly set __pydantic_private__ ([#127](https://github.com/browserbase/sdk-python/issues/127)) ([5236106](https://github.com/browserbase/sdk-python/commit/52361065d4547b06c44a07396e0679f588181053))
+* **internal:** reduce CI branch coverage ([1bd4d8b](https://github.com/browserbase/sdk-python/commit/1bd4d8bf088ac47c01a12048cb7b3c963d18eb4a))
+* **internal:** remove extra empty newlines ([#134](https://github.com/browserbase/sdk-python/issues/134)) ([2206050](https://github.com/browserbase/sdk-python/commit/22060504e0f57402decfff129778a472717e29e1))
+* **internal:** remove trailing character ([#145](https://github.com/browserbase/sdk-python/issues/145)) ([2b055d7](https://github.com/browserbase/sdk-python/commit/2b055d730b2313227a0193cfc2b95056d4731464))
+* **internal:** remove unused http client options forwarding ([#130](https://github.com/browserbase/sdk-python/issues/130)) ([c63a3bd](https://github.com/browserbase/sdk-python/commit/c63a3bdad3f35658d87d48bbd5e746a36228a8ab))
+* **internal:** slight transform perf improvement ([#147](https://github.com/browserbase/sdk-python/issues/147)) ([2d46582](https://github.com/browserbase/sdk-python/commit/2d46582e5bb55d3ca74c2a4191144743d5f0058b))
+* **internal:** update client tests ([#121](https://github.com/browserbase/sdk-python/issues/121)) ([862cd7e](https://github.com/browserbase/sdk-python/commit/862cd7efb4c694866ab385c5a70fd450b917f057))
+* **internal:** update pyright settings ([0f0e110](https://github.com/browserbase/sdk-python/commit/0f0e110388f893b86881aa67badc30af8e271b8a))
+* slight wording improvement in README ([#148](https://github.com/browserbase/sdk-python/issues/148)) ([c40603c](https://github.com/browserbase/sdk-python/commit/c40603cafa809128edeff23eca37db97dda8de54))
+
+
+### Documentation
+
+* update URLs from stainlessapi.com to stainless.com ([#128](https://github.com/browserbase/sdk-python/issues/128)) ([5e2932f](https://github.com/browserbase/sdk-python/commit/5e2932f5c13c19eb454116ffdce38863556feaf1))
+
 ## 1.2.0 (2025-02-11)
 
 Full Changelog: [v1.1.0...v1.2.0](https://github.com/browserbase/sdk-python/compare/v1.1.0...v1.2.0)
diff --git a/README.md b/README.md
index 1a508e6..d0dfb40 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ The Browserbase Python library provides convenient access to the Browserbase RES
 application. The library includes type definitions for all request params and response fields,
 and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
 
-It is generated with [Stainless](https://www.stainlessapi.com/).
+It is generated with [Stainless](https://www.stainless.com/).
 
 ## Documentation
 
@@ -90,6 +90,67 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
 
 Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
 
+## Nested params
+
+Nested parameters are dictionaries, typed using `TypedDict`, for example:
+
+```python
+from browserbase import Browserbase
+
+client = Browserbase()
+
+session = client.sessions.create(
+    project_id="projectId",
+    browser_settings={
+        "advanced_stealth": True,
+        "block_ads": True,
+        "context": {
+            "id": "id",
+            "persist": True,
+        },
+        "extension_id": "extensionId",
+        "fingerprint": {
+            "browsers": ["chrome"],
+            "devices": ["desktop"],
+            "http_version": "1",
+            "locales": ["string"],
+            "operating_systems": ["android"],
+            "screen": {
+                "max_height": 0,
+                "max_width": 0,
+                "min_height": 0,
+                "min_width": 0,
+            },
+        },
+        "log_session": True,
+        "record_session": True,
+        "solve_captchas": True,
+        "viewport": {
+            "height": 0,
+            "width": 0,
+        },
+    },
+)
+print(session.browser_settings)
+```
+
+## File uploads
+
+Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.
+
+```python
+from pathlib import Path
+from browserbase import Browserbase
+
+client = Browserbase()
+
+client.extensions.create(
+    file=Path("/path/to/file"),
+)
+```
+
+The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically.
+
 ## Handling errors
 
 When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `browserbase.APIConnectionError` is raised.
diff --git a/SECURITY.md b/SECURITY.md
index 4fdede8..e10eb19 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,9 +2,9 @@
 
 ## Reporting Security Issues
 
-This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
 
-To report a security issue, please contact the Stainless team at security@stainlessapi.com.
+To report a security issue, please contact the Stainless team at security@stainless.com.
 
 ## Responsible Disclosure
 
diff --git a/api.md b/api.md
index 3f21eb2..dbb776f 100644
--- a/api.md
+++ b/api.md
@@ -45,13 +45,19 @@ Methods:
 Types:
 
 ```python
-from browserbase.types import Session, SessionLiveURLs, SessionCreateResponse, SessionListResponse
+from browserbase.types import (
+    Session,
+    SessionLiveURLs,
+    SessionCreateResponse,
+    SessionRetrieveResponse,
+    SessionListResponse,
+)
 ```
 
 Methods:
 
 - <code title="post /v1/sessions">client.sessions.<a href="./src/browserbase/resources/sessions/sessions.py">create</a>(\*\*<a href="src/browserbase/types/session_create_params.py">params</a>) -> <a href="./src/browserbase/types/session_create_response.py">SessionCreateResponse</a></code>
-- <code title="get /v1/sessions/{id}">client.sessions.<a href="./src/browserbase/resources/sessions/sessions.py">retrieve</a>(id) -> <a href="./src/browserbase/types/session.py">Session</a></code>
+- <code title="get /v1/sessions/{id}">client.sessions.<a href="./src/browserbase/resources/sessions/sessions.py">retrieve</a>(id) -> <a href="./src/browserbase/types/session_retrieve_response.py">SessionRetrieveResponse</a></code>
 - <code title="post /v1/sessions/{id}">client.sessions.<a href="./src/browserbase/resources/sessions/sessions.py">update</a>(id, \*\*<a href="src/browserbase/types/session_update_params.py">params</a>) -> <a href="./src/browserbase/types/session.py">Session</a></code>
 - <code title="get /v1/sessions">client.sessions.<a href="./src/browserbase/resources/sessions/sessions.py">list</a>(\*\*<a href="src/browserbase/types/session_list_params.py">params</a>) -> <a href="./src/browserbase/types/session_list_response.py">SessionListResponse</a></code>
 - <code title="get /v1/sessions/{id}/debug">client.sessions.<a href="./src/browserbase/resources/sessions/sessions.py">debug</a>(id) -> <a href="./src/browserbase/types/session_live_urls.py">SessionLiveURLs</a></code>
diff --git a/bin/publish-pypi b/bin/publish-pypi
index 05bfccb..826054e 100644
--- a/bin/publish-pypi
+++ b/bin/publish-pypi
@@ -3,7 +3,4 @@
 set -eux
 mkdir -p dist
 rye build --clean
-# Patching importlib-metadata version until upstream library version is updated
-# https://github.com/pypa/twine/issues/977#issuecomment-2189800841
-"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1'
 rye publish --yes --token=$PYPI_TOKEN
diff --git a/pyproject.toml b/pyproject.toml
index f55d5c3..26da062 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.2.0"
+version = "1.3.0"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
@@ -38,7 +38,6 @@ Homepage = "https://github.com/browserbase/sdk-python"
 Repository = "https://github.com/browserbase/sdk-python"
 
 
-
 [tool.rye]
 managed = true
 # version pins are in requirements-dev.lock
@@ -96,7 +95,7 @@ typecheck = { chain = [
 "typecheck:mypy" = "mypy ."
 
 [build-system]
-requires = ["hatchling", "hatch-fancy-pypi-readme"]
+requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"]
 build-backend = "hatchling.build"
 
 [tool.hatch.build]
@@ -157,11 +156,11 @@ exclude = [
 ]
 
 reportImplicitOverride = true
+reportOverlappingOverload = false
 
 reportImportCycles = false
 reportPrivateUsage = false
 
-
 [tool.ruff]
 line-length = 120
 output-format = "grouped"
diff --git a/scripts/test b/scripts/test
index 4fa5698..2b87845 100755
--- a/scripts/test
+++ b/scripts/test
@@ -52,6 +52,8 @@ else
   echo
 fi
 
+export DEFER_PYDANTIC_BUILD=false
+
 echo "==> Running tests"
 rye run pytest "$@"
 
diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py
index 0e3d4ae..e30de64 100644
--- a/src/browserbase/_base_client.py
+++ b/src/browserbase/_base_client.py
@@ -9,7 +9,6 @@
 import inspect
 import logging
 import platform
-import warnings
 import email.utils
 from types import TracebackType
 from random import random
@@ -36,7 +35,7 @@
 import httpx
 import distro
 import pydantic
-from httpx import URL, Limits
+from httpx import URL
 from pydantic import PrivateAttr
 
 from . import _exceptions
@@ -51,19 +50,16 @@
     Timeout,
     NotGiven,
     ResponseT,
-    Transport,
     AnyMapping,
     PostParser,
-    ProxiesTypes,
     RequestFiles,
     HttpxSendArgs,
-    AsyncTransport,
     RequestOptions,
     HttpxRequestFiles,
     ModelBuilderProtocol,
 )
 from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
-from ._compat import model_copy, model_dump
+from ._compat import PYDANTIC_V2, model_copy, model_dump
 from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
 from ._response import (
     APIResponse,
@@ -207,6 +203,9 @@ def _set_private_attributes(
         model: Type[_T],
         options: FinalRequestOptions,
     ) -> None:
+        if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
+            self.__pydantic_private__ = {}
+
         self._model = model
         self._client = client
         self._options = options
@@ -292,6 +291,9 @@ def _set_private_attributes(
         client: AsyncAPIClient,
         options: FinalRequestOptions,
     ) -> None:
+        if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
+            self.__pydantic_private__ = {}
+
         self._model = model
         self._client = client
         self._options = options
@@ -331,9 +333,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
     _base_url: URL
     max_retries: int
     timeout: Union[float, Timeout, None]
-    _limits: httpx.Limits
-    _proxies: ProxiesTypes | None
-    _transport: Transport | AsyncTransport | None
     _strict_response_validation: bool
     _idempotency_header: str | None
     _default_stream_cls: type[_DefaultStreamT] | None = None
@@ -346,9 +345,6 @@ def __init__(
         _strict_response_validation: bool,
         max_retries: int = DEFAULT_MAX_RETRIES,
         timeout: float | Timeout | None = DEFAULT_TIMEOUT,
-        limits: httpx.Limits,
-        transport: Transport | AsyncTransport | None,
-        proxies: ProxiesTypes | None,
         custom_headers: Mapping[str, str] | None = None,
         custom_query: Mapping[str, object] | None = None,
     ) -> None:
@@ -356,9 +352,6 @@ def __init__(
         self._base_url = self._enforce_trailing_slash(URL(base_url))
         self.max_retries = max_retries
         self.timeout = timeout
-        self._limits = limits
-        self._proxies = proxies
-        self._transport = transport
         self._custom_headers = custom_headers or {}
         self._custom_query = custom_query or {}
         self._strict_response_validation = _strict_response_validation
@@ -416,7 +409,8 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0
 
         idempotency_header = self._idempotency_header
         if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
-            headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
+            options.idempotency_key = options.idempotency_key or self._idempotency_key()
+            headers[idempotency_header] = options.idempotency_key
 
         # Don't set these headers if they were already set or removed by the caller. We check
         # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
@@ -518,7 +512,7 @@ def _build_request(
             # so that passing a `TypedDict` doesn't cause an error.
             # https://github.com/microsoft/pyright/issues/3526#event-6715453066
             params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
-            json=json_data,
+            json=json_data if is_given(json_data) else None,
             files=files,
             **kwargs,
         )
@@ -794,46 +788,11 @@ def __init__(
         base_url: str | URL,
         max_retries: int = DEFAULT_MAX_RETRIES,
         timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
-        transport: Transport | None = None,
-        proxies: ProxiesTypes | None = None,
-        limits: Limits | None = None,
         http_client: httpx.Client | None = None,
         custom_headers: Mapping[str, str] | None = None,
         custom_query: Mapping[str, object] | None = None,
         _strict_response_validation: bool,
     ) -> None:
-        kwargs: dict[str, Any] = {}
-        if limits is not None:
-            warnings.warn(
-                "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
-                category=DeprecationWarning,
-                stacklevel=3,
-            )
-            if http_client is not None:
-                raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
-        else:
-            limits = DEFAULT_CONNECTION_LIMITS
-
-        if transport is not None:
-            kwargs["transport"] = transport
-            warnings.warn(
-                "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
-                category=DeprecationWarning,
-                stacklevel=3,
-            )
-            if http_client is not None:
-                raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
-
-        if proxies is not None:
-            kwargs["proxies"] = proxies
-            warnings.warn(
-                "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
-                category=DeprecationWarning,
-                stacklevel=3,
-            )
-            if http_client is not None:
-                raise ValueError("The `http_client` argument is mutually exclusive with `proxies`")
-
         if not is_given(timeout):
             # if the user passed in a custom http client with a non-default
             # timeout set then we use that timeout.
@@ -854,12 +813,9 @@ def __init__(
 
         super().__init__(
             version=version,
-            limits=limits,
             # cast to a valid type because mypy doesn't understand our type narrowing
             timeout=cast(Timeout, timeout),
-            proxies=proxies,
             base_url=base_url,
-            transport=transport,
             max_retries=max_retries,
             custom_query=custom_query,
             custom_headers=custom_headers,
@@ -869,9 +825,6 @@ def __init__(
             base_url=base_url,
             # cast to a valid type because mypy doesn't understand our type narrowing
             timeout=cast(Timeout, timeout),
-            limits=limits,
-            follow_redirects=True,
-            **kwargs,  # type: ignore
         )
 
     def is_closed(self) -> bool:
@@ -991,6 +944,10 @@ def _request(
         request = self._build_request(options, retries_taken=retries_taken)
         self._prepare_request(request)
 
+        if options.idempotency_key:
+            # ensure the idempotency key is reused between requests
+            input_options.idempotency_key = options.idempotency_key
+
         kwargs: HttpxSendArgs = {}
         if self.custom_auth is not None:
             kwargs["auth"] = self.custom_auth
@@ -1366,45 +1323,10 @@ def __init__(
         _strict_response_validation: bool,
         max_retries: int = DEFAULT_MAX_RETRIES,
         timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
-        transport: AsyncTransport | None = None,
-        proxies: ProxiesTypes | None = None,
-        limits: Limits | None = None,
         http_client: httpx.AsyncClient | None = None,
         custom_headers: Mapping[str, str] | None = None,
         custom_query: Mapping[str, object] | None = None,
     ) -> None:
-        kwargs: dict[str, Any] = {}
-        if limits is not None:
-            warnings.warn(
-                "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
-                category=DeprecationWarning,
-                stacklevel=3,
-            )
-            if http_client is not None:
-                raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
-        else:
-            limits = DEFAULT_CONNECTION_LIMITS
-
-        if transport is not None:
-            kwargs["transport"] = transport
-            warnings.warn(
-                "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
-                category=DeprecationWarning,
-                stacklevel=3,
-            )
-            if http_client is not None:
-                raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
-
-        if proxies is not None:
-            kwargs["proxies"] = proxies
-            warnings.warn(
-                "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
-                category=DeprecationWarning,
-                stacklevel=3,
-            )
-            if http_client is not None:
-                raise ValueError("The `http_client` argument is mutually exclusive with `proxies`")
-
         if not is_given(timeout):
             # if the user passed in a custom http client with a non-default
             # timeout set then we use that timeout.
@@ -1426,11 +1348,8 @@ def __init__(
         super().__init__(
             version=version,
             base_url=base_url,
-            limits=limits,
             # cast to a valid type because mypy doesn't understand our type narrowing
             timeout=cast(Timeout, timeout),
-            proxies=proxies,
-            transport=transport,
             max_retries=max_retries,
             custom_query=custom_query,
             custom_headers=custom_headers,
@@ -1440,9 +1359,6 @@ def __init__(
             base_url=base_url,
             # cast to a valid type because mypy doesn't understand our type narrowing
             timeout=cast(Timeout, timeout),
-            limits=limits,
-            follow_redirects=True,
-            **kwargs,  # type: ignore
         )
 
     def is_closed(self) -> bool:
@@ -1564,6 +1480,10 @@ async def _request(
         request = self._build_request(options, retries_taken=retries_taken)
         await self._prepare_request(request)
 
+        if options.idempotency_key:
+            # ensure the idempotency key is reused between requests
+            input_options.idempotency_key = options.idempotency_key
+
         kwargs: HttpxSendArgs = {}
         if self.custom_auth is not None:
             kwargs["auth"] = self.custom_auth
diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py
index b845ea8..15fde86 100644
--- a/src/browserbase/_client.py
+++ b/src/browserbase/_client.py
@@ -254,7 +254,7 @@ def __init__(
         # part of our public interface in the future.
         _strict_response_validation: bool = False,
     ) -> None:
-        """Construct a new async Browserbase client instance.
+        """Construct a new async AsyncBrowserbase client instance.
 
         This automatically infers the `api_key` argument from the `BROWSERBASE_API_KEY` environment variable if it is not provided.
         """
diff --git a/src/browserbase/_files.py b/src/browserbase/_files.py
index 715cc20..c690226 100644
--- a/src/browserbase/_files.py
+++ b/src/browserbase/_files.py
@@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
     if not is_file_content(obj):
         prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`"
         raise RuntimeError(
-            f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead."
+            f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/browserbase/sdk-python/tree/main#file-uploads"
         ) from None
 
 
diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py
index c4401ff..3493571 100644
--- a/src/browserbase/_models.py
+++ b/src/browserbase/_models.py
@@ -65,7 +65,7 @@
 from ._constants import RAW_RESPONSE_HEADER
 
 if TYPE_CHECKING:
-    from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema
+    from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
 
 __all__ = ["BaseModel", "GenericModel"]
 
@@ -646,15 +646,18 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
 
 def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None:
     schema = model.__pydantic_core_schema__
+    if schema["type"] == "definitions":
+        schema = schema["schema"]
+
     if schema["type"] != "model":
         return None
 
+    schema = cast("ModelSchema", schema)
     fields_schema = schema["schema"]
     if fields_schema["type"] != "model-fields":
         return None
 
     fields_schema = cast("ModelFieldsSchema", fields_schema)
-
     field = fields_schema["fields"].get(field_name)
     if not field:
         return None
@@ -678,7 +681,7 @@ def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None:
     setattr(typ, "__pydantic_config__", config)  # noqa: B010
 
 
-# our use of subclasssing here causes weirdness for type checkers,
+# our use of subclassing here causes weirdness for type checkers,
 # so we just pretend that we don't subclass
 if TYPE_CHECKING:
     GenericModel = BaseModel
diff --git a/src/browserbase/_utils/_sync.py b/src/browserbase/_utils/_sync.py
index 8b3aaf2..ad7ec71 100644
--- a/src/browserbase/_utils/_sync.py
+++ b/src/browserbase/_utils/_sync.py
@@ -7,16 +7,20 @@
 from typing import Any, TypeVar, Callable, Awaitable
 from typing_extensions import ParamSpec
 
+import anyio
+import sniffio
+import anyio.to_thread
+
 T_Retval = TypeVar("T_Retval")
 T_ParamSpec = ParamSpec("T_ParamSpec")
 
 
 if sys.version_info >= (3, 9):
-    to_thread = asyncio.to_thread
+    _asyncio_to_thread = asyncio.to_thread
 else:
     # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
     # for Python 3.8 support
-    async def to_thread(
+    async def _asyncio_to_thread(
         func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
     ) -> Any:
         """Asynchronously run function *func* in a separate thread.
@@ -34,6 +38,17 @@ async def to_thread(
         return await loop.run_in_executor(None, func_call)
 
 
+async def to_thread(
+    func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
+) -> T_Retval:
+    if sniffio.current_async_library() == "asyncio":
+        return await _asyncio_to_thread(func, *args, **kwargs)
+
+    return await anyio.to_thread.run_sync(
+        functools.partial(func, *args, **kwargs),
+    )
+
+
 # inspired by `asyncer`, https://github.com/tiangolo/asyncer
 def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
     """
diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py
index 18afd9d..b0cc20a 100644
--- a/src/browserbase/_utils/_transform.py
+++ b/src/browserbase/_utils/_transform.py
@@ -5,13 +5,15 @@
 import pathlib
 from typing import Any, Mapping, TypeVar, cast
 from datetime import date, datetime
-from typing_extensions import Literal, get_args, override, get_type_hints
+from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints
 
 import anyio
 import pydantic
 
 from ._utils import (
     is_list,
+    is_given,
+    lru_cache,
     is_mapping,
     is_iterable,
 )
@@ -108,6 +110,7 @@ class Params(TypedDict, total=False):
     return cast(_T, transformed)
 
 
+@lru_cache(maxsize=8096)
 def _get_annotated_type(type_: type) -> type | None:
     """If the given type is an `Annotated` type then it is returned, if not `None` is returned.
 
@@ -126,7 +129,7 @@ def _get_annotated_type(type_: type) -> type | None:
 def _maybe_transform_key(key: str, type_: type) -> str:
     """Transform the given `data` based on the annotations provided in `type_`.
 
-    Note: this function only looks at `Annotated` types that contain `PropertInfo` metadata.
+    Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata.
     """
     annotated_type = _get_annotated_type(type_)
     if annotated_type is None:
@@ -142,6 +145,10 @@ def _maybe_transform_key(key: str, type_: type) -> str:
     return key
 
 
+def _no_transform_needed(annotation: type) -> bool:
+    return annotation == float or annotation == int
+
+
 def _transform_recursive(
     data: object,
     *,
@@ -184,6 +191,15 @@ def _transform_recursive(
             return cast(object, data)
 
         inner_type = extract_type_arg(stripped_type, 0)
+        if _no_transform_needed(inner_type):
+            # for some types there is no need to transform anything, so we can get a small
+            # perf boost from skipping that work.
+            #
+            # but we still need to convert to a list to ensure the data is json-serializable
+            if is_list(data):
+                return data
+            return list(data)
+
         return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
 
     if is_union_type(stripped_type):
@@ -245,6 +261,11 @@ def _transform_typeddict(
     result: dict[str, object] = {}
     annotations = get_type_hints(expected_type, include_extras=True)
     for key, value in data.items():
+        if not is_given(value):
+            # we don't need to include `NotGiven` values here as they'll
+            # be stripped out before the request is sent anyway
+            continue
+
         type_ = annotations.get(key)
         if type_ is None:
             # we do not have a type annotation for this field, leave it as is
@@ -332,6 +353,15 @@ async def _async_transform_recursive(
             return cast(object, data)
 
         inner_type = extract_type_arg(stripped_type, 0)
+        if _no_transform_needed(inner_type):
+            # for some types there is no need to transform anything, so we can get a small
+            # perf boost from skipping that work.
+            #
+            # but we still need to convert to a list to ensure the data is json-serializable
+            if is_list(data):
+                return data
+            return list(data)
+
         return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
 
     if is_union_type(stripped_type):
@@ -393,6 +423,11 @@ async def _async_transform_typeddict(
     result: dict[str, object] = {}
     annotations = get_type_hints(expected_type, include_extras=True)
     for key, value in data.items():
+        if not is_given(value):
+            # we don't need to include `NotGiven` values here as they'll
+            # be stripped out before the request is sent anyway
+            continue
+
         type_ = annotations.get(key)
         if type_ is None:
             # we do not have a type annotation for this field, leave it as is
@@ -400,3 +435,13 @@ async def _async_transform_typeddict(
         else:
             result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_)
     return result
+
+
+@lru_cache(maxsize=8096)
+def get_type_hints(
+    obj: Any,
+    globalns: dict[str, Any] | None = None,
+    localns: Mapping[str, Any] | None = None,
+    include_extras: bool = False,
+) -> dict[str, Any]:
+    return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras)
diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py
index 278749b..1958820 100644
--- a/src/browserbase/_utils/_typing.py
+++ b/src/browserbase/_utils/_typing.py
@@ -13,6 +13,7 @@
     get_origin,
 )
 
+from ._utils import lru_cache
 from .._types import InheritsGeneric
 from .._compat import is_union as _is_union
 
@@ -66,6 +67,7 @@ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]:
 
 
 # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
+@lru_cache(maxsize=8096)
 def strip_annotated_type(typ: type) -> type:
     if is_required_type(typ) or is_annotated_type(typ):
         return strip_annotated_type(cast(type, get_args(typ)[0]))
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index 4207df3..9c23d1b 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.2.0"  # x-release-please-version
+__version__ = "1.3.0"  # x-release-please-version
diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py
index 0572d91..4481438 100644
--- a/src/browserbase/resources/sessions/sessions.py
+++ b/src/browserbase/resources/sessions/sessions.py
@@ -58,6 +58,7 @@
 from ...types.session_live_urls import SessionLiveURLs
 from ...types.session_list_response import SessionListResponse
 from ...types.session_create_response import SessionCreateResponse
+from ...types.session_retrieve_response import SessionRetrieveResponse
 
 __all__ = ["SessionsResource", "AsyncSessionsResource"]
 
@@ -127,8 +128,8 @@ def create(
           extension_id: The uploaded Extension ID. See
               [Upload Extension](/reference/api/upload-an-extension).
 
-          keep_alive: Set to true to keep the session alive even after disconnections. This is
-              available on the Startup plan only.
+          keep_alive: Set to true to keep the session alive even after disconnections. Available on
+              the Hobby Plan and above.
 
           proxies: Proxy configuration. Can be true for default proxy, or an array of proxy
               configurations.
@@ -180,7 +181,7 @@ def retrieve(
         extra_query: Query | None = None,
         extra_body: Body | None = None,
         timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
-    ) -> Session:
+    ) -> SessionRetrieveResponse:
         """
         Session
 
@@ -200,7 +201,7 @@ def retrieve(
             options=make_request_options(
                 extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
             ),
-            cast_to=Session,
+            cast_to=SessionRetrieveResponse,
         )
 
     def update(
@@ -398,8 +399,8 @@ async def create(
           extension_id: The uploaded Extension ID. See
               [Upload Extension](/reference/api/upload-an-extension).
 
-          keep_alive: Set to true to keep the session alive even after disconnections. This is
-              available on the Startup plan only.
+          keep_alive: Set to true to keep the session alive even after disconnections. Available on
+              the Hobby Plan and above.
 
           proxies: Proxy configuration. Can be true for default proxy, or an array of proxy
               configurations.
@@ -451,7 +452,7 @@ async def retrieve(
         extra_query: Query | None = None,
         extra_body: Body | None = None,
         timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
-    ) -> Session:
+    ) -> SessionRetrieveResponse:
         """
         Session
 
@@ -471,7 +472,7 @@ async def retrieve(
             options=make_request_options(
                 extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
             ),
-            cast_to=Session,
+            cast_to=SessionRetrieveResponse,
         )
 
     async def update(
diff --git a/src/browserbase/types/__init__.py b/src/browserbase/types/__init__.py
index ebc243d..4dd85dd 100644
--- a/src/browserbase/types/__init__.py
+++ b/src/browserbase/types/__init__.py
@@ -18,3 +18,4 @@
 from .context_update_response import ContextUpdateResponse as ContextUpdateResponse
 from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams
 from .session_create_response import SessionCreateResponse as SessionCreateResponse
+from .session_retrieve_response import SessionRetrieveResponse as SessionRetrieveResponse
diff --git a/src/browserbase/types/project.py b/src/browserbase/types/project.py
index afbcef6..dc3cf33 100644
--- a/src/browserbase/types/project.py
+++ b/src/browserbase/types/project.py
@@ -12,6 +12,9 @@
 class Project(BaseModel):
     id: str
 
+    concurrency: int
+    """The maximum number of sessions that this project can run concurrently."""
+
     created_at: datetime = FieldInfo(alias="createdAt")
 
     default_timeout: int = FieldInfo(alias="defaultTimeout")
diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py
index 5e76037..f3b9606 100644
--- a/src/browserbase/types/session_create_params.py
+++ b/src/browserbase/types/session_create_params.py
@@ -39,7 +39,7 @@ class SessionCreateParams(TypedDict, total=False):
     keep_alive: Annotated[bool, PropertyInfo(alias="keepAlive")]
     """Set to true to keep the session alive even after disconnections.
 
-    This is available on the Startup plan only.
+    Available on the Hobby Plan and above.
     """
 
     proxies: Union[bool, Iterable[ProxiesUnionMember1]]
@@ -88,7 +88,7 @@ class BrowserSettingsFingerprint(TypedDict, total=False):
 
     devices: List[Literal["desktop", "mobile"]]
 
-    http_version: Annotated[Literal[1, 2], PropertyInfo(alias="httpVersion")]
+    http_version: Annotated[Literal["1", "2"], PropertyInfo(alias="httpVersion")]
 
     locales: List[str]
     """
diff --git a/src/browserbase/types/session_retrieve_response.py b/src/browserbase/types/session_retrieve_response.py
new file mode 100644
index 0000000..a9a4ff2
--- /dev/null
+++ b/src/browserbase/types/session_retrieve_response.py
@@ -0,0 +1,64 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+
+__all__ = ["SessionRetrieveResponse"]
+
+
+class SessionRetrieveResponse(BaseModel):
+    id: str
+
+    created_at: datetime = FieldInfo(alias="createdAt")
+
+    expires_at: datetime = FieldInfo(alias="expiresAt")
+
+    keep_alive: bool = FieldInfo(alias="keepAlive")
+    """Indicates if the Session was created to be kept alive upon disconnections"""
+
+    project_id: str = FieldInfo(alias="projectId")
+    """The Project ID linked to the Session."""
+
+    proxy_bytes: int = FieldInfo(alias="proxyBytes")
+    """Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips)"""
+
+    region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"]
+    """The region where the Session is running."""
+
+    started_at: datetime = FieldInfo(alias="startedAt")
+
+    status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"]
+
+    updated_at: datetime = FieldInfo(alias="updatedAt")
+
+    avg_cpu_usage: Optional[int] = FieldInfo(alias="avgCpuUsage", default=None)
+    """CPU used by the Session"""
+
+    connect_url: Optional[str] = FieldInfo(alias="connectUrl", default=None)
+    """WebSocket URL to connect to the Session."""
+
+    context_id: Optional[str] = FieldInfo(alias="contextId", default=None)
+    """Optional. The Context linked to the Session."""
+
+    ended_at: Optional[datetime] = FieldInfo(alias="endedAt", default=None)
+
+    memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None)
+    """Memory used by the Session"""
+
+    selenium_remote_url: Optional[str] = FieldInfo(alias="seleniumRemoteUrl", default=None)
+    """HTTP URL to connect to the Session."""
+
+    signing_key: Optional[str] = FieldInfo(alias="signingKey", default=None)
+    """Signing key to use when connecting to the Session via HTTP."""
+
+    user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None)
+    """Arbitrary user metadata to attach to the session.
+
+    To learn more about user metadata, see
+    [User Metadata](/features/sessions#user-metadata).
+    """
diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py
index 482624f..0581655 100644
--- a/tests/api_resources/test_sessions.py
+++ b/tests/api_resources/test_sessions.py
@@ -14,6 +14,7 @@
     SessionLiveURLs,
     SessionListResponse,
     SessionCreateResponse,
+    SessionRetrieveResponse,
 )
 
 base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -44,7 +45,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None:
                 "fingerprint": {
                     "browsers": ["chrome"],
                     "devices": ["desktop"],
-                    "http_version": 1,
+                    "http_version": "1",
                     "locales": ["string"],
                     "operating_systems": ["android"],
                     "screen": {
@@ -100,7 +101,7 @@ def test_method_retrieve(self, client: Browserbase) -> None:
         session = client.sessions.retrieve(
             "id",
         )
-        assert_matches_type(Session, session, path=["response"])
+        assert_matches_type(SessionRetrieveResponse, session, path=["response"])
 
     @parametrize
     def test_raw_response_retrieve(self, client: Browserbase) -> None:
@@ -111,7 +112,7 @@ def test_raw_response_retrieve(self, client: Browserbase) -> None:
         assert response.is_closed is True
         assert response.http_request.headers.get("X-Stainless-Lang") == "python"
         session = response.parse()
-        assert_matches_type(Session, session, path=["response"])
+        assert_matches_type(SessionRetrieveResponse, session, path=["response"])
 
     @parametrize
     def test_streaming_response_retrieve(self, client: Browserbase) -> None:
@@ -122,7 +123,7 @@ def test_streaming_response_retrieve(self, client: Browserbase) -> None:
             assert response.http_request.headers.get("X-Stainless-Lang") == "python"
 
             session = response.parse()
-            assert_matches_type(Session, session, path=["response"])
+            assert_matches_type(SessionRetrieveResponse, session, path=["response"])
 
         assert cast(Any, response.is_closed) is True
 
@@ -276,7 +277,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas
                 "fingerprint": {
                     "browsers": ["chrome"],
                     "devices": ["desktop"],
-                    "http_version": 1,
+                    "http_version": "1",
                     "locales": ["string"],
                     "operating_systems": ["android"],
                     "screen": {
@@ -332,7 +333,7 @@ async def test_method_retrieve(self, async_client: AsyncBrowserbase) -> None:
         session = await async_client.sessions.retrieve(
             "id",
         )
-        assert_matches_type(Session, session, path=["response"])
+        assert_matches_type(SessionRetrieveResponse, session, path=["response"])
 
     @parametrize
     async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> None:
@@ -343,7 +344,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBrowserbase) -> No
         assert response.is_closed is True
         assert response.http_request.headers.get("X-Stainless-Lang") == "python"
         session = await response.parse()
-        assert_matches_type(Session, session, path=["response"])
+        assert_matches_type(SessionRetrieveResponse, session, path=["response"])
 
     @parametrize
     async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase) -> None:
@@ -354,7 +355,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBrowserbase)
             assert response.http_request.headers.get("X-Stainless-Lang") == "python"
 
             session = await response.parse()
-            assert_matches_type(Session, session, path=["response"])
+            assert_matches_type(SessionRetrieveResponse, session, path=["response"])
 
         assert cast(Any, response.is_closed) is True
 
diff --git a/tests/test_client.py b/tests/test_client.py
index 20ac94b..d03654d 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -23,6 +23,7 @@
 
 from browserbase import Browserbase, AsyncBrowserbase, APIResponseValidationError
 from browserbase._types import Omit
+from browserbase._utils import maybe_transform
 from browserbase._models import BaseModel, FinalRequestOptions
 from browserbase._constants import RAW_RESPONSE_HEADER
 from browserbase._exceptions import APIStatusError, APITimeoutError, BrowserbaseError, APIResponseValidationError
@@ -32,6 +33,7 @@
     BaseClient,
     make_request_options,
 )
+from browserbase.types.session_create_params import SessionCreateParams
 
 from .utils import update_env
 
@@ -727,7 +729,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No
         with pytest.raises(APITimeoutError):
             self.client.post(
                 "/v1/sessions",
-                body=cast(object, dict(project_id="your_project_id")),
+                body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)),
                 cast_to=httpx.Response,
                 options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
             )
@@ -742,7 +744,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non
         with pytest.raises(APIStatusError):
             self.client.post(
                 "/v1/sessions",
-                body=cast(object, dict(project_id="your_project_id")),
+                body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)),
                 cast_to=httpx.Response,
                 options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
             )
@@ -1507,7 +1509,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter)
         with pytest.raises(APITimeoutError):
             await self.client.post(
                 "/v1/sessions",
-                body=cast(object, dict(project_id="your_project_id")),
+                body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)),
                 cast_to=httpx.Response,
                 options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
             )
@@ -1522,7 +1524,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter)
         with pytest.raises(APIStatusError):
             await self.client.post(
                 "/v1/sessions",
-                body=cast(object, dict(project_id="your_project_id")),
+                body=cast(object, maybe_transform(dict(project_id="your_project_id"), SessionCreateParams)),
                 cast_to=httpx.Response,
                 options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
             )
@@ -1625,7 +1627,7 @@ def test_get_platform(self) -> None:
         import threading
 
         from browserbase._utils import asyncify
-        from browserbase._base_client import get_platform 
+        from browserbase._base_client import get_platform
 
         async def test_main() -> None:
             result = await asyncify(get_platform)()
diff --git a/tests/test_models.py b/tests/test_models.py
index 669d219..21043ab 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -854,3 +854,35 @@ class Model(BaseModel):
     m = construct_type(value={"cls": "foo"}, type_=Model)
     assert isinstance(m, Model)
     assert isinstance(m.cls, str)
+
+
+def test_discriminated_union_case() -> None:
+    class A(BaseModel):
+        type: Literal["a"]
+
+        data: bool
+
+    class B(BaseModel):
+        type: Literal["b"]
+
+        data: List[Union[A, object]]
+
+    class ModelA(BaseModel):
+        type: Literal["modelA"]
+
+        data: int
+
+    class ModelB(BaseModel):
+        type: Literal["modelB"]
+
+        required: str
+
+        data: Union[A, B]
+
+    # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required`
+    m = construct_type(
+        value={"type": "modelB", "data": {"type": "a", "data": True}},
+        type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]),
+    )
+
+    assert isinstance(m, ModelB)
diff --git a/tests/test_transform.py b/tests/test_transform.py
index 32c44ae..cba80b2 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -8,7 +8,7 @@
 
 import pytest
 
-from browserbase._types import Base64FileInput
+from browserbase._types import NOT_GIVEN, Base64FileInput
 from browserbase._utils import (
     PropertyInfo,
     transform as _transform,
@@ -432,3 +432,22 @@ async def test_base64_file_input(use_async: bool) -> None:
     assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == {
         "foo": "SGVsbG8sIHdvcmxkIQ=="
     }  # type: ignore[comparison-overlap]
+
+
+@parametrize
+@pytest.mark.asyncio
+async def test_transform_skipping(use_async: bool) -> None:
+    # lists of ints are left as-is
+    data = [1, 2, 3]
+    assert await transform(data, List[int], use_async) is data
+
+    # iterables of ints are converted to a list
+    data = iter([1, 2, 3])
+    assert await transform(data, Iterable[int], use_async) == [1, 2, 3]
+
+
+@parametrize
+@pytest.mark.asyncio
+async def test_strips_notgiven(use_async: bool) -> None:
+    assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"}
+    assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {}

From 89e7f0d91a73920cdac66f06bee2d8105302fe46 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
 <142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 16 May 2025 13:50:19 -0700
Subject: [PATCH 10/10] release: 1.4.0 (#150)

* chore(internal): bump pyright version

* chore(internal): base client updates

* chore(internal): update models test

* chore(ci): add timeout thresholds for CI jobs

* chore(internal): import reformatting

* chore(internal): fix list file params

* chore(internal): refactor retries to not use recursion

* fix(pydantic v1): more robust ModelField.annotation check

* chore(internal): minor formatting changes

* chore(internal): codegen related update

* chore(ci): only use depot for staging repos

* chore: broadly detect json family of content-type headers

* chore(internal): avoid errors for isinstance checks on proxies

* fix(package): support direct resource imports

* chore(ci): upload sdks to package manager

* chore(ci): fix installation instructions

* feat(api): api update

* release: 1.4.0

* Remove rule that fails lint

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: ajmcquilkin <46639306+ajmcquilkin@users.noreply.github.com>
---
 .github/workflows/ci.yml                      |  42 +-
 .release-please-manifest.json                 |   2 +-
 .stats.yml                                    |   4 +-
 CHANGELOG.md                                  |  32 ++
 README.md                                     |   2 +
 pyproject.toml                                |   6 +-
 requirements-dev.lock                         |   2 +-
 scripts/utils/upload-artifact.sh              |  25 +
 src/browserbase/__init__.py                   |   5 +
 src/browserbase/_base_client.py               | 445 ++++++++----------
 src/browserbase/_client.py                    |   5 +-
 src/browserbase/_models.py                    |   5 +-
 src/browserbase/_response.py                  |   2 +-
 src/browserbase/_utils/_proxy.py              |   5 +-
 src/browserbase/_utils/_resources_proxy.py    |  24 +
 src/browserbase/_utils/_typing.py             |   2 +-
 src/browserbase/_utils/_utils.py              |  10 +-
 src/browserbase/_version.py                   |   2 +-
 src/browserbase/resources/contexts.py         |   5 +-
 src/browserbase/resources/extensions.py       |   7 +-
 .../resources/sessions/sessions.py            |   5 +-
 src/browserbase/resources/sessions/uploads.py |   7 +-
 .../types/context_create_response.py          |   1 -
 .../types/context_update_response.py          |   1 -
 src/browserbase/types/project_usage.py        |   1 -
 .../types/session_create_params.py            |  12 +
 .../types/sessions/upload_create_response.py  |   1 -
 tests/api_resources/test_sessions.py          |   4 +
 tests/conftest.py                             |   2 +-
 tests/test_models.py                          |   5 +-
 tests/test_utils/test_proxy.py                |  11 +
 31 files changed, 389 insertions(+), 293 deletions(-)
 create mode 100755 scripts/utils/upload-artifact.sh
 create mode 100644 src/browserbase/_utils/_resources_proxy.py

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 81f6dc2..1f41588 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,17 +1,18 @@
 name: CI
 on:
   push:
-    branches:
-      - main
-  pull_request:
-    branches:
-      - main
-      - next
+    branches-ignore:
+      - 'generated'
+      - 'codegen/**'
+      - 'integrated/**'
+      - 'stl-preview-head/**'
+      - 'stl-preview-base/**'
 
 jobs:
   lint:
+    timeout-minutes: 10
     name: lint
-    runs-on: ubuntu-latest
+    runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
     steps:
       - uses: actions/checkout@v4
 
@@ -29,9 +30,34 @@ jobs:
       - name: Run lints
         run: ./scripts/lint
 
+  upload:
+    if: github.repository == 'stainless-sdks/browserbase-python'
+    timeout-minutes: 10
+    name: upload
+    permissions:
+      contents: read
+      id-token: write
+    runs-on: depot-ubuntu-24.04
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Get GitHub OIDC Token
+        id: github-oidc
+        uses: actions/github-script@v6
+        with:
+          script: core.setOutput('github_token', await core.getIDToken());
+
+      - name: Upload tarball
+        env:
+          URL: https://pkg.stainless.com/s
+          AUTH: ${{ steps.github-oidc.outputs.github_token }}
+          SHA: ${{ github.sha }}
+        run: ./scripts/utils/upload-artifact.sh
+
   test:
+    timeout-minutes: 10
     name: test
-    runs-on: ubuntu-latest
+    runs-on: ${{ github.repository == 'stainless-sdks/browserbase-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
     steps:
       - uses: actions/checkout@v4
 
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2a8f4ff..3e9af1b 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
 {
-  ".": "1.3.0"
+  ".": "1.4.0"
 }
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 92128df..38c95a8 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
 configured_endpoints: 18
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-45328621800082e652c9b2f34b176b89ebba3af423ea9f4fed91a359cf4e0ae4.yml
-openapi_spec_hash: c20658b49312b14d158ce5c88f34ee34
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-e2ed1b5267eeff92982918505349017b9155da2c7ab948787ab11cf9068af1b8.yml
+openapi_spec_hash: 6639c21dccb52ca610cae833227a9791
 config_hash: 74882e23a455dece33e43a27e67f0fbb
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7ab3da..fb20598 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,37 @@
 # Changelog
 
+## 1.4.0 (2025-05-16)
+
+Full Changelog: [v1.3.0...v1.4.0](https://github.com/browserbase/sdk-python/compare/v1.3.0...v1.4.0)
+
+### Features
+
+* **api:** api update ([d3b2ee1](https://github.com/browserbase/sdk-python/commit/d3b2ee1e3c69efbdcb2f0e53b4625e2c8a2a7430))
+
+
+### Bug Fixes
+
+* **package:** support direct resource imports ([8feb502](https://github.com/browserbase/sdk-python/commit/8feb502c7e73e8abed43afae5a0526282c4f0dfe))
+* **pydantic v1:** more robust ModelField.annotation check ([5292730](https://github.com/browserbase/sdk-python/commit/5292730dd7b1585210d7ab8e640ed78a5dd9740a))
+
+
+### Chores
+
+* broadly detect json family of content-type headers ([ffe29f8](https://github.com/browserbase/sdk-python/commit/ffe29f8dc99d5e7462a0a9bbd488c368e836acdc))
+* **ci:** add timeout thresholds for CI jobs ([3ca4458](https://github.com/browserbase/sdk-python/commit/3ca4458cf31650ce8749c56bee4549811cadec1f))
+* **ci:** fix installation instructions ([99a7328](https://github.com/browserbase/sdk-python/commit/99a7328f22f6da3cd96d70b00c2a6fa0d4c82b37))
+* **ci:** only use depot for staging repos ([646f7d8](https://github.com/browserbase/sdk-python/commit/646f7d832f269d383a0da5fe5732a52ec10787b2))
+* **ci:** upload sdks to package manager ([ff18efd](https://github.com/browserbase/sdk-python/commit/ff18efdf051eabcb52863739942b652d86ed2231))
+* **internal:** avoid errors for isinstance checks on proxies ([b33d222](https://github.com/browserbase/sdk-python/commit/b33d222fd5fdd6eaaca62fb6eb6d9f878a01d31d))
+* **internal:** base client updates ([44f575e](https://github.com/browserbase/sdk-python/commit/44f575efd621315d9bd28e7921554980045af6ed))
+* **internal:** bump pyright version ([bb6bbd3](https://github.com/browserbase/sdk-python/commit/bb6bbd36b3b0fb7595bcc6bd9b25c0aafd6a08af))
+* **internal:** codegen related update ([9f4f8d1](https://github.com/browserbase/sdk-python/commit/9f4f8d1172d5c4b9fa36c8c97ab6e10958ff2959))
+* **internal:** fix list file params ([74b3df7](https://github.com/browserbase/sdk-python/commit/74b3df7160585d981ff5390b6f354926188aaa2a))
+* **internal:** import reformatting ([bba19e4](https://github.com/browserbase/sdk-python/commit/bba19e44eb67116740b27e1fea04abe06a97e4cd))
+* **internal:** minor formatting changes ([0c58843](https://github.com/browserbase/sdk-python/commit/0c58843c75075e3803c9a5a9790f48558a78e712))
+* **internal:** refactor retries to not use recursion ([4161fdb](https://github.com/browserbase/sdk-python/commit/4161fdbcf76a18deee8b790944369225fb4331ff))
+* **internal:** update models test ([5e5dc11](https://github.com/browserbase/sdk-python/commit/5e5dc11c53c60164829b145762818545cfe36f52))
+
 ## 1.3.0 (2025-04-15)
 
 Full Changelog: [v1.2.0...v1.3.0](https://github.com/browserbase/sdk-python/compare/v1.2.0...v1.3.0)
diff --git a/README.md b/README.md
index d0dfb40..43a407e 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,8 @@ session = client.sessions.create(
     browser_settings={
         "advanced_stealth": True,
         "block_ads": True,
+        "captcha_image_selector": "captchaImageSelector",
+        "captcha_input_selector": "captchaInputSelector",
         "context": {
             "id": "id",
             "persist": True,
diff --git a/pyproject.toml b/pyproject.toml
index 26da062..e5e2f6c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "browserbase"
-version = "1.3.0"
+version = "1.4.0"
 description = "The official Python library for the Browserbase API"
 dynamic = ["readme"]
 license = "Apache-2.0"
@@ -42,7 +42,7 @@ Repository = "https://github.com/browserbase/sdk-python"
 managed = true
 # version pins are in requirements-dev.lock
 dev-dependencies = [
-    "pyright>=1.1.359",
+    "pyright==1.1.399",
     "mypy",
     "respx",
     "pytest",
@@ -185,7 +185,7 @@ select = [
   "T201",
   "T203",
   # misuse of typing.TYPE_CHECKING
-  "TC004",
+  # "TC004", # fails lint
   # import rules
   "TID251",
 ]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 30dd44d..b0045a6 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -89,9 +89,9 @@ pyee==12.0.0
     # via playwright
 pygments==2.18.0
     # via rich
-pyright==1.1.392.post0
 pysocks==1.7.1
     # via urllib3
+pyright==1.1.399
 pytest==8.3.3
     # via pytest-asyncio
     # via pytest-base-url
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
new file mode 100755
index 0000000..7c3d028
--- /dev/null
+++ b/scripts/utils/upload-artifact.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+set -exuo pipefail
+
+RESPONSE=$(curl -X POST "$URL" \
+  -H "Authorization: Bearer $AUTH" \
+  -H "Content-Type: application/json")
+
+SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url')
+
+if [[ "$SIGNED_URL" == "null" ]]; then
+  echo -e "\033[31mFailed to get signed URL.\033[0m"
+  exit 1
+fi
+
+UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \
+  -H "Content-Type: application/gzip" \
+  --data-binary @- "$SIGNED_URL" 2>&1)
+
+if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
+  echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
+  echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/browserbase-python/$SHA'\033[0m"
+else
+  echo -e "\033[31mFailed to upload artifact.\033[0m"
+  exit 1
+fi
diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py
index ce50c4e..e954b0e 100644
--- a/src/browserbase/__init__.py
+++ b/src/browserbase/__init__.py
@@ -1,5 +1,7 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
+import typing as _t
+
 from . import types
 from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes
 from ._utils import file_from_path
@@ -78,6 +80,9 @@
     "DefaultAsyncHttpxClient",
 ]
 
+if not _t.TYPE_CHECKING:
+    from ._utils._resources_proxy import resources as resources
+
 _setup_logging()
 
 # Update the __module__ attribute for exported symbols so that
diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py
index e30de64..82e76c9 100644
--- a/src/browserbase/_base_client.py
+++ b/src/browserbase/_base_client.py
@@ -98,7 +98,11 @@
 _AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any])
 
 if TYPE_CHECKING:
-    from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT
+    from httpx._config import (
+        DEFAULT_TIMEOUT_CONFIG,  # pyright: ignore[reportPrivateImportUsage]
+    )
+
+    HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG
 else:
     try:
         from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT
@@ -115,6 +119,7 @@ class PageInfo:
 
     url: URL | NotGiven
     params: Query | NotGiven
+    json: Body | NotGiven
 
     @overload
     def __init__(
@@ -130,19 +135,30 @@ def __init__(
         params: Query,
     ) -> None: ...
 
+    @overload
+    def __init__(
+        self,
+        *,
+        json: Body,
+    ) -> None: ...
+
     def __init__(
         self,
         *,
         url: URL | NotGiven = NOT_GIVEN,
+        json: Body | NotGiven = NOT_GIVEN,
         params: Query | NotGiven = NOT_GIVEN,
     ) -> None:
         self.url = url
+        self.json = json
         self.params = params
 
     @override
     def __repr__(self) -> str:
         if self.url:
             return f"{self.__class__.__name__}(url={self.url})"
+        if self.json:
+            return f"{self.__class__.__name__}(json={self.json})"
         return f"{self.__class__.__name__}(params={self.params})"
 
 
@@ -191,6 +207,19 @@ def _info_to_options(self, info: PageInfo) -> FinalRequestOptions:
             options.url = str(url)
             return options
 
+        if not isinstance(info.json, NotGiven):
+            if not is_mapping(info.json):
+                raise TypeError("Pagination is only supported with mappings")
+
+            if not options.json_data:
+                options.json_data = {**info.json}
+            else:
+                if not is_mapping(options.json_data):
+                    raise TypeError("Pagination is only supported with mappings")
+
+                options.json_data = {**options.json_data, **info.json}
+            return options
+
         raise ValueError("Unexpected PageInfo state")
 
 
@@ -408,8 +437,7 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0
         headers = httpx.Headers(headers_dict)
 
         idempotency_header = self._idempotency_header
-        if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
-            options.idempotency_key = options.idempotency_key or self._idempotency_key()
+        if idempotency_header and options.idempotency_key and idempotency_header not in headers:
             headers[idempotency_header] = options.idempotency_key
 
         # Don't set these headers if they were already set or removed by the caller. We check
@@ -874,7 +902,6 @@ def request(
         self,
         cast_to: Type[ResponseT],
         options: FinalRequestOptions,
-        remaining_retries: Optional[int] = None,
         *,
         stream: Literal[True],
         stream_cls: Type[_StreamT],
@@ -885,7 +912,6 @@ def request(
         self,
         cast_to: Type[ResponseT],
         options: FinalRequestOptions,
-        remaining_retries: Optional[int] = None,
         *,
         stream: Literal[False] = False,
     ) -> ResponseT: ...
@@ -895,7 +921,6 @@ def request(
         self,
         cast_to: Type[ResponseT],
         options: FinalRequestOptions,
-        remaining_retries: Optional[int] = None,
         *,
         stream: bool = False,
         stream_cls: Type[_StreamT] | None = None,
@@ -905,125 +930,109 @@ def request(
         self,
         cast_to: Type[ResponseT],
         options: FinalRequestOptions,
-        remaining_retries: Optional[int] = None,
         *,
         stream: bool = False,
         stream_cls: type[_StreamT] | None = None,
     ) -> ResponseT | _StreamT:
-        if remaining_retries is not None:
-            retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
-        else:
-            retries_taken = 0
-
-        return self._request(
-            cast_to=cast_to,
-            options=options,
-            stream=stream,
-            stream_cls=stream_cls,
-            retries_taken=retries_taken,
-        )
+        cast_to = self._maybe_override_cast_to(cast_to, options)
 
-    def _request(
-        self,
-        *,
-        cast_to: Type[ResponseT],
-        options: FinalRequestOptions,
-        retries_taken: int,
-        stream: bool,
-        stream_cls: type[_StreamT] | None,
-    ) -> ResponseT | _StreamT:
         # create a copy of the options we were given so that if the
         # options are mutated later & we then retry, the retries are
         # given the original options
         input_options = model_copy(options)
-
-        cast_to = self._maybe_override_cast_to(cast_to, options)
-        options = self._prepare_options(options)
-
-        remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
-        request = self._build_request(options, retries_taken=retries_taken)
-        self._prepare_request(request)
-
-        if options.idempotency_key:
+        if input_options.idempotency_key is None and input_options.method.lower() != "get":
             # ensure the idempotency key is reused between requests
-            input_options.idempotency_key = options.idempotency_key
+            input_options.idempotency_key = self._idempotency_key()
 
-        kwargs: HttpxSendArgs = {}
-        if self.custom_auth is not None:
-            kwargs["auth"] = self.custom_auth
+        response: httpx.Response | None = None
+        max_retries = input_options.get_max_retries(self.max_retries)
 
-        log.debug("Sending HTTP Request: %s %s", request.method, request.url)
+        retries_taken = 0
+        for retries_taken in range(max_retries + 1):
+            options = model_copy(input_options)
+            options = self._prepare_options(options)
 
-        try:
-            response = self._client.send(
-                request,
-                stream=stream or self._should_stream_response_body(request=request),
-                **kwargs,
-            )
-        except httpx.TimeoutException as err:
-            log.debug("Encountered httpx.TimeoutException", exc_info=True)
+            remaining_retries = max_retries - retries_taken
+            request = self._build_request(options, retries_taken=retries_taken)
+            self._prepare_request(request)
 
-            if remaining_retries > 0:
-                return self._retry_request(
-                    input_options,
-                    cast_to,
-                    retries_taken=retries_taken,
-                    stream=stream,
-                    stream_cls=stream_cls,
-                    response_headers=None,
-                )
+            kwargs: HttpxSendArgs = {}
+            if self.custom_auth is not None:
+                kwargs["auth"] = self.custom_auth
 
-            log.debug("Raising timeout error")
-            raise APITimeoutError(request=request) from err
-        except Exception as err:
-            log.debug("Encountered Exception", exc_info=True)
+            log.debug("Sending HTTP Request: %s %s", request.method, request.url)
 
-            if remaining_retries > 0:
-                return self._retry_request(
-                    input_options,
-                    cast_to,
-                    retries_taken=retries_taken,
-                    stream=stream,
-                    stream_cls=stream_cls,
-                    response_headers=None,
+            response = None
+            try:
+                response = self._client.send(
+                    request,
+                    stream=stream or self._should_stream_response_body(request=request),
+                    **kwargs,
                 )
+            except httpx.TimeoutException as err:
+                log.debug("Encountered httpx.TimeoutException", exc_info=True)
+
+                if remaining_retries > 0:
+                    self._sleep_for_retry(
+                        retries_taken=retries_taken,
+                        max_retries=max_retries,
+                        options=input_options,
+                        response=None,
+                    )
+                    continue
+
+                log.debug("Raising timeout error")
+                raise APITimeoutError(request=request) from err
+            except Exception as err:
+                log.debug("Encountered Exception", exc_info=True)
+
+                if remaining_retries > 0:
+                    self._sleep_for_retry(
+                        retries_taken=retries_taken,
+                        max_retries=max_retries,
+                        options=input_options,
+                        response=None,
+                    )
+                    continue
+
+                log.debug("Raising connection error")
+                raise APIConnectionError(request=request) from err
+
+            log.debug(
+                'HTTP Response: %s %s "%i %s" %s',
+                request.method,
+                request.url,
+                response.status_code,
+                response.reason_phrase,
+                response.headers,
+            )
 
-            log.debug("Raising connection error")
-            raise APIConnectionError(request=request) from err
-
-        log.debug(
-            'HTTP Response: %s %s "%i %s" %s',
-            request.method,
-            request.url,
-            response.status_code,
-            response.reason_phrase,
-            response.headers,
-        )
+            try:
+                response.raise_for_status()
+            except httpx.HTTPStatusError as err:  # thrown on 4xx and 5xx status code
+                log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
+
+                if remaining_retries > 0 and self._should_retry(err.response):
+                    err.response.close()
+                    self._sleep_for_retry(
+                        retries_taken=retries_taken,
+                        max_retries=max_retries,
+                        options=input_options,
+                        response=response,
+                    )
+                    continue
 
-        try:
-            response.raise_for_status()
-        except httpx.HTTPStatusError as err:  # thrown on 4xx and 5xx status code
-            log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
-
-            if remaining_retries > 0 and self._should_retry(err.response):
-                err.response.close()
-                return self._retry_request(
-                    input_options,
-                    cast_to,
-                    retries_taken=retries_taken,
-                    response_headers=err.response.headers,
-                    stream=stream,
-                    stream_cls=stream_cls,
-                )
+                # If the response is streamed then we need to explicitly read the response
+                # to completion before attempting to access the response text.
+                if not err.response.is_closed:
+                    err.response.read()
 
-            # If the response is streamed then we need to explicitly read the response
-            # to completion before attempting to access the response text.
-            if not err.response.is_closed:
-                err.response.read()
+                log.debug("Re-raising status error")
+                raise self._make_status_error_from_response(err.response) from None
 
-            log.debug("Re-raising status error")
-            raise self._make_status_error_from_response(err.response) from None
+            break
 
+        assert response is not None, "could not resolve response (should never happen)"
         return self._process_response(
             cast_to=cast_to,
             options=options,
@@ -1033,37 +1042,20 @@ def _request(
             retries_taken=retries_taken,
         )
 
-    def _retry_request(
-        self,
-        options: FinalRequestOptions,
-        cast_to: Type[ResponseT],
-        *,
-        retries_taken: int,
-        response_headers: httpx.Headers | None,
-        stream: bool,
-        stream_cls: type[_StreamT] | None,
-    ) -> ResponseT | _StreamT:
-        remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
+    def _sleep_for_retry(
+        self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
+    ) -> None:
+        remaining_retries = max_retries - retries_taken
         if remaining_retries == 1:
             log.debug("1 retry left")
         else:
             log.debug("%i retries left", remaining_retries)
 
-        timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
+        timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None)
         log.info("Retrying request to %s in %f seconds", options.url, timeout)
 
-        # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a
-        # different thread if necessary.
         time.sleep(timeout)
 
-        return self._request(
-            options=options,
-            cast_to=cast_to,
-            retries_taken=retries_taken + 1,
-            stream=stream,
-            stream_cls=stream_cls,
-        )
-
     def _process_response(
         self,
         *,
@@ -1407,7 +1399,6 @@ async def request(
         options: FinalRequestOptions,
         *,
         stream: Literal[False] = False,
-        remaining_retries: Optional[int] = None,
     ) -> ResponseT: ...
 
     @overload
@@ -1418,7 +1409,6 @@ async def request(
         *,
         stream: Literal[True],
         stream_cls: type[_AsyncStreamT],
-        remaining_retries: Optional[int] = None,
     ) -> _AsyncStreamT: ...
 
     @overload
@@ -1429,7 +1419,6 @@ async def request(
         *,
         stream: bool,
         stream_cls: type[_AsyncStreamT] | None = None,
-        remaining_retries: Optional[int] = None,
     ) -> ResponseT | _AsyncStreamT: ...
 
     async def request(
@@ -1439,120 +1428,111 @@ async def request(
         *,
         stream: bool = False,
         stream_cls: type[_AsyncStreamT] | None = None,
-        remaining_retries: Optional[int] = None,
-    ) -> ResponseT | _AsyncStreamT:
-        if remaining_retries is not None:
-            retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
-        else:
-            retries_taken = 0
-
-        return await self._request(
-            cast_to=cast_to,
-            options=options,
-            stream=stream,
-            stream_cls=stream_cls,
-            retries_taken=retries_taken,
-        )
-
-    async def _request(
-        self,
-        cast_to: Type[ResponseT],
-        options: FinalRequestOptions,
-        *,
-        stream: bool,
-        stream_cls: type[_AsyncStreamT] | None,
-        retries_taken: int,
     ) -> ResponseT | _AsyncStreamT:
         if self._platform is None:
             # `get_platform` can make blocking IO calls so we
             # execute it earlier while we are in an async context
             self._platform = await asyncify(get_platform)()
 
+        cast_to = self._maybe_override_cast_to(cast_to, options)
+
         # create a copy of the options we were given so that if the
         # options are mutated later & we then retry, the retries are
         # given the original options
         input_options = model_copy(options)
-
-        cast_to = self._maybe_override_cast_to(cast_to, options)
-        options = await self._prepare_options(options)
-
-        remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
-        request = self._build_request(options, retries_taken=retries_taken)
-        await self._prepare_request(request)
-
-        if options.idempotency_key:
+        if input_options.idempotency_key is None and input_options.method.lower() != "get":
             # ensure the idempotency key is reused between requests
-            input_options.idempotency_key = options.idempotency_key
+            input_options.idempotency_key = self._idempotency_key()
 
-        kwargs: HttpxSendArgs = {}
-        if self.custom_auth is not None:
-            kwargs["auth"] = self.custom_auth
+        response: httpx.Response | None = None
+        max_retries = input_options.get_max_retries(self.max_retries)
 
-        try:
-            response = await self._client.send(
-                request,
-                stream=stream or self._should_stream_response_body(request=request),
-                **kwargs,
-            )
-        except httpx.TimeoutException as err:
-            log.debug("Encountered httpx.TimeoutException", exc_info=True)
+        retries_taken = 0
+        for retries_taken in range(max_retries + 1):
+            options = model_copy(input_options)
+            options = await self._prepare_options(options)
 
-            if remaining_retries > 0:
-                return await self._retry_request(
-                    input_options,
-                    cast_to,
-                    retries_taken=retries_taken,
-                    stream=stream,
-                    stream_cls=stream_cls,
-                    response_headers=None,
-                )
+            remaining_retries = max_retries - retries_taken
+            request = self._build_request(options, retries_taken=retries_taken)
+            await self._prepare_request(request)
 
-            log.debug("Raising timeout error")
-            raise APITimeoutError(request=request) from err
-        except Exception as err:
-            log.debug("Encountered Exception", exc_info=True)
+            kwargs: HttpxSendArgs = {}
+            if self.custom_auth is not None:
+                kwargs["auth"] = self.custom_auth
 
-            if remaining_retries > 0:
-                return await self._retry_request(
-                    input_options,
-                    cast_to,
-                    retries_taken=retries_taken,
-                    stream=stream,
-                    stream_cls=stream_cls,
-                    response_headers=None,
-                )
+            log.debug("Sending HTTP Request: %s %s", request.method, request.url)
 
-            log.debug("Raising connection error")
-            raise APIConnectionError(request=request) from err
+            response = None
+            try:
+                response = await self._client.send(
+                    request,
+                    stream=stream or self._should_stream_response_body(request=request),
+                    **kwargs,
+                )
+            except httpx.TimeoutException as err:
+                log.debug("Encountered httpx.TimeoutException", exc_info=True)
+
+                if remaining_retries > 0:
+                    await self._sleep_for_retry(
+                        retries_taken=retries_taken,
+                        max_retries=max_retries,
+                        options=input_options,
+                        response=None,
+                    )
+                    continue
+
+                log.debug("Raising timeout error")
+                raise APITimeoutError(request=request) from err
+            except Exception as err:
+                log.debug("Encountered Exception", exc_info=True)
+
+                if remaining_retries > 0:
+                    await self._sleep_for_retry(
+                        retries_taken=retries_taken,
+                        max_retries=max_retries,
+                        options=input_options,
+                        response=None,
+                    )
+                    continue
+
+                log.debug("Raising connection error")
+                raise APIConnectionError(request=request) from err
+
+            log.debug(
+                'HTTP Response: %s %s "%i %s" %s',
+                request.method,
+                request.url,
+                response.status_code,
+                response.reason_phrase,
+                response.headers,
+            )
 
-        log.debug(
-            'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase
-        )
+            try:
+                response.raise_for_status()
+            except httpx.HTTPStatusError as err:  # thrown on 4xx and 5xx status code
+                log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
+
+                if remaining_retries > 0 and self._should_retry(err.response):
+                    await err.response.aclose()
+                    await self._sleep_for_retry(
+                        retries_taken=retries_taken,
+                        max_retries=max_retries,
+                        options=input_options,
+                        response=response,
+                    )
+                    continue
 
-        try:
-            response.raise_for_status()
-        except httpx.HTTPStatusError as err:  # thrown on 4xx and 5xx status code
-            log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
-
-            if remaining_retries > 0 and self._should_retry(err.response):
-                await err.response.aclose()
-                return await self._retry_request(
-                    input_options,
-                    cast_to,
-                    retries_taken=retries_taken,
-                    response_headers=err.response.headers,
-                    stream=stream,
-                    stream_cls=stream_cls,
-                )
+                # If the response is streamed then we need to explicitly read the response
+                # to completion before attempting to access the response text.
+                if not err.response.is_closed:
+                    await err.response.aread()
 
-            # If the response is streamed then we need to explicitly read the response
-            # to completion before attempting to access the response text.
-            if not err.response.is_closed:
-                await err.response.aread()
+                log.debug("Re-raising status error")
+                raise self._make_status_error_from_response(err.response) from None
 
-            log.debug("Re-raising status error")
-            raise self._make_status_error_from_response(err.response) from None
+            break
 
+        assert response is not None, "could not resolve response (should never happen)"
         return await self._process_response(
             cast_to=cast_to,
             options=options,
@@ -1562,35 +1542,20 @@ async def _request(
             retries_taken=retries_taken,
         )
 
-    async def _retry_request(
-        self,
-        options: FinalRequestOptions,
-        cast_to: Type[ResponseT],
-        *,
-        retries_taken: int,
-        response_headers: httpx.Headers | None,
-        stream: bool,
-        stream_cls: type[_AsyncStreamT] | None,
-    ) -> ResponseT | _AsyncStreamT:
-        remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
+    async def _sleep_for_retry(
+        self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
+    ) -> None:
+        remaining_retries = max_retries - retries_taken
         if remaining_retries == 1:
             log.debug("1 retry left")
         else:
             log.debug("%i retries left", remaining_retries)
 
-        timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
+        timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None)
         log.info("Retrying request to %s in %f seconds", options.url, timeout)
 
         await anyio.sleep(timeout)
 
-        return await self._request(
-            options=options,
-            cast_to=cast_to,
-            retries_taken=retries_taken + 1,
-            stream=stream,
-            stream_cls=stream_cls,
-        )
-
     async def _process_response(
         self,
         *,
diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py
index 15fde86..a7039a2 100644
--- a/src/browserbase/_client.py
+++ b/src/browserbase/_client.py
@@ -19,10 +19,7 @@
     ProxiesTypes,
     RequestOptions,
 )
-from ._utils import (
-    is_given,
-    get_async_library,
-)
+from ._utils import is_given, get_async_library
 from ._version import __version__
 from .resources import contexts, projects, extensions
 from ._streaming import Stream as Stream, AsyncStream as AsyncStream
diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py
index 3493571..798956f 100644
--- a/src/browserbase/_models.py
+++ b/src/browserbase/_models.py
@@ -19,7 +19,6 @@
 )
 
 import pydantic
-import pydantic.generics
 from pydantic.fields import FieldInfo
 
 from ._types import (
@@ -627,8 +626,8 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
                 # Note: if one variant defines an alias then they all should
                 discriminator_alias = field_info.alias
 
-                if field_info.annotation and is_literal_type(field_info.annotation):
-                    for entry in get_args(field_info.annotation):
+                if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
+                    for entry in get_args(annotation):
                         if isinstance(entry, str):
                             mapping[entry] = variant
 
diff --git a/src/browserbase/_response.py b/src/browserbase/_response.py
index e79cb15..5f8d0f4 100644
--- a/src/browserbase/_response.py
+++ b/src/browserbase/_response.py
@@ -235,7 +235,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
         # split is required to handle cases where additional information is included
         # in the response, e.g. application/json; charset=utf-8
         content_type, *_ = response.headers.get("content-type", "*").split(";")
-        if content_type != "application/json":
+        if not content_type.endswith("json"):
             if is_basemodel(cast_to):
                 try:
                     data = response.json()
diff --git a/src/browserbase/_utils/_proxy.py b/src/browserbase/_utils/_proxy.py
index ffd883e..0f239a3 100644
--- a/src/browserbase/_utils/_proxy.py
+++ b/src/browserbase/_utils/_proxy.py
@@ -46,7 +46,10 @@ def __dir__(self) -> Iterable[str]:
     @property  # type: ignore
     @override
     def __class__(self) -> type:  # pyright: ignore
-        proxied = self.__get_proxied__()
+        try:
+            proxied = self.__get_proxied__()
+        except Exception:
+            return type(self)
         if issubclass(type(proxied), LazyProxy):
             return type(proxied)
         return proxied.__class__
diff --git a/src/browserbase/_utils/_resources_proxy.py b/src/browserbase/_utils/_resources_proxy.py
new file mode 100644
index 0000000..3901271
--- /dev/null
+++ b/src/browserbase/_utils/_resources_proxy.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from typing import Any
+from typing_extensions import override
+
+from ._proxy import LazyProxy
+
+
+class ResourcesProxy(LazyProxy[Any]):
+    """A proxy for the `browserbase.resources` module.
+
+    This is used so that we can lazily import `browserbase.resources` only when
+    needed *and* so that users can just import `browserbase` and reference `browserbase.resources`
+    """
+
+    @override
+    def __load__(self) -> Any:
+        import importlib
+
+        mod = importlib.import_module("browserbase.resources")
+        return mod
+
+
+resources = ResourcesProxy().__as_proxied__()
diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py
index 1958820..1bac954 100644
--- a/src/browserbase/_utils/_typing.py
+++ b/src/browserbase/_utils/_typing.py
@@ -110,7 +110,7 @@ class MyResponse(Foo[_T]):
     ```
     """
     cls = cast(object, get_origin(typ) or typ)
-    if cls in generic_bases:
+    if cls in generic_bases:  # pyright: ignore[reportUnnecessaryContains]
         # we're given the class directly
         return extract_type_arg(typ, index)
 
diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py
index e5811bb..ea3cf3f 100644
--- a/src/browserbase/_utils/_utils.py
+++ b/src/browserbase/_utils/_utils.py
@@ -72,8 +72,16 @@ def _extract_items(
         from .._files import assert_is_file_content
 
         # We have exhausted the path, return the entry we found.
-        assert_is_file_content(obj, key=flattened_key)
         assert flattened_key is not None
+
+        if is_list(obj):
+            files: list[tuple[str, FileTypes]] = []
+            for entry in obj:
+                assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
+                files.append((flattened_key + "[]", cast(FileTypes, entry)))
+            return files
+
+        assert_is_file_content(obj, key=flattened_key)
         return [(flattened_key, cast(FileTypes, obj))]
 
     index += 1
diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py
index 9c23d1b..3c0492e 100644
--- a/src/browserbase/_version.py
+++ b/src/browserbase/_version.py
@@ -1,4 +1,4 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
 __title__ = "browserbase"
-__version__ = "1.3.0"  # x-release-please-version
+__version__ = "1.4.0"  # x-release-please-version
diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py
index 486cd5f..0a438ed 100644
--- a/src/browserbase/resources/contexts.py
+++ b/src/browserbase/resources/contexts.py
@@ -6,10 +6,7 @@
 
 from ..types import context_create_params
 from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from .._utils import (
-    maybe_transform,
-    async_maybe_transform,
-)
+from .._utils import maybe_transform, async_maybe_transform
 from .._compat import cached_property
 from .._resource import SyncAPIResource, AsyncAPIResource
 from .._response import (
diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py
index a98685a..c7b0fae 100644
--- a/src/browserbase/resources/extensions.py
+++ b/src/browserbase/resources/extensions.py
@@ -8,12 +8,7 @@
 
 from ..types import extension_create_params
 from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileTypes
-from .._utils import (
-    extract_files,
-    maybe_transform,
-    deepcopy_minimal,
-    async_maybe_transform,
-)
+from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
 from .._compat import cached_property
 from .._resource import SyncAPIResource, AsyncAPIResource
 from .._response import (
diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py
index 4481438..bf3314a 100644
--- a/src/browserbase/resources/sessions/sessions.py
+++ b/src/browserbase/resources/sessions/sessions.py
@@ -25,10 +25,7 @@
     AsyncUploadsResourceWithStreamingResponse,
 )
 from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ..._utils import (
-    maybe_transform,
-    async_maybe_transform,
-)
+from ..._utils import maybe_transform, async_maybe_transform
 from ..._compat import cached_property
 from .downloads import (
     DownloadsResource,
diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py
index eed9349..69b6ccb 100644
--- a/src/browserbase/resources/sessions/uploads.py
+++ b/src/browserbase/resources/sessions/uploads.py
@@ -7,12 +7,7 @@
 import httpx
 
 from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import (
-    extract_files,
-    maybe_transform,
-    deepcopy_minimal,
-    async_maybe_transform,
-)
+from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
 from ..._compat import cached_property
 from ..._resource import SyncAPIResource, AsyncAPIResource
 from ..._response import (
diff --git a/src/browserbase/types/context_create_response.py b/src/browserbase/types/context_create_response.py
index c168596..8e2f7aa 100644
--- a/src/browserbase/types/context_create_response.py
+++ b/src/browserbase/types/context_create_response.py
@@ -1,6 +1,5 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
-
 from pydantic import Field as FieldInfo
 
 from .._models import BaseModel
diff --git a/src/browserbase/types/context_update_response.py b/src/browserbase/types/context_update_response.py
index d07e50e..7e16c62 100644
--- a/src/browserbase/types/context_update_response.py
+++ b/src/browserbase/types/context_update_response.py
@@ -1,6 +1,5 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
-
 from pydantic import Field as FieldInfo
 
 from .._models import BaseModel
diff --git a/src/browserbase/types/project_usage.py b/src/browserbase/types/project_usage.py
index f68cc2d..c8a03f5 100644
--- a/src/browserbase/types/project_usage.py
+++ b/src/browserbase/types/project_usage.py
@@ -1,6 +1,5 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
-
 from pydantic import Field as FieldInfo
 
 from .._models import BaseModel
diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py
index f3b9606..1f5324f 100644
--- a/src/browserbase/types/session_create_params.py
+++ b/src/browserbase/types/session_create_params.py
@@ -120,6 +120,18 @@ class BrowserSettings(TypedDict, total=False):
     block_ads: Annotated[bool, PropertyInfo(alias="blockAds")]
     """Enable or disable ad blocking in the browser. Defaults to `false`."""
 
+    captcha_image_selector: Annotated[str, PropertyInfo(alias="captchaImageSelector")]
+    """Custom selector for captcha image.
+
+    See [Custom Captcha Solving](/features/stealth-mode#custom-captcha-solving)
+    """
+
+    captcha_input_selector: Annotated[str, PropertyInfo(alias="captchaInputSelector")]
+    """Custom selector for captcha input.
+
+    See [Custom Captcha Solving](/features/stealth-mode#custom-captcha-solving)
+    """
+
     context: BrowserSettingsContext
 
     extension_id: Annotated[str, PropertyInfo(alias="extensionId")]
diff --git a/src/browserbase/types/sessions/upload_create_response.py b/src/browserbase/types/sessions/upload_create_response.py
index ceece2c..abeed01 100644
--- a/src/browserbase/types/sessions/upload_create_response.py
+++ b/src/browserbase/types/sessions/upload_create_response.py
@@ -1,6 +1,5 @@
 # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 
-
 from ..._models import BaseModel
 
 __all__ = ["UploadCreateResponse"]
diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py
index 0581655..4a17c4f 100644
--- a/tests/api_resources/test_sessions.py
+++ b/tests/api_resources/test_sessions.py
@@ -37,6 +37,8 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None:
             browser_settings={
                 "advanced_stealth": True,
                 "block_ads": True,
+                "captcha_image_selector": "captchaImageSelector",
+                "captcha_input_selector": "captchaInputSelector",
                 "context": {
                     "id": "id",
                     "persist": True,
@@ -269,6 +271,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas
             browser_settings={
                 "advanced_stealth": True,
                 "block_ads": True,
+                "captcha_image_selector": "captchaImageSelector",
+                "captcha_input_selector": "captchaInputSelector",
                 "context": {
                     "id": "id",
                     "persist": True,
diff --git a/tests/conftest.py b/tests/conftest.py
index 15ddbca..94b8e72 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,7 +10,7 @@
 from browserbase import Browserbase, AsyncBrowserbase
 
 if TYPE_CHECKING:
-    from _pytest.fixtures import FixtureRequest
+    from _pytest.fixtures import FixtureRequest  # pyright: ignore[reportPrivateImportUsage]
 
 pytest.register_assert_rewrite("tests.utils")
 
diff --git a/tests/test_models.py b/tests/test_models.py
index 21043ab..b5335f9 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -492,12 +492,15 @@ class Model(BaseModel):
         resource_id: Optional[str] = None
 
     m = Model.construct()
+    assert m.resource_id is None
     assert "resource_id" not in m.model_fields_set
 
     m = Model.construct(resource_id=None)
+    assert m.resource_id is None
     assert "resource_id" in m.model_fields_set
 
     m = Model.construct(resource_id="foo")
+    assert m.resource_id == "foo"
     assert "resource_id" in m.model_fields_set
 
 
@@ -832,7 +835,7 @@ class B(BaseModel):
 
 @pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1")
 def test_type_alias_type() -> None:
-    Alias = TypeAliasType("Alias", str)
+    Alias = TypeAliasType("Alias", str)  # pyright: ignore
 
     class Model(BaseModel):
         alias: Alias
diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py
index 986bef9..d4e2f31 100644
--- a/tests/test_utils/test_proxy.py
+++ b/tests/test_utils/test_proxy.py
@@ -21,3 +21,14 @@ def test_recursive_proxy() -> None:
     assert dir(proxy) == []
     assert type(proxy).__name__ == "RecursiveLazyProxy"
     assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy"
+
+
+def test_isinstance_does_not_error() -> None:
+    class AlwaysErrorProxy(LazyProxy[Any]):
+        @override
+        def __load__(self) -> Any:
+            raise RuntimeError("Mocking missing dependency")
+
+    proxy = AlwaysErrorProxy()
+    assert not isinstance(proxy, dict)
+    assert isinstance(proxy, LazyProxy)
