From 6c06902ffd91d83903de4367c9616cdeb6da9265 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 17:46:11 +0000 Subject: [PATCH 1/8] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 391cde3..57aa7a5 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/stagehand-6f6bfb81d092f30a5e2005328c97d61b9ea36132bb19e9e79e55294b9534ce20.yml -openapi_spec_hash: f3fc1e3688a38dc2c28f7178f7d534e5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-e629569417ad17cad5c73180109b4c3ae778f38063fc72146fa82f82de145911.yml +openapi_spec_hash: 42e4eedbc0fcc772bb271191a067bce1 config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 From 8cdd2acbd3633487f53a2884017ec27071396f3c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 18:23:02 +0000 Subject: [PATCH 2/8] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 57aa7a5..094ac49 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/stagehand-e629569417ad17cad5c73180109b4c3ae778f38063fc72146fa82f82de145911.yml -openapi_spec_hash: 42e4eedbc0fcc772bb271191a067bce1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-49b40c7425adba9e67fc102838c5216c45ca1f7ef4c10823c5665fd413538504.yml +openapi_spec_hash: 6880dc029df2e88dfe8943c0dec5a3a5 config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 From 872590fe228fd2f0ce70d7cbefdddbf3c22fe147 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 18:25:47 +0000 Subject: [PATCH 3/8] feat: [feat]: add `ignoreSelectors` to `observe()` --- .stats.yml | 4 +-- src/Sessions/SessionObserveParams/Options.php | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 094ac49..0339c57 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/stagehand-49b40c7425adba9e67fc102838c5216c45ca1f7ef4c10823c5665fd413538504.yml -openapi_spec_hash: 6880dc029df2e88dfe8943c0dec5a3a5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-eae8400fade7b2c8329c4148f56de92e147c34c0feecb420c015aab6544a9acc.yml +openapi_spec_hash: 0a9eff1ac1d464e89cbd9db64709b08a config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 diff --git a/src/Sessions/SessionObserveParams/Options.php b/src/Sessions/SessionObserveParams/Options.php index 02bc11c..5130e30 100644 --- a/src/Sessions/SessionObserveParams/Options.php +++ b/src/Sessions/SessionObserveParams/Options.php @@ -17,6 +17,7 @@ * @phpstan-import-type VariableShape from \Stagehand\Sessions\SessionObserveParams\Options\Variable * * @phpstan-type OptionsShape = array{ + * ignoreSelectors?: list|null, * model?: ModelShape|null, * selector?: string|null, * timeout?: float|null, @@ -28,6 +29,14 @@ final class Options implements BaseModel /** @use SdkModel */ use SdkModel; + /** + * Selectors for elements and subtrees that should be excluded from observation. + * + * @var list|null $ignoreSelectors + */ + #[Optional(list: 'string')] + public ?array $ignoreSelectors; + /** * Model configuration object or model name string (e.g., 'openai/gpt-5-nano'). * @@ -66,10 +75,12 @@ public function __construct() * * You must use named parameters to construct any parameters with a default value. * + * @param list|null $ignoreSelectors * @param ModelShape|null $model * @param array|null $variables */ public static function with( + ?array $ignoreSelectors = null, string|ModelConfig|array|null $model = null, ?string $selector = null, ?float $timeout = null, @@ -77,6 +88,7 @@ public static function with( ): self { $self = new self; + null !== $ignoreSelectors && $self['ignoreSelectors'] = $ignoreSelectors; null !== $model && $self['model'] = $model; null !== $selector && $self['selector'] = $selector; null !== $timeout && $self['timeout'] = $timeout; @@ -85,6 +97,19 @@ public static function with( return $self; } + /** + * Selectors for elements and subtrees that should be excluded from observation. + * + * @param list $ignoreSelectors + */ + public function withIgnoreSelectors(array $ignoreSelectors): self + { + $self = clone $this; + $self['ignoreSelectors'] = $ignoreSelectors; + + return $self; + } + /** * Model configuration object or model name string (e.g., 'openai/gpt-5-nano'). * From 9d1c43a1435dc0ba0f66094ffaf4c3e6d463b17a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 02:00:55 +0000 Subject: [PATCH 4/8] fix: guzzle requires special handling to enable streaming --- README.md | 9 + composer.json | 1 + composer.lock | 374 +++++++++++++++++- src/Client.php | 6 + src/Core/BaseClient.php | 6 +- src/Core/FileParam.php | 2 +- .../Implementation/StreamingHttpClient.php | 29 ++ src/Core/Util.php | 12 + src/RequestOptions.php | 17 + tests/Core/StreamingTransportTest.php | 122 ++++++ 10 files changed, 574 insertions(+), 4 deletions(-) create mode 100644 src/Core/Implementation/StreamingHttpClient.php create mode 100644 tests/Core/StreamingTransportTest.php diff --git a/README.md b/README.md index 574163f..ac162ed 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,15 @@ foreach ($stream as $response) { } ``` +Streaming requests are dispatched through a separate `streamingTransporter` PSR-18 HTTP client. When unset, the SDK uses the configured `transporter`. +Some PSR-18 HTTP clients will by default try to read the entire response, so you may need to specify a streaming capable implementation. + +```php +$client = new Stagehand\Client( + requestOptions: Stagehand\RequestOptions::with(streamingTransporter: $myStreamingClient), +); +``` + ### Handling errors When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `Stagehand\Core\Exceptions\APIException` will be thrown: diff --git a/composer.json b/composer.json index 5d324e7..1d6aac5 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3", + "guzzlehttp/guzzle": "^7", "nyholm/psr7": "^1", "pestphp/pest": "^3", "php-http/mock-client": "^1", diff --git a/composer.lock b/composer.lock index 7a5e63d..b0e2283 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ffa287ea8babf60e021f37e62c6c207a", + "content-hash": "bbae64cb4d21c987158bc3723eda4226", "packages": [ { "name": "php-http/discovery", @@ -968,6 +968,332 @@ ], "time": "2026-01-08T21:57:37+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-03-10T16:41:02+00:00" + }, { "name": "jean85/pretty-package-versions", "version": "2.1.1", @@ -3250,6 +3576,50 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "react/cache", "version": "v1.2.0", @@ -6680,5 +7050,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Client.php b/src/Client.php index c571bb6..5a3ebb0 100644 --- a/src/Client.php +++ b/src/Client.php @@ -7,6 +7,7 @@ use Http\Discovery\Psr17FactoryDiscovery; use Http\Discovery\Psr18ClientDiscovery; use Stagehand\Core\BaseClient; +use Stagehand\Core\Implementation\StreamingHttpClient; use Stagehand\Core\Util; use Stagehand\Services\SessionsService; @@ -63,6 +64,11 @@ public function __construct( $requestOptions, ); + if (is_null($options->streamingTransporter)) { + assert(!is_null($options->transporter)); + $options->streamingTransporter = new StreamingHttpClient($options->transporter); + } + /** @var array $headers */ $headers = [ 'Content-Type' => 'application/json', diff --git a/src/Core/BaseClient.php b/src/Core/BaseClient.php index 98f540d..29e21ec 100644 --- a/src/Core/BaseClient.php +++ b/src/Core/BaseClient.php @@ -241,11 +241,15 @@ protected function sendRequest( $req = $req->withHeader('X-Stainless-Retry-Count', strval($retryCount)); $req = Util::withSetBody($opts->streamFactory, req: $req, body: $data); + $transporter = Util::isStreamingRequest($req) + ? ($opts->streamingTransporter ?? $opts->transporter) + : $opts->transporter; + $rsp = null; $err = null; try { - $rsp = $opts->transporter->sendRequest($req); + $rsp = $transporter->sendRequest($req); } catch (ClientExceptionInterface $e) { $err = $e; } diff --git a/src/Core/FileParam.php b/src/Core/FileParam.php index bc52309..5924990 100644 --- a/src/Core/FileParam.php +++ b/src/Core/FileParam.php @@ -41,7 +41,7 @@ public static function fromResource(mixed $resource, ?string $filename = null, s throw new \InvalidArgumentException('Expected a resource, got '.get_debug_type($resource)); } - if (null === $filename) { + if (is_null($filename)) { $meta = stream_get_meta_data($resource); $filename = basename($meta['uri'] ?? 'upload'); } diff --git a/src/Core/Implementation/StreamingHttpClient.php b/src/Core/Implementation/StreamingHttpClient.php new file mode 100644 index 0000000..fcc7983 --- /dev/null +++ b/src/Core/Implementation/StreamingHttpClient.php @@ -0,0 +1,29 @@ +inner, '\GuzzleHttp\Client')) { + return $this->inner->send($request, ['stream' => true]); + } + + return $this->inner->sendRequest($request); + } +} diff --git a/src/Core/Util.php b/src/Core/Util.php index 19a90c3..1285cc0 100644 --- a/src/Core/Util.php +++ b/src/Core/Util.php @@ -25,6 +25,8 @@ final class Util public const JSONL_CONTENT_TYPE = '/^application\/(:?x-(?:n|l)djson)|(:?(?:x-)?jsonl)/'; + public const STREAMING_CONTENT_TYPE = ['/^text\/event-stream/', self::JSONL_CONTENT_TYPE]; + public static function getenv(string $key): ?string { if (array_key_exists($key, array: $_ENV)) { @@ -217,6 +219,16 @@ public static function joinUri( return $base->withQuery($qs); } + public static function isStreamingRequest(RequestInterface $request): bool + { + $accept = $request->getHeaderLine('Accept'); + + return !empty(array_filter( + self::STREAMING_CONTENT_TYPE, + static fn (string $pattern) => (bool) preg_match($pattern, subject: $accept), + )); + } + /** * @param array|null> $headers */ diff --git a/src/RequestOptions.php b/src/RequestOptions.php index 3c8fbd0..4aea98b 100644 --- a/src/RequestOptions.php +++ b/src/RequestOptions.php @@ -23,6 +23,7 @@ * extraQueryParams?: array|null, * extraBodyParams?: mixed, * transporter?: ClientInterface|null, + * streamingTransporter?: ClientInterface|null, * uriFactory?: UriFactoryInterface|null, * streamFactory?: StreamFactoryInterface|null, * requestFactory?: RequestFactoryInterface|null, @@ -60,6 +61,9 @@ final class RequestOptions implements BaseModel #[Optional] public ?ClientInterface $transporter; + #[Optional] + public ?ClientInterface $streamingTransporter; + #[Optional] public ?UriFactoryInterface $uriFactory; @@ -98,6 +102,7 @@ public static function with( ?array $extraQueryParams = null, mixed $extraBodyParams = null, ?ClientInterface $transporter = null, + ?ClientInterface $streamingTransporter = null, ?UriFactoryInterface $uriFactory = null, ?StreamFactoryInterface $streamFactory = null, ?RequestFactoryInterface $requestFactory = null, @@ -114,6 +119,9 @@ public static function with( null !== $extraQueryParams && $self->extraQueryParams = $extraQueryParams; null !== $extraBodyParams && $self->extraBodyParams = $extraBodyParams; null !== $transporter && $self->transporter = $transporter; + null !== $streamingTransporter && $self + ->streamingTransporter = $streamingTransporter + ; null !== $uriFactory && $self->uriFactory = $uriFactory; null !== $streamFactory && $self->streamFactory = $streamFactory; null !== $requestFactory && $self->requestFactory = $requestFactory; @@ -191,6 +199,15 @@ public function withTransporter(ClientInterface $transporter): self return $self; } + public function withStreamingTransporter( + ClientInterface $streamingTransporter + ): self { + $self = clone $this; + $self->streamingTransporter = $streamingTransporter; + + return $self; + } + public function withUriFactory(UriFactoryInterface $uriFactory): self { $self = clone $this; diff --git a/tests/Core/StreamingTransportTest.php b/tests/Core/StreamingTransportTest.php new file mode 100644 index 0000000..0f3e046 --- /dev/null +++ b/tests/Core/StreamingTransportTest.php @@ -0,0 +1,122 @@ + true, + 'application/x-ndjson' => true, + 'application/x-ldjson' => true, + 'application/jsonl' => true, + 'application/x-jsonl' => true, + 'text/event-stream; charset=utf-8' => true, + 'application/json' => false, + 'text/plain' => false, + '' => false, + ]; + + foreach ($cases as $accept => $expected) { + $req = $factory->createRequest('GET', 'http://localhost'); + if ('' !== $accept) { + $req = $req->withHeader('Accept', $accept); + } + $this->assertSame( + $expected, + Util::isStreamingRequest($req), + "Accept: '{$accept}'", + ); + } + } + + #[Test] + public function testRoutesNonStreamingRequestToTransporter(): void + { + [$client, $plain, $streaming] = $this->buildClient(); + + $client->request('GET', '/'); + + $this->assertCount(1, $plain->getRequests()); + $this->assertCount(0, $streaming->getRequests()); + } + + #[Test] + public function testRoutesStreamingRequestToStreamingTransporter(): void + { + [$client, $plain, $streaming] = $this->buildClient(); + + $client->request('GET', '/', headers: ['Accept' => 'text/event-stream']); + + $this->assertCount(0, $plain->getRequests()); + $this->assertCount(1, $streaming->getRequests()); + + $sent = $streaming->getRequests()[0]; + $this->assertStringContainsString('text/event-stream', $sent->getHeaderLine('Accept')); + } + + /** + * @return array{BaseClient, MockClient, MockClient} + */ + private function buildClient(): array + { + $plain = new MockClient; + $plain->setDefaultResponse($this->jsonResponse()); + + $streaming = new MockClient; + $streaming->setDefaultResponse($this->sseResponse()); + + $options = RequestOptions::with( + transporter: $plain, + streamingTransporter: $streaming, + uriFactory: Psr17FactoryDiscovery::findUriFactory(), + requestFactory: Psr17FactoryDiscovery::findRequestFactory(), + streamFactory: Psr17FactoryDiscovery::findStreamFactory(), + ); + + $client = new class(headers: [], baseUrl: 'http://localhost', options: $options) extends BaseClient {}; + + return [$client, $plain, $streaming]; + } + + private function jsonResponse(): ResponseInterface + { + $responseFactory = Psr17FactoryDiscovery::findResponseFactory(); + $streamFactory = Psr17FactoryDiscovery::findStreamFactory(); + + return $responseFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($streamFactory->createStream('{}')) + ; + } + + private function sseResponse(): ResponseInterface + { + $responseFactory = Psr17FactoryDiscovery::findResponseFactory(); + $streamFactory = Psr17FactoryDiscovery::findStreamFactory(); + + return $responseFactory->createResponse(200) + ->withHeader('Content-Type', 'text/event-stream') + ->withBody($streamFactory->createStream('')) + ; + } +} From 9122bda2fc30f3bfaa4c5da2de6faad7ce7345ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 02:00:40 +0000 Subject: [PATCH 5/8] ci: pin GitHub Actions to commit SHAs Pin all GitHub Actions referenced in generated workflows (both first-party `actions/*` and third-party) to immutable commit SHAs. Updating pinned actions is now a deliberate codegen-side bump rather than implicit on every workflow run. --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/publish-packagist.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5721b93..d4a9bcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,10 +22,10 @@ jobs: 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: Set up PHP - uses: 'shivammathur/setup-php@v2' + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 with: php-version: '8.3' @@ -40,10 +40,10 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/stagehand-php' && '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: Set up PHP - uses: 'shivammathur/setup-php@v2' + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 with: php-version: '8.3' diff --git a/.github/workflows/publish-packagist.yml b/.github/workflows/publish-packagist.yml index 6c6f68e..dc56960 100644 --- a/.github/workflows/publish-packagist.yml +++ b/.github/workflows/publish-packagist.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Publish to Packagist run: |- diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 1358383..ebc8b13 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'browserbase/stagehand-php' && (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: | From 786c9fda24ea073f57bde1ec3cc6e332c7d50300 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 19:16:19 +0000 Subject: [PATCH 6/8] feat: STG-1756 add Vertex auth params to Stagehand spec --- .stats.yml | 4 +- src/Sessions/ModelConfig.php | 67 ++++++ .../ModelConfig/GoogleAuthOptions.php | 134 +++++++++++ .../GoogleAuthOptions/Credentials.php | 217 ++++++++++++++++++ .../GoogleAuthOptions/Credentials/Type.php | 10 + .../ModelConfig/GoogleAuthOptions/Scopes.php | 29 +++ src/Sessions/ModelConfig/Provider.php | 2 + tests/Services/SessionsTest.php | 60 +++++ 8 files changed, 521 insertions(+), 2 deletions(-) create mode 100644 src/Sessions/ModelConfig/GoogleAuthOptions.php create mode 100644 src/Sessions/ModelConfig/GoogleAuthOptions/Credentials.php create mode 100644 src/Sessions/ModelConfig/GoogleAuthOptions/Credentials/Type.php create mode 100644 src/Sessions/ModelConfig/GoogleAuthOptions/Scopes.php diff --git a/.stats.yml b/.stats.yml index 0339c57..220445b 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/stagehand-eae8400fade7b2c8329c4148f56de92e147c34c0feecb420c015aab6544a9acc.yml -openapi_spec_hash: 0a9eff1ac1d464e89cbd9db64709b08a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-e77d6b15f0a94b16a54ef87a84d2cabe49eb11cff5ceba76f00dd788ff483eab.yml +openapi_spec_hash: a1dab7fe72a772d188a15305124ebd73 config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 diff --git a/src/Sessions/ModelConfig.php b/src/Sessions/ModelConfig.php index c03b239..deb36cf 100644 --- a/src/Sessions/ModelConfig.php +++ b/src/Sessions/ModelConfig.php @@ -8,14 +8,20 @@ use Stagehand\Core\Attributes\Required; use Stagehand\Core\Concerns\SdkModel; use Stagehand\Core\Contracts\BaseModel; +use Stagehand\Sessions\ModelConfig\GoogleAuthOptions; use Stagehand\Sessions\ModelConfig\Provider; /** + * @phpstan-import-type GoogleAuthOptionsShape from \Stagehand\Sessions\ModelConfig\GoogleAuthOptions + * * @phpstan-type ModelConfigShape = array{ * modelName: string, * apiKey?: string|null, * baseURL?: string|null, + * googleAuthOptions?: null|GoogleAuthOptions|GoogleAuthOptionsShape, * headers?: array|null, + * location?: string|null, + * project?: string|null, * provider?: null|Provider|value-of, * } */ @@ -42,6 +48,12 @@ final class ModelConfig implements BaseModel #[Optional] public ?string $baseURL; + /** + * google-auth-library options used to authenticate Vertex AI models. + */ + #[Optional] + public ?GoogleAuthOptions $googleAuthOptions; + /** * Custom headers sent with every request to the model provider. * @@ -50,6 +62,18 @@ final class ModelConfig implements BaseModel #[Optional(map: 'string')] public ?array $headers; + /** + * Google Cloud location for Vertex AI models. + */ + #[Optional] + public ?string $location; + + /** + * Google Cloud project ID for Vertex AI models. + */ + #[Optional] + public ?string $project; + /** * AI provider for the model (or provide a baseURL endpoint instead). * @@ -82,6 +106,7 @@ public function __construct() * * You must use named parameters to construct any parameters with a default value. * + * @param GoogleAuthOptions|GoogleAuthOptionsShape|null $googleAuthOptions * @param array|null $headers * @param Provider|value-of|null $provider */ @@ -89,7 +114,10 @@ public static function with( string $modelName, ?string $apiKey = null, ?string $baseURL = null, + GoogleAuthOptions|array|null $googleAuthOptions = null, ?array $headers = null, + ?string $location = null, + ?string $project = null, Provider|string|null $provider = null, ): self { $self = new self; @@ -98,7 +126,10 @@ public static function with( null !== $apiKey && $self['apiKey'] = $apiKey; null !== $baseURL && $self['baseURL'] = $baseURL; + null !== $googleAuthOptions && $self['googleAuthOptions'] = $googleAuthOptions; null !== $headers && $self['headers'] = $headers; + null !== $location && $self['location'] = $location; + null !== $project && $self['project'] = $project; null !== $provider && $self['provider'] = $provider; return $self; @@ -137,6 +168,20 @@ public function withBaseURL(string $baseURL): self return $self; } + /** + * google-auth-library options used to authenticate Vertex AI models. + * + * @param GoogleAuthOptions|GoogleAuthOptionsShape $googleAuthOptions + */ + public function withGoogleAuthOptions( + GoogleAuthOptions|array $googleAuthOptions + ): self { + $self = clone $this; + $self['googleAuthOptions'] = $googleAuthOptions; + + return $self; + } + /** * Custom headers sent with every request to the model provider. * @@ -150,6 +195,28 @@ public function withHeaders(array $headers): self return $self; } + /** + * Google Cloud location for Vertex AI models. + */ + public function withLocation(string $location): self + { + $self = clone $this; + $self['location'] = $location; + + return $self; + } + + /** + * Google Cloud project ID for Vertex AI models. + */ + public function withProject(string $project): self + { + $self = clone $this; + $self['project'] = $project; + + return $self; + } + /** * AI provider for the model (or provide a baseURL endpoint instead). * diff --git a/src/Sessions/ModelConfig/GoogleAuthOptions.php b/src/Sessions/ModelConfig/GoogleAuthOptions.php new file mode 100644 index 0000000..28b5ded --- /dev/null +++ b/src/Sessions/ModelConfig/GoogleAuthOptions.php @@ -0,0 +1,134 @@ + */ + use SdkModel; + + /** + * Google Cloud service account credentials. + */ + #[Optional] + public ?Credentials $credentials; + + /** + * Google Cloud project ID used by google-auth-library. + */ + #[Optional('projectId')] + public ?string $projectID; + + /** + * Google auth scopes for the desired API request. + * + * @var ScopesVariants|null $scopes + */ + #[Optional(union: Scopes::class)] + public string|array|null $scopes; + + /** + * Google Cloud universe domain. + */ + #[Optional] + public ?string $universeDomain; + + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + * + * @param Credentials|CredentialsShape|null $credentials + * @param ScopesShape|null $scopes + */ + public static function with( + Credentials|array|null $credentials = null, + ?string $projectID = null, + string|array|null $scopes = null, + ?string $universeDomain = null, + ): self { + $self = new self; + + null !== $credentials && $self['credentials'] = $credentials; + null !== $projectID && $self['projectID'] = $projectID; + null !== $scopes && $self['scopes'] = $scopes; + null !== $universeDomain && $self['universeDomain'] = $universeDomain; + + return $self; + } + + /** + * Google Cloud service account credentials. + * + * @param Credentials|CredentialsShape $credentials + */ + public function withCredentials(Credentials|array $credentials): self + { + $self = clone $this; + $self['credentials'] = $credentials; + + return $self; + } + + /** + * Google Cloud project ID used by google-auth-library. + */ + public function withProjectID(string $projectID): self + { + $self = clone $this; + $self['projectID'] = $projectID; + + return $self; + } + + /** + * Google auth scopes for the desired API request. + * + * @param ScopesShape $scopes + */ + public function withScopes(string|array $scopes): self + { + $self = clone $this; + $self['scopes'] = $scopes; + + return $self; + } + + /** + * Google Cloud universe domain. + */ + public function withUniverseDomain(string $universeDomain): self + { + $self = clone $this; + $self['universeDomain'] = $universeDomain; + + return $self; + } +} diff --git a/src/Sessions/ModelConfig/GoogleAuthOptions/Credentials.php b/src/Sessions/ModelConfig/GoogleAuthOptions/Credentials.php new file mode 100644 index 0000000..5d3a7aa --- /dev/null +++ b/src/Sessions/ModelConfig/GoogleAuthOptions/Credentials.php @@ -0,0 +1,217 @@ +, + * universeDomain?: string|null, + * } + */ +final class Credentials implements BaseModel +{ + /** @use SdkModel */ + use SdkModel; + + #[Required('client_email')] + public string $clientEmail; + + #[Required('private_key')] + public string $privateKey; + + #[Optional('auth_provider_x509_cert_url')] + public ?string $authProviderX509CertURL; + + #[Optional('auth_uri')] + public ?string $authUri; + + #[Optional('client_id')] + public ?string $clientID; + + #[Optional('client_x509_cert_url')] + public ?string $clientX509CertURL; + + #[Optional('private_key_id')] + public ?string $privateKeyID; + + #[Optional('project_id')] + public ?string $projectID; + + #[Optional('token_uri')] + public ?string $tokenUri; + + /** @var value-of|null $type */ + #[Optional(enum: Type::class)] + public ?string $type; + + #[Optional('universe_domain')] + public ?string $universeDomain; + + /** + * `new Credentials()` is missing required properties by the API. + * + * To enforce required parameters use + * ``` + * Credentials::with(clientEmail: ..., privateKey: ...) + * ``` + * + * Otherwise ensure the following setters are called + * + * ``` + * (new Credentials)->withClientEmail(...)->withPrivateKey(...) + * ``` + */ + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + * + * @param Type|value-of|null $type + */ + public static function with( + string $clientEmail, + string $privateKey, + ?string $authProviderX509CertURL = null, + ?string $authUri = null, + ?string $clientID = null, + ?string $clientX509CertURL = null, + ?string $privateKeyID = null, + ?string $projectID = null, + ?string $tokenUri = null, + Type|string|null $type = null, + ?string $universeDomain = null, + ): self { + $self = new self; + + $self['clientEmail'] = $clientEmail; + $self['privateKey'] = $privateKey; + + null !== $authProviderX509CertURL && $self['authProviderX509CertURL'] = $authProviderX509CertURL; + null !== $authUri && $self['authUri'] = $authUri; + null !== $clientID && $self['clientID'] = $clientID; + null !== $clientX509CertURL && $self['clientX509CertURL'] = $clientX509CertURL; + null !== $privateKeyID && $self['privateKeyID'] = $privateKeyID; + null !== $projectID && $self['projectID'] = $projectID; + null !== $tokenUri && $self['tokenUri'] = $tokenUri; + null !== $type && $self['type'] = $type; + null !== $universeDomain && $self['universeDomain'] = $universeDomain; + + return $self; + } + + public function withClientEmail(string $clientEmail): self + { + $self = clone $this; + $self['clientEmail'] = $clientEmail; + + return $self; + } + + public function withPrivateKey(string $privateKey): self + { + $self = clone $this; + $self['privateKey'] = $privateKey; + + return $self; + } + + public function withAuthProviderX509CertURL( + string $authProviderX509CertURL + ): self { + $self = clone $this; + $self['authProviderX509CertURL'] = $authProviderX509CertURL; + + return $self; + } + + public function withAuthUri(string $authUri): self + { + $self = clone $this; + $self['authUri'] = $authUri; + + return $self; + } + + public function withClientID(string $clientID): self + { + $self = clone $this; + $self['clientID'] = $clientID; + + return $self; + } + + public function withClientX509CertURL(string $clientX509CertURL): self + { + $self = clone $this; + $self['clientX509CertURL'] = $clientX509CertURL; + + return $self; + } + + public function withPrivateKeyID(string $privateKeyID): self + { + $self = clone $this; + $self['privateKeyID'] = $privateKeyID; + + return $self; + } + + public function withProjectID(string $projectID): self + { + $self = clone $this; + $self['projectID'] = $projectID; + + return $self; + } + + public function withTokenUri(string $tokenUri): self + { + $self = clone $this; + $self['tokenUri'] = $tokenUri; + + return $self; + } + + /** + * @param Type|value-of $type + */ + public function withType(Type|string $type): self + { + $self = clone $this; + $self['type'] = $type; + + return $self; + } + + public function withUniverseDomain(string $universeDomain): self + { + $self = clone $this; + $self['universeDomain'] = $universeDomain; + + return $self; + } +} diff --git a/src/Sessions/ModelConfig/GoogleAuthOptions/Credentials/Type.php b/src/Sessions/ModelConfig/GoogleAuthOptions/Credentials/Type.php new file mode 100644 index 0000000..92e4a49 --- /dev/null +++ b/src/Sessions/ModelConfig/GoogleAuthOptions/Credentials/Type.php @@ -0,0 +1,10 @@ + + * @phpstan-type ScopesShape = ScopesVariants + */ +final class Scopes implements ConverterSource +{ + use SdkUnion; + + /** + * @return list|array + */ + public static function variants(): array + { + return ['string', new ListOf('string')]; + } +} diff --git a/src/Sessions/ModelConfig/Provider.php b/src/Sessions/ModelConfig/Provider.php index 71e33e4..e7295ee 100644 --- a/src/Sessions/ModelConfig/Provider.php +++ b/src/Sessions/ModelConfig/Provider.php @@ -18,4 +18,6 @@ enum Provider: string case MICROSOFT = 'microsoft'; case BEDROCK = 'bedrock'; + + case VERTEX = 'vertex'; } diff --git a/tests/Services/SessionsTest.php b/tests/Services/SessionsTest.php index 5d00fc6..b3f6282 100644 --- a/tests/Services/SessionsTest.php +++ b/tests/Services/SessionsTest.php @@ -72,7 +72,27 @@ public function testActWithOptionalParams(): void 'modelName' => 'openai/gpt-5.4-mini', 'apiKey' => 'sk-some-openai-api-key', 'baseURL' => 'https://api.openai.com/v1', + 'googleAuthOptions' => [ + 'credentials' => [ + 'clientEmail' => 'client_email', + 'privateKey' => 'private_key', + 'authProviderX509CertURL' => 'https://example.com', + 'authUri' => 'https://example.com', + 'clientID' => 'client_id', + 'clientX509CertURL' => 'https://example.com', + 'privateKeyID' => 'private_key_id', + 'projectID' => 'project_id', + 'tokenUri' => 'https://example.com', + 'type' => 'service_account', + 'universeDomain' => 'universe_domain', + ], + 'projectID' => 'projectId', + 'scopes' => 'string', + 'universeDomain' => 'universeDomain', + ], 'headers' => ['foo' => 'string'], + 'location' => 'us-central1', + 'project' => 'my-gcp-project', 'provider' => 'openai', ], 'timeout' => 30000, @@ -139,7 +159,27 @@ public function testExecuteWithOptionalParams(): void 'modelName' => 'openai/gpt-5.4-mini', 'apiKey' => 'sk-some-openai-api-key', 'baseURL' => 'https://api.openai.com/v1', + 'googleAuthOptions' => [ + 'credentials' => [ + 'clientEmail' => 'client_email', + 'privateKey' => 'private_key', + 'authProviderX509CertURL' => 'https://example.com', + 'authUri' => 'https://example.com', + 'clientID' => 'client_id', + 'clientX509CertURL' => 'https://example.com', + 'privateKeyID' => 'private_key_id', + 'projectID' => 'project_id', + 'tokenUri' => 'https://example.com', + 'type' => 'service_account', + 'universeDomain' => 'universe_domain', + ], + 'projectID' => 'projectId', + 'scopes' => 'string', + 'universeDomain' => 'universeDomain', + ], 'headers' => ['foo' => 'string'], + 'location' => 'us-central1', + 'project' => 'my-gcp-project', 'provider' => 'openai', ], 'mode' => 'cua', @@ -147,7 +187,27 @@ public function testExecuteWithOptionalParams(): void 'modelName' => 'openai/gpt-5.4-mini', 'apiKey' => 'sk-some-openai-api-key', 'baseURL' => 'https://api.openai.com/v1', + 'googleAuthOptions' => [ + 'credentials' => [ + 'clientEmail' => 'client_email', + 'privateKey' => 'private_key', + 'authProviderX509CertURL' => 'https://example.com', + 'authUri' => 'https://example.com', + 'clientID' => 'client_id', + 'clientX509CertURL' => 'https://example.com', + 'privateKeyID' => 'private_key_id', + 'projectID' => 'project_id', + 'tokenUri' => 'https://example.com', + 'type' => 'service_account', + 'universeDomain' => 'universe_domain', + ], + 'projectID' => 'projectId', + 'scopes' => 'string', + 'universeDomain' => 'universeDomain', + ], 'headers' => ['foo' => 'string'], + 'location' => 'us-central1', + 'project' => 'my-gcp-project', 'provider' => 'openai', ], 'provider' => 'openai', From 944e2e3a0a55618c9742a0defa3e06f9778719a4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 21:00:21 +0000 Subject: [PATCH 7/8] feat: Add `screenshot` option to Extract --- .stats.yml | 4 ++-- src/Sessions/SessionExtractParams/Options.php | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 220445b..9043f09 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/stagehand-e77d6b15f0a94b16a54ef87a84d2cabe49eb11cff5ceba76f00dd788ff483eab.yml -openapi_spec_hash: a1dab7fe72a772d188a15305124ebd73 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-80502d74c1be605e77d45ff2b54297fe34ce85dbad1e8f2dfa30ba6d09601219.yml +openapi_spec_hash: fd62f768756a400c3ecd695bfcf3845a config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 diff --git a/src/Sessions/SessionExtractParams/Options.php b/src/Sessions/SessionExtractParams/Options.php index 0e8a6c3..327c96c 100644 --- a/src/Sessions/SessionExtractParams/Options.php +++ b/src/Sessions/SessionExtractParams/Options.php @@ -16,6 +16,7 @@ * @phpstan-type OptionsShape = array{ * ignoreSelectors?: list|null, * model?: ModelShape|null, + * screenshot?: bool|null, * selector?: string|null, * timeout?: float|null, * } @@ -41,6 +42,12 @@ final class Options implements BaseModel #[Optional] public string|ModelConfig|null $model; + /** + * When true, include a screenshot of the current viewport in the extraction LLM call. Defaults to false. + */ + #[Optional] + public ?bool $screenshot; + /** * CSS selector to scope extraction to a specific element. */ @@ -69,6 +76,7 @@ public function __construct() public static function with( ?array $ignoreSelectors = null, string|ModelConfig|array|null $model = null, + ?bool $screenshot = null, ?string $selector = null, ?float $timeout = null, ): self { @@ -76,6 +84,7 @@ public static function with( null !== $ignoreSelectors && $self['ignoreSelectors'] = $ignoreSelectors; null !== $model && $self['model'] = $model; + null !== $screenshot && $self['screenshot'] = $screenshot; null !== $selector && $self['selector'] = $selector; null !== $timeout && $self['timeout'] = $timeout; @@ -108,6 +117,17 @@ public function withModel(string|ModelConfig|array $model): self return $self; } + /** + * When true, include a screenshot of the current viewport in the extraction LLM call. Defaults to false. + */ + public function withScreenshot(bool $screenshot): self + { + $self = clone $this; + $self['screenshot'] = $screenshot; + + return $self; + } + /** * CSS selector to scope extraction to a specific element. */ From 1c2907ae49ad9132baaae0a9323c11ec0af15276 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 21:00:47 +0000 Subject: [PATCH 8/8] release: 3.21.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 15 +++++++++++++++ README.md | 2 +- src/Version.php | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d11c8fc..eba8a04 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.20.0" + ".": "3.21.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ba66f7..503f39e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 3.21.0 (2026-05-20) + +Full Changelog: [v3.20.0...v3.21.0](https://github.com/browserbase/stagehand-php/compare/v3.20.0...v3.21.0) + +### Features + +* [feat]: add `ignoreSelectors` to `observe()` ([872590f](https://github.com/browserbase/stagehand-php/commit/872590fe228fd2f0ce70d7cbefdddbf3c22fe147)) +* Add `screenshot` option to Extract ([944e2e3](https://github.com/browserbase/stagehand-php/commit/944e2e3a0a55618c9742a0defa3e06f9778719a4)) +* STG-1756 add Vertex auth params to Stagehand spec ([786c9fd](https://github.com/browserbase/stagehand-php/commit/786c9fda24ea073f57bde1ec3cc6e332c7d50300)) + + +### Bug Fixes + +* guzzle requires special handling to enable streaming ([9d1c43a](https://github.com/browserbase/stagehand-php/commit/9d1c43a1435dc0ba0f66094ffaf4c3e6d463b17a)) + ## 3.20.0 (2026-05-06) Full Changelog: [v3.19.3...v3.20.0](https://github.com/browserbase/stagehand-php/compare/v3.19.3...v3.20.0) diff --git a/README.md b/README.md index ac162ed..dd7e623 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ The REST API documentation can be found on [docs.stagehand.dev](https://docs.sta ``` -composer require "browserbase/stagehand 3.20.0" +composer require "browserbase/stagehand 3.21.0" ``` diff --git a/src/Version.php b/src/Version.php index 7c36484..b10e37c 100644 --- a/src/Version.php +++ b/src/Version.php @@ -5,5 +5,5 @@ namespace Stagehand; // x-release-please-start-version -const VERSION = '3.20.0'; +const VERSION = '3.21.0'; // x-release-please-end