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 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/.stats.yml b/.stats.yml index 391cde3..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-6f6bfb81d092f30a5e2005328c97d61b9ea36132bb19e9e79e55294b9534ce20.yml -openapi_spec_hash: f3fc1e3688a38dc2c28f7178f7d534e5 +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/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/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} } 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/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 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 +} 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)) + } + }) + } +} diff --git a/session.go b/session.go index f218fe0..b5d1a93 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 @@ -1399,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 @@ -1534,6 +1623,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..cb15772 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,14 +273,37 @@ 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, }, }, - 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", @@ -292,14 +381,37 @@ 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", 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, }, },