diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 911c623..d11c8fc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.19.3" + ".": "3.20.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index ac9e78a..391cde3 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-b969ce378479c79ee64c05127c0ed6c6ce2edbee017ecd037242fb618a5ebc9f.yml -openapi_spec_hash: a24aabaa5214effb679808b7f2be0ad4 -config_hash: a962ae71493deb11a1c903256fb25386 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-6f6bfb81d092f30a5e2005328c97d61b9ea36132bb19e9e79e55294b9534ce20.yml +openapi_spec_hash: f3fc1e3688a38dc2c28f7178f7d534e5 +config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 diff --git a/CHANGELOG.md b/CHANGELOG.md index 55aaaec..f94f7eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 3.20.0 (2026-05-06) + +Full Changelog: [v3.19.3...v3.20.0](https://github.com/browserbase/stagehand-go/compare/v3.19.3...v3.20.0) + +### Features + +* [feat]: add `ignoreSelectors` to `extract()` ([5471190](https://github.com/browserbase/stagehand-go/commit/547119099d9201d53820f12345e44acd940cccc6)) +* [STG-1798] feat: support Browserbase verified sessions ([30133d3](https://github.com/browserbase/stagehand-go/commit/30133d320e2093815d9714e32a64ffe6242b94aa)) +* [STG-1808] Deprecate Browserbase project ID ([fa76f5f](https://github.com/browserbase/stagehand-go/commit/fa76f5f6c41e668a9d22a75d4a9ab92829a36f4e)) +* Bedrock auth passthrough ([b41e3cb](https://github.com/browserbase/stagehand-go/commit/b41e3cb38fe07e0d19f8204b106320e3dee9c50b)) +* **go:** add default http client with timeout ([a8bc1d5](https://github.com/browserbase/stagehand-go/commit/a8bc1d57f59c471301dce65cbbabffafeaa3ab6b)) +* remove experimental requirement on agent variables ([#2079](https://github.com/browserbase/stagehand-go/issues/2079)) ([9316089](https://github.com/browserbase/stagehand-go/commit/93160890e958a17d3159273abc12a33c5b1f9d57)) +* Revert "[STG-1573] Add providerOptions for extensible model auth ([#1822](https://github.com/browserbase/stagehand-go/issues/1822))" ([f56e93f](https://github.com/browserbase/stagehand-go/commit/f56e93ff49643c78a6a39da36b30a010dbf59b7d)) +* support setting headers via env ([661576d](https://github.com/browserbase/stagehand-go/commit/661576dfbbaba6e019e9f32e05f6484ec3f17c5b)) + + +### Chores + +* avoid embedding reflect.Type for dead code elimination ([f071757](https://github.com/browserbase/stagehand-go/commit/f0717577f5de09f968fac0178dacd8662ec40c44)) +* **internal:** more robust bootstrap script ([6fc0d10](https://github.com/browserbase/stagehand-go/commit/6fc0d10fc68841748a3f99cfbdc85930b18455d0)) + ## 3.19.3 (2026-04-03) Full Changelog: [v3.18.0...v3.19.3](https://github.com/browserbase/stagehand-go/compare/v3.18.0...v3.19.3) diff --git a/README.md b/README.md index f33a90d..841a26a 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Or to pin the version: ```sh -go get -u 'github.com/browserbase/stagehand-go@v3.19.3' +go get -u 'github.com/browserbase/stagehand-go@v3.20.0' ``` @@ -192,10 +192,10 @@ func main() { Set your environment variables (from `examples/.env.example`): -- `STAGEHAND_API_URL` - `MODEL_API_KEY` - `BROWSERBASE_API_KEY` -- `BROWSERBASE_PROJECT_ID` + +`STAGEHAND_API_URL` is optional and defaults to the hosted Stagehand API. `STAGEHAND_BASE_URL` remains supported as a deprecated fallback when `STAGEHAND_API_URL` is unset. ```bash cp examples/.env.example examples/.env diff --git a/client.go b/client.go index f5d2065..9852909 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "slices" + "strings" "github.com/browserbase/stagehand-go/v3/internal/requestconfig" "github.com/browserbase/stagehand-go/v3/option" @@ -21,30 +22,37 @@ type Client struct { } // DefaultClientOptions read from the environment (BROWSERBASE_API_KEY, -// MODEL_API_KEY, BROWSERBASE_PROJECT_ID, STAGEHAND_BASE_URL). This should be used -// to initialize new clients. +// MODEL_API_KEY, STAGEHAND_API_URL, with STAGEHAND_BASE_URL as a fallback). This +// should be used to initialize new clients. func DefaultClientOptions() []option.RequestOption { - defaults := []option.RequestOption{option.WithEnvironmentProduction()} - if o, ok := os.LookupEnv("STAGEHAND_BASE_URL"); ok { + defaults := []option.RequestOption{option.WithHTTPClient(defaultHTTPClient()), option.WithEnvironmentProduction()} + if o, ok := os.LookupEnv("STAGEHAND_API_URL"); ok { + defaults = append(defaults, option.WithBaseURL(o)) + } else if o, ok := os.LookupEnv("STAGEHAND_BASE_URL"); ok { defaults = append(defaults, option.WithBaseURL(o)) } if o, ok := os.LookupEnv("BROWSERBASE_API_KEY"); ok { defaults = append(defaults, option.WithBrowserbaseAPIKey(o)) } - if o, ok := os.LookupEnv("BROWSERBASE_PROJECT_ID"); ok { - defaults = append(defaults, option.WithBrowserbaseProjectID(o)) - } if o, ok := os.LookupEnv("MODEL_API_KEY"); ok { defaults = append(defaults, option.WithModelAPIKey(o)) } + if o, ok := os.LookupEnv("STAGEHAND_CUSTOM_HEADERS"); ok { + for _, line := range strings.Split(o, "\n") { + colon := strings.Index(line, ":") + if colon >= 0 { + defaults = append(defaults, option.WithHeader(strings.TrimSpace(line[:colon]), strings.TrimSpace(line[colon+1:]))) + } + } + } return defaults } // NewClient generates a new client with the default option read from the -// environment (BROWSERBASE_API_KEY, MODEL_API_KEY, BROWSERBASE_PROJECT_ID, -// STAGEHAND_BASE_URL). The option passed in as arguments are applied after these -// default arguments, and all option will be passed down to the services and -// requests that this client makes. +// environment (BROWSERBASE_API_KEY, MODEL_API_KEY, STAGEHAND_API_URL, with +// STAGEHAND_BASE_URL as a fallback). The option passed in as arguments are +// applied after these default arguments, and all option will be passed down to +// the services and requests that this client makes. func NewClient(opts ...option.RequestOption) (r Client) { opts = append(DefaultClientOptions(), opts...) // BEGIN CUSTOM CODE - not generated by Stainless. diff --git a/client_test.go b/client_test.go index 440e646..c2d00fd 100644 --- a/client_test.go +++ b/client_test.go @@ -7,7 +7,9 @@ import ( "fmt" "io" "net/http" + "os" "reflect" + "strings" "testing" "time" @@ -49,6 +51,111 @@ func TestUserAgentHeader(t *testing.T) { } } +func TestBaseURLFromStagehandAPIURLEnv(t *testing.T) { + t.Setenv("STAGEHAND_API_URL", "http://localhost:5000/from-api-env") + t.Setenv("STAGEHAND_BASE_URL", "http://localhost:5000/from-base-env") + + var requestURL string + client := stagehand.NewClient( + option.WithBrowserbaseAPIKey("My Browserbase API Key"), + option.WithBrowserbaseProjectID("My Browserbase Project ID"), + option.WithModelAPIKey("My Model API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + requestURL = req.URL.String() + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("{}"))}, nil + }, + }, + }), + ) + _, _ = client.Sessions.Start(context.Background(), stagehand.SessionStartParams{ + ModelName: "openai/gpt-5.4-mini", + }) + if requestURL != "http://localhost:5000/from-api-env/v1/sessions/start" { + t.Errorf("Expected STAGEHAND_API_URL to take precedence, got: %s", requestURL) + } +} + +func TestBaseURLFromLegacyStagehandBaseURLEnv(t *testing.T) { + oldAPIURL, hadAPIURL := os.LookupEnv("STAGEHAND_API_URL") + os.Unsetenv("STAGEHAND_API_URL") + t.Cleanup(func() { + if hadAPIURL { + os.Setenv("STAGEHAND_API_URL", oldAPIURL) + } + }) + t.Setenv("STAGEHAND_BASE_URL", "http://localhost:5000/from-base-env") + + var requestURL string + client := stagehand.NewClient( + option.WithBrowserbaseAPIKey("My Browserbase API Key"), + option.WithBrowserbaseProjectID("My Browserbase Project ID"), + option.WithModelAPIKey("My Model API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + requestURL = req.URL.String() + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("{}"))}, nil + }, + }, + }), + ) + _, _ = client.Sessions.Start(context.Background(), stagehand.SessionStartParams{ + ModelName: "openai/gpt-5.4-mini", + }) + if requestURL != "http://localhost:5000/from-base-env/v1/sessions/start" { + t.Errorf("Expected STAGEHAND_BASE_URL fallback, got: %s", requestURL) + } +} + +func TestBrowserbaseProjectIDEnvIsIgnored(t *testing.T) { + t.Setenv("BROWSERBASE_PROJECT_ID", "My Browserbase Project ID") + + var projectIDHeader string + client := stagehand.NewClient( + option.WithBrowserbaseAPIKey("My Browserbase API Key"), + option.WithModelAPIKey("My Model API Key"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + projectIDHeader = req.Header.Get("x-bb-project-id") + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("{}"))}, nil + }, + }, + }), + ) + _, _ = client.Sessions.Start(context.Background(), stagehand.SessionStartParams{ + ModelName: "openai/gpt-5.4-mini", + }) + if projectIDHeader != "" { + t.Errorf("Expected x-bb-project-id header to be omitted, got: %s", projectIDHeader) + } +} + +func TestBrowserbaseProjectIDOptionIsNoOp(t *testing.T) { + var projectIDHeader string + client := stagehand.NewClient( + option.WithBrowserbaseAPIKey("My Browserbase API Key"), + option.WithModelAPIKey("My Model API Key"), + option.WithBrowserbaseProjectID("My Browserbase Project ID"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + projectIDHeader = req.Header.Get("x-bb-project-id") + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("{}"))}, nil + }, + }, + }), + ) + _, _ = client.Sessions.Start(context.Background(), stagehand.SessionStartParams{ + ModelName: "openai/gpt-5.4-mini", + }) + if projectIDHeader != "" { + t.Errorf("Expected x-bb-project-id header to be omitted, got: %s", projectIDHeader) + } +} + func TestRetryAfter(t *testing.T) { retryCountHeaders := make([]string, 0) client := stagehand.NewClient( diff --git a/default_http_client.go b/default_http_client.go new file mode 100644 index 0000000..9338fb8 --- /dev/null +++ b/default_http_client.go @@ -0,0 +1,24 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package stagehand + +import ( + "net/http" + "time" +) + +// defaultResponseHeaderTimeout bounds the time between a fully written request +// and the server's response headers. It does not apply to the response body, +// so long-running streams are unaffected. Without this, a server that accepts +// the connection but never responds would hang the request indefinitely. +const defaultResponseHeaderTimeout = 10 * time.Minute + +// defaultHTTPClient returns an [*http.Client] used when the caller does not +// supply one via [option.WithHTTPClient]. It clones [http.DefaultTransport] +// and adds a [http.Transport.ResponseHeaderTimeout] so stuck connections +// fail fast instead of compounding across retries. +func defaultHTTPClient() *http.Client { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: transport} +} diff --git a/examples/.env.example b/examples/.env.example index 6272bb0..0033442 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -1,4 +1,2 @@ -STAGEHAND_API_URL=https://api.stagehand.browserbase.com MODEL_API_KEY=sk-proj-your-llm-api-key-here BROWSERBASE_API_KEY=bb_live_your_api_key_here -BROWSERBASE_PROJECT_ID=your-bb-project-uuid-here diff --git a/examples/basic/env.go b/examples/basic/env.go index 9fa67a5..019378f 100644 --- a/examples/basic/env.go +++ b/examples/basic/env.go @@ -9,10 +9,8 @@ import ( ) var requiredEnv = []string{ - "STAGEHAND_API_URL", "MODEL_API_KEY", "BROWSERBASE_API_KEY", - "BROWSERBASE_PROJECT_ID", } func loadExampleEnv() { @@ -54,10 +52,6 @@ func loadExampleEnv() { if len(missing) > 0 { panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)") } - - if os.Getenv("STAGEHAND_BASE_URL") == "" { - os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL")) - } } func findEnvPath() (string, bool) { diff --git a/examples/basic/main.go b/examples/basic/main.go index 04e4f01..157e52e 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -2,7 +2,6 @@ // // Prerequisites: // - Set BROWSERBASE_API_KEY -// - Set BROWSERBASE_PROJECT_ID // - Set MODEL_API_KEY // // Run: @@ -24,7 +23,7 @@ import ( func main() { loadExampleEnv() - client := stagehand.NewClient() // Uses env vars: BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY + client := stagehand.NewClient() // Uses env vars: BROWSERBASE_API_KEY and MODEL_API_KEY startResponse, err := client.Sessions.Start(context.TODO(), stagehand.SessionStartParams{ ModelName: "anthropic/claude-sonnet-4-6", diff --git a/examples/chromedp_local_example/env.go b/examples/chromedp_local_example/env.go index 9fa67a5..019378f 100644 --- a/examples/chromedp_local_example/env.go +++ b/examples/chromedp_local_example/env.go @@ -9,10 +9,8 @@ import ( ) var requiredEnv = []string{ - "STAGEHAND_API_URL", "MODEL_API_KEY", "BROWSERBASE_API_KEY", - "BROWSERBASE_PROJECT_ID", } func loadExampleEnv() { @@ -54,10 +52,6 @@ func loadExampleEnv() { if len(missing) > 0 { panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)") } - - if os.Getenv("STAGEHAND_BASE_URL") == "" { - os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL")) - } } func findEnvPath() (string, bool) { diff --git a/examples/local/env.go b/examples/local/env.go index 9fa67a5..019378f 100644 --- a/examples/local/env.go +++ b/examples/local/env.go @@ -9,10 +9,8 @@ import ( ) var requiredEnv = []string{ - "STAGEHAND_API_URL", "MODEL_API_KEY", "BROWSERBASE_API_KEY", - "BROWSERBASE_PROJECT_ID", } func loadExampleEnv() { @@ -54,10 +52,6 @@ func loadExampleEnv() { if len(missing) > 0 { panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)") } - - if os.Getenv("STAGEHAND_BASE_URL") == "" { - os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL")) - } } func findEnvPath() (string, bool) { diff --git a/examples/local_browser_chromedp_example/env.go b/examples/local_browser_chromedp_example/env.go index 9fa67a5..019378f 100644 --- a/examples/local_browser_chromedp_example/env.go +++ b/examples/local_browser_chromedp_example/env.go @@ -9,10 +9,8 @@ import ( ) var requiredEnv = []string{ - "STAGEHAND_API_URL", "MODEL_API_KEY", "BROWSERBASE_API_KEY", - "BROWSERBASE_PROJECT_ID", } func loadExampleEnv() { @@ -54,10 +52,6 @@ func loadExampleEnv() { if len(missing) > 0 { panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)") } - - if os.Getenv("STAGEHAND_BASE_URL") == "" { - os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL")) - } } func findEnvPath() (string, bool) { diff --git a/examples/local_server_multiregion_browser_example/env.go b/examples/local_server_multiregion_browser_example/env.go index 9fa67a5..019378f 100644 --- a/examples/local_server_multiregion_browser_example/env.go +++ b/examples/local_server_multiregion_browser_example/env.go @@ -9,10 +9,8 @@ import ( ) var requiredEnv = []string{ - "STAGEHAND_API_URL", "MODEL_API_KEY", "BROWSERBASE_API_KEY", - "BROWSERBASE_PROJECT_ID", } func loadExampleEnv() { @@ -54,10 +52,6 @@ func loadExampleEnv() { if len(missing) > 0 { panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)") } - - if os.Getenv("STAGEHAND_BASE_URL") == "" { - os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL")) - } } func findEnvPath() (string, bool) { diff --git a/examples/local_server_multiregion_browser_example/main.go b/examples/local_server_multiregion_browser_example/main.go index a174476..3ff59a0 100644 --- a/examples/local_server_multiregion_browser_example/main.go +++ b/examples/local_server_multiregion_browser_example/main.go @@ -2,7 +2,6 @@ // // Prerequisites: // - Set BROWSERBASE_API_KEY -// - Set BROWSERBASE_PROJECT_ID // - Set MODEL_API_KEY // // Run: @@ -29,7 +28,7 @@ import ( func main() { loadExampleEnv() - requireEnv("BROWSERBASE_API_KEY", "BROWSERBASE_PROJECT_ID", "MODEL_API_KEY") + requireEnv("BROWSERBASE_API_KEY", "MODEL_API_KEY") // Run the Stagehand driver locally (required for Browserbase regions other than us-west-2) client := stagehand.NewClient(option.WithServer("local")) diff --git a/examples/remote_browser_chromedp_example/env.go b/examples/remote_browser_chromedp_example/env.go index 9fa67a5..019378f 100644 --- a/examples/remote_browser_chromedp_example/env.go +++ b/examples/remote_browser_chromedp_example/env.go @@ -9,10 +9,8 @@ import ( ) var requiredEnv = []string{ - "STAGEHAND_API_URL", "MODEL_API_KEY", "BROWSERBASE_API_KEY", - "BROWSERBASE_PROJECT_ID", } func loadExampleEnv() { @@ -54,10 +52,6 @@ func loadExampleEnv() { if len(missing) > 0 { panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)") } - - if os.Getenv("STAGEHAND_BASE_URL") == "" { - os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL")) - } } func findEnvPath() (string, bool) { diff --git a/examples/remote_browser_chromedp_example/main.go b/examples/remote_browser_chromedp_example/main.go index 1059d26..3dc9f06 100644 --- a/examples/remote_browser_chromedp_example/main.go +++ b/examples/remote_browser_chromedp_example/main.go @@ -2,7 +2,6 @@ // // Prerequisites: // - Set BROWSERBASE_API_KEY -// - Set BROWSERBASE_PROJECT_ID // - Set MODEL_API_KEY // // Run: @@ -32,7 +31,6 @@ func main() { loadExampleEnv() // Environment variables required (same as other examples): // - BROWSERBASE_API_KEY - // - BROWSERBASE_PROJECT_ID // - MODEL_API_KEY client := stagehand.NewClient() diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 66b5879..04769ad 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -58,7 +58,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string arrayFmt string root bool @@ -76,7 +76,7 @@ func (e *encoder) marshal(value any, writer *multipart.Writer) error { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, arrayFmt: e.arrayFmt, root: e.root, diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index 4242e2a..2d1e1fb 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -80,7 +80,7 @@ type decoderField struct { } type decoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -108,7 +108,7 @@ func (d *decoderBuilder) unmarshalWithExactness(raw []byte, to any) (exactness, func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc { entry := decoderEntry{ - Type: t, + typ: t, dateFormat: d.dateFormat, root: d.root, } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index 8a881d6..686161b 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -46,7 +46,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -63,7 +63,7 @@ func (e *encoder) marshal(value any) ([]byte, error) { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 587075a..be6989f 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -29,7 +29,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool settings QuerySettings @@ -42,7 +42,7 @@ type Pair struct { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, settings: e.settings, diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index 754396d..3cf94be 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -219,7 +219,6 @@ type RequestConfig struct { HTTPClient *http.Client Middlewares []middleware BrowserbaseAPIKey string - BrowserbaseProjectID string ModelAPIKey string // If ResponseBodyInto not nil, then we will attempt to deserialize into // ResponseBodyInto. If Destination is a []byte, then it will return the body as @@ -595,7 +594,6 @@ func (cfg *RequestConfig) Clone(ctx context.Context) *RequestConfig { HTTPClient: cfg.HTTPClient, Middlewares: cfg.Middlewares, BrowserbaseAPIKey: cfg.BrowserbaseAPIKey, - BrowserbaseProjectID: cfg.BrowserbaseProjectID, ModelAPIKey: cfg.ModelAPIKey, } diff --git a/internal/version.go b/internal/version.go index eb91275..2210a0d 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "3.19.3" // x-release-please-version +const PackageVersion = "3.20.0" // x-release-please-version diff --git a/lib/local/local.go b/lib/local/local.go index 5bd96bf..891360f 100644 --- a/lib/local/local.go +++ b/lib/local/local.go @@ -266,7 +266,6 @@ type ServerManager struct { baseURL string modelAPIKey string browserbaseAPIKey string - browserbaseProjectID string mu sync.Mutex started bool } @@ -328,14 +327,8 @@ func (m *ServerManager) SetBrowserbaseAPIKey(key string) { m.browserbaseAPIKey = key } -// SetBrowserbaseProjectID sets the Browserbase project ID used by local mode. +// SetBrowserbaseProjectID is deprecated and retained as a no-op for backwards compatibility. func (m *ServerManager) SetBrowserbaseProjectID(projectID string) { - if projectID == "" { - return - } - m.mu.Lock() - defer m.mu.Unlock() - m.browserbaseProjectID = projectID } // startLocked starts local mode. Must be called with m.mu held. @@ -350,8 +343,7 @@ func (m *ServerManager) startLocked(ctx context.Context) (string, error) { return "", fmt.Errorf("failed to find free port: %w", err) } - // Build environment - env := os.Environ() + env := append([]string{}, os.Environ()...) env = append(env, "NODE_ENV=production", "BB_ENV=local", @@ -365,9 +357,6 @@ func (m *ServerManager) startLocked(ctx context.Context) (string, error) { if m.browserbaseAPIKey != "" { env = append(env, fmt.Sprintf("BROWSERBASE_API_KEY=%s", m.browserbaseAPIKey)) } - if m.browserbaseProjectID != "" { - env = append(env, fmt.Sprintf("BROWSERBASE_PROJECT_ID=%s", m.browserbaseProjectID)) - } // Start the process m.cmd = exec.Command(m.binaryPath) diff --git a/local_client.go b/local_client.go index ae373b3..4b5dd6a 100644 --- a/local_client.go +++ b/local_client.go @@ -25,11 +25,9 @@ func newLocalServerOption() *localServerOption { func (o *localServerOption) Apply(cfg *requestconfig.RequestConfig) error { var modelAPIKey string var browserbaseAPIKey string - var browserbaseProjectID string if cfg != nil { modelAPIKey = cfg.ModelAPIKey browserbaseAPIKey = cfg.BrowserbaseAPIKey - browserbaseProjectID = cfg.BrowserbaseProjectID } if modelAPIKey == "" { if key := os.Getenv("MODEL_API_KEY"); key != "" { @@ -51,16 +49,6 @@ func (o *localServerOption) Apply(cfg *requestconfig.RequestConfig) error { } } } - if browserbaseProjectID == "" { - if key := os.Getenv("BROWSERBASE_PROJECT_ID"); key != "" { - browserbaseProjectID = key - if cfg != nil { - if err := option.WithBrowserbaseProjectID(browserbaseProjectID).Apply(cfg); err != nil { - return err - } - } - } - } if modelAPIKey == "" { return fmt.Errorf("MODEL_API_KEY is required for local mode") } @@ -75,9 +63,6 @@ func (o *localServerOption) Apply(cfg *requestconfig.RequestConfig) error { if browserbaseAPIKey != "" { manager.SetBrowserbaseAPIKey(browserbaseAPIKey) } - if browserbaseProjectID != "" { - manager.SetBrowserbaseProjectID(browserbaseProjectID) - } ctx := cfg.Context if ctx == nil { diff --git a/option/requestoption.go b/option/requestoption.go index ebee770..8a63eca 100644 --- a/option/requestoption.go +++ b/option/requestoption.go @@ -274,11 +274,10 @@ func WithBrowserbaseAPIKey(value string) RequestOption { }) } -// WithBrowserbaseProjectID returns a RequestOption that sets the client setting "BROWSERBASE_PROJECT_ID". +// WithBrowserbaseProjectID is deprecated and retained as a no-op for backwards compatibility. func WithBrowserbaseProjectID(value string) RequestOption { return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { - r.BrowserbaseProjectID = value - return r.Apply(WithHeader("x-bb-project-id", r.BrowserbaseProjectID)) + return nil }) } diff --git a/scripts/bootstrap b/scripts/bootstrap index 5ab3066..46547f1 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response diff --git a/session.go b/session.go index e1a9d29..f218fe0 100644 --- a/session.go +++ b/session.go @@ -1280,6 +1280,8 @@ type SessionExecuteParamsExecuteOptions struct { ToolTimeout param.Opt[float64] `json:"toolTimeout,omitzero"` // Whether to enable the web search tool powered by Browserbase Search API UseSearch param.Opt[bool] `json:"useSearch,omitzero"` + // Variables available to the agent via %variableName% syntax in supported tools + Variables map[string]SessionExecuteParamsExecuteOptionsVariableUnion `json:"variables,omitzero"` paramObj } @@ -1291,6 +1293,80 @@ func (r *SessionExecuteParamsExecuteOptions) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type SessionExecuteParamsExecuteOptionsVariableUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + OfBool param.Opt[bool] `json:",omitzero,inline"` + OfSessionExecutesExecuteOptionsVariableObject *SessionExecuteParamsExecuteOptionsVariableObject `json:",omitzero,inline"` + paramUnion +} + +func (u SessionExecuteParamsExecuteOptionsVariableUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat, u.OfBool, u.OfSessionExecutesExecuteOptionsVariableObject) +} +func (u *SessionExecuteParamsExecuteOptionsVariableUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *SessionExecuteParamsExecuteOptionsVariableUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } else if !param.IsOmitted(u.OfBool) { + return &u.OfBool.Value + } else if !param.IsOmitted(u.OfSessionExecutesExecuteOptionsVariableObject) { + return u.OfSessionExecutesExecuteOptionsVariableObject + } + return nil +} + +// The property Value is required. +type SessionExecuteParamsExecuteOptionsVariableObject struct { + Value SessionExecuteParamsExecuteOptionsVariableObjectValueUnion `json:"value,omitzero" api:"required"` + Description param.Opt[string] `json:"description,omitzero"` + paramObj +} + +func (r SessionExecuteParamsExecuteOptionsVariableObject) MarshalJSON() (data []byte, err error) { + type shadow SessionExecuteParamsExecuteOptionsVariableObject + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *SessionExecuteParamsExecuteOptionsVariableObject) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type SessionExecuteParamsExecuteOptionsVariableObjectValueUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + OfBool param.Opt[bool] `json:",omitzero,inline"` + paramUnion +} + +func (u SessionExecuteParamsExecuteOptionsVariableObjectValueUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat, u.OfBool) +} +func (u *SessionExecuteParamsExecuteOptionsVariableObjectValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *SessionExecuteParamsExecuteOptionsVariableObjectValueUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } else if !param.IsOmitted(u.OfBool) { + return &u.OfBool.Value + } + return nil +} + // Whether to stream the response via SSE type SessionExecuteParamsXStreamResponse string @@ -1327,6 +1403,8 @@ type SessionExtractParamsOptions struct { Selector param.Opt[string] `json:"selector,omitzero"` // Timeout in ms for the extraction Timeout param.Opt[float64] `json:"timeout,omitzero"` + // Selectors for elements and subtrees that should be excluded from extraction + IgnoreSelectors []string `json:"ignoreSelectors,omitzero"` // Model configuration object or model name string (e.g., 'openai/gpt-5-nano') Model SessionExtractParamsOptionsModelUnion `json:"model,omitzero"` paramObj @@ -1748,8 +1826,12 @@ func (r *SessionStartParamsBrowserLaunchOptionsViewport) UnmarshalJSON(data []by } type SessionStartParamsBrowserbaseSessionCreateParams struct { - ExtensionID param.Opt[string] `json:"extensionId,omitzero"` - KeepAlive param.Opt[bool] `json:"keepAlive,omitzero"` + ExtensionID param.Opt[string] `json:"extensionId,omitzero"` + KeepAlive param.Opt[bool] `json:"keepAlive,omitzero"` + // Deprecated. Browserbase API keys are now project-scoped, so this field is no + // longer required. + // + // Deprecated: deprecated ProjectID param.Opt[string] `json:"projectId,omitzero"` Timeout param.Opt[float64] `json:"timeout,omitzero"` BrowserSettings SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettings `json:"browserSettings,omitzero"` @@ -1775,15 +1857,20 @@ func init() { } type SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettings struct { - AdvancedStealth param.Opt[bool] `json:"advancedStealth,omitzero"` - BlockAds param.Opt[bool] `json:"blockAds,omitzero"` - ExtensionID param.Opt[string] `json:"extensionId,omitzero"` - LogSession param.Opt[bool] `json:"logSession,omitzero"` - RecordSession param.Opt[bool] `json:"recordSession,omitzero"` - SolveCaptchas param.Opt[bool] `json:"solveCaptchas,omitzero"` - Context SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsContext `json:"context,omitzero"` - Fingerprint SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsFingerprint `json:"fingerprint,omitzero"` - Viewport SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsViewport `json:"viewport,omitzero"` + AdvancedStealth param.Opt[bool] `json:"advancedStealth,omitzero"` + BlockAds param.Opt[bool] `json:"blockAds,omitzero"` + CaptchaImageSelector param.Opt[string] `json:"captchaImageSelector,omitzero"` + CaptchaInputSelector param.Opt[string] `json:"captchaInputSelector,omitzero"` + ExtensionID param.Opt[string] `json:"extensionId,omitzero"` + LogSession param.Opt[bool] `json:"logSession,omitzero"` + RecordSession param.Opt[bool] `json:"recordSession,omitzero"` + SolveCaptchas param.Opt[bool] `json:"solveCaptchas,omitzero"` + Verified param.Opt[bool] `json:"verified,omitzero"` + Context SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsContext `json:"context,omitzero"` + Fingerprint SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsFingerprint `json:"fingerprint,omitzero"` + // Any of "windows", "mac", "linux", "mobile", "tablet". + Os string `json:"os,omitzero"` + Viewport SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsViewport `json:"viewport,omitzero"` paramObj } @@ -1795,6 +1882,12 @@ func (r *SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettings) Unmars return apijson.UnmarshalRoot(data, r) } +func init() { + apijson.RegisterFieldValidator[SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettings]( + "os", "windows", "mac", "linux", "mobile", "tablet", + ) +} + // The property ID is required. type SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsContext struct { ID string `json:"id" api:"required"` diff --git a/session_test.go b/session_test.go index cc07536..9c26e94 100644 --- a/session_test.go +++ b/session_test.go @@ -159,6 +159,11 @@ func TestSessionExecuteWithOptionalParams(t *testing.T) { MaxSteps: stagehand.Float(20), ToolTimeout: stagehand.Float(30000), UseSearch: stagehand.Bool(true), + Variables: map[string]stagehand.SessionExecuteParamsExecuteOptionsVariableUnion{ + "foo": { + OfString: stagehand.String("string"), + }, + }, }, FrameID: stagehand.String("frameId"), ShouldCache: stagehand.Bool(true), @@ -196,6 +201,7 @@ func TestSessionExtractWithOptionalParams(t *testing.T) { FrameID: stagehand.String("frameId"), Instruction: stagehand.String("Extract all product names and prices from the page"), Options: stagehand.SessionExtractParamsOptions{ + IgnoreSelectors: []string{"nav", ".cookie-banner", "#sidebar-ads"}, Model: stagehand.SessionExtractParamsOptionsModelUnion{ OfModelConfig: &stagehand.ModelConfigParam{ ModelName: "openai/gpt-5.4-mini", @@ -414,8 +420,10 @@ func TestSessionStartWithOptionalParams(t *testing.T) { }, BrowserbaseSessionCreateParams: stagehand.SessionStartParamsBrowserbaseSessionCreateParams{ BrowserSettings: stagehand.SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettings{ - AdvancedStealth: stagehand.Bool(true), - BlockAds: stagehand.Bool(true), + AdvancedStealth: stagehand.Bool(true), + BlockAds: stagehand.Bool(true), + CaptchaImageSelector: stagehand.String("captchaImageSelector"), + CaptchaInputSelector: stagehand.String("captchaInputSelector"), Context: stagehand.SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsContext{ ID: "id", Persist: stagehand.Bool(true), @@ -435,8 +443,10 @@ func TestSessionStartWithOptionalParams(t *testing.T) { }, }, LogSession: stagehand.Bool(true), + Os: "windows", RecordSession: stagehand.Bool(true), SolveCaptchas: stagehand.Bool(true), + Verified: stagehand.Bool(true), Viewport: stagehand.SessionStartParamsBrowserbaseSessionCreateParamsBrowserSettingsViewport{ Height: stagehand.Float(0), Width: stagehand.Float(0),