From 03b5aea699bfc300fc172617fa46a8a60913c989 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:10 +0000 Subject: [PATCH 01/10] 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 13699f218c42404fd866e5d61f8100dea2cd8a6b 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:01 +0000 Subject: [PATCH 02/10] 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 501d30b5aa52a72eed5a25b064609b5c7c84d733 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:46 +0000 Subject: [PATCH 03/10] feat: [feat]: add `ignoreSelectors` to `observe()` --- .stats.yml | 4 ++-- session.go | 2 ++ session_test.go | 1 + 3 files changed, 5 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/session.go b/session.go index f218fe0..c3bf416 100644 --- a/session.go +++ b/session.go @@ -1534,6 +1534,8 @@ type SessionObserveParamsOptions struct { Selector param.Opt[string] `json:"selector,omitzero"` // Timeout in ms for the observation Timeout param.Opt[float64] `json:"timeout,omitzero"` + // Selectors for elements and subtrees that should be excluded from observation + IgnoreSelectors []string `json:"ignoreSelectors,omitzero"` // Model configuration object or model name string (e.g., 'openai/gpt-5-nano') Model SessionObserveParamsOptionsModelUnion `json:"model,omitzero"` // Variables whose names are exposed to the model so observe() returns diff --git a/session_test.go b/session_test.go index 9c26e94..26a8f1b 100644 --- a/session_test.go +++ b/session_test.go @@ -292,6 +292,7 @@ func TestSessionObserveWithOptionalParams(t *testing.T) { FrameID: stagehand.String("frameId"), Instruction: stagehand.String("Find all clickable navigation links"), Options: stagehand.SessionObserveParamsOptions{ + IgnoreSelectors: []string{"nav", ".cookie-banner", "#sidebar-ads"}, Model: stagehand.SessionObserveParamsOptionsModelUnion{ OfModelConfig: &stagehand.ModelConfigParam{ ModelName: "openai/gpt-5.4-mini", From 637868870522a96e2e1d585949926a6116cb1e01 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 02:01:20 +0000 Subject: [PATCH 04/10] chore: redact api-key headers in debug logs --- option/middleware.go | 46 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/option/middleware.go b/option/middleware.go index 8ec9dd6..69f78f3 100644 --- a/option/middleware.go +++ b/option/middleware.go @@ -8,6 +8,10 @@ import ( "net/http/httputil" ) +// sensitiveLogHeaders are redacted before request and response content is +// written to the debug logger. +var sensitiveLogHeaders = []string{"authorization", "api-key", "x-api-key", "cookie", "set-cookie", "x-bb-api-key", "x-bb-project-id", "x-model-api-key"} + // WithDebugLog logs the HTTP request and response content. // If the logger parameter is nil, it uses the default logger. // @@ -20,7 +24,7 @@ func WithDebugLog(logger *log.Logger) RequestOption { logger = log.Default() } - if reqBytes, err := httputil.DumpRequest(req, true); err == nil { + if reqBytes, err := dumpRedactedRequest(req); err == nil { logger.Printf("Request Content:\n%s\n", reqBytes) } @@ -29,10 +33,48 @@ func WithDebugLog(logger *log.Logger) RequestOption { return resp, err } - if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + if respBytes, err := dumpRedactedResponse(resp); err == nil { logger.Printf("Response Content:\n%s\n", respBytes) } return resp, err }) } + +// dumpRedactedRequest dumps req with sensitive headers replaced. The +// original headers are restored via defer so a panic in DumpRequest cannot +// leak the placeholder map into the live request sent downstream. +func dumpRedactedRequest(req *http.Request) ([]byte, error) { + origHeaders := req.Header + req.Header = redactDebugHeaders(origHeaders) + defer func() { req.Header = origHeaders }() + return httputil.DumpRequest(req, true) +} + +func dumpRedactedResponse(resp *http.Response) ([]byte, error) { + origHeaders := resp.Header + resp.Header = redactDebugHeaders(origHeaders) + defer func() { resp.Header = origHeaders }() + return httputil.DumpResponse(resp, true) +} + +func redactDebugHeaders(headers http.Header) http.Header { + var redacted http.Header + for _, name := range sensitiveLogHeaders { + values := headers.Values(name) + if len(values) == 0 { + continue + } + if redacted == nil { + redacted = headers.Clone() + } + redacted.Del(name) + for range values { + redacted.Add(name, "***") + } + } + if redacted == nil { + return headers + } + return redacted +} From cea79d5e25d6f072af7b2dde01d34be71106cb4b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 02:01:58 +0000 Subject: [PATCH 05/10] fix(go): avoid panic when http.DefaultTransport is wrapped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defaultHTTPClient performed an unchecked type assertion on http.DefaultTransport, which panicked for any caller that had wrapped the global transport (e.g. otelhttp.NewTransport for distributed tracing). When the assertion fails, fall back to the wrapped RoundTripper as-is — preserving the caller's wrapping at the cost of ResponseHeaderTimeout, which is strictly better than panicking. --- default_http_client.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/default_http_client.go b/default_http_client.go index 9338fb8..c362211 100644 --- a/default_http_client.go +++ b/default_http_client.go @@ -14,11 +14,17 @@ import ( 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. +// supply one via [option.WithHTTPClient]. When [http.DefaultTransport] is the +// stdlib [*http.Transport], it is cloned and a [http.Transport.ResponseHeaderTimeout] +// is set so stuck connections fail fast instead of compounding across retries. +// If [http.DefaultTransport] has been wrapped (for example by otelhttp for +// distributed tracing), the wrapping is preserved and the header timeout is +// skipped. func defaultHTTPClient() *http.Client { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout - return &http.Client{Transport: transport} + if t, ok := http.DefaultTransport.(*http.Transport); ok { + t = t.Clone() + t.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: t} + } + return &http.Client{Transport: http.DefaultTransport} } From 39ed37210e15594043fba9cc5c756aa038314c1c 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:44 +0000 Subject: [PATCH 06/10] 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 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6d8887..1e10b7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,14 +26,14 @@ jobs: github.repository == 'stainless-sdks/stagehand-go' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get GitHub OIDC Token if: |- github.repository == 'stainless-sdks/stagehand-go' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -53,10 +53,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: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: ./go.mod @@ -68,10 +68,10 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/stagehand-go' && '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: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: ./go.mod From 539a65e84dfdd9a1330c6142d229ea47b2575376 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 02:02:04 +0000 Subject: [PATCH 07/10] feat(client): optimize json encoder for internal types --- internal/encoding/json/encode.go | 21 ++-- internal/encoding/json/indent.go | 17 ++- internal/encoding/json/opt.go | 24 +++++ internal/encoding/json/stream.go | 53 +++++----- internal/encoding/json/time.go | 2 +- packages/param/encoder.go | 4 +- packages/param/encoder_test.go | 176 +++++++++++++++++++++++++++++++ 7 files changed, 260 insertions(+), 37 deletions(-) create mode 100644 internal/encoding/json/opt.go diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go index 6603ba3..6b9c902 100644 --- a/internal/encoding/json/encode.go +++ b/internal/encoding/json/encode.go @@ -173,15 +173,21 @@ import ( // JSON cannot represent cyclic data structures and Marshal does not // handle them. Passing cyclic structures to Marshal will result in // an error. -func Marshal(v any) ([]byte, error) { +// EDIT(begin): add optimization options +func Marshal(v any, opts ...Option) ([]byte, error) { + // EDIT(end): add optimization options e := newEncodeState() defer encodeStatePool.Put(e) - // SHIM(begin): don't escape HTML by default - err := e.marshal(v, encOpts{escapeHTML: shims.EscapeHTMLByDefault}) + // EDIT(begin): don't escape HTML by default, and apply options + encOpts := encOpts{escapeHTML: shims.EscapeHTMLByDefault} + if opts != nil { + encOpts = encOpts.apply(opts...) + } + err := e.marshal(v, encOpts) // ORIGINAL: // err := e.marshal(v, encOpts{escapeHTML: true}) - // SHIM(end) + // EDIT(end) if err != nil { return nil, err } @@ -352,6 +358,9 @@ type encOpts struct { // EDIT(begin): save the timefmt timefmt string // EDIT(end) + // EDIT(begin): add optimization to skip compaction + skipCompaction bool + // EDIT(end) } type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts) @@ -483,7 +492,7 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { if err == nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, err = appendCompact(out, b, opts.escapeHTML) + out, err = appendCompact(out, b, opts) e.Buffer.Write(out) } if err != nil { @@ -509,7 +518,7 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { if err == nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, err = appendCompact(out, b, opts.escapeHTML) + out, err = appendCompact(out, b, opts) e.Buffer.Write(out) } if err != nil { diff --git a/internal/encoding/json/indent.go b/internal/encoding/json/indent.go index 01bfdf6..c9d6ca5 100644 --- a/internal/encoding/json/indent.go +++ b/internal/encoding/json/indent.go @@ -4,7 +4,9 @@ package json -import "bytes" +import ( + "bytes" +) // HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 // characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 @@ -41,12 +43,21 @@ func appendHTMLEscape(dst, src []byte) []byte { func Compact(dst *bytes.Buffer, src []byte) error { dst.Grow(len(src)) b := dst.AvailableBuffer() - b, err := appendCompact(b, src, false) + b, err := appendCompact(b, src, encOpts{}) dst.Write(b) return err } -func appendCompact(dst, src []byte, escape bool) ([]byte, error) { +func appendCompact(dst, src []byte, opts encOpts) ([]byte, error) { + // EDIT(begin): optimize for skipCompaction + if opts.skipCompaction { + dst = append(dst, src...) + return dst, nil + } + + escape := opts.escapeHTML + // EDIT(end) + origLen := len(dst) scan := newScanner() defer freeScanner(scan) diff --git a/internal/encoding/json/opt.go b/internal/encoding/json/opt.go new file mode 100644 index 0000000..fd6f8d2 --- /dev/null +++ b/internal/encoding/json/opt.go @@ -0,0 +1,24 @@ +// EDIT(begin): add custom options for JSON encoding +package json + +type Option func(*encOpts) + +// Every time a sub-type of [json.Marshaler] is encountered, +// skip a redundant and costly compaction step, trust it to self-compact. +// +// This is a divergence from the standard library behavior, and is only guaranteed +// safe with SDK types. +func WithSkipCompaction(b bool) Option { + return func(eos *encOpts) { + eos.skipCompaction = true + } +} + +func (eos encOpts) apply(opts ...Option) encOpts { + for _, opt := range opts { + opt(&eos) + } + return eos +} + +// EDIT(end) diff --git a/internal/encoding/json/stream.go b/internal/encoding/json/stream.go index e2d9470..652522c 100644 --- a/internal/encoding/json/stream.go +++ b/internal/encoding/json/stream.go @@ -6,7 +6,6 @@ package json import ( "bytes" - "errors" "io" ) @@ -253,30 +252,34 @@ func (enc *Encoder) SetEscapeHTML(on bool) { enc.escapeHTML = on } -// RawMessage is a raw encoded JSON value. -// It implements [Marshaler] and [Unmarshaler] and can -// be used to delay JSON decoding or precompute a JSON encoding. -type RawMessage []byte - -// MarshalJSON returns m as the JSON encoding of m. -func (m RawMessage) MarshalJSON() ([]byte, error) { - if m == nil { - return []byte("null"), nil - } - return m, nil -} - -// UnmarshalJSON sets *m to a copy of data. -func (m *RawMessage) UnmarshalJSON(data []byte) error { - if m == nil { - return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") - } - *m = append((*m)[0:0], data...) - return nil -} - -var _ Marshaler = (*RawMessage)(nil) -var _ Unmarshaler = (*RawMessage)(nil) +// EDIT(begin): remove RawMessage +// +// // RawMessage is a raw encoded JSON value. +// // It implements [Marshaler] and [Unmarshaler] and can +// // be used to delay JSON decoding or precompute a JSON encoding. +// type RawMessage []byte +// +// // MarshalJSON returns m as the JSON encoding of m. +// func (m RawMessage) MarshalJSON() ([]byte, error) { +// if m == nil { +// return []byte("null"), nil +// } +// return m, nil +// } +// +// // UnmarshalJSON sets *m to a copy of data. +// func (m *RawMessage) UnmarshalJSON(data []byte) error { +// if m == nil { +// return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") +// } +// *m = append((*m)[0:0], data...) +// return nil +// } +// +// var _ Marshaler = (*RawMessage)(nil) +// var _ Unmarshaler = (*RawMessage)(nil) +// +// EDIT(end) // A Token holds a value of one of these types: // diff --git a/internal/encoding/json/time.go b/internal/encoding/json/time.go index c70443d..9a4f70d 100644 --- a/internal/encoding/json/time.go +++ b/internal/encoding/json/time.go @@ -50,7 +50,7 @@ func timeMarshalEncoder(e *encodeState, v reflect.Value, opts encOpts) bool { if b != nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, _ = appendCompact(out, b, opts.escapeHTML) + out, _ = appendCompact(out, b, opts) e.Buffer.Write(out) return true } diff --git a/packages/param/encoder.go b/packages/param/encoder.go index 8faa2b4..33465f3 100644 --- a/packages/param/encoder.go +++ b/packages/param/encoder.go @@ -66,7 +66,7 @@ func MarshalWithExtras[T ParamStruct, R any](f T, underlying any, extras map[str } else if ovr, ok := f.Overrides(); ok { return shimjson.Marshal(ovr) } else { - return shimjson.Marshal(underlying) + return shimjson.Marshal(underlying, shimjson.WithSkipCompaction(true)) } } @@ -96,7 +96,7 @@ func MarshalUnion[T ParamStruct](metadata T, variants ...any) ([]byte, error) { Err: fmt.Errorf("expected union to have only one present variant, got %d", nPresent), } } - return shimjson.Marshal(variants[presentIdx]) + return shimjson.Marshal(variants[presentIdx], shimjson.WithSkipCompaction(true)) } // typeFor is shimmed from Go 1.23 "reflect" package diff --git a/packages/param/encoder_test.go b/packages/param/encoder_test.go index 90cee05..ff0bdc0 100644 --- a/packages/param/encoder_test.go +++ b/packages/param/encoder_test.go @@ -1,10 +1,13 @@ package param_test import ( + "bytes" "encoding/json" + "reflect" "testing" "time" + shimjson "github.com/browserbase/stagehand-go/v3/internal/encoding/json" "github.com/browserbase/stagehand-go/v3/packages/param" ) @@ -375,3 +378,176 @@ func TestNullStructUnion(t *testing.T) { t.Fatalf("expected null, received %s", string(b)) } } + +// +// Compaction optimization +// + +type NonCompactedDoubleParent struct { + Prop string `json:"prop"` + Parent NonCompactedParent `json:"parent"` + + param.APIObject +} + +type NonCompactedParent struct { + BadChild NonCompacted `json:"bad_child"` + + param.APIObject +} + +type NonCompacted struct { + Raw string + + param.APIObject +} + +func (a NonCompactedDoubleParent) MarshalJSON() ([]byte, error) { + type shadow NonCompactedDoubleParent + return param.MarshalObject(a, (*shadow)(&a)) +} + +func (a NonCompactedParent) MarshalJSON() ([]byte, error) { + type shadow NonCompactedParent + return param.MarshalObject(a, (*shadow)(&a)) +} + +func (a NonCompacted) MarshalJSON() ([]byte, error) { + if a.Raw == "" { + a.Raw = nonCompactedRaw + } + return []byte(a.Raw), nil +} + +var nonCompactedRaw string = ` { "foo": "bar" } ` + +func TestAppendCompactBroken(t *testing.T) { + tests := map[string]struct { + value json.Marshaler + }{ + "red/illegal-json": { + NonCompacted{Raw: `{ "broken": "json" `}, + }, + "red/nested-with-illegal-json": { + NonCompactedParent{BadChild: NonCompacted{ + Raw: `{ "broken": "json" `, + }}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + v, err := json.Marshal(test.value) + if err == nil { + t.Fatal("expected error got", v) + } + }) + } +} + +// TestAppendCompact validates an optimization for internal SDK types to +// avoid O(keys^2) iteration over each JSON object. +// +// It's possible to intentionally trigger this behavior as both a user and +// SDK developer. However, the edge case is quite pathological and requires +// calling [json.Marshaler.MarshalJSON] rather than [json.Marshal]. +func TestAppendCompact(t *testing.T) { + + tests := map[string]struct { + value json.Marshaler + expected string + }{ + // + // Non-compacted cases + // + // Note this is how to exploit the compacter to fail, you must call [json.Marshaler.MarshalJSON] rather than [json.Marshal]. + // The type must also embed [param.APIObject] and return non-compacted JSON. + // + + "no-compact/fails-compaction": { + NonCompacted{Raw: nonCompactedRaw}, + nonCompactedRaw, + }, + "no-compact/nested-with-bad-child": { + NonCompactedParent{BadChild: NonCompacted{ + Raw: nonCompactedRaw, + }}, + `{"bad_child":` + nonCompactedRaw + `}`, + }, + "no-compact/double-nested-with-bad-child": { + NonCompactedDoubleParent{Prop: "1", Parent: NonCompactedParent{BadChild: NonCompacted{ + Raw: nonCompactedRaw, + }}}, + `{"prop":"1","parent":{"bad_child":` + nonCompactedRaw + `}}`, + }, + + // + // Compacted cases + // + + "override/spaces-within": { + param.Override[NonCompactedDoubleParent](json.RawMessage(`{"com": "pact"}`)), + `{"com":"pact"}`, + }, + "override/spaces-after": { + param.Override[NonCompactedDoubleParent](json.RawMessage(`{"com":"pact"} `)), + `{"com":"pact"}`, + }, + "override/spaces-before": { + param.Override[NonCompactedDoubleParent](json.RawMessage(` {"com":"pact"}`)), + `{"com":"pact"}`, + }, + "override/spaces-around": { + param.Override[NonCompactedDoubleParent](json.RawMessage(` { "com": "pact" }`)), + `{"com":"pact"}`, + }, + "override/override-with-nested": { + param.Override[NonCompactedDoubleParent](NonCompactedParent{}), + `{"bad_child":{"foo":"bar"}}`, + }, + "override/override-with-non-compacted": { + param.Override[NonCompactedDoubleParent](NonCompacted{}), + `{"foo":"bar"}`, + }, + } + + for name, test := range tests { + t.Run(name+"/marshal-json", func(t *testing.T) { + b, err := test.value.MarshalJSON() + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + if string(b) != test.expected { + t.Fatalf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + + t.Run(name+"/json-marshal", func(t *testing.T) { + b, err := json.Marshal(test.value) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + + // expected output of JSON Marshal should always be compacted + var compactedExpected bytes.Buffer + err = json.Compact(&compactedExpected, []byte(test.expected)) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + + if string(b) != compactedExpected.String() { + t.Fatalf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + + t.Run(name+"/shimjson-marshal", func(t *testing.T) { + b, err := shimjson.Marshal(test.value) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + if string(b) != test.expected { + t.Logf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + } +} From b381980c635b1d7968a2022709fa91921e77556e 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:18 +0000 Subject: [PATCH 08/10] feat: STG-1756 add Vertex auth params to Stagehand spec --- .stats.yml | 4 +- session.go | 88 +++++++++++++++++++++++++++++++++++++- session_test.go | 110 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 3 deletions(-) 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/session.go b/session.go index c3bf416..0de7570 100644 --- a/session.go +++ b/session.go @@ -272,11 +272,17 @@ type ModelConfigParam struct { APIKey param.Opt[string] `json:"apiKey,omitzero"` // Base URL for the model provider BaseURL param.Opt[string] `json:"baseURL,omitzero" format:"uri"` + // Google Cloud location for Vertex AI models + Location param.Opt[string] `json:"location,omitzero"` + // Google Cloud project ID for Vertex AI models + Project param.Opt[string] `json:"project,omitzero"` + // google-auth-library options used to authenticate Vertex AI models + GoogleAuthOptions ModelConfigGoogleAuthOptionsParam `json:"googleAuthOptions,omitzero"` // Custom headers sent with every request to the model provider Headers map[string]string `json:"headers,omitzero"` // AI provider for the model (or provide a baseURL endpoint instead) // - // Any of "openai", "anthropic", "google", "microsoft", "bedrock". + // Any of "openai", "anthropic", "google", "microsoft", "bedrock", "vertex". Provider ModelConfigProvider `json:"provider,omitzero"` paramObj } @@ -289,6 +295,85 @@ func (r *ModelConfigParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// google-auth-library options used to authenticate Vertex AI models +type ModelConfigGoogleAuthOptionsParam struct { + // Google Cloud project ID used by google-auth-library + ProjectID param.Opt[string] `json:"projectId,omitzero"` + // Google Cloud universe domain + UniverseDomain param.Opt[string] `json:"universeDomain,omitzero"` + // Google Cloud service account credentials + Credentials ModelConfigGoogleAuthOptionsCredentialsParam `json:"credentials,omitzero"` + // Google auth scopes for the desired API request + Scopes ModelConfigGoogleAuthOptionsScopesUnionParam `json:"scopes,omitzero"` + paramObj +} + +func (r ModelConfigGoogleAuthOptionsParam) MarshalJSON() (data []byte, err error) { + type shadow ModelConfigGoogleAuthOptionsParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ModelConfigGoogleAuthOptionsParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Google Cloud service account credentials +// +// The properties ClientEmail, PrivateKey are required. +type ModelConfigGoogleAuthOptionsCredentialsParam struct { + ClientEmail string `json:"client_email" api:"required"` + PrivateKey string `json:"private_key" api:"required"` + AuthProviderX509CertURL param.Opt[string] `json:"auth_provider_x509_cert_url,omitzero" format:"uri"` + AuthUri param.Opt[string] `json:"auth_uri,omitzero" format:"uri"` + ClientID param.Opt[string] `json:"client_id,omitzero"` + ClientX509CertURL param.Opt[string] `json:"client_x509_cert_url,omitzero" format:"uri"` + PrivateKeyID param.Opt[string] `json:"private_key_id,omitzero"` + ProjectID param.Opt[string] `json:"project_id,omitzero"` + TokenUri param.Opt[string] `json:"token_uri,omitzero" format:"uri"` + UniverseDomain param.Opt[string] `json:"universe_domain,omitzero"` + // Any of "service_account". + Type string `json:"type,omitzero"` + paramObj +} + +func (r ModelConfigGoogleAuthOptionsCredentialsParam) MarshalJSON() (data []byte, err error) { + type shadow ModelConfigGoogleAuthOptionsCredentialsParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ModelConfigGoogleAuthOptionsCredentialsParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func init() { + apijson.RegisterFieldValidator[ModelConfigGoogleAuthOptionsCredentialsParam]( + "type", "service_account", + ) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type ModelConfigGoogleAuthOptionsScopesUnionParam struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfStringArray []string `json:",omitzero,inline"` + paramUnion +} + +func (u ModelConfigGoogleAuthOptionsScopesUnionParam) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfStringArray) +} +func (u *ModelConfigGoogleAuthOptionsScopesUnionParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *ModelConfigGoogleAuthOptionsScopesUnionParam) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfStringArray) { + return &u.OfStringArray + } + return nil +} + // AI provider for the model (or provide a baseURL endpoint instead) type ModelConfigProvider string @@ -298,6 +383,7 @@ const ( ModelConfigProviderGoogle ModelConfigProvider = "google" ModelConfigProviderMicrosoft ModelConfigProvider = "microsoft" ModelConfigProviderBedrock ModelConfigProvider = "bedrock" + ModelConfigProviderVertex ModelConfigProvider = "vertex" ) // Server-Sent Event emitted during streaming responses. Events are sent as diff --git a/session_test.go b/session_test.go index 26a8f1b..1a643c8 100644 --- a/session_test.go +++ b/session_test.go @@ -42,9 +42,31 @@ func TestSessionActWithOptionalParams(t *testing.T) { ModelName: "openai/gpt-5.4-mini", APIKey: stagehand.String("sk-some-openai-api-key"), BaseURL: stagehand.String("https://api.openai.com/v1"), + GoogleAuthOptions: stagehand.ModelConfigGoogleAuthOptionsParam{ + Credentials: stagehand.ModelConfigGoogleAuthOptionsCredentialsParam{ + ClientEmail: "client_email", + PrivateKey: "private_key", + AuthProviderX509CertURL: stagehand.String("https://example.com"), + AuthUri: stagehand.String("https://example.com"), + ClientID: stagehand.String("client_id"), + ClientX509CertURL: stagehand.String("https://example.com"), + PrivateKeyID: stagehand.String("private_key_id"), + ProjectID: stagehand.String("project_id"), + TokenUri: stagehand.String("https://example.com"), + Type: "service_account", + UniverseDomain: stagehand.String("universe_domain"), + }, + ProjectID: stagehand.String("projectId"), + Scopes: stagehand.ModelConfigGoogleAuthOptionsScopesUnionParam{ + OfString: stagehand.String("string"), + }, + UniverseDomain: stagehand.String("universeDomain"), + }, Headers: map[string]string{ "foo": "string", }, + Location: stagehand.String("us-central1"), + Project: stagehand.String("my-gcp-project"), Provider: stagehand.ModelConfigProviderOpenAI, }, }, @@ -132,9 +154,31 @@ func TestSessionExecuteWithOptionalParams(t *testing.T) { ModelName: "openai/gpt-5.4-mini", APIKey: stagehand.String("sk-some-openai-api-key"), BaseURL: stagehand.String("https://api.openai.com/v1"), + GoogleAuthOptions: stagehand.ModelConfigGoogleAuthOptionsParam{ + Credentials: stagehand.ModelConfigGoogleAuthOptionsCredentialsParam{ + ClientEmail: "client_email", + PrivateKey: "private_key", + AuthProviderX509CertURL: stagehand.String("https://example.com"), + AuthUri: stagehand.String("https://example.com"), + ClientID: stagehand.String("client_id"), + ClientX509CertURL: stagehand.String("https://example.com"), + PrivateKeyID: stagehand.String("private_key_id"), + ProjectID: stagehand.String("project_id"), + TokenUri: stagehand.String("https://example.com"), + Type: "service_account", + UniverseDomain: stagehand.String("universe_domain"), + }, + ProjectID: stagehand.String("projectId"), + Scopes: stagehand.ModelConfigGoogleAuthOptionsScopesUnionParam{ + OfString: stagehand.String("string"), + }, + UniverseDomain: stagehand.String("universeDomain"), + }, Headers: map[string]string{ "foo": "string", }, + Location: stagehand.String("us-central1"), + Project: stagehand.String("my-gcp-project"), Provider: stagehand.ModelConfigProviderOpenAI, }, }, @@ -144,9 +188,31 @@ func TestSessionExecuteWithOptionalParams(t *testing.T) { ModelName: "openai/gpt-5.4-mini", APIKey: stagehand.String("sk-some-openai-api-key"), BaseURL: stagehand.String("https://api.openai.com/v1"), + GoogleAuthOptions: stagehand.ModelConfigGoogleAuthOptionsParam{ + Credentials: stagehand.ModelConfigGoogleAuthOptionsCredentialsParam{ + ClientEmail: "client_email", + PrivateKey: "private_key", + AuthProviderX509CertURL: stagehand.String("https://example.com"), + AuthUri: stagehand.String("https://example.com"), + ClientID: stagehand.String("client_id"), + ClientX509CertURL: stagehand.String("https://example.com"), + PrivateKeyID: stagehand.String("private_key_id"), + ProjectID: stagehand.String("project_id"), + TokenUri: stagehand.String("https://example.com"), + Type: "service_account", + UniverseDomain: stagehand.String("universe_domain"), + }, + ProjectID: stagehand.String("projectId"), + Scopes: stagehand.ModelConfigGoogleAuthOptionsScopesUnionParam{ + OfString: stagehand.String("string"), + }, + UniverseDomain: stagehand.String("universeDomain"), + }, Headers: map[string]string{ "foo": "string", }, + Location: stagehand.String("us-central1"), + Project: stagehand.String("my-gcp-project"), Provider: stagehand.ModelConfigProviderOpenAI, }, }, @@ -207,9 +273,31 @@ func TestSessionExtractWithOptionalParams(t *testing.T) { ModelName: "openai/gpt-5.4-mini", APIKey: stagehand.String("sk-some-openai-api-key"), BaseURL: stagehand.String("https://api.openai.com/v1"), + GoogleAuthOptions: stagehand.ModelConfigGoogleAuthOptionsParam{ + Credentials: stagehand.ModelConfigGoogleAuthOptionsCredentialsParam{ + ClientEmail: "client_email", + PrivateKey: "private_key", + AuthProviderX509CertURL: stagehand.String("https://example.com"), + AuthUri: stagehand.String("https://example.com"), + ClientID: stagehand.String("client_id"), + ClientX509CertURL: stagehand.String("https://example.com"), + PrivateKeyID: stagehand.String("private_key_id"), + ProjectID: stagehand.String("project_id"), + TokenUri: stagehand.String("https://example.com"), + Type: "service_account", + UniverseDomain: stagehand.String("universe_domain"), + }, + ProjectID: stagehand.String("projectId"), + Scopes: stagehand.ModelConfigGoogleAuthOptionsScopesUnionParam{ + OfString: stagehand.String("string"), + }, + UniverseDomain: stagehand.String("universeDomain"), + }, Headers: map[string]string{ "foo": "string", }, + Location: stagehand.String("us-central1"), + Project: stagehand.String("my-gcp-project"), Provider: stagehand.ModelConfigProviderOpenAI, }, }, @@ -298,9 +386,31 @@ func TestSessionObserveWithOptionalParams(t *testing.T) { ModelName: "openai/gpt-5.4-mini", APIKey: stagehand.String("sk-some-openai-api-key"), BaseURL: stagehand.String("https://api.openai.com/v1"), + GoogleAuthOptions: stagehand.ModelConfigGoogleAuthOptionsParam{ + Credentials: stagehand.ModelConfigGoogleAuthOptionsCredentialsParam{ + ClientEmail: "client_email", + PrivateKey: "private_key", + AuthProviderX509CertURL: stagehand.String("https://example.com"), + AuthUri: stagehand.String("https://example.com"), + ClientID: stagehand.String("client_id"), + ClientX509CertURL: stagehand.String("https://example.com"), + PrivateKeyID: stagehand.String("private_key_id"), + ProjectID: stagehand.String("project_id"), + TokenUri: stagehand.String("https://example.com"), + Type: "service_account", + UniverseDomain: stagehand.String("universe_domain"), + }, + ProjectID: stagehand.String("projectId"), + Scopes: stagehand.ModelConfigGoogleAuthOptionsScopesUnionParam{ + OfString: stagehand.String("string"), + }, + UniverseDomain: stagehand.String("universeDomain"), + }, Headers: map[string]string{ "foo": "string", }, + Location: stagehand.String("us-central1"), + Project: stagehand.String("my-gcp-project"), Provider: stagehand.ModelConfigProviderOpenAI, }, }, From ba29e1e97d4744a504adf2990fa4cc1eeabf9bba 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:23 +0000 Subject: [PATCH 09/10] feat: Add `screenshot` option to Extract --- .stats.yml | 4 ++-- session.go | 3 +++ session_test.go | 5 +++-- 3 files changed, 8 insertions(+), 4 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/session.go b/session.go index 0de7570..b5d1a93 100644 --- a/session.go +++ b/session.go @@ -1485,6 +1485,9 @@ func (r *SessionExtractParams) UnmarshalJSON(data []byte) error { } type SessionExtractParamsOptions struct { + // When true, include a screenshot of the current viewport in the extraction LLM + // call. Defaults to false. + Screenshot param.Opt[bool] `json:"screenshot,omitzero"` // CSS selector to scope extraction to a specific element Selector param.Opt[string] `json:"selector,omitzero"` // Timeout in ms for the extraction diff --git a/session_test.go b/session_test.go index 1a643c8..cb15772 100644 --- a/session_test.go +++ b/session_test.go @@ -301,8 +301,9 @@ func TestSessionExtractWithOptionalParams(t *testing.T) { Provider: stagehand.ModelConfigProviderOpenAI, }, }, - Selector: stagehand.String("#main-content"), - Timeout: stagehand.Float(30000), + Screenshot: stagehand.Bool(false), + Selector: stagehand.String("#main-content"), + Timeout: stagehand.Float(30000), }, Schema: map[string]any{ "foo": "bar", From 3bcbdf9af8dd1ecac401d7c3b448f7c908770270 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:53 +0000 Subject: [PATCH 10/10] release: 3.21.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 21 +++++++++++++++++++++ README.md | 2 +- internal/version.go | 2 +- 4 files changed, 24 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 f94f7eb..90e98ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 3.21.0 (2026-05-20) + +Full Changelog: [v3.20.0...v3.21.0](https://github.com/browserbase/stagehand-go/compare/v3.20.0...v3.21.0) + +### Features + +* [feat]: add `ignoreSelectors` to `observe()` ([501d30b](https://github.com/browserbase/stagehand-go/commit/501d30b5aa52a72eed5a25b064609b5c7c84d733)) +* Add `screenshot` option to Extract ([ba29e1e](https://github.com/browserbase/stagehand-go/commit/ba29e1e97d4744a504adf2990fa4cc1eeabf9bba)) +* **client:** optimize json encoder for internal types ([539a65e](https://github.com/browserbase/stagehand-go/commit/539a65e84dfdd9a1330c6142d229ea47b2575376)) +* STG-1756 add Vertex auth params to Stagehand spec ([b381980](https://github.com/browserbase/stagehand-go/commit/b381980c635b1d7968a2022709fa91921e77556e)) + + +### Bug Fixes + +* **go:** avoid panic when http.DefaultTransport is wrapped ([cea79d5](https://github.com/browserbase/stagehand-go/commit/cea79d5e25d6f072af7b2dde01d34be71106cb4b)) + + +### Chores + +* redact api-key headers in debug logs ([6378688](https://github.com/browserbase/stagehand-go/commit/637868870522a96e2e1d585949926a6116cb1e01)) + ## 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) diff --git a/README.md b/README.md index 841a26a..a3282f4 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.20.0' +go get -u 'github.com/browserbase/stagehand-go@v3.21.0' ``` diff --git a/internal/version.go b/internal/version.go index 2210a0d..f43b02b 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "3.20.0" // x-release-please-version +const PackageVersion = "3.21.0" // x-release-please-version