From 5acaa6f97a3fd926e3406c3af3504576a86a05bb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 04:31:59 +0000 Subject: [PATCH 1/5] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 433374d5..b8dd6b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ From 493abf4ee2023c3a88701c01bdd4bfdd1f4e0b63 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:21:21 +0000 Subject: [PATCH 2/5] feat: Add explicit SSE event names for local v3 streaming --- .stats.yml | 6 +++--- src/stagehand/_streaming.py | 24 ++++++++++++++---------- src/stagehand/types/stream_event.py | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4f6d212a..c5e5d8a0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-975ca868b31b1e45fb00b31a53d9df1ceec8663f6c2851bf40fdaa84171afadc.yml -openapi_spec_hash: 37891379e0f47e5c69769fbaa1064dab -config_hash: 0209737a4ab2a71afececb0ff7459998 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-a4a5ea048bb50d06460d81d6828b53b12b19e9224121ee6338dcd1f0781e22a1.yml +openapi_spec_hash: 9b81c0ae04576318d13d7a80d4ab7b5a +config_hash: d6c6f623d03971bdba921650e5eb7e5f diff --git a/src/stagehand/_streaming.py b/src/stagehand/_streaming.py index 78f7b697..83a250a5 100644 --- a/src/stagehand/_streaming.py +++ b/src/stagehand/_streaming.py @@ -59,10 +59,7 @@ def __stream__(self) -> Iterator[_T]: try: for sse in iterator: - if sse.data.startswith('{"data":{"status":"finished"'): - break - - if sse.data.startswith("error"): + if sse.event == "error": body = sse.data try: @@ -77,7 +74,12 @@ def __stream__(self) -> Iterator[_T]: response=self.response, ) - if sse.event is None: + if ( + sse.event == "starting" + or sse.event == "connected" + or sse.event == "running" + or sse.event == "finished" + ): yield process_data(data=sse.json(), cast_to=cast_to, response=response) finally: # Ensure the response is closed even if the consumer doesn't read all data @@ -144,10 +146,7 @@ async def __stream__(self) -> AsyncIterator[_T]: try: async for sse in iterator: - if sse.data.startswith('{"data":{"status":"finished"'): - break - - if sse.data.startswith("error"): + if sse.event == "error": body = sse.data try: @@ -162,7 +161,12 @@ async def __stream__(self) -> AsyncIterator[_T]: response=self.response, ) - if sse.event is None: + if ( + sse.event == "starting" + or sse.event == "connected" + or sse.event == "running" + or sse.event == "finished" + ): yield process_data(data=sse.json(), cast_to=cast_to, response=response) finally: # Ensure the response is closed even if the consumer doesn't read all data diff --git a/src/stagehand/types/stream_event.py b/src/stagehand/types/stream_event.py index 3d5b21ee..6fd47585 100644 --- a/src/stagehand/types/stream_event.py +++ b/src/stagehand/types/stream_event.py @@ -32,7 +32,7 @@ class DataStreamEventLogDataOutput(BaseModel): class StreamEvent(BaseModel): """Server-Sent Event emitted during streaming responses. - Events are sent as `data: \n\n`. Key order: data (with status first), type, id. + Events are sent as `event: \ndata: \n\n`, where the JSON payload has the shape `{ data, type, id }`. """ id: str From b0df6bcddaa9dbf206dee0d97ee9e303d703530b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:41:34 +0000 Subject: [PATCH 3/5] feat: Include LLM headers in ModelConfig --- .stats.yml | 4 +- src/stagehand/types/model_config_param.py | 4 + tests/api_resources/test_sessions.py | 112 ++++++++++++++++++---- 3 files changed, 98 insertions(+), 22 deletions(-) diff --git a/.stats.yml b/.stats.yml index c5e5d8a0..129faad5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-a4a5ea048bb50d06460d81d6828b53b12b19e9224121ee6338dcd1f0781e22a1.yml -openapi_spec_hash: 9b81c0ae04576318d13d7a80d4ab7b5a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-bc309fd00fe0507f4cbe3bc77fa27d0fbffeaa6e71998778da34de42608a67e8.yml +openapi_spec_hash: 1db1af5c1b068bba1d652102f4454668 config_hash: d6c6f623d03971bdba921650e5eb7e5f diff --git a/src/stagehand/types/model_config_param.py b/src/stagehand/types/model_config_param.py index 4c711077..3ed433f7 100644 --- a/src/stagehand/types/model_config_param.py +++ b/src/stagehand/types/model_config_param.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Dict from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo @@ -19,5 +20,8 @@ class ModelConfigParam(TypedDict, total=False): base_url: Annotated[str, PropertyInfo(alias="baseURL")] """Base URL for the model provider""" + headers: Dict[str, str] + """Custom headers sent with every request to the model provider""" + provider: Literal["openai", "anthropic", "google", "microsoft", "bedrock"] """AI provider for the model (or provide a baseURL endpoint instead)""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 8ee68229..65125733 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -43,7 +43,13 @@ def test_method_act_with_all_params_overload_1(self, client: Stagehand) -> None: input="Click the login button", frame_id="frameId", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "timeout": 30000, "variables": { "username": "john_doe", @@ -114,7 +120,13 @@ def test_method_act_with_all_params_overload_2(self, client: Stagehand) -> None: stream_response=True, frame_id="frameId", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "timeout": 30000, "variables": { "username": "john_doe", @@ -241,6 +253,7 @@ def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> N "model_name": "openai/gpt-5-nano", "api_key": "sk-some-openai-api-key", "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, "provider": "openai", }, "mode": "cua", @@ -248,10 +261,9 @@ def test_method_execute_with_all_params_overload_1(self, client: Stagehand) -> N "model_name": "openai/gpt-5-nano", "api_key": "sk-some-openai-api-key", "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, "provider": "openai", }, - "mode": "cua", - "model": {"model_name": "openai/gpt-5-nano"}, "provider": "openai", "system_prompt": "systemPrompt", }, @@ -339,6 +351,7 @@ def test_method_execute_with_all_params_overload_2(self, client: Stagehand) -> N "model_name": "openai/gpt-5-nano", "api_key": "sk-some-openai-api-key", "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, "provider": "openai", }, "mode": "cua", @@ -346,10 +359,9 @@ def test_method_execute_with_all_params_overload_2(self, client: Stagehand) -> N "model_name": "openai/gpt-5-nano", "api_key": "sk-some-openai-api-key", "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, "provider": "openai", }, - "mode": "cua", - "model": {"model_name": "openai/gpt-5-nano"}, "provider": "openai", "system_prompt": "systemPrompt", }, @@ -431,7 +443,13 @@ def test_method_extract_with_all_params_overload_1(self, client: Stagehand) -> N frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "selector": "#main-content", "timeout": 30000, }, @@ -493,7 +511,13 @@ def test_method_extract_with_all_params_overload_2(self, client: Stagehand) -> N frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "selector": "#main-content", "timeout": 30000, }, @@ -617,7 +641,13 @@ def test_method_observe_with_all_params_overload_1(self, client: Stagehand) -> N frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "selector": "nav", "timeout": 30000, "variables": { @@ -685,7 +715,13 @@ def test_method_observe_with_all_params_overload_2(self, client: Stagehand) -> N frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "selector": "nav", "timeout": 30000, "variables": { @@ -933,7 +969,13 @@ async def test_method_act_with_all_params_overload_1(self, async_client: AsyncSt input="Click the login button", frame_id="frameId", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "timeout": 30000, "variables": { "username": "john_doe", @@ -1004,7 +1046,13 @@ async def test_method_act_with_all_params_overload_2(self, async_client: AsyncSt stream_response=True, frame_id="frameId", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "timeout": 30000, "variables": { "username": "john_doe", @@ -1131,6 +1179,7 @@ async def test_method_execute_with_all_params_overload_1(self, async_client: Asy "model_name": "openai/gpt-5-nano", "api_key": "sk-some-openai-api-key", "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, "provider": "openai", }, "mode": "cua", @@ -1138,10 +1187,9 @@ async def test_method_execute_with_all_params_overload_1(self, async_client: Asy "model_name": "openai/gpt-5-nano", "api_key": "sk-some-openai-api-key", "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, "provider": "openai", }, - "mode": "cua", - "model": {"model_name": "openai/gpt-5-nano"}, "provider": "openai", "system_prompt": "systemPrompt", }, @@ -1229,6 +1277,7 @@ async def test_method_execute_with_all_params_overload_2(self, async_client: Asy "model_name": "openai/gpt-5-nano", "api_key": "sk-some-openai-api-key", "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, "provider": "openai", }, "mode": "cua", @@ -1236,10 +1285,9 @@ async def test_method_execute_with_all_params_overload_2(self, async_client: Asy "model_name": "openai/gpt-5-nano", "api_key": "sk-some-openai-api-key", "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, "provider": "openai", }, - "mode": "cua", - "model": {"model_name": "openai/gpt-5-nano"}, "provider": "openai", "system_prompt": "systemPrompt", }, @@ -1321,7 +1369,13 @@ async def test_method_extract_with_all_params_overload_1(self, async_client: Asy frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "selector": "#main-content", "timeout": 30000, }, @@ -1383,7 +1437,13 @@ async def test_method_extract_with_all_params_overload_2(self, async_client: Asy frame_id="frameId", instruction="Extract all product names and prices from the page", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "selector": "#main-content", "timeout": 30000, }, @@ -1507,7 +1567,13 @@ async def test_method_observe_with_all_params_overload_1(self, async_client: Asy frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "selector": "nav", "timeout": 30000, "variables": { @@ -1575,7 +1641,13 @@ async def test_method_observe_with_all_params_overload_2(self, async_client: Asy frame_id="frameId", instruction="Find all clickable navigation links", options={ - "model": {"model_name": "openai/gpt-5-nano"}, + "model": { + "model_name": "openai/gpt-5-nano", + "api_key": "sk-some-openai-api-key", + "base_url": "https://api.openai.com/v1", + "headers": {"foo": "string"}, + "provider": "openai", + }, "selector": "nav", "timeout": 30000, "variables": { From 4049e44cb7855c62d2231c34c6211532860dfdbf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:19:47 +0000 Subject: [PATCH 4/5] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a2ca624..46c4fb40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint 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 + 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 @@ -35,7 +35,7 @@ jobs: run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: From e2c8f6f04fdac3aa39c5dfb4b455ced9d838dbe2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:41:58 +0000 Subject: [PATCH 5/5] release: 3.18.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 2 +- src/stagehand/_version.py | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2a266f81..da0b97f9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.7.0" + ".": "3.18.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index b7705e0b..c67dde45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 3.18.0 (2026-03-25) + +Full Changelog: [v3.7.0...v3.18.0](https://github.com/browserbase/stagehand-python/compare/v3.7.0...v3.18.0) + +### Features + +* Add explicit SSE event names for local v3 streaming ([493abf4](https://github.com/browserbase/stagehand-python/commit/493abf4ee2023c3a88701c01bdd4bfdd1f4e0b63)) +* Include LLM headers in ModelConfig ([b0df6bc](https://github.com/browserbase/stagehand-python/commit/b0df6bcddaa9dbf206dee0d97ee9e303d703530b)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([4049e44](https://github.com/browserbase/stagehand-python/commit/4049e44cb7855c62d2231c34c6211532860dfdbf)) +* **internal:** update gitignore ([5acaa6f](https://github.com/browserbase/stagehand-python/commit/5acaa6f97a3fd926e3406c3af3504576a86a05bb)) + ## 3.7.0 (2026-03-23) Full Changelog: [v3.6.0...v3.7.0](https://github.com/browserbase/stagehand-python/compare/v3.6.0...v3.7.0) diff --git a/pyproject.toml b/pyproject.toml index b890e29a..beb416c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "3.7.0" +version = "3.18.0" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "MIT" diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index 5c32ea26..e5144c2d 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "3.7.0" # x-release-please-version +__version__ = "3.18.0" # x-release-please-version