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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.5.0"
".": "3.6.0"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 8
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-43e6dd4ce19381de488d296e9036fea15bfea9a6f946cf8ccf4e02aecc8fb765.yml
openapi_spec_hash: f736e7a8acea0d73e1031c86ea803246
config_hash: b375728ccf7d33287335852f4f59c293
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-8fbb3fa8f3a37c1c7408de427fe125aadec49f705e8e30d191601a9b69c4cc41.yml
openapi_spec_hash: 48b4dfac35a842d7fb0d228caf87544e
config_hash: 7386d24e2f03a3b2a89b3f6881446348
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 3.6.0 (2026-02-05)

Full Changelog: [v3.5.0...v3.6.0](https://github.com/browserbase/stagehand-python/compare/v3.5.0...v3.6.0)

### Features

* Add executionModel serialization to api client ([22dd688](https://github.com/browserbase/stagehand-python/commit/22dd68831f5b599dc070798bb991b349211631d9))
* **client:** add custom JSON encoder for extended type support ([f9017c8](https://github.com/browserbase/stagehand-python/commit/f9017c8fff8c58992739c6924ed6efbae552e027))


### Chores

* **internal:** codegen related update ([555a9c4](https://github.com/browserbase/stagehand-python/commit/555a9c44a902a6735e585e09ed974d9a7915a6bb))
* sync repo ([0c9bb8c](https://github.com/browserbase/stagehand-python/commit/0c9bb8cb3b791bf8c60ad0065fed9ad16b912b8e))

## 3.5.0 (2026-01-29)

Full Changelog: [v3.4.8...v3.5.0](https://github.com/browserbase/stagehand-python/compare/v3.4.8...v3.5.0)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,9 +606,9 @@ session = response.parse() # get the object that `sessions.start()` would have
print(session.data)
```

These methods return an [`APIResponse`](https://github.com/browserbase/stagehand-python/tree/main/src/stagehand/_response.py) object.
These methods return an [`APIResponse`](https://github.com/browserbase/stagehand-python/tree/stainless/src/stagehand/_response.py) object.

The async client returns an [`AsyncAPIResponse`](https://github.com/browserbase/stagehand-python/tree/main/src/stagehand/_response.py) with the same structure, the only difference being `await`able methods for reading the response content.
The async client returns an [`AsyncAPIResponse`](https://github.com/browserbase/stagehand-python/tree/stainless/src/stagehand/_response.py) with the same structure, the only difference being `await`able methods for reading the response content.

#### `.with_streaming_response`

Expand Down
2 changes: 1 addition & 1 deletion examples/act_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async def main() -> None:
# This is the key test - passing a string instead of an Action object
print("\nAttempting to call act() with string input...")
act_response = await session.act(
input="click the 'More information' link", # String instruction
input="click the 'Learn more' link", # String instruction
)

print(f"Act completed successfully!")
Expand Down
2 changes: 1 addition & 1 deletion examples/playwright_page_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def main() -> None:

print("🖱️ Stagehand.act(page=...) ...")
_ = session.act(
input="Click the 'More information' link",
input="Click the 'Learn more' link",
page=page,
)
print("Done.")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "stagehand"
version = "3.5.0"
version = "3.6.0"
description = "The official Python library for the stagehand API"
dynamic = ["readme"]
license = "MIT"
Expand Down
7 changes: 5 additions & 2 deletions src/stagehand/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
APIConnectionError,
APIResponseValidationError,
)
from ._utils._json import openapi_dumps

log: logging.Logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -554,8 +555,10 @@ def _build_request(
kwargs["content"] = options.content
elif isinstance(json_data, bytes):
kwargs["content"] = json_data
else:
kwargs["json"] = json_data if is_given(json_data) else None
elif not files:
# Don't set content when JSON is sent as multipart/form-data,
# since httpx's content param overrides other body arguments
kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
Comment on lines +558 to +561
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This change introduces a silent failure when files are provided along with json_data (but without a multipart/form-data header).

Previously, httpx would raise a ValueError because json and files cannot be used together. With this change, content is not set when files are present, causing json_data to be silently ignored/dropped while the request proceeds with only the files.

Restoring the error ensures users are aware of the invalid usage (or missing header).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/stagehand/_base_client.py, line 558:

<comment>This change introduces a silent failure when `files` are provided along with `json_data` (but without a `multipart/form-data` header).

Previously, `httpx` would raise a `ValueError` because `json` and `files` cannot be used together. With this change, `content` is not set when `files` are present, causing `json_data` to be silently ignored/dropped while the request proceeds with only the files.

Restoring the error ensures users are aware of the invalid usage (or missing header).</comment>

<file context>
@@ -554,8 +555,10 @@ def _build_request(
                 kwargs["content"] = json_data
-            else:
-                kwargs["json"] = json_data if is_given(json_data) else None
+            elif not files:
+                # Don't set content when JSON is sent as multipart/form-data,
+                # since httpx's content param overrides other body arguments
</file context>
Suggested change
elif not files:
# Don't set content when JSON is sent as multipart/form-data,
# since httpx's content param overrides other body arguments
kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
elif not files:
# Don't set content when JSON is sent as multipart/form-data,
# since httpx's content param overrides other body arguments
kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
elif is_given(json_data) and json_data is not None and "data" not in kwargs:
raise TypeError("Passing both `json_data` and `files` is not supported.")
Fix with Cubic

kwargs["files"] = files
else:
headers.pop("Content-Type", None)
Expand Down
6 changes: 3 additions & 3 deletions src/stagehand/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def model_dump(
exclude_defaults: bool = False,
warnings: bool = True,
mode: Literal["json", "python"] = "python",
by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
return model.model_dump(
Expand All @@ -148,13 +149,12 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
by_alias=by_alias,
)
return cast(
"dict[str, Any]",
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
exclude=exclude,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
),
)

Expand Down
35 changes: 35 additions & 0 deletions src/stagehand/_utils/_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json
from typing import Any
from datetime import datetime
from typing_extensions import override

import pydantic

from .._compat import model_dump


def openapi_dumps(obj: Any) -> bytes:
"""
Serialize an object to UTF-8 encoded JSON bytes.

Extends the standard json.dumps with support for additional types
commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
"""
return json.dumps(
obj,
cls=_CustomEncoder,
# Uses the same defaults as httpx's JSON serialization
ensure_ascii=False,
separators=(",", ":"),
allow_nan=False,
).encode()


class _CustomEncoder(json.JSONEncoder):
@override
def default(self, o: Any) -> Any:
if isinstance(o, datetime):
return o.isoformat()
if isinstance(o, pydantic.BaseModel):
return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
return super().default(o)
2 changes: 1 addition & 1 deletion src/stagehand/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "stagehand"
__version__ = "3.5.0" # x-release-please-version
__version__ = "3.6.0" # x-release-please-version
16 changes: 15 additions & 1 deletion src/stagehand/types/session_execute_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
from __future__ import annotations

from typing import Union, Optional
from typing_extensions import Literal, Required, Annotated, TypedDict
from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict

from .._utils import PropertyInfo
from .model_config_param import ModelConfigParam

__all__ = [
"SessionExecuteParamsBase",
"AgentConfig",
"AgentConfigExecutionModel",
"AgentConfigModel",
"ExecuteOptions",
"SessionExecuteParamsNonStreaming",
"SessionExecuteParamsStreaming",
Expand All @@ -32,13 +34,25 @@ class SessionExecuteParamsBase(TypedDict, total=False):
"""Whether to stream the response via SSE"""


AgentConfigExecutionModel: TypeAlias = Union[ModelConfigParam, str]

AgentConfigModel: TypeAlias = Union[ModelConfigParam, str]


class AgentConfig(TypedDict, total=False):
cua: bool
"""Deprecated.

Use mode: 'cua' instead. If both are provided, mode takes precedence.
"""

execution_model: Annotated[AgentConfigExecutionModel, PropertyInfo(alias="executionModel")]
"""
Model configuration object or model name string (e.g., 'openai/gpt-5-nano') for
tool execution (observe/act calls within agent tools). If not specified,
inherits from the main model configuration.
"""

mode: Literal["dom", "hybrid", "cua"]
"""Tool mode for the agent (dom, hybrid, cua). If set, overrides cua."""

Expand Down
52 changes: 52 additions & 0 deletions tests/api_resources/test_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,19 @@ def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> N
id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123",
agent_config={
"cua": True,
"execution_model": {
"model_name": "openai/gpt-5-nano",
"api_key": "sk-some-openai-api-key",
"base_url": "https://api.openai.com/v1",
"provider": "openai",
},
"mode": "cua",
"model": {
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Duplicate "mode"/"model" keys in agent_config mean the newly added model configuration is ignored (later keys override earlier ones). Consolidate these entries so the intended values are actually used.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/api_resources/test_sessions.py, line 235:

<comment>Duplicate "mode"/"model" keys in agent_config mean the newly added model configuration is ignored (later keys override earlier ones). Consolidate these entries so the intended values are actually used.</comment>

<file context>
@@ -225,6 +225,19 @@ def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> N
+                    "provider": "openai",
+                },
+                "mode": "cua",
+                "model": {
+                    "model_name": "openai/gpt-5-nano",
+                    "api_key": "sk-some-openai-api-key",
</file context>
Fix with Cubic

"model_name": "openai/gpt-5-nano",
"api_key": "sk-some-openai-api-key",
"base_url": "https://api.openai.com/v1",
"provider": "openai",
},
"mode": "cua",
"model": {"model_name": "openai/gpt-5-nano"},
"provider": "openai",
Expand Down Expand Up @@ -308,6 +321,19 @@ def test_method_execute_with_all_params_overload_2(self, client: Stagehand) -> N
id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123",
agent_config={
"cua": True,
"execution_model": {
"model_name": "openai/gpt-5-nano",
"api_key": "sk-some-openai-api-key",
"base_url": "https://api.openai.com/v1",
"provider": "openai",
},
"mode": "cua",
"model": {
"model_name": "openai/gpt-5-nano",
"api_key": "sk-some-openai-api-key",
"base_url": "https://api.openai.com/v1",
"provider": "openai",
},
"mode": "cua",
"model": {"model_name": "openai/gpt-5-nano"},
"provider": "openai",
Expand Down Expand Up @@ -1058,6 +1084,19 @@ async def test_method_execute_with_all_params_overload_1(self, async_client: Asy
id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123",
agent_config={
"cua": True,
"execution_model": {
"model_name": "openai/gpt-5-nano",
"api_key": "sk-some-openai-api-key",
"base_url": "https://api.openai.com/v1",
"provider": "openai",
},
"mode": "cua",
"model": {
"model_name": "openai/gpt-5-nano",
"api_key": "sk-some-openai-api-key",
"base_url": "https://api.openai.com/v1",
"provider": "openai",
},
"mode": "cua",
"model": {"model_name": "openai/gpt-5-nano"},
"provider": "openai",
Expand Down Expand Up @@ -1141,6 +1180,19 @@ async def test_method_execute_with_all_params_overload_2(self, async_client: Asy
id="c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123",
agent_config={
"cua": True,
"execution_model": {
"model_name": "openai/gpt-5-nano",
"api_key": "sk-some-openai-api-key",
"base_url": "https://api.openai.com/v1",
"provider": "openai",
},
"mode": "cua",
"model": {
"model_name": "openai/gpt-5-nano",
"api_key": "sk-some-openai-api-key",
"base_url": "https://api.openai.com/v1",
"provider": "openai",
},
"mode": "cua",
"model": {"model_name": "openai/gpt-5-nano"},
"provider": "openai",
Expand Down
Loading