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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/stagehand-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'

Expand All @@ -43,10 +43,10 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/stagehand-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'

Expand All @@ -61,7 +61,7 @@ jobs:
github.repository == 'stainless-sdks/stagehand-python' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setOutput('github_token', await core.getIDToken());

Expand All @@ -81,10 +81,10 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/stagehand-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ jobs:
contents: read

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: "0.9.13"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-doctor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
if: github.repository == 'browserbase/stagehand-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Check release environment
run: |
Expand Down
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.20.0"
".": "3.21.0"
}
4 changes: 2 additions & 2 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/stagehand-6f6bfb81d092f30a5e2005328c97d61b9ea36132bb19e9e79e55294b9534ce20.yml
openapi_spec_hash: f3fc1e3688a38dc2c28f7178f7d534e5
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-c7910965e66e73ad8b65b6cc391d431094b2a6c6577c3e9d82feaa8138e74cff.yml
openapi_spec_hash: 37748bb69c22a9ce721d9b5a5861f964
config_hash: 1fb12ae9b478488bc1e56bfbdc210b01
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## 3.21.0 (2026-05-29)

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

### Features

* [feat]: add `ignoreSelectors` to `observe()` ([77be2c2](https://github.com/browserbase/stagehand-python/commit/77be2c2463c7b1d0ce6768eecf130adc3ef91149))
* [STG-1756] forward Vertex model config ([09bdd54](https://github.com/browserbase/stagehand-python/commit/09bdd5484e09c3827bed41e8fca5dbefd6d9ada9))
* Add `screenshot` option to Extract ([393ebbe](https://github.com/browserbase/stagehand-python/commit/393ebbee487278360fc844e1fb5034651f37cce4))
* **internal/types:** support eagerly validating pydantic iterators ([95a4cc8](https://github.com/browserbase/stagehand-python/commit/95a4cc8cd9228e21ea7eefa7138e498bf628e376))
* STG-1756 add Vertex auth params to Stagehand spec ([2bdeae8](https://github.com/browserbase/stagehand-python/commit/2bdeae8adfe292c4bffc8ff93444f5be4c255c2e))


### Bug Fixes

* **client:** add missing f-string prefix in file type error message ([fb40f50](https://github.com/browserbase/stagehand-python/commit/fb40f50c2fb3337be84a3f973b8a27a8f53fbfae))

## 3.20.0 (2026-05-06)

Full Changelog: [v3.19.5...v3.20.0](https://github.com/browserbase/stagehand-python/compare/v3.19.5...v3.20.0)
Expand Down
180 changes: 180 additions & 0 deletions examples/vertex_auth_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""
Example: use Google Vertex AI model auth with the Stagehand Python SDK.

This runs the same model-backed flow against both:
- server="remote" (hosted Stagehand API + Browserbase browser)
- server="local" (local Stagehand server + local browser)

Required environment variables:
- BROWSERBASE_API_KEY
- GOOGLE_APPLICATION_CREDENTIALS: path to a service account JSON file
OR VERTEX_SERVICE_ACCOUNT_JSON: raw service account JSON

Optional environment variables:
- VERTEX_PROJECT: Google Cloud project ID. Defaults to credentials.project_id.
- VERTEX_LOCATION: Google Cloud location. Defaults to us-central1.
- VERTEX_MODEL: Vertex model name. Defaults to vertex/gemini-2.5-flash.
- STAGEHAND_BOOTSTRAP_MODEL: session start model. Defaults to openai/gpt-4.1-mini.

The service account JSON is read by this script and sent inline as credentials.
Do not pass key file paths to the Stagehand API server.
"""

from __future__ import annotations

import os
import sys
import json
from typing import Any, Literal, cast
from pathlib import Path

from stagehand import Stagehand
from stagehand.types.session_extract_params import (
OptionsModelVertexModelConfigObject as VertexModelConfig,
OptionsModelVertexModelConfigObjectAuthCredentials as VertexCredentials,
)


def load_example_env() -> None:
env_path = Path(__file__).parent / ".env"
if not env_path.exists():
return

for line in env_path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key, value)


def load_vertex_credentials() -> VertexCredentials:
inline_json = os.environ.get("VERTEX_SERVICE_ACCOUNT_JSON")
if inline_json:
credentials = json.loads(inline_json)
else:
credentials_path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
if not credentials_path:
sys.exit(
"Set GOOGLE_APPLICATION_CREDENTIALS to a service account JSON file "
"or VERTEX_SERVICE_ACCOUNT_JSON to raw service account JSON."
)
credentials = json.loads(Path(credentials_path).expanduser().read_text())

if not isinstance(credentials, dict):
sys.exit("Vertex credentials must be a JSON object.")

missing = [
key for key in ("client_email", "private_key") if not credentials.get(key)
]
if missing:
sys.exit("Vertex credentials are missing: " + ", ".join(missing))

return cast(VertexCredentials, credentials)


def build_vertex_model_config() -> VertexModelConfig:
credentials = load_vertex_credentials()
project = os.environ.get("VERTEX_PROJECT") or credentials.get("project_id")
if not project:
sys.exit("Set VERTEX_PROJECT or include project_id in the credentials JSON.")

location = os.environ.get("VERTEX_LOCATION", "us-central1")
model_name = os.environ.get("VERTEX_MODEL", "vertex/gemini-2.5-flash")

return {
"provider": "vertex",
"model_name": model_name,
"auth": {
"type": "googleServiceAccount",
"credentials": credentials,
},
"provider_options": {
"vertex": {
"project": project,
"location": location,
}
},
}


def run_vertex_flow(
server: Literal["remote", "local"], vertex_model: VertexModelConfig
) -> None:
browserbase_api_key = os.environ.get("BROWSERBASE_API_KEY")
if not browserbase_api_key:
sys.exit("Set BROWSERBASE_API_KEY to run this example.")

client_kwargs: dict[str, Any] = {
"server": server,
"browserbase_api_key": browserbase_api_key,
}
browser: dict[str, Any]

if server == "local":
client_kwargs["local_ready_timeout_s"] = 30.0
browser = {
"type": "local",
"launchOptions": {
"headless": True,
},
}
else:
browser = {"type": "browserbase"}

bootstrap_model = os.environ.get("STAGEHAND_BOOTSTRAP_MODEL", "openai/gpt-4.1-mini")

with Stagehand(**client_kwargs) as client:
session = client.sessions.start(
model_name=bootstrap_model,
browser=browser,
)

try:
session.navigate(url="https://example.com")

observe_result = session.observe(
instruction="Find the main heading on the page.",
options={"model": vertex_model},
)
print(f"[{server}] observe:", observe_result)

extract_result = session.extract(
instruction="Extract the page title and main heading.",
schema={
"type": "object",
"properties": {
"title": {"type": "string"},
"heading": {"type": "string"},
},
"required": ["title", "heading"],
"additionalProperties": False,
},
options={"model": vertex_model},
)
print(f"[{server}] extract:", extract_result)

execute_result = session.execute(
agent_config={
"model": vertex_model,
"cua": False,
},
execute_options={
"instruction": "Summarize the current page in one sentence.",
"max_steps": 3,
},
)
print(f"[{server}] execute:", execute_result)
finally:
session.end()


def main() -> None:
load_example_env()
vertex_model = build_vertex_model_config()
run_vertex_flow("remote", vertex_model)
run_vertex_flow("local", vertex_model)


if __name__ == "__main__":
main()
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.20.0"
version = "3.21.0"
description = "The official Python library for the stagehand API"
dynamic = ["readme"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/stagehand/_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
elif is_sequence_t(files):
files = [(key, await _async_transform_file(file)) for key, file in files]
else:
raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")

return files

Expand Down
Loading
Loading